From ff1710d9ea8354be6b4bc9427c1b31f350d9cc3b Mon Sep 17 00:00:00 2001 From: maj00r Date: Mon, 19 Oct 2026 15:27:50 +0200 Subject: [PATCH] feat(editor): labels for a layman, light at the start; whole sceneries fitted to the terrain model Labels: - every button, field, tooltip and message of the editor reread for a beginner: short nouns for the fields with the sentence in the tooltip, the less used options under More settings, one glossary (field of work, template, track, grade), sides told from directions, degrees and per mille as signs, messages built from one format string - plural forms through STR_P; the last entry of a po file is read too, and the Polish plural rule is in the header - lang/editor_pl.po: the code pass entries merged, 391 entries no code uses and 3 duplicates dropped - roads: the notes and errors naming a road translated; the build options and the layout fold the texture length, kerb sizes, traffic share, roundabout part, height over the ground and junction speed limit under More settings, the lanes start folded Grade lines fitted to the terrain model (NMT), a line or the whole scenery at once: - fit_ground: a robust fit on nodes 5 m apart; the ground parting from the line (a bridge, a viaduct, a wall) is gone past rather than followed, the bends keep off the switches as far as their curves reach, a run of bends the same way is one point of intersection, a curve with no room is as sharp as the room lets it be - the ends of the line and the other paths of its switches leading into lines fitted already bound the line by what the tracks between can climb; ends meeting in the plan, laid apart in height by hand, are joined before the run - apply_profile: the paths are split where split_path takes the parameter (a straight was cut in the wrong place, into pieces of a few centimetres the game takes for zero length), no piece shorter than the margin; the paths off the ends of the route are brought to its height - Bytom: 1600 lines, grade breaks at the joints 89 -> 20; Tarnowskie Gory: 848 lines, 223 -> 3; no step over 2 cm --- CMakeLists.txt | 3 + application/application.cpp | 4 + application/editorgestures.h | 251 + application/editormode.cpp | 1052 +++- application/editormode.h | 171 +- application/editormodeconnection.cpp | 23 +- application/editormodeconnectioncurve.cpp | 993 ++++ application/editormodedigitize.cpp | 423 ++ application/editormodegauge.cpp | 79 +- application/editormodeinfra.cpp | 113 +- application/editormodejoints.cpp | 58 +- application/editormodelay.cpp | 310 +- application/editormodelineside.cpp | 179 +- application/editormodenmt.cpp | 1173 +++- application/editormodeparallel.cpp | 141 +- application/editormodeprofile.cpp | 1224 ++++- application/editormoderecords.cpp | 50 +- application/editormoderoad.cpp | 768 +-- application/editormoderoute.cpp | 579 +- application/editormodeselection.cpp | 10 +- application/editormodeselftest.cpp | 2 +- application/editormodeselftestjoin.cpp | 5 +- application/editormodeselftestmonkey.cpp | 352 ++ application/editormodeselftestswitch.cpp | 29 +- application/editormodeselftestterrain.cpp | 119 +- application/editormodeselftesttrack.cpp | 220 +- application/editormodeshared.h | 352 ++ application/editormodesignal.cpp | 138 +- application/editormodespeed.cpp | 84 +- application/editormodestraights.cpp | 170 +- application/editormodesweep.cpp | 434 +- application/editormodeswitchdesign.cpp | 245 +- application/editormodeswitchdrive.cpp | 17 +- application/editormodeswitchplace.cpp | 38 +- application/editormodeswitchslide.cpp | 5 +- application/editormodeswitchtool.cpp | 385 +- application/editormodetrack.cpp | 554 +- application/editormodetrackprops.cpp | 97 +- application/editormodetrackshared.h | 7 +- application/editormodetrackux.cpp | 80 +- application/editormodeturntable.cpp | 121 +- application/editormodevehicle.cpp | 219 +- application/editormodeviewport.cpp | 94 +- application/editoruilayer.cpp | 8 +- application/editoruipanels.cpp | 281 +- application/scenarioloadermode.cpp | 4 + application/uilayer.cpp | 10 +- editor/editorAlignment.cpp | 76 +- editor/editorAlignment.hpp | 2 + editor/editorGeometry.hpp | 4 + editor/editorIncludeInfo.cpp | 27 +- editor/editorInfra.cpp | 20 +- editor/editorInfra.hpp | 10 +- editor/editorNmt.cpp | 5 +- editor/editorProfile.cpp | 681 ++- editor/editorProfile.hpp | 38 +- editor/editorRoad.cpp | 31 +- editor/editorSpeed.cpp | 45 +- editor/editorSwitchDesign.cpp | 5 +- editor/editorSwitchStore.cpp | 20 +- editor/editorSwitchStore.hpp | 5 + editor/editorTerrainStreamer.cpp | 110 +- editor/editorTerrainStreamer.hpp | 7 + editor/editorTrack.cpp | 256 +- editor/editorTrack.hpp | 17 +- lang/editor_pl.po | 5889 +++++++++++++++------ launcher/scenery_wizard.cpp | 105 +- scene/heightmapterrain.cpp | 24 +- scene/heightmapterrain.h | 9 + scene/scenelayers.cpp | 92 +- scene/scenelayerssave.cpp | 29 +- tools/editor/usage_headers.py | 152 + utilities/parser.cpp | 9 + utilities/translation.cpp | 26 +- utilities/utilities.cpp | 14 + utilities/utilities.h | 3 + world/Track.cpp | 10 + world/Track.h | 2 + 78 files changed, 15011 insertions(+), 4386 deletions(-) create mode 100644 application/editorgestures.h create mode 100644 application/editormodeconnectioncurve.cpp create mode 100644 application/editormodedigitize.cpp create mode 100644 application/editormodeselftestmonkey.cpp create mode 100644 tools/editor/usage_headers.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 83bdf621..84ac684e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -194,6 +194,8 @@ set(SOURCES "editor/editorSwitchDesign.cpp" "editor/editorConnections.cpp" "application/editormodeconnection.cpp" +"application/editormodeconnectioncurve.cpp" +"application/editormodedigitize.cpp" "editor/editorSpeed.cpp" "editor/editorProfile.cpp" "editor/editorGauge.cpp" @@ -246,6 +248,7 @@ set(SOURCES "application/editormodeselftestsignal.cpp" "application/editormodeselftestterrain.cpp" "application/editormodeselftestrails.cpp" +"application/editormodeselftestmonkey.cpp" "application/scriptedinput.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" diff --git a/application/application.cpp b/application/application.cpp index 3edcc8c0..3c0acc54 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -282,7 +282,11 @@ void eu07_application::DiscordRPCService() } if (currentMode != mode::driver) + { + // the editor and the other modes: nothing to tell, without spinning a core on it + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); continue; + } // Discord RPC updater if (simulation::is_ready) diff --git a/application/editorgestures.h b/application/editorgestures.h new file mode 100644 index 00000000..59c73aa6 --- /dev/null +++ b/application/editorgestures.h @@ -0,0 +1,251 @@ +/* +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/. +*/ + +// the mouse and the keys drawn as pictures: the lessons of the tutorial, the gestures of the tools and of the vertical profile + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "imgui/imgui.h" +#include "utilities/translation.h" + +ImU32 const kGestureLit{IM_COL32(90, 175, 255, 255)}; + +// the mouse with the part used lit: LMB, RMB, MMB, wheel (with arrows), move (with arrows); the width it took +inline float gesture_mouse(ImDrawList &List, ImVec2 const At, float const Height, std::string const &Part) +{ + auto const width{std::round(Height * 0.68f)}; + auto const line{ImGui::GetColorU32(ImGuiCol_Text)}; + auto const thickness{std::max(1.0f, Height * 0.07f)}; + auto const rounding{width * 0.5f}; + auto const split{At.y + std::round(Height * 0.45f)}; + auto const middle{At.x + width * 0.5f}; + ImVec2 const high{At.x + width, At.y + Height}; + if (Part == "LMB") + List.AddRectFilled(At, ImVec2(middle, split), kGestureLit, rounding, ImDrawFlags_RoundCornersTopLeft); + if (Part == "RMB") + List.AddRectFilled(ImVec2(middle, At.y), ImVec2(high.x, split), kGestureLit, rounding, ImDrawFlags_RoundCornersTopRight); + List.AddRect(At, high, line, rounding, 0, thickness); + List.AddLine(ImVec2(At.x, split), ImVec2(high.x, split), line, thickness); + List.AddLine(ImVec2(middle, At.y), ImVec2(middle, split), line, thickness); + auto const wheel{std::max(3.0f, std::round(width * 0.24f))}; + ImVec2 const wheel_low{middle - wheel * 0.5f, At.y + Height * 0.1f}; + ImVec2 const wheel_high{middle + wheel * 0.5f, At.y + Height * 0.36f}; + List.AddRectFilled(wheel_low, wheel_high, (Part == "MMB" || Part == "wheel") ? kGestureLit : ImGui::GetColorU32(ImGuiCol_WindowBg), wheel * 0.5f); + List.AddRect(wheel_low, wheel_high, line, wheel * 0.5f, 0, thickness * 0.8f); + auto right{high.x}; + auto const arrow{Height * 0.16f}; + if (Part == "wheel") + { + auto const x{right + arrow * 1.6f}; + List.AddTriangleFilled(ImVec2(x - arrow, At.y + arrow * 1.8f), ImVec2(x + arrow, At.y + arrow * 1.8f), ImVec2(x, At.y + arrow * 0.3f), kGestureLit); + List.AddTriangleFilled(ImVec2(x - arrow, high.y - arrow * 1.8f), ImVec2(x + arrow, high.y - arrow * 1.8f), ImVec2(x, high.y - arrow * 0.3f), kGestureLit); + right = x + arrow; + } + if (Part == "move") + { + auto const reach{Height * 0.42f}; + ImVec2 const centre{right + arrow * 0.8f + reach, At.y + Height * 0.5f}; + List.AddLine(ImVec2(centre.x - reach + arrow, centre.y), ImVec2(centre.x + reach - arrow, centre.y), kGestureLit, thickness); + List.AddLine(ImVec2(centre.x, centre.y - reach + arrow), ImVec2(centre.x, centre.y + reach - arrow), kGestureLit, thickness); + List.AddTriangleFilled(ImVec2(centre.x - reach, centre.y), ImVec2(centre.x - reach + arrow * 1.4f, centre.y - arrow), ImVec2(centre.x - reach + arrow * 1.4f, centre.y + arrow), kGestureLit); + List.AddTriangleFilled(ImVec2(centre.x + reach, centre.y), ImVec2(centre.x + reach - arrow * 1.4f, centre.y + arrow), ImVec2(centre.x + reach - arrow * 1.4f, centre.y - arrow), kGestureLit); + List.AddTriangleFilled(ImVec2(centre.x, centre.y - reach), ImVec2(centre.x + arrow, centre.y - reach + arrow * 1.4f), ImVec2(centre.x - arrow, centre.y - reach + arrow * 1.4f), kGestureLit); + List.AddTriangleFilled(ImVec2(centre.x, centre.y + reach), ImVec2(centre.x - arrow, centre.y + reach - arrow * 1.4f), ImVec2(centre.x + arrow, centre.y + reach - arrow * 1.4f), kGestureLit); + right = centre.x + reach; + } + return right - At.x; +} + +// a key cap with its name; the width it took +inline float gesture_key(ImDrawList &List, ImVec2 const At, float const Height, std::string const &Text) +{ + auto const size{ImGui::CalcTextSize(Text.c_str())}; + auto const width{std::max(std::round(size.x + Height * 0.6f), Height)}; + auto const rounding{Height * 0.2f}; + ImVec2 const high{At.x + width, At.y + Height}; + List.AddRectFilled(At, high, ImGui::GetColorU32(ImGuiCol_FrameBg), rounding); + List.AddRect(At, high, ImGui::GetColorU32(ImGuiCol_TextDisabled), rounding, 0, std::max(1.0f, Height * 0.06f)); + List.AddText(ImVec2(std::round(At.x + (width - size.x) * 0.5f), std::round(At.y + (Height - size.y) * 0.5f)), ImGui::GetColorU32(ImGuiCol_Text), Text.c_str()); + return width; +} + +// the words of a gesture as they're drawn: the parts of the mouse, the keys, "+" between them, other words as text +struct gesture_word +{ + enum class kind + { + mouse, + key, + plus, + text + } what; + std::string text; +}; + +// "Ctrl+wheel", "Shift+click x4", "Drag a point", "Q W E": the keys and the parts of the mouse it names, the rest as text +inline std::vector gesture_words(std::string const &Keys) +{ + static char const *const keys[]{"Ctrl", "Shift", "Alt", "Esc", "Del", "Enter", "Backspace", "Tab", "Space", "Home", "End", "PrtScr"}; + auto const mouse = [](std::string const &Word) -> std::string { + if (Word == "LMB" || Word == "click" || Word == "Click" || Word == "Press") + return "LMB"; + if (Word == "RMB" || Word == "MMB" || Word == "wheel") + return Word; + if (Word == "Wheel") + return "wheel"; + if (Word == "Drag" || Word == "drag" || Word == "move") + return "move"; + return {}; + }; + auto const key = [&](std::string const &Word) { + if (Word.size() == 1 && (std::isupper(static_cast(Word[0])) || std::isdigit(static_cast(Word[0])))) + return true; + if (Word.size() >= 2 && Word[0] == 'F' && std::isdigit(static_cast(Word[1]))) + return true; + return std::find_if(std::begin(keys), std::end(keys), [&](char const *Key) { return Word == Key; }) != std::end(keys); + }; + std::vector words; + std::string text; + auto const flush = [&]() { + if (false == text.empty()) + words.push_back({gesture_word::kind::text, text}); + text.clear(); + }; + std::istringstream stream{Keys}; + for (std::string word; stream >> word;) + { + std::vector parts; + std::size_t from{0}; + for (std::size_t plus; (plus = word.find('+', from)) != std::string::npos && plus > from; from = plus + 1) + parts.push_back(word.substr(from, plus - from)); + parts.push_back(word.substr(from)); + bool const all{std::all_of(parts.begin(), parts.end(), [&](std::string const &Part) { return false == mouse(Part).empty() || key(Part); })}; + if (false == all || (parts.size() == 1 && words.empty() == false && words.back().what == gesture_word::kind::text && false == key(parts[0]) && mouse(parts[0]).empty())) + { + text += (text.empty() ? "" : " ") + word; + continue; + } + flush(); + for (std::size_t index = 0; index < parts.size(); ++index) + { + if (index > 0) + words.push_back({gesture_word::kind::plus, "+"}); + if (auto const part{mouse(parts[index])}; false == part.empty()) + words.push_back({gesture_word::kind::mouse, part}); + else + words.push_back({gesture_word::kind::key, parts[index]}); + } + } + flush(); + return words; +} + +// the mouse and the keys of a gesture only, its words left to the text beside it +inline std::vector gesture_picture(std::string const &Keys) +{ + std::vector words; + for (auto const &word : gesture_words(Keys)) + if (word.what != gesture_word::kind::text && (word.what != gesture_word::kind::plus || (false == words.empty() && words.back().what != gesture_word::kind::plus))) + words.push_back(word); + while (false == words.empty() && words.back().what == gesture_word::kind::plus) + words.pop_back(); + return words; +} + +// the picture says all the words of the gesture: keys alone, no mouse nor words +inline bool gesture_keys_only(std::string const &Keys) +{ + auto const words{gesture_words(Keys)}; + return false == words.empty() && std::all_of(words.begin(), words.end(), [](gesture_word const &Word) { return Word.what == gesture_word::kind::key || Word.what == gesture_word::kind::plus; }); +} + +// the gesture drawn in a row; the width it took +inline float gesture_draw(ImDrawList &List, ImVec2 const At, float const Height, std::vector const &Words, ImU32 const Text) +{ + auto const gap{std::round(Height * 0.2f)}; + float x{0.0f}; + for (auto const &word : Words) + { + ImVec2 const place{At.x + x, At.y}; + switch (word.what) + { + case gesture_word::kind::mouse: x += gesture_mouse(List, place, Height, word.text) + gap; break; + case gesture_word::kind::key: x += gesture_key(List, place, Height, word.text) + gap; break; + case gesture_word::kind::plus: + case gesture_word::kind::text: + { + auto const size{ImGui::CalcTextSize(word.text.c_str())}; + List.AddText(ImVec2(place.x, std::round(At.y + (Height - size.y) * 0.5f)), word.what == gesture_word::kind::plus ? ImGui::GetColorU32(ImGuiCol_TextDisabled) : Text, word.text.c_str()); + x += size.x + gap; + break; + } + } + } + return std::max(0.0f, x - gap); +} + +inline float gesture_width(float const Height, std::vector const &Words) +{ + auto const gap{std::round(Height * 0.2f)}; + auto const arrow{Height * 0.16f}; + float x{0.0f}; + for (auto const &word : Words) + { + switch (word.what) + { + case gesture_word::kind::mouse: + x += std::round(Height * 0.68f) + (word.text == "wheel" ? arrow * 2.6f : word.text == "move" ? arrow * 0.8f + Height * 0.84f : 0.0f); + break; + case gesture_word::kind::key: x += std::max(std::round(ImGui::CalcTextSize(word.text.c_str()).x + Height * 0.6f), Height); break; + default: x += ImGui::CalcTextSize(word.text.c_str()).x; break; + } + x += gap; + } + return std::max(0.0f, x - gap); +} + +// rows of gestures, each drawn with its key and what it does beside it; the keys and the texts are the English ones, translated here +inline void gesture_rows(std::vector> const &Rows) +{ + auto *list{ImGui::GetWindowDrawList()}; + auto const font{ImGui::GetFontSize()}; + auto const size{std::round(font * 1.25f)}; + auto const pad{std::round(font * 0.6f)}; + std::vector> pictures; + float column{0.0f}; + for (auto const &row : Rows) + { + pictures.push_back(gesture_picture(row.first)); + column = std::max(column, gesture_width(size, pictures.back())); + } + for (std::size_t i = 0; i < Rows.size(); ++i) + { + auto const at{ImGui::GetCursorScreenPos()}; + auto const line{ImGui::GetTextLineHeight()}; + auto const height{std::max(size, line)}; + gesture_draw(*list, ImVec2(at.x, at.y + std::round((height - size) * 0.5f)), size, pictures[i], ImGui::GetColorU32(ImGuiCol_Text)); + ImGui::Dummy(ImVec2(column, height)); + ImGui::SameLine(0.0f, pad); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + std::round((height - line) * 0.5f)); + if (false == gesture_keys_only(Rows[i].first)) + { + ImGui::TextColored(ImVec4(1.0f, 0.83f, 0.3f, 1.0f), "%s", STR_C(Rows[i].first)); + ImGui::SameLine(); + } + ImGui::TextWrapped("%s", STR_C(Rows[i].second)); + } +} diff --git a/application/editormode.cpp b/application/editormode.cpp index ae048a29..1e0c27ee 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include #include "application/editormode.h" #include "application/editormodeshared.h" +#include "application/editorgestures.h" #include "scene/scenenodegroups.h" #include "application/editoruilayer.h" #ifndef _WIN32 @@ -518,7 +519,7 @@ editor_mode::editor_mode() { std::string reason; if (Node->from_template() || false == scene::Layers.editable(Node, &reason)) { - ui()->set_status("\"" + (Node->name().empty() ? std::string{"(unnamed node)"} : Node->name()) + "\" can't be edited: " + reason, true); + ui()->set_status(format(STR_C("\"%s\" can't be edited: %s"), (Node->name().empty() ? std::string{STR_C("(no name)")} : Node->name()).c_str(), reason.c_str()), true); return; } m_track_window_open = false; @@ -1448,6 +1449,7 @@ bool editor_mode::update() speed_step(); joints_step(); draw_track_hints(); + draw_empty_scenery_hint(); draw_profile_overlay(); draw_infra_overlay(); draw_speed_overlay(); @@ -1570,47 +1572,18 @@ void editor_mode::render_terrain_ui() { if (false == m_streamer.active()) { - ImGui::TextWrapped("%s", STR_C("The scenery has no heightmap terrain yet. It's kept in the folder terrain// of the simulator, " - "and written there when the scenery is saved.")); + ImGui::TextWrapped("%s", STR_C("The scenery has no terrain yet. Make an empty one, or load it from the terrain model below")); if (ImGui::Button(STR_C("Make heightmap terrain for this scenery"))) ensure_terrain(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Kept in the folder terrain// and written there with the scenery (Ctrl+S)")); + // the terrain model makes the terrain itself + if (ImGui::CollapsingHeader(STR_C("Terrain model (NMT)"), ImGuiTreeNodeFlags_DefaultOpen)) + render_nmt_ui(); return; } - ImGui::TextDisabled(STR_C("Folder: %s"), m_streamer.directory().c_str()); - ImGui::TextDisabled(STR_C("Chunks in memory: %zu%s"), m_streamer.resident(), m_streamer.modified() ? STR_C(", changes not saved yet") : ""); - - // what the left mouse button does - auto tool = static_cast(m_terrain_tool); - auto const radio = [&](char const *Label, terrain_tool const Tool, char const *Tooltip) { - ImGui::RadioButton(Label, &tool, static_cast(Tool)); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", Tooltip); - }; - // the tools are named like the headers below, which would give them the same IDs - ImGui::PushID("terrain_tools"); - // the tools working on a click, then the brushes - radio(STR_C("Select"), terrain_tool::none, STR_C("LMB picks the models, as in the surroundings")); - ImGui::SameLine(); - radio(STR_C("Chunks"), terrain_tool::chunks, STR_C("LMB adds a chunk shaped as the ground under it, next to the clicked one; Shift+LMB removes the clicked chunk")); - ImGui::SameLine(); - radio(STR_C("Spacing"), terrain_tool::spacing, STR_C("LMB selects the clicked chunk, Ctrl+LMB adds it to the selection or takes it away;\nthe point spacing of the selected chunks is converted in \"Chunk resolution\" below")); - ImGui::SameLine(); - radio(STR_C("Water"), terrain_tool::water, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back")); - ImGui::SameLine(); - radio(STR_C("Fill"), terrain_tool::fill, STR_C("LMB adds a point of the outline of an area to fill with a material, Shift+LMB takes the last one back")); - radio(STR_C("Sculpt"), terrain_tool::sculpt, STR_C("LMB raises the terrain under the brush, Shift+LMB lowers it")); - ImGui::SameLine(); - radio(STR_C("Smooth"), terrain_tool::smooth, STR_C("LMB evens the terrain out under the brush")); - ImGui::SameLine(); - radio(STR_C("Level"), terrain_tool::level, STR_C("LMB leads the terrain under the brush to the target height, Ctrl+LMB takes the target height from what's under the cursor")); - ImGui::SameLine(); - radio(STR_C("Paint"), terrain_tool::paint, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette")); - ImGui::SameLine(); - radio(STR_C("Restore"), terrain_tool::restore, STR_C("LMB takes back the touch-ups made over the tracks under the brush:\nthe terrain returns to the shape the modifiers (tracks) give it")); - ImGui::PopID(); - m_terrain_tool = static_cast(tool); - - if (ImGui::CollapsingHeader(STR_C("Chunks"), ImGuiTreeNodeFlags_DefaultOpen)) + // the tools are chosen in the toolbar, here are their settings + if (ImGui::CollapsingHeader(STR_C("Chunks"), m_terrain_tool == terrain_tool::chunks ? ImGuiTreeNodeFlags_DefaultOpen : 0)) { if (spacing_combo(STR_C("Point spacing"), m_terrain_spacing)) m_streamer.default_spacing(m_terrain_spacing); @@ -1639,7 +1612,7 @@ void editor_mode::render_terrain_ui() } } - if (ImGui::CollapsingHeader(STR_C("Terrain model (NMT)"), ImGuiTreeNodeFlags_DefaultOpen)) + if (ImGui::CollapsingHeader(STR_C("Terrain model (NMT)"))) render_nmt_ui(); if (ImGui::CollapsingHeader(STR_C("Chunk resolution"), m_terrain_tool == terrain_tool::spacing ? ImGuiTreeNodeFlags_DefaultOpen : 0)) @@ -1648,7 +1621,7 @@ void editor_mode::render_terrain_ui() for (auto it = m_terrain_selection.begin(); it != m_terrain_selection.end();) it = m_streamer.exists(*it) ? std::next(it) : m_terrain_selection.erase(it); if (m_terrain_selection.empty()) - ImGui::TextWrapped("%s", STR_C("No chunk selected. With the Spacing tool LMB selects the clicked chunk, Ctrl+LMB adds one or takes it away.")); + ImGui::TextWrapped("%s", STR_C("No chunk selected. With the Point spacing tool LMB selects the clicked chunk, Ctrl+LMB adds one or takes it away.")); else { bool coarser = false; @@ -1739,9 +1712,11 @@ void editor_mode::render_terrain_ui() { auto label{layers[i].size > 0.f ? format("%zu: %s (%.1f m)", i, layers[i].material.c_str(), layers[i].size) : format("%zu: %s", i, layers[i].material.c_str())}; if (layers[i].rotation != 0.f) - label += format(STR_C(", turned %.0f deg"), layers[i].rotation); + label += format(STR_C(", turned %.0f°"), layers[i].rotation); if (ImGui::Selectable(label.c_str(), static_cast(i) == m_terrain_layer)) m_terrain_layer = static_cast(i); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("New chunks are covered with the first material. A texture dragged from the browser onto an entry of the palette takes its place")); // a material dropped on an entry takes its place, on every chunk painted with it std::string dropped; if (material_drop(dropped)) @@ -1777,8 +1752,7 @@ void editor_mode::render_terrain_ui() ImGui::SameLine(); if (ImGui::Button(STR_C("Add to the palette")) && m_terrain_material[0] != '\0') m_terrain_layer = m_streamer.layer(m_terrain_material, m_terrain_material_size); - ImGui::TextDisabled("%s", STR_C("New chunks are covered with the first material. A chunk holds up to 8 of them. " - "A texture dragged from the browser onto an entry of the palette takes its place.")); + ImGui::TextDisabled("%s", STR_C("A chunk holds up to 8 materials")); } if (ImGui::CollapsingHeader(STR_C("Fill"), m_terrain_tool == terrain_tool::fill ? ImGuiTreeNodeFlags_DefaultOpen : 0)) @@ -1787,7 +1761,7 @@ void editor_mode::render_terrain_ui() if (false == layers.empty()) { auto const &chosen{layers[std::clamp(m_terrain_layer, 0, static_cast(layers.size()) - 1)]}; - ImGui::Text(STR_C("Material: %s (chosen in the palette below)"), chosen.material.c_str()); + ImGui::Text(STR_C("Material: %s (chosen in the palette above)"), chosen.material.c_str()); } ImGui::Text(STR_C("Outline: %zu points"), m_paint_outline.size()); ImGui::SetNextItemWidth(120.0f); @@ -1854,7 +1828,7 @@ void editor_mode::render_terrain_ui() { ImGui::PushID(static_cast(i)); auto const material = water[i].material.empty() ? std::string(STR_C("default material")) : water[i].material; - if (ImGui::Selectable(format("%s, %.2f m, %zu points, %s", water[i].name.c_str(), water[i].level, water[i].outline.size(), material.c_str()).c_str(), static_cast(i) == m_water_selected)) + if (ImGui::Selectable(format(STR_C("%s, %.2f m, %zu points, %s"), water[i].name.c_str(), water[i].level, water[i].outline.size(), material.c_str()).c_str(), static_cast(i) == m_water_selected)) m_water_selected = static_cast(i); // a texture dropped on an entry becomes the material of that water std::string name; @@ -1945,7 +1919,7 @@ void editor_mode::render_terrain_ui() for (auto const &modifier : modifiers) { ImGui::PushID(modifier.name.c_str()); - ImGui::TextUnformatted(format("%s, %zu points", modifier.name.c_str(), modifier.points.size()).c_str()); + ImGui::TextUnformatted(format(STR_C("%s, %zu points"), modifier.name.c_str(), modifier.points.size()).c_str()); ImGui::SameLine(); if (ImGui::SmallButton(STR_C("Bake"))) baked = modifier.name; @@ -1973,8 +1947,13 @@ void editor_mode::render_terrain_ui() } } - if (ImGui::CollapsingHeader(STR_C("Drawing and files"))) + auto const files{format(STR_C("Folder: %s"), m_streamer.directory().c_str()) + "\n" + format(STR_C("Chunks in memory: %zu%s"), m_streamer.resident(), m_streamer.modified() ? STR_C(", changes not saved yet") : "")}; + auto const drawing{ImGui::CollapsingHeader(STR_C("Drawing and files"))}; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", files.c_str()); + if (drawing) { + ImGui::TextDisabled("%s", files.c_str()); auto detail{m_streamer.detail()}; ImGui::SetNextItemWidth(160.0f); if (ImGui::SliderFloat(STR_C("Detail"), &detail, 30.0f, 400.0f, "%.0f")) @@ -2217,6 +2196,8 @@ void editor_mode::render_orthophoto_ui() ImGui::Separator(); ImGui::TextUnformatted(STR_C("Scenery origin (0,0,0) in PUWG 1992 / EPSG:2180")); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("PUWG 1992 (EPSG:2180): the coordinate system of the maps of Poland, in metres; the numbers as on geoportal.gov.pl")); // geodetic convention, as displayed by geoportal.gov.pl: X grows north, Y grows east. // applied once editing ends, so half-typed numbers don't trigger downloads ImGui::SetNextItemWidth(160.0f); @@ -2277,115 +2258,123 @@ void editor_mode::render_orthophoto_ui() ImGui::PopItemFlag(); }; - if (ImGui::Checkbox(STR_C("Fit to terrain"), &config.drape)) - { - changed = true; - persist = true; - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip(STR_C("Lays the imagery onto the ground geometry (terrain shapes, terrain tile models, heightmap terrain).\n" - "Places without any ground use the height below.")); - if (config.drape) + auto const stats = m_orthophoto.stats(); + if (stats.failed > 0) { + ImGui::Text(STR_C("Tiles: %d shown, %d loading, %d failed"), stats.resident, stats.loading, stats.failed); ImGui::SameLine(); - if (ImGui::Button(STR_C("Refit"))) - { - // the ground was edited: what's kept of it has to go as well - ground_tiles.clear(); - forget_ground_shapes(); - m_orthophoto.refit(); - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip(STR_C("Samples the ground again, after it was edited")); + if (ImGui::Button(STR_C("Retry"))) + m_orthophoto.retry_failed(); } - begin_disabled(!config.drape); - ImGui::SetNextItemWidth(160.0f); - changed |= ImGui::DragFloat(STR_C("Lift above ground (m)"), &config.lift, 0.01f, 0.0f, 50.0f, "%.2f"); - persist |= ImGui::IsItemDeactivatedAfterEdit(); - end_disabled(!config.drape); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip(STR_C("Raises the fitted imagery above the ground. Keep it below the objects you want to see:\n" - "in the scene everything lower than this ends up under the imagery.")); - - begin_disabled(config.drape); - ImGui::SetNextItemWidth(160.0f); - changed |= ImGui::DragFloat(STR_C("Height (m)"), &config.height, 0.1f, -1000.0f, 3000.0f, "%.2f"); - persist |= ImGui::IsItemDeactivatedAfterEdit(); - end_disabled(config.drape); - - if (ImGui::Checkbox(STR_C("Covered by objects and terrain"), &config.in_scene)) + if (ImGui::TreeNode(STR_C("More settings"))) { - changed = true; - persist = true; - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip(STR_C("Draws the imagery as part of the scene, so tracks, models and terrain in front of it hide it.\n" - "It is lit like the scene. Fully opaque, mouse placement lands on it as on any surface;\n" - "translucent, it doesn't catch the mouse.\n" - "Not available with the Better Renderer, which doesn't pick up geometry added in the editor.")); - - ImGui::SetNextItemWidth(160.0f); - changed |= ImGui::SliderFloat(STR_C("Opacity"), &config.opacity, 0.0f, 1.0f, "%.2f"); - persist |= ImGui::IsItemDeactivatedAfterEdit(); - if (config.in_scene && ImGui::IsItemHovered()) - ImGui::SetTooltip(STR_C("In the scene the opacity is part of the textures: the tiles are read again from the cache\n" - "shortly after the slider stops, which takes a moment.")); - - // newest imagery, or the newest imagery taken up to the end of the selected year - int const thisyear = static_cast(std::chrono::year_month_day{std::chrono::floor(std::chrono::system_clock::now())}.year()); - std::string const yearlabel = config.year ? std::to_string(config.year) : std::string(STR_C("Newest")); - ImGui::SetNextItemWidth(160.0f); - if (ImGui::BeginCombo("Photo year", yearlabel.c_str())) - { - if (ImGui::Selectable(STR_C("Newest"), config.year == 0)) + if (ImGui::Checkbox(STR_C("Fit to terrain"), &config.drape)) { - config.year = 0; changed = true; persist = true; } - for (int year = thisyear; year >= editor_orthophoto::oldest_year; --year) + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Lays the imagery onto the ground geometry (terrain shapes, terrain tile models, heightmap terrain).\n" + "Places without any ground use the height below.")); + if (config.drape) { - if (ImGui::Selectable(std::to_string(year).c_str(), config.year == year)) + ImGui::SameLine(); + if (ImGui::Button(STR_C("Refit"))) { - config.year = year; + // the ground was edited: what's kept of it has to go as well + ground_tiles.clear(); + forget_ground_shapes(); + m_orthophoto.refit(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Samples the ground again, after it was edited")); + } + + begin_disabled(!config.drape); + ImGui::SetNextItemWidth(160.0f); + changed |= ImGui::DragFloat(STR_C("Lift above ground (m)"), &config.lift, 0.01f, 0.0f, 50.0f, "%.2f"); + persist |= ImGui::IsItemDeactivatedAfterEdit(); + end_disabled(!config.drape); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Raises the fitted imagery above the ground. Keep it below the objects you want to see:\n" + "in the scene everything lower than this ends up under the imagery.")); + + begin_disabled(config.drape); + ImGui::SetNextItemWidth(160.0f); + changed |= ImGui::DragFloat(STR_C("Height (m)"), &config.height, 0.1f, -1000.0f, 3000.0f, "%.2f"); + persist |= ImGui::IsItemDeactivatedAfterEdit(); + end_disabled(config.drape); + + if (ImGui::Checkbox(STR_C("Covered by objects and terrain"), &config.in_scene)) + { + changed = true; + persist = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Draws the imagery as part of the scene, so tracks, models and terrain in front of it hide it.\n" + "It is lit like the scene. Fully opaque, mouse placement lands on it as on any surface;\n" + "translucent, it doesn't catch the mouse.\n" + "Not available with the Better Renderer, which doesn't pick up geometry added in the editor.")); + + ImGui::SetNextItemWidth(160.0f); + changed |= ImGui::SliderFloat(STR_C("Opacity"), &config.opacity, 0.0f, 1.0f, "%.2f"); + persist |= ImGui::IsItemDeactivatedAfterEdit(); + if (config.in_scene && ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("In the scene the opacity is part of the textures: the tiles are read again from the cache\n" + "shortly after the slider stops, which takes a moment.")); + + // newest imagery, or the newest imagery taken up to the end of the selected year + int const thisyear = static_cast(std::chrono::year_month_day{std::chrono::floor(std::chrono::system_clock::now())}.year()); + std::string const yearlabel = config.year ? std::to_string(config.year) : std::string(STR_C("Newest")); + ImGui::SetNextItemWidth(160.0f); + if (ImGui::BeginCombo(STR_C("Photo year"), yearlabel.c_str())) + { + if (ImGui::Selectable(STR_C("Newest"), config.year == 0)) + { + config.year = 0; changed = true; persist = true; } + for (int year = thisyear; year >= editor_orthophoto::oldest_year; --year) + { + if (ImGui::Selectable(std::to_string(year).c_str(), config.year == year)) + { + config.year = year; + changed = true; + persist = true; + } + } + ImGui::EndCombo(); } - ImGui::EndCombo(); - } - if (ImGui::Checkbox(STR_C("4K tiles where available"), &config.hires)) - { - changed = true; - persist = true; + if (ImGui::Checkbox(STR_C("4K tiles where available"), &config.hires)) + { + changed = true; + persist = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("4096 px tiles (~6 cm/px) from the high-resolution orthophoto, for the tiles around the camera.\n" + "Areas it doesn't cover use the standard imagery. Each 4K tile takes ~85 MB of video memory.")); + + ImGui::Separator(); + if (stats.failed == 0) + ImGui::Text(STR_C("Tiles: %d shown, %d loading, %d failed"), stats.resident, stats.loading, stats.failed); + if (!editor_orthophoto::can_download()) + ImGui::TextDisabled(STR_C("This build has no HTTP client, only cached tiles are shown")); + ImGui::TextDisabled(STR_C("Cache: %s"), editor_orthophoto::cache_directory().c_str()); + if (ImGui::Button(STR_C("Clear cache"))) + m_orthophoto.clear_cache(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Deletes downloaded tiles from disk. They are fetched again around the camera.")); + ImGui::TreePop(); } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip(STR_C("4096 px tiles (~6 cm/px) from the high-resolution orthophoto, for the tiles around the camera.\n" - "Areas it doesn't cover use the standard imagery. Each 4K tile takes ~85 MB of video memory.")); if (changed) m_orthophoto.settings(config); if (persist) save_orthophoto_settings(); - ImGui::Separator(); - auto const stats = m_orthophoto.stats(); - ImGui::Text(STR_C("Tiles: %d shown, %d loading, %d failed"), stats.resident, stats.loading, stats.failed); - if (stats.failed > 0) - { - ImGui::SameLine(); - if (ImGui::Button(STR_C("Retry"))) - m_orthophoto.retry_failed(); - } - if (!editor_orthophoto::can_download()) - ImGui::TextDisabled(STR_C("This build has no HTTP client, only cached tiles are shown")); - ImGui::TextDisabled(STR_C("Cache: %s"), editor_orthophoto::cache_directory().c_str()); - if (ImGui::Button(STR_C("Clear cache"))) - m_orthophoto.clear_cache(); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip(STR_C("Deletes downloaded tiles from disk. They are fetched again around the camera.")); ImGui::TextDisabled(STR_C("Imagery: GUGiK, geoportal.gov.pl")); } @@ -2975,11 +2964,11 @@ void editor_mode::toggle_include(scene::instance_handle const Instance) if (included.dead) { // its directive is gone from the scenery file, and the place it was at with it - ui()->set_status("Include of \"" + *included.file + "\" was removed from the saved scenery, this can't be taken back.", true); + ui()->set_status(format(STR_C("Include of \"%s\" was removed from the saved scenery, this can't be taken back"), included.file->c_str()), true); return; } scene::Layers.removed(Instance, false == included.removed); - ui()->set_status("Include of \"" + *included.file + "\" " + (included.removed ? "removed." : "restored.")); + ui()->set_status(format(included.removed ? STR_C("Include of \"%s\" removed") : STR_C("Include of \"%s\" restored"), included.file->c_str())); } void editor_mode::restore_include(EditorSnapshot &Snapshot) @@ -2995,7 +2984,7 @@ void editor_mode::restore_include(EditorSnapshot &Snapshot) auto current = scene::Layers.directive(Snapshot.instance); if (current.empty() || scene::Layers.instance(Snapshot.instance).dead) { - ui()->set_status("Include of \"" + Snapshot.node_name + "\" is gone from the scenery, the change can't be taken back.", true); + ui()->set_status(format(STR_C("Include of \"%s\" is gone from the scenery, the change can't be taken back"), Snapshot.node_name.c_str()), true); return; } set_include_directive(Snapshot.instance, Snapshot.serialized); @@ -3027,7 +3016,7 @@ void editor_mode::restore_includes(EditorSnapshot &Snapshot) entry.second = std::move(current); } select_include(0); - ui()->set_status(gone > 0 ? std::to_string(gone) + " of the includes are gone from the saved scenery, they can't be taken back." : std::to_string(Snapshot.instances.size() + Snapshot.directives.size()) + " includes restored.", gone > 0); + ui()->set_status(gone > 0 ? format(STR_C("Includes gone from the saved scenery, they can't be taken back: %zu"), gone) : format(STR_C("Includes restored: %zu"), Snapshot.instances.size() + Snapshot.directives.size()), gone > 0); } void editor_mode::select_include(scene::instance_handle const Instance) @@ -3049,7 +3038,7 @@ void editor_mode::select_include(scene::instance_handle const Instance) if (false == editor_includes::parse_directive(scene::Layers.directive(m_instance), m_include.target, m_include.values)) { m_include.values.clear(); - m_include.issue = "The directive of the include can't be read from the scenery file, its parameters can't be changed."; + m_include.issue = STR_C("The directive of the include can't be read from the scenery file, its parameters can't be changed"); } // with a rotation or a scale in effect the parameters don't translate to a location in the scene in a single way, // see place_include() @@ -3077,7 +3066,7 @@ void editor_mode::apply_include() if (value == "end" || value.find_first_of("\"\r\n") != std::string::npos) { // either would cut the directive short - ui()->set_status("A parameter of an include can't be \"end\", or have a quotation mark in it.", true); + ui()->set_status(STR_C("A parameter of an include can't be \"end\", or have a quotation mark in it"), true); select_include(m_instance); // back to the values of the directive return; } @@ -3121,7 +3110,7 @@ void editor_mode::place_include(std::string const &File, int const RotationMode, if (scene::Layers.empty()) { // the directive has to go to a scenery file, and without the sources tracked there's no telling which - ui()->set_status("Scenery templates can be placed only in a scenery opened for editing (-edit).", true); + ui()->set_status(STR_C("Scenery templates can be placed only in a scenery opened for editing (-edit)"), true); WriteLog("Editor: scenery templates can be placed only in a scenery opened for editing (-edit)", logtype::generic); return; } @@ -3130,7 +3119,7 @@ void editor_mode::place_include(std::string const &File, int const RotationMode, auto const parameters = editor_includes::parameter_count(File); if (false == editor_includes::load(File, info, issue) || false == editor_includes::complete(info, parameters, &issue)) { - ui()->set_status("Template \"" + File + "\" can't be placed: " + issue, true); + ui()->set_status(format(STR_C("Template \"%s\" can't be placed: %s"), File.c_str(), issue.c_str()), true); return; } @@ -3138,7 +3127,7 @@ void editor_mode::place_include(std::string const &File, int const RotationMode, auto const layer = scene::Layers.resolve(scene::Layers.active()); if (std::string reason; false == scene::Layers.accepts(layer, &reason)) { - ui()->set_status("Template \"" + File + "\" can't be placed: " + (reason.empty() ? "there's no active layer to put it in" : "the active layer can't take it, " + reason), true); + ui()->set_status(format(STR_C("Template \"%s\" can't be placed: %s"), File.c_str(), (reason.empty() ? std::string{STR_C("there's no active layer to put it in")} : format(STR_C("the active layer can't take it, %s"), reason.c_str())).c_str()), true); return; } auto const context = scene::Layers.layer(layer).context_insert(); @@ -3146,7 +3135,7 @@ void editor_mode::place_include(std::string const &File, int const RotationMode, { // a template can pass its parameters to an origin directive, which isn't affected by these, or straight to // the nodes, which are. there's no single set of parameter values which puts both at the cursor - ui()->set_status("Template \"" + File + "\" can't be placed: the file of the active layer would receive it with a rotation or a scale in effect. Pick another layer.", true); + ui()->set_status(format(STR_C("Template \"%s\" can't be placed: the file of the active layer would receive it with a rotation or a scale in effect. Pick another layer"), File.c_str()), true); return; } glm::dvec3 const location = placement_on_ground(Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position())); @@ -3160,7 +3149,7 @@ void editor_mode::place_include(std::string const &File, int const RotationMode, auto const instance = scene::Layers.place(layer, File, directive); if (instance == 0) { - ui()->set_status("Template \"" + File + "\" can't be placed in the active layer.", true); + ui()->set_status(format(STR_C("Template \"%s\" can't be placed in the active layer"), File.c_str()), true); return; } // show the models of the template right away. the rest of it comes into play when the saved scenery is loaded @@ -3173,9 +3162,8 @@ void editor_mode::place_include(std::string const &File, int const RotationMode, g_redo.clear(); select_include(instance); - ui()->set_status( - "\"" + info.name + "\" placed in layer \"" + scene::Layers.layer(layer).name + "\": " + std::to_string(preview.first) + " model(s) shown" - + (preview.second > 0 ? ", " + std::to_string(preview.second) + " other statement(s) of the template take effect once the scenery is saved and loaded again." : ".")); + ui()->set_status(format(STR_C("\"%s\" placed in layer \"%s\", models shown: %d"), info.name.c_str(), scene::Layers.layer(layer).name.c_str(), static_cast(preview.first)) + + (preview.second > 0 ? format(STR_C(", other statements of the template, working once the scenery is saved and loaded again: %d"), static_cast(preview.second)) : std::string{})); } void editor_mode::export_scenery() @@ -3398,7 +3386,7 @@ void editor_mode::drop_model_instances() if (selected->radius() >= 50.0f) { // it'd be put on top of itself - ui()->set_status("\"" + selected->Model()->NameGet() + "\" is large enough to count as ground itself, so there's nothing to put it on", true); + ui()->set_status(format(STR_C("\"%s\" is large enough to count as ground itself, so there's nothing to put it on"), selected->Model()->NameGet().c_str()), true); return; } std::vector instances; @@ -3436,11 +3424,11 @@ void editor_mode::drop_model_instances() instance->mark_dirty(); ++moved; } - std::string status = std::to_string(moved) + " of " + std::to_string(instances.size()) + " instances of \"" + selected->Model()->NameGet() + "\" put on the ground"; + std::string status = format(STR_C("%d of %d copies of \"%s\" put on the ground"), moved, static_cast(instances.size()), selected->Model()->NameGet().c_str()); if (moved > 0) - status += "; Ctrl+Z takes them back one at a time"; + status += STR_C("; Ctrl+Z takes them back one at a time"); if (locked > 0) - status += ". " + std::to_string(locked) + " left alone: placed by a template, or on a layer which can't be edited"; + status += format(STR_C(". Left alone, placed by a template or on a layer which can't be edited: %d"), locked); ui()->set_status(status); } @@ -3524,7 +3512,7 @@ void editor_mode::render_view_menu() ImGui::SetTooltip("%s", STR_C("The view straight down without perspective, as on a map; again for the camera as it was")); ImGui::MenuItem(STR_C("Show lanes"), nullptr, &m_roadtool.lanes); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Draws the lanes of the roads with the ways the vehicles take through the junctions"); + ImGui::SetTooltip("%s", STR_C("The lanes of the roads drawn as the paths the vehicles take")); if (ImGui::MenuItem(STR_C("Structure gauge"), nullptr, m_gauge.open)) m_gauge.open = !m_gauge.open; if (ImGui::IsItemHovered()) @@ -3588,7 +3576,7 @@ void editor_mode::render_environment_window() if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Up to 1 the sky clouds over, past it rain or snow falls, by the air temperature")); auto temperature{Global.AirTemperature}; - auto const air{format("%.1f deg C", Global.AirTemperature)}; + auto const air{format("%.1f °C", Global.AirTemperature)}; if (ImGui::SliderFloat((std::string(STR_C("Air temperature")) + "###airtemperature").c_str(), &temperature, -35.0f, 40.0f, air.c_str())) { Global.AirTemperature = std::clamp(temperature, -35.0f, 40.0f); @@ -3959,9 +3947,9 @@ void editor_mode::run_area_fill() m_fill_status = format(STR_C("Placed %zu of %zu objects"), m_fill_last.size(), static_cast(target)); if (points.size() < target) - m_fill_status += " (limited by min. spacing)"; + m_fill_status += STR_C(" (limited by min. spacing)"); if (unsupported > 0) - m_fill_status += ", " + std::to_string(unsupported) + " without ground found (height interpolated)"; + m_fill_status += format(STR_C(", without ground under them (height from the outline): %zu"), unsupported); WriteLog("Editor: area fill - " + m_fill_status, logtype::generic); } @@ -4017,7 +4005,7 @@ void editor_mode::render_area_fill() { ImGui::TextDisabled(STR_C("LMB: add outline point Backspace: remove last point")); double const area = polygon_area_xz(m_fill_points); - ImGui::Text(STR_C("Points: %zu Area: %.0f m2 (%.2f ha)"), m_fill_points.size(), area, area / 10000.0); + ImGui::Text(STR_C("Points: %zu Area: %.0f m² (%.2f ha)"), m_fill_points.size(), area, area / 10000.0); if (ImGui::Button(STR_C("Remove last point")) && !m_fill_points.empty()) m_fill_points.pop_back(); ImGui::SameLine(); @@ -4039,15 +4027,21 @@ void editor_mode::render_area_fill() ImGui::SetNextItemWidth(120.0f); ImGui::InputFloat(STR_C("Density (objects/ha)"), &m_fill_density, 10.0f, 100.0f, "%.1f"); m_fill_density = std::max(0.0f, m_fill_density); - ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat(STR_C("Min. spacing (m)"), &m_fill_min_spacing, 0.5f, 2.0f, "%.1f"); - m_fill_min_spacing = std::max(0.0f, m_fill_min_spacing); - ImGui::SetNextItemWidth(200.0f); - ImGui::DragFloatRange2("Scale", &m_fill_scale_min, &m_fill_scale_max, 0.01f, 0.1f, 5.0f, "min %.2f", "max %.2f"); - ImGui::Checkbox(STR_C("Random rotation"), &m_fill_random_rotation); - if (!m_fill_random_rotation) - ui()->render_rotation_controls(); - ImGui::Checkbox(STR_C("Large models count as ground (terrain tiles)"), &m_fill_models_as_ground); + ImGui::PushID("areafill"); + if (ImGui::TreeNode(STR_C("More settings"))) + { + ImGui::SetNextItemWidth(120.0f); + ImGui::InputFloat(STR_C("Min. spacing (m)"), &m_fill_min_spacing, 0.5f, 2.0f, "%.1f"); + m_fill_min_spacing = std::max(0.0f, m_fill_min_spacing); + ImGui::SetNextItemWidth(200.0f); + ImGui::DragFloatRange2(STR_C("Scale"), &m_fill_scale_min, &m_fill_scale_max, 0.01f, 0.1f, 5.0f, "min %.2f", "max %.2f"); + ImGui::Checkbox(STR_C("Random rotation"), &m_fill_random_rotation); + if (!m_fill_random_rotation) + ui()->render_rotation_controls(); + ImGui::Checkbox(STR_C("Large models count as ground (terrain tiles)"), &m_fill_models_as_ground); + ImGui::TreePop(); + } + ImGui::PopID(); ImGui::Text(STR_C("Estimated objects: %.0f"), std::round(area / 10000.0 * m_fill_density)); @@ -4357,33 +4351,37 @@ void editor_mode::render_array() ImGui::SetTooltip("%s", STR_C("As many copies are made as fit in this length, measured along the row")); } - ImGui::Checkbox(STR_C("Relative offset"), &settings.relative); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Distance between the copies as a part of the size of the model, along its axes: x, y (up), z.\n" - "1 along an axis puts the copies end to end")); - if (settings.relative) - { - ImGui::SetNextItemWidth(200.0f); - ImGui::DragFloat3(STR_C("x size"), &settings.relative_offset.x, 0.01f, 0.0f, 0.0f, "%.3f"); - } - ImGui::Checkbox(STR_C("Constant offset"), &settings.constant); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Distance between the copies in metres, along the axes of the model: x, y (up), z.\n" - "With both offsets on the copies are set apart by their sum")); - if (settings.constant) - { - ImGui::SetNextItemWidth(200.0f); - ImGui::DragFloat3("m", &settings.constant_offset.x, 0.05f, 0.0f, 0.0f, "%.3f"); - } ImGui::SetNextItemWidth(120.0f); ImGui::DragFloat(STR_C("Turn per copy (deg)"), &settings.turn, 0.1f, -180.0f, 180.0f, "%.2f"); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Each copy is turned by this much around the vertical axis against the one before it,\n" "and the row bends with them into an arc")); - ImGui::Checkbox(STR_C("Follow the ground"), &settings.ground); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("The copies are put as high over the ground as the model is, instead of level with it.\n" - "Not for the models large enough to count as ground themselves")); + if (ImGui::TreeNode(STR_C("More settings"))) + { + ImGui::Checkbox(STR_C("Relative offset"), &settings.relative); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Distance between the copies as a part of the size of the model, along its axes: x, y (up), z.\n" + "1 along an axis puts the copies end to end")); + if (settings.relative) + { + ImGui::SetNextItemWidth(200.0f); + ImGui::DragFloat3(STR_C("x size"), &settings.relative_offset.x, 0.01f, 0.0f, 0.0f, "%.3f"); + } + ImGui::Checkbox(STR_C("Constant offset"), &settings.constant); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Distance between the copies in metres, along the axes of the model: x, y (up), z.\n" + "With both offsets on the copies are set apart by their sum")); + if (settings.constant) + { + ImGui::SetNextItemWidth(200.0f); + ImGui::DragFloat3("m", &settings.constant_offset.x, 0.05f, 0.0f, 0.0f, "%.3f"); + } + ImGui::Checkbox(STR_C("Follow the ground"), &settings.ground); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The copies are put as high over the ground as the model is, instead of level with it.\n" + "Not for the models large enough to count as ground themselves")); + ImGui::TreePop(); + } if (selected != nullptr) { @@ -4554,7 +4552,7 @@ void editor_mode::render_edit_menu() if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Puts the copied models at the cursor, around it as they were around the selected one; Ctrl+RMB in the view pastes them where it was clicked")); ImGui::Separator(); - if (ImGui::MenuItem(STR_C("Group"), "Ctrl+G", false, models && selected_nodes().size() > 1)) + if (ImGui::MenuItem(STR_C("Group together"), "Ctrl+G", false, models && selected_nodes().size() > 1)) group_selection(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("The selected models move together from now on; a click on one selects them all. Shift+LMB adds a model to the selection")); @@ -4563,7 +4561,7 @@ void editor_mode::render_edit_menu() ungroup_selection(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("The models of the group move each by itself again")); - if (ImGui::MenuItem(STR_C("Drop to the ground"), STR_C("End"), false, selected)) + if (ImGui::MenuItem(STR_C("Drop to the ground"), "End", false, selected)) drop_selection_to_ground(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Puts each selected model, also each model of their groups, down onto the terrain or the object under it")); @@ -4577,7 +4575,7 @@ void editor_mode::render_edit_menu() show_track_tab(m_track_window_open ? m_track_tab : track_tab::path); m_track_search.focus = true; } - if (ImGui::MenuItem(STR_C("Bend along the track"), "B", false, ui()->mode() != nodebank_panel::TRACK)) + if (ImGui::MenuItem(STR_C("Bend the selected along the track"), "B", false, ui()->mode() != nodebank_panel::TRACK)) bend_shortcut(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Bends the selected model, e.g. a platform edge or a fence, along the nearest track; the settings are in the inspector")); @@ -4593,7 +4591,7 @@ void editor_mode::render_help_menu() ImGui::SetTooltip("%s", STR_C("The keys of the editor, for each field of work")); ImGui::MenuItem(STR_C("Tutorial"), nullptr, &m_first_steps_open); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Seven short lessons with clips: the camera, laying track, curves, joining, switches, fixing, saving")); + ImGui::SetTooltip("%s", STR_C("Twelve short lessons with clips and drawings: the camera, laying track, curves, correcting a curve, joining, switches, signals and posts, fixing, the grade line, terrain, older sceneries, saving and driving")); ImGui::EndMenu(); } @@ -4605,65 +4603,6 @@ void editor_mode::first_step_done(first_step const Step) namespace { -ImU32 const kTutorialLit{IM_COL32(90, 175, 255, 255)}; - -float tutorial_mouse(ImDrawList &List, ImVec2 const At, float const Height, std::string const &Part) -{ - auto const width{std::round(Height * 0.68f)}; - auto const line{ImGui::GetColorU32(ImGuiCol_Text)}; - auto const thickness{std::max(1.0f, Height * 0.07f)}; - auto const rounding{width * 0.5f}; - auto const split{At.y + std::round(Height * 0.45f)}; - auto const middle{At.x + width * 0.5f}; - ImVec2 const high{At.x + width, At.y + Height}; - if (Part == "LMB") - List.AddRectFilled(At, ImVec2(middle, split), kTutorialLit, rounding, ImDrawFlags_RoundCornersTopLeft); - if (Part == "RMB") - List.AddRectFilled(ImVec2(middle, At.y), ImVec2(high.x, split), kTutorialLit, rounding, ImDrawFlags_RoundCornersTopRight); - List.AddRect(At, high, line, rounding, 0, thickness); - List.AddLine(ImVec2(At.x, split), ImVec2(high.x, split), line, thickness); - List.AddLine(ImVec2(middle, At.y), ImVec2(middle, split), line, thickness); - auto const wheel{std::max(3.0f, std::round(width * 0.24f))}; - ImVec2 const wheel_low{middle - wheel * 0.5f, At.y + Height * 0.1f}; - ImVec2 const wheel_high{middle + wheel * 0.5f, At.y + Height * 0.36f}; - List.AddRectFilled(wheel_low, wheel_high, (Part == "MMB" || Part == "wheel") ? kTutorialLit : ImGui::GetColorU32(ImGuiCol_WindowBg), wheel * 0.5f); - List.AddRect(wheel_low, wheel_high, line, wheel * 0.5f, 0, thickness * 0.8f); - auto right{high.x}; - auto const arrow{Height * 0.16f}; - if (Part == "wheel") - { - auto const x{right + arrow * 1.6f}; - List.AddTriangleFilled(ImVec2(x - arrow, At.y + arrow * 1.8f), ImVec2(x + arrow, At.y + arrow * 1.8f), ImVec2(x, At.y + arrow * 0.3f), kTutorialLit); - List.AddTriangleFilled(ImVec2(x - arrow, high.y - arrow * 1.8f), ImVec2(x + arrow, high.y - arrow * 1.8f), ImVec2(x, high.y - arrow * 0.3f), kTutorialLit); - right = x + arrow; - } - if (Part == "move") - { - auto const reach{Height * 0.42f}; - ImVec2 const centre{right + arrow * 0.8f + reach, At.y + Height * 0.5f}; - List.AddLine(ImVec2(centre.x - reach + arrow, centre.y), ImVec2(centre.x + reach - arrow, centre.y), kTutorialLit, thickness); - List.AddLine(ImVec2(centre.x, centre.y - reach + arrow), ImVec2(centre.x, centre.y + reach - arrow), kTutorialLit, thickness); - List.AddTriangleFilled(ImVec2(centre.x - reach, centre.y), ImVec2(centre.x - reach + arrow * 1.4f, centre.y - arrow), ImVec2(centre.x - reach + arrow * 1.4f, centre.y + arrow), kTutorialLit); - List.AddTriangleFilled(ImVec2(centre.x + reach, centre.y), ImVec2(centre.x + reach - arrow * 1.4f, centre.y + arrow), ImVec2(centre.x + reach - arrow * 1.4f, centre.y - arrow), kTutorialLit); - List.AddTriangleFilled(ImVec2(centre.x, centre.y - reach), ImVec2(centre.x + arrow, centre.y - reach + arrow * 1.4f), ImVec2(centre.x - arrow, centre.y - reach + arrow * 1.4f), kTutorialLit); - List.AddTriangleFilled(ImVec2(centre.x, centre.y + reach), ImVec2(centre.x - arrow, centre.y + reach - arrow * 1.4f), ImVec2(centre.x + arrow, centre.y + reach - arrow * 1.4f), kTutorialLit); - right = centre.x + reach; - } - return right - At.x; -} - -float tutorial_key(ImDrawList &List, ImVec2 const At, float const Height, std::string const &Text) -{ - auto const size{ImGui::CalcTextSize(Text.c_str())}; - auto const width{std::max(std::round(size.x + Height * 0.6f), Height)}; - auto const rounding{Height * 0.2f}; - ImVec2 const high{At.x + width, At.y + Height}; - List.AddRectFilled(At, high, ImGui::GetColorU32(ImGuiCol_FrameBg), rounding); - List.AddRect(At, high, ImGui::GetColorU32(ImGuiCol_TextDisabled), rounding, 0, std::max(1.0f, Height * 0.06f)); - List.AddText(ImVec2(std::round(At.x + (width - size.x) * 0.5f), std::round(At.y + (Height - size.y) * 0.5f)), ImGui::GetColorU32(ImGuiCol_Text), Text.c_str()); - return width; -} - void tutorial_input(char const *Keys, char const *Text, float const Column) { auto &list{*ImGui::GetWindowDrawList()}; @@ -4682,9 +4621,9 @@ void tutorial_input(char const *Keys, char const *Text, float const Column) x += size.x + gap; } else if (word == "LMB" || word == "RMB" || word == "MMB" || word == "wheel" || word == "move") - x += tutorial_mouse(list, place, height, word) + gap; + x += gesture_mouse(list, place, height, word) + gap; else - x += tutorial_key(list, place, height, word) + gap; + x += gesture_key(list, place, height, word) + gap; } ImGui::Dummy(ImVec2(std::max(x, Column), height)); ImGui::SameLine(); @@ -4692,6 +4631,373 @@ void tutorial_input(char const *Keys, char const *Text, float const Column) ImGui::TextWrapped("%s", STR_C(Text)); } +// a window that grew past the bottom or the right of the screen is moved back onto it +void keep_on_screen(char const *Name) +{ + auto const *window{ImGui::FindWindowByName(Name)}; + if (window == nullptr) + return; + auto const display{ImGui::GetIO().DisplaySize}; + ImVec2 position{window->Pos}; + position.x = std::max(0.0f, std::min(position.x, display.x - window->Size.x)); + position.y = std::max(20.0f, std::min(position.y, display.y - window->Size.y)); + if (position.x != window->Pos.x || position.y != window->Pos.y) + ImGui::SetNextWindowPos(position); +} + +// the strip of the profile: the ground, the track as it lies, the grade line over them with a point of it dragged up and down, +// the grades on both sides of it following +void tutorial_profile_sketch(ImDrawList &List, ImVec2 const Low, ImVec2 const High, double const Time) +{ + auto const width{High.x - Low.x}; + auto const height{High.y - Low.y}; + auto const x_of = [&](float const Share) { return Low.x + width * (0.06f + 0.88f * Share); }; + auto const y_of = [&](float const Level) { return Low.y + height * (0.82f - 0.6f * Level); }; + std::vector ground; + for (int i = 0; i <= 60; ++i) + { + auto const share{i / 60.0f}; + ground.emplace_back(x_of(share), y_of(0.25f + 0.35f * std::sin(share * 5.2f + 0.6f) * 0.5f + 0.18f * share)); + } + auto const flags{List.Flags}; + List.Flags &= ~ImDrawListFlags_AntiAliasedFill; + for (std::size_t i = 0; i + 1 < ground.size(); ++i) + List.AddQuadFilled(ground[i], ground[i + 1], ImVec2(ground[i + 1].x, High.y), ImVec2(ground[i].x, High.y), IM_COL32(110, 85, 55, 70)); + List.Flags = flags; + List.AddPolyline(ground.data(), static_cast(ground.size()), IM_COL32(170, 130, 80, 220), 0, 1.5f); + auto const lift{static_cast(0.5 + 0.5 * std::sin(Time * 1.6))}; + ImVec2 const points[]{ImVec2(x_of(0.0f), y_of(0.32f)), ImVec2(x_of(0.38f), y_of(0.42f)), ImVec2(x_of(0.66f), y_of(0.35f + 0.4f * lift)), ImVec2(x_of(1.0f), y_of(0.55f))}; + List.AddPolyline(points, 4, IM_COL32(170, 170, 170, 160), 0, 1.0f); + List.AddPolyline(points, 4, IM_COL32(90, 220, 110, 255), 0, 2.5f); + for (int i = 0; i < 4; ++i) + List.AddCircleFilled(points[i], i == 2 ? 6.0f : 4.5f, i == 2 ? kGestureLit : IM_COL32(250, 210, 80, 255)); + List.AddCircle(points[2], 9.0f, kGestureLit, 0, 1.5f); + for (int i = 0; i < 3; ++i) + { + auto const grade{-(points[i + 1].y - points[i].y) / (points[i + 1].x - points[i].x) * 40.0f}; + char text[32]; + std::snprintf(text, sizeof(text), "%+.1f", std::abs(grade) < 0.05f ? 0.0f : grade); + ImVec2 const middle{(points[i].x + points[i + 1].x) * 0.5f, (points[i].y + points[i + 1].y) * 0.5f - ImGui::GetTextLineHeight() - 2.0f}; + List.AddText(ImVec2(middle.x - ImGui::CalcTextSize(text).x * 0.5f, middle.y), i == 0 ? IM_COL32(200, 200, 200, 255) : IM_COL32(255, 255, 255, 255), text); + } + gesture_mouse(List, ImVec2(points[2].x + 12.0f, points[2].y + 10.0f), height * 0.18f, "LMB"); +} + +// the route of the grade line picked on the map: a click lights up the whole line through the track, Shift and a box only the +// tracks between the outermost ones in it; beside it the strip with a point of the line dragged +void tutorial_route_sketch(ImDrawList &List, ImVec2 const Low, ImVec2 const High, double const Time) +{ + auto const split{std::round(Low.x + (High.x - Low.x) * 0.48f)}; + tutorial_profile_sketch(List, ImVec2(split + 6.0f, Low.y), High, Time); + List.AddLine(ImVec2(split + 2.0f, Low.y + 6.0f), ImVec2(split + 2.0f, High.y - 6.0f), ImGui::GetColorU32(ImGuiCol_Border), 1.0f); + auto const width{split - Low.x}; + auto const height{High.y - Low.y}; + auto const at = [&](float const X, float const Y) { return ImVec2(Low.x + width * X, Low.y + height * Y); }; + auto const mix = [](ImVec2 const A, ImVec2 const B, float const T) { return ImVec2(A.x + (B.x - A.x) * T, A.y + (B.y - A.y) * T); }; + auto const ease = [](float const T) { auto const t{std::clamp(T, 0.0f, 1.0f)}; return t * t * (3.0f - 2.0f * t); }; + // the joints of the tracks of the line, and a siding going off it at a switch + ImVec2 const joints[]{at(0.04f, 0.72f), at(0.18f, 0.64f), at(0.32f, 0.54f), at(0.46f, 0.48f), at(0.60f, 0.46f), at(0.74f, 0.42f), at(0.88f, 0.32f), at(0.98f, 0.24f)}; + int constexpr tracks{7}; + List.AddBezierQuadratic(joints[3], at(0.58f, 0.52f), at(0.80f, 0.66f), IM_COL32(130, 130, 130, 200), 2.0f); + auto const cycle{static_cast(std::fmod(Time, 8.0))}; + bool const boxing{cycle >= 4.0f}; + int first{0}; + int last{tracks - 1}; + bool lit{false}; + ImVec2 cursor; + if (false == boxing) + { + auto const target{mix(joints[4], joints[5], 0.5f)}; + cursor = mix(at(0.75f, 0.92f), target, ease(cycle / 1.0f)); + lit = cycle > 1.3f; + } + else + { + auto const corner{at(0.29f, 0.28f)}; + auto const drag{ease((cycle - 4.6f) / 1.5f)}; + auto const other{mix(corner, at(0.72f, 0.70f), drag)}; + cursor = cycle < 4.6f ? mix(at(0.75f, 0.92f), corner, ease((cycle - 4.0f) / 0.6f)) : other; + if (cycle >= 4.6f && cycle < 6.6f) + { + List.AddRectFilled(corner, other, IM_COL32(90, 175, 255, 40)); + List.AddRect(corner, other, kGestureLit, 0.0f, 0, 1.5f); + } + lit = cycle >= 6.2f; + first = 2; + last = 4; + gesture_key(List, ImVec2(Low.x + 6.0f, Low.y + 6.0f), std::round(ImGui::GetTextLineHeight() * 1.3f), "Shift"); + } + for (int i = 0; i < tracks; ++i) + { + auto const on{lit && i >= first && i <= last}; + List.AddLine(joints[i], joints[i + 1], on ? IM_COL32(90, 220, 110, 255) : IM_COL32(170, 170, 170, 220), on ? 4.0f : 2.0f); + } + for (auto const &joint : joints) + List.AddCircleFilled(joint, 2.5f, IM_COL32(220, 220, 220, 255)); + gesture_mouse(List, ImVec2(cursor.x + 2.0f, cursor.y + 2.0f), std::round(height * 0.16f), "LMB"); + auto const caption{boxing ? STR_C("Shift + a box: between the outermost") : STR_C("LMB on a track: its whole line")}; + List.AddText(ImVec2(Low.x + 6.0f, High.y - ImGui::GetTextLineHeight() - 4.0f), IM_COL32(230, 230, 230, 230), caption); +} + +// a signal put by a track: pressed beside it and dragged along, the arrow tells the way of the trains which see it; then the +// hectometre posts of the line stand up one after the other +void tutorial_signal_sketch(ImDrawList &List, ImVec2 const Low, ImVec2 const High, double const Time) +{ + auto const width{High.x - Low.x}; + auto const height{High.y - Low.y}; + auto const at = [&](float const X, float const Y) { return ImVec2(Low.x + width * X, Low.y + height * Y); }; + auto const ease = [](float const T) { auto const t{std::clamp(T, 0.0f, 1.0f)}; return t * t * (3.0f - 2.0f * t); }; + auto const cycle{static_cast(std::fmod(Time, 8.0))}; + // the track seen from above: sleepers and the two rails + auto const middle{at(0.0f, 0.5f).y}; + auto const gauge{height * 0.07f}; + for (float x = 0.02f; x < 1.0f; x += 0.025f) + List.AddLine(ImVec2(at(x, 0.0f).x, middle - gauge * 1.6f), ImVec2(at(x, 0.0f).x, middle + gauge * 1.6f), IM_COL32(110, 90, 70, 255), 3.0f); + List.AddLine(ImVec2(Low.x, middle - gauge), ImVec2(High.x, middle - gauge), IM_COL32(200, 200, 205, 255), 2.0f); + List.AddLine(ImVec2(Low.x, middle + gauge), ImVec2(High.x, middle + gauge), IM_COL32(200, 200, 205, 255), 2.0f); + auto const line{ImGui::GetTextLineHeight()}; + if (cycle < 4.0f) + { + // pressed beside the track, dragged along it + auto const press{at(0.32f, 0.67f)}; + auto const drag{ease((cycle - 1.0f) / 1.4f)}; + ImVec2 const cursor{cycle < 1.0f ? ImVec2(press.x + (1.0f - ease(cycle)) * width * 0.3f, press.y + (1.0f - ease(cycle)) * height * 0.2f) : ImVec2(press.x + width * 0.3f * drag, press.y)}; + if (cycle >= 1.0f) + { + ImVec2 const tip{cursor.x, press.y}; + List.AddLine(press, tip, IM_COL32(255, 210, 60, 255), 3.0f); + if (drag > 0.2f) + List.AddTriangleFilled(ImVec2(tip.x + 8.0f, tip.y), ImVec2(tip.x - 4.0f, tip.y - 7.0f), ImVec2(tip.x - 4.0f, tip.y + 7.0f), IM_COL32(255, 210, 60, 255)); + } + if (cycle >= 2.6f) + { + // the signal by the track: the mast and the head with its lights + auto const foot{at(0.32f, 0.98f)}; + List.AddLine(foot, ImVec2(foot.x, foot.y - height * 0.06f), IM_COL32(200, 200, 200, 255), 3.0f); + ImVec2 const head_low{foot.x - height * 0.04f, foot.y - height * 0.27f}; + ImVec2 const head_high{foot.x + height * 0.04f, foot.y - height * 0.06f}; + List.AddRectFilled(head_low, head_high, IM_COL32(25, 25, 25, 255), height * 0.03f); + List.AddRect(head_low, head_high, IM_COL32(150, 150, 150, 255), height * 0.03f); + for (int i = 0; i < 3; ++i) + { + ImU32 const lights[]{IM_COL32(230, 60, 50, 255), IM_COL32(70, 210, 90, 255), IM_COL32(240, 240, 240, 255)}; + List.AddCircleFilled(ImVec2(foot.x, head_low.y + height * (0.04f + 0.065f * i)), height * 0.025f, lights[i]); + } + List.AddText(ImVec2(foot.x - height * 0.06f - ImGui::CalcTextSize("S1").x, foot.y - line), IM_COL32(255, 255, 255, 230), "S1"); + } + gesture_mouse(List, ImVec2(cursor.x + 2.0f, cursor.y + 2.0f), std::round(height * 0.16f), "LMB"); + List.AddText(ImVec2(Low.x + 6.0f, Low.y + 4.0f), IM_COL32(230, 230, 230, 230), STR_C("H: by the track, drag along: the way the trains run")); + return; + } + // the hectometre posts of the line, one after the other + auto const shown{static_cast((cycle - 4.3f) / 0.35f) + 1}; + for (int i = 0; i < std::min(shown, 9); ++i) + { + auto const x{at(0.07f + 0.105f * i, 0.0f).x}; + auto const foot{middle - gauge * 3.2f}; + List.AddRectFilled(ImVec2(x - 3.0f, foot - height * 0.14f), ImVec2(x + 3.0f, foot), IM_COL32(240, 240, 240, 255), 1.0f); + char text[16]; + std::snprintf(text, sizeof(text), "12.%d", i + 1); + auto const size{ImGui::CalcTextSize(text)}; + List.AddText(ImVec2(std::round(x - size.x * 0.5f), foot - height * 0.14f - size.y - 1.0f), IM_COL32(255, 220, 120, 255), text); + } + List.AddText(ImVec2(Low.x + 6.0f, High.y - line - 4.0f), IM_COL32(230, 230, 230, 230), STR_C("B: the hectometre posts of the whole line at once")); +} + +// a vehicle seen from the side, drawn simply: the body with its windows on two bogies +void tutorial_vehicle(ImDrawList &List, ImVec2 const Low, ImVec2 const High, ImU32 const Body, bool const Locomotive) +{ + auto const width{High.x - Low.x}; + auto const height{High.y - Low.y}; + auto const wheel{height * 0.14f}; + ImVec2 const body_high{High.x, High.y - wheel * 1.6f}; + List.AddRectFilled(Low, body_high, Body, height * 0.12f); + auto const windows{Locomotive ? 2 : 5}; + for (int i = 0; i < windows; ++i) + { + auto const left{Locomotive ? (i == 0 ? Low.x + width * 0.06f : High.x - width * 0.2f) : Low.x + width * (0.08f + 0.18f * i)}; + auto const size{Locomotive ? width * 0.14f : width * 0.12f}; + List.AddRectFilled(ImVec2(left, Low.y + height * 0.15f), ImVec2(left + size, Low.y + height * 0.42f), IM_COL32(30, 40, 50, 255), 2.0f); + } + for (float const share : {0.22f, 0.78f}) + { + ImVec2 const middle{Low.x + width * share, High.y - wheel}; + List.AddCircleFilled(ImVec2(middle.x - wheel * 1.1f, middle.y), wheel, IM_COL32(40, 40, 40, 255)); + List.AddCircleFilled(ImVec2(middle.x + wheel * 1.1f, middle.y), wheel, IM_COL32(40, 40, 40, 255)); + } +} + +// the trainset tool: a double click on the list puts a vehicle into the train, the train grows on the track, Place and drive +// and it runs off +void tutorial_trainset_sketch(ImDrawList &List, ImVec2 const Low, ImVec2 const High, double const Time) +{ + auto const width{High.x - Low.x}; + auto const height{High.y - Low.y}; + auto const at = [&](float const X, float const Y) { return ImVec2(Low.x + width * X, Low.y + height * Y); }; + auto const cycle{static_cast(std::fmod(Time, 9.0))}; + ImU32 const colours[]{IM_COL32(200, 60, 50, 255), IM_COL32(60, 120, 70, 255), IM_COL32(70, 100, 160, 255)}; + auto const line{ImGui::GetTextLineHeight()}; + // the list: the side view of each vehicle and its name + for (int i = 0; i < 3; ++i) + { + auto const top{0.08f + 0.28f * i}; + auto const clicked{cycle > 0.8f + 1.4f * i && cycle < 1.2f + 1.4f * i}; + if (clicked) + List.AddRectFilled(at(0.02f, top - 0.02f), at(0.40f, top + 0.24f), IM_COL32(90, 175, 255, 70), 3.0f); + tutorial_vehicle(List, at(0.03f, top), at(0.16f, top + 0.2f), colours[i], i == 0); + List.AddRectFilled(at(0.19f, top + 0.03f), at(0.36f, top + 0.03f + line / height * 0.6f), IM_COL32(220, 220, 220, 200), 2.0f); + List.AddRectFilled(at(0.19f, top + 0.12f), at(0.31f, top + 0.12f + line / height * 0.5f), IM_COL32(150, 150, 150, 160), 2.0f); + } + // the cursor goes down the list double clicking, then to the button + auto const row{std::min(2, static_cast(std::max(0.0f, cycle - 0.4f) / 1.4f))}; + ImVec2 cursor{at(0.30f, 0.16f + 0.28f * row)}; + auto const added{std::clamp(static_cast((cycle - 0.8f) / 1.4f) + 1, 0, 3)}; + auto const button_low{at(0.55f, 0.08f)}; + auto const button_high{at(0.95f, 0.30f)}; + if (cycle > 4.6f) + cursor = ImVec2((button_low.x + button_high.x) * 0.5f, (button_low.y + button_high.y) * 0.5f); + bool const pressed{cycle > 5.2f && cycle < 5.6f}; + List.AddRectFilled(button_low, button_high, pressed ? IM_COL32(60, 170, 75, 255) : IM_COL32(50, 140, 64, 255), 4.0f); + auto const label{STR_C("Place and drive")}; + auto const size{ImGui::CalcTextSize(label)}; + List.AddText(ImVec2(std::round((button_low.x + button_high.x - size.x) * 0.5f), std::round((button_low.y + button_high.y - size.y) * 0.5f)), IM_COL32(255, 255, 255, 255), label); + // the track and the train on it, the head on the left; after the button it runs off + auto const rail{at(0.0f, 0.86f).y}; + List.AddLine(ImVec2(at(0.44f, 0.0f).x, rail), ImVec2(High.x, rail), IM_COL32(170, 170, 170, 255), 2.0f); + auto const run{cycle > 5.6f ? (cycle - 5.6f) * (cycle - 5.6f) * width * 0.06f : 0.0f}; + auto const car{width * 0.15f}; + List.PushClipRect(ImVec2(at(0.44f, 0.0f).x, Low.y), High, true); + for (int i = 0; i < added; ++i) + { + auto const left{at(0.46f, 0.0f).x + (car + 3.0f) * i - run}; + tutorial_vehicle(List, ImVec2(left, rail - height * 0.24f), ImVec2(left + car, rail), colours[i], i == 0); + } + List.PopClipRect(); + gesture_mouse(List, ImVec2(cursor.x + 2.0f, cursor.y + 2.0f), std::round(height * 0.16f), "LMB"); + if (cycle < 4.6f) + List.AddText(ImVec2(cursor.x + height * 0.16f, cursor.y), IM_COL32(255, 220, 120, 255), "x2"); +} + +// the plan of a curve over the band of its curvature: the plateau of the arc goes up and down under the mouse, the curve tightens +// and opens with it over the plan kept faint as it was +void tutorial_curve_sketch(ImDrawList &List, ImVec2 const Low, ImVec2 const High, double const Time) +{ + auto const width{High.x - Low.x}; + auto const height{High.y - Low.y}; + auto const lift{static_cast(0.5 + 0.5 * std::sin(Time * 1.3))}; + auto const curvature = [](float const Share, float const Peak) { + if (Share < 0.15f || Share > 0.85f) + return 0.0f; + if (Share < 0.35f) + return Peak * (Share - 0.15f) / 0.2f; + if (Share > 0.65f) + return Peak * (0.85f - Share) / 0.2f; + return Peak; + }; + auto const colour = [](float const Share) { + if (Share < 0.15f || Share > 0.85f) + return IM_COL32(255, 255, 255, 230); + if (Share < 0.35f || Share > 0.65f) + return IM_COL32(255, 140, 30, 255); + return IM_COL32(60, 230, 90, 255); + }; + int constexpr steps{90}; + auto const plan_height{height * 0.5f}; + float scale{1.0f}; + ImVec2 start{0.0f, 0.0f}; + auto const plan = [&](float const Peak) { + std::vector points; + float heading{0.0f}, x{0.0f}, y{0.0f}; + for (int i = 0; i <= steps; ++i) + { + points.emplace_back(start.x + x * scale, start.y + y * scale); + auto const share{(i + 0.5f) / steps}; + heading += curvature(share, Peak) / steps; + x += std::cos(heading) / steps; + y -= std::sin(heading) / steps; + } + return points; + }; + // the scale fits the plan of the sharpest curve, the least sharp one is the widest + auto const sharpest{plan(2.8f)}; + auto const widest{plan(1.6f)}; + scale = std::min(width * 0.85f / std::max(1e-3f, widest.back().x), plan_height / std::max(1e-3f, -sharpest.back().y)); + start = ImVec2(Low.x + (width - widest.back().x * scale) * 0.5f, Low.y + height * 0.06f + plan_height); + auto const before{plan(1.6f)}; + List.AddPolyline(before.data(), static_cast(before.size()), IM_COL32(170, 170, 170, 90), 0, 1.5f); + auto const peak{1.6f + 1.2f * lift}; + auto const now{plan(peak)}; + for (int i = 0; i < steps; ++i) + List.AddLine(now[i], now[i + 1], colour((i + 0.5f) / steps), 3.0f); + // the band under the plan + auto const band_top{Low.y + height * 0.66f}; + auto const band_bottom{High.y - height * 0.06f}; + auto const left{Low.x + width * 0.06f}; + auto const band_width{width * 0.88f}; + List.AddRectFilled(ImVec2(left - 6.0f, band_top - 6.0f), ImVec2(left + band_width + 6.0f, band_bottom + 4.0f), ImGui::GetColorU32(ImGuiCol_FrameBg), 4.0f); + List.AddLine(ImVec2(left, band_bottom), ImVec2(left + band_width, band_bottom), ImGui::GetColorU32(ImGuiCol_TextDisabled)); + auto const k_scale{(band_bottom - band_top) * 0.9f / 2.8f}; + for (int i = 0; i < steps; ++i) + { + auto const a{static_cast(i) / steps}; + auto const b{static_cast(i + 1) / steps}; + ImVec2 const p{left + a * band_width, band_bottom - curvature(a, peak) * k_scale}; + ImVec2 const q{left + b * band_width, band_bottom - curvature(b, peak) * k_scale}; + auto const tint{colour((a + b) * 0.5f)}; + auto const flags{List.Flags}; + List.Flags &= ~ImDrawListFlags_AntiAliasedFill; + List.AddQuadFilled(ImVec2(p.x, band_bottom), p, q, ImVec2(q.x, band_bottom), (tint & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 50)); + List.Flags = flags; + List.AddLine(p, q, tint, 2.0f); + } + char text[32]; + std::snprintf(text, sizeof(text), "R %.0f", std::round(900.0f / peak / 5.0f) * 5.0f); + ImVec2 const plateau{left + band_width * 0.5f, band_bottom - peak * k_scale}; + List.AddText(ImVec2(plateau.x - ImGui::CalcTextSize(text).x * 0.5f, plateau.y - ImGui::GetTextLineHeight() - 2.0f), IM_COL32(60, 230, 90, 255), text); + gesture_mouse(List, ImVec2(plateau.x + band_width * 0.08f, plateau.y - height * 0.06f), height * 0.16f, "LMB"); +} + +// the terrain model under the tracks: the track laid level sinks onto the ground along, the formation and the slopes under it +void tutorial_terrain_sketch(ImDrawList &List, ImVec2 const Low, ImVec2 const High, double const Time) +{ + auto const width{High.x - Low.x}; + auto const height{High.y - Low.y}; + auto const phase{static_cast(std::fmod(Time * 0.3, 1.4))}; + auto const settle{std::clamp(phase / 0.8f, 0.0f, 1.0f)}; + auto const eased{settle * settle * (3.0f - 2.0f * settle)}; + auto const ground_at = [&](float const Share) { return 0.45f + 0.22f * std::sin(Share * 4.0f) + 0.08f * std::sin(Share * 13.0f); }; + auto const smooth_at = [&](float const Share) { return 0.45f + 0.18f * std::sin(Share * 4.0f); }; + auto const x_of = [&](float const Share) { return Low.x + width * Share; }; + auto const y_of = [&](float const Level) { return Low.y + height * (0.95f - 0.75f * Level); }; + int constexpr steps{80}; + std::vector ground, track; + for (int i = 0; i <= steps; ++i) + { + auto const share{static_cast(i) / steps}; + ground.emplace_back(x_of(share), y_of(ground_at(share))); + track.emplace_back(x_of(share), y_of(0.55f + (smooth_at(share) + 0.04f - 0.55f) * eased)); + } + auto const flags{List.Flags}; + List.Flags &= ~ImDrawListFlags_AntiAliasedFill; + for (int i = 0; i < steps; ++i) + { + ImVec2 const quad[]{ground[i], ground[i + 1], ImVec2(ground[i + 1].x, High.y), ImVec2(ground[i].x, High.y)}; + List.AddConvexPolyFilled(quad, 4, IM_COL32(95, 120, 70, 120)); + } + if (eased > 0.99f) + for (int i = 0; i < steps; ++i) + { + auto const fill{track[i].y < ground[i].y}; + ImVec2 const quad[]{track[i], track[i + 1], ground[i + 1], ground[i]}; + List.AddQuadFilled(quad[0], quad[1], quad[2], quad[3], fill ? IM_COL32(190, 150, 90, 150) : IM_COL32(60, 45, 30, 150)); + } + List.Flags = flags; + List.AddPolyline(ground.data(), static_cast(ground.size()), IM_COL32(150, 190, 110, 255), 0, 1.5f); + List.AddPolyline(track.data(), static_cast(track.size()), IM_COL32(230, 230, 230, 255), 0, 3.0f); + auto const label{eased > 0.99f ? STR_C("embankment and cutting") : STR_C("the terrain model")}; + List.AddText(ImVec2(Low.x + 6.0f, Low.y + 4.0f), IM_COL32(220, 220, 220, 200), label); +} + } // namespace void editor_mode::render_first_steps() @@ -4708,7 +5014,11 @@ void editor_mode::render_first_steps() first_step_done(first_step::lay); if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::path && selected_track() != nullptr) first_step_done(first_step::select); - enum class trial { none, top, lay, turnout, select }; + if (current_work_area() == work_area::terrain) + first_step_done(first_step::terrain); + if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::route && false == m_route.chain.tracks.empty()) + first_step_done(first_step::plan); + enum class trial { none, top, lay, turnout, select, profile, terrain, plan, trainset, signals }; struct input { char const *keys; @@ -4726,6 +5036,7 @@ void editor_mode::render_first_steps() std::vector inputs; std::vector goals; trial action; + void (*sketch)(ImDrawList &, ImVec2, ImVec2, double){nullptr}; }; static std::vector const lessons{ {"Looking around", @@ -4759,6 +5070,16 @@ void editor_mode::render_first_steps() {"wheel", "Alone it zooms, as everywhere"}}, {{first_step::wheel, "Change the radius with Ctrl+wheel"}, {first_step::shape, "Change the shape with Shift+wheel"}}, trial::lay}, + {"Correcting a curve", + nullptr, + {{"G", "The plan tool: a click on a curve shows its design, the vertex and the radius to drag in the view"}, + {"LMB + move", "In the panel, the band of the curvature: drag the plateau of the arc up or down for its radius"}, + {"LMB + move", "Drag a ramp sideways: the transition curve gets longer or shorter"}, + {"wheel", "Over the band: the radius by 5%, a transition by 5 m"}, + {"LMB", "Apply to the scenery: the tracks are laid by the design"}}, + {{first_step::plan, "Open the plan of a curve"}, {first_step::plan_band, "Change the curve in the band"}, {first_step::plan_apply, "Apply it to the scenery"}}, + trial::plan, + tutorial_curve_sketch}, {"Joining tracks", "join", {{"LMB + move", "Press on a free end (a magenta ring) and drag: a new track goes out of it"}, @@ -4776,26 +5097,70 @@ void editor_mode::render_first_steps() {"Esc", "Back to selecting"}}, {{first_step::turnout, "Put a switch into a track"}}, trial::turnout}, + {"Signals and posts", + nullptr, + {{"H", "The signals tool: the type of the signal in the panel, from the templates of the scenery"}, + {"LMB", "A click by a track places the signal on that side"}, + {"LMB + move", "Press by the track and drag along it: the trains running that way see the signal"}, + {"LMB + move", "Drag a standing signal: it moves along its track"}, + {"B", "Objects along the track: the hectometre posts and the W17 markers of a whole line at once, and trainsets"}}, + {{first_step::signal, "Place a signal"}, {first_step::posts, "Place the hectometre posts of a line"}}, + trial::signals, + tutorial_signal_sketch}, {"Selecting and fixing", "edit", {{"Esc", "The select tool"}, {"LMB", "Click a track: it is selected, with its points"}, {"LMB + move", "Drag a point: the end or the curve follows"}, {"Q W E", "Move, turn or scale with the gizmo"}, - {"Gizmo", "Tool options, Gizmo, Enabled off: the track won't move by chance"}, + {"Gizmo", "Tool options, Gizmo, Off: the track won't move by chance"}, {"RMB", "A click on a track: what can be done with it"}, {"Del", "Deletes the selected"}, {"Ctrl + Z", "Takes the last change back; Ctrl+Y brings it back again"}}, {{first_step::select, "Select a track"}, {first_step::undo, "Take a change back"}}, trial::select}, + {"The grade line", + nullptr, + {{"P", "The profile tool: a click on a track opens the grade line of its whole line in the strip below"}, + {"Shift + LMB", "A click adds a track, a box from the empty ground takes the tracks in it: the route runs between the outermost of them"}, + {"LMB + move", "Drag a point of the grade line: the grades on both sides of it follow"}, + {"LMB LMB", "A double click on the line adds a point"}, + {"Del", "Removes the chosen point"}, + {"Ctrl + wheel", "Over the plot: the heights stretched, the grades seen better"}, + {"RMB + move", "Over the plot: the view moves along the line"}}, + {{first_step::profile, "Open the grade line of a track"}, {first_step::profile_point, "Move or add a point"}, {first_step::profile_apply, "Apply it: the tracks take its heights"}}, + trial::profile, + tutorial_route_sketch}, + {"Terrain from the terrain model", + nullptr, + {{"F5", "The Terrain field of work: the terrain model (NMT) is in its tool options"}, + {"1", "Location: where the scenery lies in PUWG 1992; a scenery begun from nothing asks for it with its first track"}, + {"2", "What gives way: the tracks go onto the terrain, or the terrain is drawn to the tracks"}, + {"3", "Load: the model comes from geoportal.gov.pl, the terrain is made along the tracks with the formation and the slopes"}, + {"Ctrl + Z", "Takes the changes of the tracks back"}}, + {{first_step::terrain, "Open the Terrain field of work"}, {first_step::nmt, "Load terrain from the model"}}, + trial::terrain, + tutorial_terrain_sketch}, + {"Sceneries made before", + nullptr, + {{"LMB", "A track of an include or of a file that isn't saved: the inspector says why it can't be changed"}, + {"T", "Switches laid before keep their shape; Replace the geometry with the template, under More settings of the switch, gives them the one of the catalogue"}, + {"P", "Grade lines kept by an older editor are read as they were kept"}, + {"Ctrl + S", "Only what changed is written: the other lines stay as they were, the comment lines of other tools too"}}, + {}, + trial::none}, {"Saving and driving", nullptr, - {{"Ctrl + S", "Saves the scenery"}, {"LMB", "More, Trainset: pick a locomotive, click the track, Place and drive"}}, + {{"Ctrl + S", "Saves the scenery"}, + {"LMB", "Trainset tool: a click on the track where the train stands"}, + {"LMB LMB", "A double click on a vehicle of the list puts it into the train; a click on a vehicle of the train moves it, turns it or takes it out"}, + {"LMB", "Place and drive: the train goes onto the track and the cab is yours"}}, {{first_step::save, "Save the scenery"}, {first_step::drive, "Drive on the new track"}}, - trial::none}, + trial::trainset, + tutorial_trainset_sketch}, }; auto const done = [&](lesson const &Lesson) { - return std::all_of(Lesson.goals.begin(), Lesson.goals.end(), [&](goal const &Goal) { return m_first_steps_done[static_cast(Goal.step)]; }); + return false == Lesson.goals.empty() && std::all_of(Lesson.goals.begin(), Lesson.goals.end(), [&](goal const &Goal) { return m_first_steps_done[static_cast(Goal.step)]; }); }; m_tutorial_page = std::clamp(m_tutorial_page, 0, static_cast(lessons.size()) - 1); auto const &lesson{lessons[m_tutorial_page]}; @@ -4810,6 +5175,8 @@ void editor_mode::render_first_steps() auto const width{520.0f * scale}; ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.5f), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); ImGui::SetNextWindowBgAlpha(1.0f); + ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, 0.0f), ImVec2(FLT_MAX, ImGui::GetIO().DisplaySize.y - 60.0f)); + keep_on_screen("###tutorial"); if (ImGui::Begin((std::string(STR_C("Tutorial")) + "###tutorial").c_str(), &m_first_steps_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + width); @@ -4834,10 +5201,22 @@ void editor_mode::render_first_steps() ImGui::Dummy(ImVec2(width, height)); } } + if (lesson.sketch != nullptr) + { + auto const at{ImGui::GetCursorScreenPos()}; + ImVec2 const end{at.x + width, at.y + std::round(width * 0.36f)}; + auto *list{ImGui::GetWindowDrawList()}; + list->AddRectFilled(at, end, ImGui::GetColorU32(ImGuiCol_FrameBg), 4.0f); + list->PushClipRect(at, end, true); + lesson.sketch(*list, at, end, ImGui::GetTime()); + list->PopClipRect(); + ImGui::Dummy(ImVec2(width, end.y - at.y)); + } ImGui::SeparatorText(STR_C("Mouse and keys")); for (auto const &entry : lesson.inputs) tutorial_input(entry.keys, entry.text, 150.0f * scale); - ImGui::SeparatorText(STR_C("Try it")); + if (false == lesson.goals.empty()) + ImGui::SeparatorText(STR_C("Try it")); for (auto const &entry : lesson.goals) { bool const reached{m_first_steps_done[static_cast(entry.step)]}; @@ -4856,6 +5235,60 @@ void editor_mode::render_first_steps() if (ImGui::Button(Global.EditorOrtho ? STR_C("3D view (O)") : STR_C("Top view, orthographic (O)"))) toggle_ortho(); break; + case trial::profile: + { + auto const on{ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::profile}; + ImGui::BeginDisabled(on); + if (ImGui::Button(STR_C("The profile tool (P)"))) + { + choose_work_area(work_area::tracks); + show_track_tab(track_tab::profile); + } + ImGui::EndDisabled(); + break; + } + case trial::signals: + { + ImGui::BeginDisabled(ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::signals); + if (ImGui::Button(STR_C("The signals tool (H)"))) + { + choose_work_area(work_area::tracks); + show_track_tab(track_tab::signals); + } + ImGui::EndDisabled(); + break; + } + case trial::trainset: + { + ImGui::BeginDisabled(ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::lineside && m_vehicle.open); + if (ImGui::Button(STR_C("The trainset tool"))) + { + choose_work_area(work_area::tracks); + show_track_tab(track_tab::lineside); + m_vehicle.expand = true; + } + ImGui::EndDisabled(); + break; + } + case trial::plan: + { + ImGui::BeginDisabled(ui()->mode() == nodebank_panel::TRACK && (m_track_tab == track_tab::route || m_track_tab == track_tab::straights)); + if (ImGui::Button(STR_C("The plan tool (G)"))) + { + choose_work_area(work_area::tracks); + show_track_tab(track_tab::straights); + } + ImGui::EndDisabled(); + break; + } + case trial::terrain: + { + ImGui::BeginDisabled(current_work_area() == work_area::terrain); + if (ImGui::Button(STR_C("Terrain (F5)"))) + choose_work_area(work_area::terrain); + ImGui::EndDisabled(); + break; + } case trial::lay: case trial::turnout: case trial::select: @@ -4892,7 +5325,7 @@ void editor_mode::render_first_steps() ImVec2 const centre{at.x + dot * 0.4f, at.y + dot * 0.5f}; auto const colour{done(lessons[index]) ? IM_COL32(100, 230, 100, 255) : ImGui::GetColorU32(ImGuiCol_TextDisabled)}; if (index == m_tutorial_page) - ImGui::GetWindowDrawList()->AddCircle(centre, dot * 0.3f, kTutorialLit, 0, 2.0f); + ImGui::GetWindowDrawList()->AddCircle(centre, dot * 0.3f, kGestureLit, 0, 2.0f); ImGui::GetWindowDrawList()->AddCircleFilled(centre, dot * 0.17f, colour); ImGui::PopID(); ImGui::SameLine(0.0f, 0.0f); @@ -4939,7 +5372,7 @@ void editor_mode::render_shortcuts_window() {"PrtScr", "Screenshot"}, {"Ctrl+Shift+F11", "Export scenery dump"}, {"F11", "Leave the editor"}, - {"F10", "Quit"}}}, + {"F10", "Quit the simulator"}}}, {"Fields of work", {{"F2", "Surroundings"}, {"F3", "Tracks"}, {"F4", "Roads"}, {"F5", "Terrain"}}}, {"Camera", {{"RMB held, mouse", "Look around"}, @@ -4948,7 +5381,10 @@ void editor_mode::render_shortcuts_window() {"F", "Fly to the selected"}, {"O", "Top view, orthographic"}}}, {"Gizmo", - {{"Q / W / E", "Translate / rotate / scale"}, {"R", "Local space"}, {"Ctrl held while dragging", "Snap to the step set in the gizmo settings"}}}, + {{"Q / W / E", "Translate / rotate / scale"}, + {"R", "Local space"}, + {"Ctrl held while dragging", "Snap to the step set in the gizmo settings"}, + {"Tool options, Gizmo, Off", "Hides the gizmo, so nothing moves by chance"}}}, {"Surroundings", {{"1 2 3 4 5", "Select, insert, brush, area fill, copy to bank"}, {"LMB", "Select a model, or place one in the insert and brush modes"}, @@ -4965,7 +5401,7 @@ void editor_mode::render_shortcuts_window() {"L / T / U", "Lay track / switch / turntable"}, {"G (C)", "Plan: a straight, its ends and the middle; a curve, its vertex and radius"}, {"H / B", "Signals / objects along the track"}, - {"P / V / J / I", "Profile / speed / joints / infrastructure"}, + {"P / V / J", "Profile / speed / joints"}, {"Ctrl+F", "Find a track by its name"}, {"Shift+LMB", "Add a track to the selection, drag: a box"}, {"RMB on a track", "What can be done with it"}, @@ -4976,10 +5412,31 @@ void editor_mode::render_shortcuts_window() {"Shift+wheel", "Laying: arc then straight, straight then arc, arc, straight; at a free end: another way to join"}, {"0-9 while laying", "Type the length (or the radius) of the next piece, Enter lays it"}, {"Alt held while laying", "The direction in steps of 15 degrees"}, - {"N", "Parallel track: LMB on a straight, then the side and the spacing"}, + {"N", "Parallel track: LMB on a track, then the side and the spacing"}, {"Shift+LMB on a track in L", "The Parallel tool (N) with it"}, {"Enter / Backspace", "Laying track: lay up to the cursor / take the last piece back"}, {"Del", "Delete the selected tracks"}}}, + {"Plan of a curve", + {{"G, LMB on a curve", "Its design: the vertex and the radius to drag in the view, the curvature in the panel"}, + {"Drag a plateau of the curvature band", "The radius of that arc"}, + {"Drag a ramp sideways", "The length of the transition curve"}, + {"Wheel over the band", "The radius by 5%, a transition by 5 m"}, + {"Apply to the scenery", "Lay the paths by the design; Ctrl+Z takes it back"}}}, + {"Trainset", + {{"B, Trainset, LMB on a track", "Where the train stands"}, + {"LMB LMB on the list", "Adds the vehicle to the train"}, + {"LMB on a vehicle of the train", "Moves it, turns it around or takes it out"}, + {"Place and drive", "Puts the train on the track and gives the cab at once"}}}, + {"Vertical profile", + {{"P, LMB on a track", "The grade line of its whole line, in the strip under the view"}, + {"Shift+LMB box", "The route between the outermost of the tracks, through the switches"}, + {"LMB + move on a point", "Change the grade before and after it"}, + {"LMB LMB on the line", "Add a point"}, + {"Del", "Remove the chosen point"}, + {"Wheel", "Zoom along the line"}, + {"Ctrl+wheel", "Stretch the heights"}, + {"RMB + move", "Move the view along the line"}, + {"Apply in the scenery", "Write the heights into the tracks; Ctrl+Z takes them back"}}}, {"Roads", {{"Esc", "Finish the road being built"}, {"Ctrl held", "Build: straight ahead"}, @@ -4989,22 +5446,54 @@ void editor_mode::render_shortcuts_window() {"Terrain", {{"LMB", "Sculpt: raise; smooth: even out; chunks: add one"}, {"Shift+LMB", "Sculpt: lower; chunks: delete one"}}}, }; ImGui::SetNextWindowSize(ImVec2(620.0f * Global.ui_scale, 560.0f * Global.ui_scale), ImGuiCond_FirstUseEver); + keep_on_screen("###shortcuts"); // it floats over the view, read better on a solid background ImGui::SetNextWindowBgAlpha(0.97f); if (ImGui::Begin((std::string(STR_C("Keyboard shortcuts")) + "###shortcuts").c_str(), &m_shortcuts_open, ImGuiWindowFlags_NoCollapse)) { + static char filter[64]{}; + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.6f); + ImGui::InputTextWithHint("##shortcutfilter", STR_C("Find: a key or what it does"), filter, sizeof(filter)); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Tutorial"))) + m_first_steps_open = true; + auto const lower = [](std::string Text) { + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + return Text; + }; + auto const wanted{lower(filter)}; + auto const matches = [&](shortcut const &Item) { return wanted.empty() || lower(STR_C(Item.keys)).find(wanted) != std::string::npos || lower(STR_C(Item.action)).find(wanted) != std::string::npos; }; + auto const picture{std::round(ImGui::GetTextLineHeight() * 1.15f)}; for (auto const &entry : groups) { + if (std::none_of(entry.shortcuts.begin(), entry.shortcuts.end(), matches)) + continue; ImGui::SeparatorText(STR_C(entry.title)); if (false == ImGui::BeginTable(entry.title, 2, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) continue; - ImGui::TableSetupColumn("##keys", ImGuiTableColumnFlags_WidthFixed, 190.0f * Global.ui_scale); + ImGui::TableSetupColumn("##keys", ImGuiTableColumnFlags_WidthFixed, 250.0f * Global.ui_scale); ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthStretch); for (auto const &item : entry.shortcuts) { + if (false == matches(item)) + continue; ImGui::TableNextColumn(); - ImGui::TextColored(ImGui::GetStyleColorVec4(ImGuiCol_CheckMark), "%s", STR_C(item.keys)); + auto const words{gesture_picture(item.keys)}; + if (false == words.empty()) + { + auto const at{ImGui::GetCursorScreenPos()}; + auto const drawn{gesture_draw(*ImGui::GetWindowDrawList(), at, picture, words, ImGui::GetColorU32(ImGuiCol_Text))}; + ImGui::Dummy(ImVec2(drawn, picture)); + ImGui::SameLine(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + std::round((picture - ImGui::GetTextLineHeight()) * 0.5f)); + } + if (words.empty() || false == gesture_keys_only(item.keys)) + ImGui::TextColored(ImGui::GetStyleColorVec4(ImGuiCol_CheckMark), "%s", STR_C(item.keys)); + else + ImGui::NewLine(); ImGui::TableNextColumn(); + if (false == words.empty()) + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + std::round((picture - ImGui::GetTextLineHeight()) * 0.5f)); ImGui::TextWrapped("%s", STR_C(item.action)); } ImGui::EndTable(); @@ -5030,7 +5519,17 @@ void editor_mode::render_gizmo_options() if (ui()->mode() == nodebank_panel::TRACK && m_track_tab != track_tab::path && m_track_tab != track_tab::turnout && m_track_tab != track_tab::turntable && m_track_tab != track_tab::signals && m_track_tab != track_tab::lineside) return; - ImGui::Checkbox(STR_C("Enabled"), &m_gizmo_enabled); + int op{m_gizmo_enabled ? static_cast(m_gizmo_op) : -1}; + if (segmented("gizmo", op, + {{-1, STR_C("Off"), "The gizmo hidden: the selected things won't move by chance"}, + {static_cast(gizmo_operation::translate), STR_C("Translate (Q)"), "Arrows which move the selected thing"}, + {static_cast(gizmo_operation::rotate), STR_C("Rotate (W)"), "Rings which turn the selected thing"}, + {static_cast(gizmo_operation::scale), STR_C("Scale (E)"), "Handles which scale the selected thing; a track is lengthened"}})) + { + m_gizmo_enabled = op >= 0; + if (m_gizmo_enabled) + m_gizmo_op = static_cast(op); + } if (!m_gizmo_enabled) return; if (terrain_brush() || terrain_clicking()) @@ -5039,15 +5538,6 @@ void editor_mode::render_gizmo_options() return; } - // lets the user pick the transform mode without keyboard shortcuts - int op = static_cast(m_gizmo_op); - ImGui::RadioButton(STR_C("Translate (Q)"), &op, static_cast(gizmo_operation::translate)); - ImGui::SameLine(); - ImGui::RadioButton(STR_C("Rotate (W)"), &op, static_cast(gizmo_operation::rotate)); - ImGui::SameLine(); - ImGui::RadioButton(STR_C("Scale (E)"), &op, static_cast(gizmo_operation::scale)); - m_gizmo_op = static_cast(op); - if (m_gizmo_op != gizmo_operation::scale) // ImGuizmo always scales in local space ImGui::Checkbox(STR_C("Local space (R)"), &m_gizmo_local); if (m_gizmo_op == gizmo_operation::translate) @@ -5629,7 +6119,7 @@ void editor_mode::delete_selected() snap.node_name = *scene::Layers.instance(m_instance).file; m_history.push_back(std::move(snap)); g_redo.clear(); - ui()->set_status("Include of \"" + *scene::Layers.instance(m_instance).file + "\" removed, Ctrl+Z brings it back. Its directive is erased when the scenery is saved."); + ui()->set_status(format(STR_C("Include of \"%s\" removed, Ctrl+Z brings it back. Its directive is erased when the scenery is saved"), scene::Layers.instance(m_instance).file->c_str())); select_include(0); return; } @@ -6048,12 +6538,12 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const if (node->from_template() && scene::Layers.removable(node->m_instance, &reason)) { select_include(node->m_instance); - ui()->set_status("Include of \"" + *scene::Layers.instance(m_instance).file + "\" selected. Its parameters are in the node properties, Del removes it from the scenery."); + ui()->set_status(format(STR_C("Include of \"%s\" selected. Its parameters are in the inspector, Del removes it from the scenery"), scene::Layers.instance(m_instance).file->c_str())); node = nullptr; } else if (node->from_template() || false == scene::Layers.editable(node, &reason)) { - ui()->set_status("\"" + (node->name().empty() ? std::string{"(unnamed node)"} : node->name()) + "\" can't be edited: " + reason, true); + ui()->set_status(format(STR_C("\"%s\" can't be edited: %s"), (node->name().empty() ? std::string{STR_C("(no name)")} : node->name()).c_str(), reason.c_str()), true); node = nullptr; } } @@ -6195,7 +6685,7 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const void editor_mode::render_change_history(){ - ImGui::Begin((std::string(STR_C("Editor History")) + "###editorhistory").c_str(), &m_change_history); + ImGui::Begin((std::string(STR_C("Change History")) + "###editorhistory").c_str(), &m_change_history); int maxsize = m_max_history_size; if (ImGui::InputInt(STR_C("Max history size"), &maxsize)) { @@ -6235,7 +6725,7 @@ void editor_mode::render_change_history(){ s.action == EditorSnapshot::Action::RoadEdit ? "ROAD" : s.action == EditorSnapshot::Action::Array ? "ARR" : s.action == EditorSnapshot::Action::Terrain ? "TER" : "OTH", - s.node_name.empty() ? "(noname)" : s.node_name.c_str(), + s.node_name.empty() ? STR_C("(no name)") : s.node_name.c_str(), s.position.x, s.position.y, s.position.z); if (ImGui::Selectable(buf, m_selected_history_idx == i)) @@ -6253,7 +6743,7 @@ void editor_mode::render_change_history(){ ImGui::SameLine(); ImGui::SameLine(); - if (ImGui::Button(STR_C("Undo Selected"))) + if (ImGui::Button(STR_C("Undo back to the selected"))) { if (m_selected_history_idx >= 0 && m_selected_history_idx < (int)m_history.size()) { diff --git a/application/editormode.h b/application/editormode.h index 74414ebb..746c5cdf 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -31,6 +31,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorInfra.hpp" #include "editor/editorGauge.hpp" #include "editor/editorClips.hpp" +#include "editor/editorIcons.hpp" #include "application/scriptedinput.h" #include "world/Sweep.h" @@ -41,6 +42,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include #include @@ -186,6 +188,7 @@ class editor_mode : public application_mode, private editor_track::observer std::vector m_history; // history of changes to nodes, used for undo functionality std::vector g_redo; + int m_track_edits{0}; // methods void update_camera(double const Deltatime); @@ -256,7 +259,7 @@ class editor_mode : public application_mode, private editor_track::observer void render_help_menu(); void render_shortcuts_window(); bool m_shortcuts_open{false}; - enum class first_step { tracks, lay, laid, wheel, join, turnout, undo, save, drive, look, top, select, shape, count }; + enum class first_step { tracks, lay, laid, wheel, join, turnout, undo, save, drive, look, top, select, shape, profile, profile_point, profile_apply, terrain, nmt, plan, plan_band, plan_apply, signal, posts, count }; void render_first_steps(); void first_step_done(first_step const Step); bool m_first_steps_open{false}; @@ -325,7 +328,8 @@ class editor_mode : public application_mode, private editor_track::observer struct nmt_place { glm::dvec3 position{0.0}; // on the axis of the path - double formation{0.0}; // the height of the formation under it + double formation{0.0}; // the height of the formation under it, the foot of the ballast + double crown{0.0}; // the height of the top of the ballast under the rails: what the terrain model measures on a track double half_width{3.0}; // from the axis to the foot of the ballast TTrack *track{nullptr}; int path{0}; @@ -348,20 +352,55 @@ class editor_mode : public application_mode, private editor_track::observer std::size_t shaped{0}; std::string status; std::string error; + // the tracks of a scenery built at any height put onto the terrain model: the whole scenery lifted to it, then the grade + // line of each line of tracks fitted to the model, the longest first, then the terrain made along them + enum class automatic + { + idle, + fetching, // the model under the tracks + fitting // the grade lines, a few each frame + }; + automatic stage{automatic::idle}; + std::vector probes; // places of the tracks whose model is waited for + std::size_t tracks{0}; // rail tracks when the grade lines began, and the paths the grade lines split off since + std::size_t split{0}; + std::vector left; // tracks whose grade line is still to be fitted + std::vector done; // tracks of the lines fitted already: the lines fitted later end on them and don't change them + // the rail tops of the lines fitted already, in squares of the plan: the tracks of a station lie in one plane, a line laid + // near them takes their height for its ground too, the more closely the nearer it is + std::map, std::vector> laid; + std::size_t lines{0}; // grade lines fitted and applied + std::size_t refused{0}; // grade lines the checks didn't let apply + std::size_t departures{0}; // grade lines parting from the terrain model by much + double lifted{0.0}; // m the scenery went up or down + // the terrain made under the tracks as they are: the model drawn to them by the difference found at the places along them, + // fading out away from them into the offset of the whole scenery + bool under{false}; + std::vector anchors; // x, z of a place of the tracks, the formation there less the model + std::map, std::vector> anchor_cells; // the anchors in squares of the plan bool offered{false}; // the terrain was offered to the scenery begun without it bool prompt{false}; // the offer is shown double north{0.0}; // the place typed into the offer, PUWG 1992 double east{0.0}; + int mode{0}; // 0: the tracks go onto the terrain, 1: the terrain is drawn to the tracks + bool moving{false}; // the place being typed again }; editor_nmt m_nmt; nmt_state m_nmt_state; std::vector nmt_track_places(double const Step) const; void nmt_locate(); + bool nmt_choice_card(int const Index, char const *Title, char const *Text, float const Width, float const TextHeight, bool const Chosen) const; void nmt_fit(); void nmt_finish_fit(); + // the terrain under the tracks, the tracks left as they are: the model drawn to each of them, its relief kept + void nmt_under_start(); + void nmt_under_anchors(); + // added to the height of the model at the place: the offset of the scenery, drawn to the tracks near them + double nmt_offset_at(double const X, double const Z) const; std::vector nmt_keys_around() const; std::vector nmt_keys_along_tracks() const; - void nmt_make(std::vector const &Keys); + // Rebuild: the chunks there already are made anew too, and the formation laid before is forgotten, as when the tracks moved + void nmt_make(std::vector const &Keys, bool const Rebuild = false); bool add_chunk_from_nmt(heightmap::chunk_key const &Key); void nmt_shape_tracks(); void nmt_update(); @@ -369,7 +408,16 @@ class editor_mode : public application_mode, private editor_track::observer // a scenery begun from nothing, with no tracks nor terrain, is asked as the first track is laid whether it wants the terrain void nmt_offer(); void render_nmt_prompt(); + // the place typed in the terrain window: the terrain model and the orthophoto take it + void nmt_place_scenery(); void nmt_accept_offer(); + // the whole scenery: tracks, includes and models, moved up or down as one undo step + void nmt_lift_scenery(double const Offset); + std::size_t nmt_join_ends(); + void nmt_auto_start(); + void nmt_auto_step(); + // the grade line of the line of tracks through the track fitted to the terrain model and applied; false if it couldn't be + bool nmt_fit_line(TTrack &Track); // the tool of the terrain the left mouse button works enum class terrain_tool { @@ -868,12 +916,35 @@ 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 + bool relaying{false}; // the crossovers of the line were taken out, they go back once it's laid + // crossovers taken out with a line which didn't fit it laid again, tried again as the other lines are laid + struct crossover_out + { + int shape{-1}; + glm::dvec3 press{0.0}, release{0.0}; + std::string name; + std::vector names; // of the switches at the press and at the release + }; + std::vector crossovers_out; 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}; + // the band of the curvature in the panel: the element under the mouse, the point of it, and what's dragged in it + struct curvature_band + { + int hot{-1}; + double at{-1.0}; + int vertex{-1}; + int part{0}; // 1 the radius, 2 the transition in, 3 the transition out + double value{0.0}; + ImVec2 press{0.0f, 0.0f}; + float metres{1.0f}; // pixels of a metre and of a unit of the curvature, kept as the drag goes + float curvature{1.0f}; + } band; }; void render_route_ui(); + bool render_route_band(); bool render_route_parameters(); bool render_route_vertices(); bool render_route_vertex(int const Index); @@ -906,7 +977,37 @@ class editor_mode : public application_mode, private editor_track::observer bool digitize_drawn(std::vector const &Created, bool const Alone = true); bool digitize_chain(editor_track::chain const &Chain, std::string *Reason = nullptr, TTrack **Culprit = nullptr); int digitize_parts(editor_track::chain const &Chain, std::string *Reason, int &Skipped); - void digitize_all(); + // the lines kept, the paths left out + void digitize_all(int *Kept = nullptr, int *Failed = nullptr); + // what the digitization of the scenery did, or would do + struct digitize_report + { + int lines{0}; + int lines_failed{0}; + int switches{0}; + int known{0}; // kept in the scenery already + int designed{0}; // of a template of the catalogue + int measured{0}; + int slips{0}; + int unnamed{0}; // no record finds them + int odd{0}; + int recorded{0}; + int crossovers{0}; + int crossovers_known{0}; + int crossovers_recorded{0}; + int crossovers_odd{0}; + int found{0}; + int bound{0}; + std::map types; // of the catalogue + std::map shapes; // measured + std::vector notes; + }; + // the switches without a type kept get the one of the catalogue they are of, or the size measured from their shape + void digitize_switches(digitize_report &Report, bool const Write); + // two switches facing each other from two tracks, their diverging tracks joined by a plain one, are kept as a crossover + void digitize_crossovers(digitize_report &Report, bool const Write); + // the lines, the switches, the crossovers and the objects along the tracks at one go; without Write only counts them + std::string digitize_scenery(bool const Write); void render_route_stored(); void route_update() { @@ -1028,6 +1129,7 @@ class editor_mode : public application_mode, private editor_track::observer }; struct switch_tool { + std::vector keep_names; // the names the switches laid next take, before the ones of the tool bool curved{false}; std::vector frame; std::vector frame_tracks; @@ -1261,6 +1363,7 @@ class editor_mode : public application_mode, private editor_track::observer std::vector lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const; alignment::result lay_shape(std::vector const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &Design) const; void render_switch_ui(); + void render_rail_profile_hint(TTrack &Track); bool start_switch_placement(); void finish_switch_placement(); std::vector switch_preview() const; @@ -1271,6 +1374,30 @@ class editor_mode : public application_mode, private editor_track::observer static double nearest_station(std::vector const &Frame, glm::dvec3 const &Point); static std::vector curved_switch_paths(editor_track::switch_template const &Shape, std::vector const &Frame, double const Station, int const Direction, int const Side); void insert_curved_switch(int const Direction, int const Side); + // a crossover between two tracks running side by side along a curve (editormodeconnectioncurve.cpp) + struct curve_mapping; + static bool curve_of(TTrack &Track, editor_track::straight_tolerance const &Tolerance, double const Gauge, std::vector &Frame, std::vector &Tracks); + static void cut_curve(std::vector &Candidates, std::vector const &Frame, glm::dvec3 const &First, glm::dvec3 const &Second, std::vector> &States, std::vector &Created, std::vector &Removed); + void insert_curved_connection(); + struct curved_crossover + { + switch_store::record record; + TTrack *first{nullptr}; + TTrack *second{nullptr}; + TTrack *insert{nullptr}; + }; + // the single crossover the switch is a part of, along a curve or straights + std::optional crossover_of(TTrack const &Switch) const; + bool take_out_crossover(curved_crossover const &Crossover, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack *&First, TTrack *&Second); + bool slide_curved_connection(TTrack &Switch, double const Distance); + // the template a crossover was laid of, -1 if it's not among the templates + int crossover_template(switch_store::record const &Record); + // a crossover laid again from the press on one track to the release by the other, as by the mouse. false if it doesn't fit + // Names: of the switch at the press and of the one at the release, kept when a crossover is laid again + bool lay_crossover(int const Shape, glm::dvec3 const &Press, glm::dvec3 const &Release, double const Reach = 3.0, std::vector const &Names = {}); + // after the grade line of the open route went on its paths: the line on the other side of each crossover of the route + // takes the same change of the heights where it runs beside, and its design with it. returns: how many lines followed + std::size_t follow_crossovers(std::vector const &Before); bool place_switch_on_straight(editor_track::straight const &Line, editor_track::switch_template const &Shape, TTrack const *Style, double const Along, int const Direction, int const Side, bool const Snap, std::vector> &States, std::vector &Created, std::vector &Removed, std::string const &Name = {}); struct crossover_plan { @@ -1341,6 +1468,8 @@ class editor_mode : public application_mode, private editor_track::observer char const *key; track_tab tab; char const *tooltip; + editor_icons::icon picture; + int family; // 0 builds the track, 1 equips it, 2 checks it }; static std::array const &track_modes(); bool track_plan_tab() const { return m_track_tab == track_tab::straights || m_track_tab == track_tab::route; } @@ -1404,11 +1533,14 @@ class editor_mode : public application_mode, private editor_track::observer { char const *key; std::string action; + char const *raw{nullptr}; // the key before it's translated, its gesture drawn from it }; std::vector track_key_hints(bool const All) const; void render_track_guide(); std::string track_readout() const; void draw_track_hints(); + // a scenery with no tracks yet: in the middle of the view, how the first one is laid + void draw_empty_scenery_hint(); // path under the cursor, outlined before a click so it's known which one the click takes struct track_hover { @@ -1842,6 +1974,8 @@ class editor_mode : public application_mode, private editor_track::observer bool turned{false}; }; std::vector consist; + int picked{-1}; // vehicle of the train whose buttons are shown + std::set minis; // skins whose side view was asked for int facing{1}; bool write{true}; char timetable[64]{"none"}; @@ -2112,6 +2246,7 @@ class editor_mode : public application_mode, private editor_track::observer selftest_result selftest_signal(std::string const &command, std::istringstream &words); selftest_result selftest_terrain(std::string const &command, std::istringstream &words); selftest_result selftest_props(std::string const &command, std::istringstream &words); + selftest_result selftest_monkey(std::string const &command, std::istringstream &words); selftest_result selftest_dispatch(std::string const &command, std::istringstream &words); void selftest_step(); // a value typed during a gesture, taken instead of the one the cursor gives @@ -2132,10 +2267,17 @@ class editor_mode : public application_mode, private editor_track::observer TTrack *to{nullptr}; TTrack *picked_from{nullptr}; TTrack *picked_to{nullptr}; - double run_length{3000.0}; + double run_length{std::numeric_limits::infinity()}; // the whole line through the track clicked + std::vector followed; // the selection the route was last taken from editor_track::route route; std::vector samples; + std::vector created; // the paths the last apply split off the route std::vector terrain; // NaN where there's no terrain + // what the joined ends take their height and grade from, when it's a switch: shown on the plot + std::string start_inherits, end_inherits; + // depth of the formation under the grade line, the rail top, of the track of each sample: the formation, not the rails, + // lies on the ground + std::vector beds; profile::line line; profile::context context; std::vector issues; @@ -2166,6 +2308,10 @@ class editor_mode : public application_mode, private editor_track::observer bool show_plan{true}; int selected{-1}; int dragging{-1}; + // where the point was grabbed: the mouse there, the point against the mouse; the point moves once the mouse does + ImVec2 drag_press{0.0f, 0.0f}; + glm::dvec2 drag_grab{0.0}; + bool drag_moved{false}; int curve_grip{-1}; bool panning{false}; bool changed{false}; @@ -2175,6 +2321,9 @@ class editor_mode : public application_mode, private editor_track::observer int hover_grip{-1}; bool hover_line{false}; int shown{-2}; // point in the side panel + bool show_remarks{false}; + float strip_top{-1.0f}; // the top of the strip over the 3d view, -1 while it isn't shown + int edits{-1}; // of the tracks, when the route was last read std::string status; std::string error; }; @@ -2187,6 +2336,7 @@ class editor_mode : public application_mode, private editor_track::observer glm::dvec3 start{0.0}, end{0.0}, middle{0.0}; // of the route, world space double origin{0.0}; profile::line line; + double departure{-1.0}; // of the track from the design when it was kept; -1 as kept by an older editor }; std::vector m_profiles; bool m_profiles_loaded{false}; @@ -2194,12 +2344,17 @@ class editor_mode : public application_mode, private editor_track::observer void profile_library_mark(std::vector Layers); int profile_find(bool &Reversed) const; bool profile_restore(); + bool profile_restore_parts(); + // the route a kept design was laid over, as it runs now; none when its paths moved or are gone + std::optional profile_stored_route(stored_profile const &Entry, TTrack **From = nullptr, TTrack **To = nullptr) const; void profile_store(); void profile_forget(); void profile_open_stored(std::size_t const Index); void profile_edited(); void render_profile_body(); void render_profile_strip(); + void render_profile_header(); + void render_profile_empty(); void render_profile_toolbar(); void render_profile_points(); void render_profile_source(); @@ -2208,10 +2363,15 @@ class editor_mode : public application_mode, private editor_track::observer void profile_open(TTrack *From, TTrack *To); void profile_open_run(TTrack &Track); void profile_take(editor_track::route Route, TTrack *From, TTrack *To); + void profile_open_selection(std::vector const &Tracks); + void profile_follow_selection(); void profile_resample(); void profile_recognize(); void profile_fit_ground(); + // depth of the foot of the ballast under the rail top, and its distance from the axis static std::pair profile_ballast(TTrack const &Track); + // false for a track with no trackbed of its own, as the switches of the older sceneries whose bed is an include + static bool profile_own_bed(TTrack const &Track); void profile_shape_ground(); std::pair profile_generate_ground(std::vector> const &Beds, double const Reach); void profile_restore_ground(); @@ -2220,6 +2380,7 @@ class editor_mode : public application_mode, private editor_track::observer void profile_apply(); void profile_fit_view(); void profile_after_undo(); + void profile_follow_edits(); struct profile_view; void render_profile_canvas(float const Height); void profile_fit_exaggeration(profile_view const &View); diff --git a/application/editormodeconnection.cpp b/application/editormodeconnection.cpp index c2d344d3..459d2480 100644 --- a/application/editormodeconnection.cpp +++ b/application/editormodeconnection.cpp @@ -239,7 +239,7 @@ void editor_mode::insert_connection() } for (auto *track : removed) { - track->m_editorremoved = false; + editor_track::revive(*track); editor_track::commit({track}); } if (auto before{switch_store::take_changes()}) @@ -333,9 +333,11 @@ void editor_mode::render_connection_options() auto &tool{m_switch}; if (tool.connection < 0) return; - ImGui::PushItemWidth(220.0f); + if (false == begin_fields("##connectionoptions")) + return; auto const label = [&](int const Index) { return Index < 0 || Index >= static_cast(tool.templates.size()) ? std::string{STR_C("The same as the first one")} : switch_template_label(tool.templates[Index]); }; - if (ImGui::BeginCombo(STR_C("Other switches##connectionsecond"), label(tool.connection_second).c_str())) + field_row("Other switches##connectionsecond"); + if (ImGui::BeginCombo("##connectionsecond", label(tool.connection_second).c_str())) { if (ImGui::Selectable(STR_C("The same as the first one"), tool.connection_second < 0)) tool.connection_second = -1; @@ -345,24 +347,23 @@ void editor_mode::render_connection_options() ImGui::EndCombo(); } item_tooltip("Template of the switches on the other track and along a ladder; the first switch is the one chosen on the list above"); - ImGui::InputDouble(STR_C("Aligning curve R, m##connectionradius"), &tool.connection_radius, 10.0, 100.0, "%.0f"); + field_row("Aligning curve R, m##connectionradius"); + ImGui::InputDouble("##connectionradius", &tool.connection_radius, 10.0, 100.0, "%.0f"); tool.connection_radius = std::clamp(tool.connection_radius, 0.0, 10000.0); item_tooltip("Of the curve between switches of different angles or on tracks that aren't parallel, and at the end of a ladder; 0: the radius of the sharper switch"); if (tool.connection == static_cast(connection::kind::double_crossover)) { - ImGui::InputDouble(STR_C("Gap between the switches, m##connectiongap"), &tool.connection_gap, 0.5, 1.0, "%.2f"); + field_row("Gap between the switches, m##connectiongap"); + ImGui::InputDouble("##connectiongap", &tool.connection_gap, 0.5, 1.0, "%.2f"); tool.connection_gap = std::clamp(tool.connection_gap, 0.0, 100.0); item_tooltip("Straight between the starts of the two switches meeting on one track"); - ImGui::Checkbox(STR_C("The switches meet on this track##connectiontogether"), &tool.connection_together); - item_tooltip("Otherwise the two crossovers meet on the other track; Alt+wheel while dragging switches it"); } if (tool.connection == static_cast(connection::kind::ladder)) { - ImGui::InputText(STR_C("Spacings, m##connectionspacings"), tool.connection_spacings, sizeof(tool.connection_spacings)); - item_tooltip("Distances between the tracks of the fan from this track on, such as 4.75;4.75;5.0; used when the drag doesn't end on a parallel track, otherwise the parallel tracks up to that one make the fan"); - ImGui::InputDouble(STR_C("New tracks, m##connectionstub"), &tool.connection_stub, 5.0, 25.0, "%.0f"); + field_row("New tracks, m##connectionstub"); + ImGui::InputDouble("##connectionstub", &tool.connection_stub, 5.0, 25.0, "%.0f"); tool.connection_stub = std::clamp(tool.connection_stub, 1.0, 1000.0); item_tooltip("Length of the new tracks of the fan where there are none to lead onto"); } - ImGui::PopItemWidth(); + ImGui::EndTable(); } diff --git a/application/editormodeconnectioncurve.cpp b/application/editormodeconnectioncurve.cpp new file mode 100644 index 00000000..20e81fdb --- /dev/null +++ b/application/editormodeconnectioncurve.cpp @@ -0,0 +1,993 @@ +/* +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/. +*/ + +// a crossover between two tracks running side by side along a curve: planned as between straights in the frame of the curve +// (along it, across it) and laid onto the curve, the switches, the insert and the other track on the plane the cant tilts the +// inner track to. The outer track goes up to that plane over the crossover, with ramps on both sides + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editormodeshared.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorSwitchStore.hpp" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/translation.h" +#include "world/Track.h" + +#include + +using geometry::bezier; +using geometry::plan_distance; +using geometry::plan_of; + +namespace +{ + +// of the ramps the outer track goes up to the crossover and down from it by, a share of the change of grade which needs a +// vertical curve: the ramps break the grade line without one +double constexpr kRampShare{0.9}; +double constexpr kOtherReach{10.0}; // m, from the release to the other track + +} // namespace + +// the plan of a connection between straights laid onto the curve: its x across the curve, minus to the left, its z the station +struct editor_mode::curve_mapping +{ + std::vector const &frame; + + glm::dvec3 point(glm::dvec3 const &Local) const + { + auto const sample{sample_at(frame, Local.z)}; + glm::dvec2 const left{-sample.tangent.y, sample.tangent.x}; + auto const lateral{-Local.x}; + auto const planar{plan_of(sample.position) + left * lateral}; + return {planar.x, sample.position.y + Local.y - lateral * std::tan(glm::radians(static_cast(sample.roll))), planar.y}; + } + glm::dvec2 tangent(glm::dvec3 const &Local, glm::dvec2 const &Direction) const + { + auto const sample{sample_at(frame, Local.z)}; + glm::dvec2 const left{-sample.tangent.y, sample.tangent.x}; + return glm::normalize(sample.tangent * Direction.y - left * Direction.x); + } + float roll(glm::dvec3 const &Local) const { return sample_at(frame, Local.z).roll; } + // the height of the tilted plane at the point of the plan + double height(glm::dvec2 const &Planar) const + { + auto const sample{sample_at(frame, nearest_station(frame, {Planar.x, 0.0, Planar.y}))}; + auto const lateral{glm::dot(Planar - plan_of(sample.position), glm::dvec2{-sample.tangent.y, sample.tangent.x})}; + return sample.position.y - lateral * std::tan(glm::radians(static_cast(sample.roll))); + } + + segment_data path(segment_data const &Local) const + { + auto const &start{Local.points[segment_data::point::start]}; + auto const &end{Local.points[segment_data::point::end]}; + auto const &control1{Local.points[segment_data::point::control1]}; + auto const &control2{Local.points[segment_data::point::control2]}; + glm::dvec2 const chord{end.x - start.x, end.z - start.z}; + auto const localstart{glm::normalize(control1 != glm::dvec3{} ? plan_of(control1) : chord)}; + auto const localend{glm::normalize(control2 != glm::dvec3{} ? -plan_of(control2) : chord)}; + auto const p0{point(start)}; + auto const p3{point(end)}; + auto const t0{tangent(start, localstart)}; + auto const t3{tangent(end, localend)}; + auto const handle{plan_distance(p0, p3) / 3.0}; + segment_data result; + result.points[segment_data::point::start] = p0; + result.points[segment_data::point::end] = p3; + result.points[segment_data::point::control1] = {t0.x * handle, (p3.y - p0.y) / 3.0, t0.y * handle}; + result.points[segment_data::point::control2] = {-t3.x * handle, -(p3.y - p0.y) / 3.0, -t3.y * handle}; + // the heights of the inner points on the tilted plane + auto const h1{height(plan_of(bezier{result}.point(1.0 / 3.0)))}; + auto const h2{height(plan_of(bezier{result}.point(2.0 / 3.0)))}; + auto const y1{3.0 * h1 - 1.5 * h2 - 5.0 / 6.0 * p0.y + p3.y / 3.0}; + auto const y2{3.0 * h2 - 1.5 * h1 + p0.y / 3.0 - 5.0 / 6.0 * p3.y}; + result.points[segment_data::point::control1].y = y1 - p0.y; + result.points[segment_data::point::control2].y = y2 - p3.y; + // the cant of a path is told along it: a path running against the stations of the curve has it the other way round + result.rolls = {roll(start) * (localstart.y >= 0.0 ? 1.f : -1.f), roll(end) * (localend.y >= 0.0 ? 1.f : -1.f)}; + result.radius = Local.radius; + bezier const curve{result}; + auto const begin{plan_of(curve.first(0.0))}; + auto const finish{plan_of(curve.first(1.0))}; + auto const turn{glm::length(begin) > 1e-9 && glm::length(finish) > 1e-9 ? geometry::signed_angle(begin, finish) : 0.0}; + if (std::abs(turn) > 1e-6) + result.radius = static_cast(curve.plan_length() / turn); + else + result.radius = 0.f; + for (auto const &piece : Local.pieces) + result.pieces.push_back(path(piece)); + return result; + } +}; + +// the curve the track is a part of, as a frame of samples, and its tracks +bool editor_mode::curve_of(TTrack &Track, editor_track::straight_tolerance const &Tolerance, double const Gauge, std::vector &Frame, std::vector &Tracks) +{ + editor_track::curve curve; + editor_track::chain chain; + std::string error; + if (false == editor_track::find_curve(Track, Tolerance, Gauge, curve) || false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) + return false; + for (auto const &part : editor_track::chain_parts(chain)) + if (std::find(part.tracks.begin(), part.tracks.end(), &Track) != part.tracks.end()) + { + chain = part; + break; + } + Frame = sample_chain(chain); + Tracks = chain.tracks; + return Frame.size() >= 2; +} + +// the paths of the curve between the points cut off at both, right at the points, and taken out, the switch to go in their place +void editor_mode::cut_curve(std::vector &Candidates, std::vector const &Frame, glm::dvec3 const &First, glm::dvec3 const &Second, std::vector> &States, std::vector &Created, std::vector &Removed) +{ + auto const one{nearest_station(Frame, First)}; + auto const two{nearest_station(Frame, Second)}; + auto const From{std::min(one, two)}; + auto const To{std::max(one, two)}; + for (auto const &point : {First, Second}) + { + for (auto *candidate : std::vector(Candidates)) + { + if (candidate->m_editorremoved || candidate->eType != tt_Normal) + continue; + auto const t{editor_track::nearest_parameter(*candidate, point)}; + if (plan_distance(editor_track::point_at(*candidate, t), point) > 0.05) + continue; + if (t > 0.002 && t < 0.998) + { + if (std::find(Created.begin(), Created.end(), candidate) == Created.end() && std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == candidate; })) + States.emplace_back(candidate, editor_track::capture(*candidate)); + if (auto *second{editor_track::split_path(*candidate, t)}) + { + Created.push_back(second); + Candidates.push_back(second); + } + } + break; + } + } + for (auto *candidate : Candidates) + { + if (candidate->m_editorremoved) + continue; + auto const where{nearest_station(Frame, editor_track::point_at(*candidate, 0.5))}; + if (where > From + 0.01 && where < To - 0.01) + { + editor_track::retire(*candidate); + Removed.push_back(candidate); + } + } +} + +void editor_mode::insert_curved_connection() +{ + auto &tool{m_switch}; + tool.placing = false; + auto const fail = [&](std::string const &Reason) { + tool.status = Reason; + ui()->set_status(Reason, true); + }; + if (tool.connection != static_cast(connection::kind::crossover)) + return fail(STR("Along a curve only a crossover can be laid")); + // the other track: one beside this curve near the release, running along with it + TTrack *other{nullptr}; + auto best{kOtherReach}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty() || std::find(tool.frame_tracks.begin(), tool.frame_tracks.end(), track) != tool.frame_tracks.end()) + continue; + auto const distance{plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, tool.mouse)), tool.mouse)}; + if (distance < best) + { + best = distance; + other = track; + } + } + if (other == nullptr) + return fail(STR("Drag onto the other track")); + std::vector otherframe; + std::vector othertracks; + if (false == curve_of(*other, m_straights.tolerance, m_route.design.norms.gauge, otherframe, othertracks)) + return fail(STR("The other track isn't a curve running along this one")); + // the inner track is the frame: the plane of its cant carries the crossover, the outer track goes up onto it + auto innerframe{tool.frame}; + auto innertracks{tool.frame_tracks}; + auto station{tool.along}; + auto const at{sample_at(innerframe, station)}; + auto const across{sample_at(otherframe, nearest_station(otherframe, at.position))}; + glm::dvec2 const left{-at.tangent.y, at.tangent.x}; + auto offset{glm::dot(plan_of(across.position) - plan_of(at.position), left)}; + auto lift{at.position.y - offset * std::tan(glm::radians(static_cast(at.roll))) - across.position.y}; + bool swapped{false}; + if (lift < -0.005) + { + // the plane falls towards the other track: that one is the inner one + std::swap(innerframe, otherframe); + std::swap(innertracks, othertracks); + station = nearest_station(innerframe, at.position); + auto const inner{sample_at(innerframe, station)}; + glm::dvec2 const innerleft{-inner.tangent.y, inner.tangent.x}; + offset = glm::dot(plan_of(at.position) - plan_of(inner.position), innerleft); + lift = inner.position.y - offset * std::tan(glm::radians(static_cast(inner.roll))) - at.position.y; + swapped = true; + } + WriteLog(format("Editor: curved crossover - at station %.1f, inner roll %.3f deg, other roll %.3f deg, offset %.3f m, lift %.3f m%s", station, at.roll, across.roll, offset, lift, swapped ? ", the other track is the inner one" : ""), logtype::generic); + if (std::abs(offset) < 3.0 || std::abs(offset) > 30.0) + return fail(format(STR_C("The tracks are %.2f m apart, too close or too far for a crossover"), std::abs(offset))); + + // the crossover planned as between two straights, in the frame of the inner curve + auto const total{innerframe.back().station}; + connection::request request; + request.type = connection::kind::crossover; + request.from.start = {0.0, 0.0, 0.0}; + request.from.direction = {0.0, 1.0}; + request.from.length = total; + connection::line to; + to.start = {-offset, 0.0, 0.0}; + to.direction = {0.0, 1.0}; + to.length = total; + request.to.push_back(to); + request.first = tool.templates[tool.armed]; + auto const second{tool.connection_second >= 0 && tool.connection_second < static_cast(tool.templates.size()) ? tool.connection_second : tool.armed}; + request.second = tool.templates[second]; + request.radius = tool.connection_radius; + auto const release{nearest_station(innerframe, tool.mouse)}; + auto const press{swapped ? nearest_station(innerframe, tool.point) : station}; + request.forward = release >= press ? 1 : -1; + // the plan starts on the track pressed on + if (swapped) + { + request.from.start = {-offset, 0.0, 0.0}; + request.to.front().start = {0.0, 0.0, 0.0}; + } + request.along = press; + request.target = release; + // the side of the travel along the starting track the other one lies on + auto const across_plan{request.to.front().start.x - request.from.start.x}; + request.side = -request.forward * across_plan >= 0.0 ? 1 : -1; + auto plan{connection::plan(request)}; + if (false == plan.valid) + { + // the other side, if the side was taken the wrong way round + request.side = -request.side; + plan = connection::plan(request); + } + if (false == plan.valid) + return fail(plan.error.empty() ? std::string{STR("The crossover doesn't fit on the curve")} : plan.error); + + curve_mapping const mapping{innerframe}; + std::vector> states; + std::vector created; + std::vector removed; + auto *style{tool.frame_tracks.front()}; + auto const undo_all = [&]() { + for (auto *track : created) + editor_track::retire(*track); + for (auto entry{states.rbegin()}; entry != states.rend(); ++entry) + { + editor_track::apply(*entry->first, entry->second); + editor_track::commit({entry->first}); + } + for (auto *track : removed) + { + editor_track::revive(*track); + editor_track::commit({track}); + } + straight_refresh(); + }; + + // the outer track up onto the plane of the inner one over the crossover, with ramps on both sides + auto const outerline{swapped ? 0 : 1}; + double low{std::numeric_limits::max()}, high{std::numeric_limits::lowest()}; + for (auto const &entry : plan.switches) + for (auto const &path : entry.paths) + for (auto const end : {segment_data::point::start, segment_data::point::end}) + { + low = std::min(low, path.points[end].z); + high = std::max(high, path.points[end].z); + } + auto *outertrack{swapped ? tool.frame_tracks.front() : other}; + for (auto const &entry : plan.switches) + WriteLog(format("Editor: curved crossover - planned switch on %d: %.3f %.3f -> %.3f %.3f; extent %.3f..%.3f", entry.line, entry.paths.front().points[segment_data::point::start].x, entry.paths.front().points[segment_data::point::start].z, entry.paths.front().points[segment_data::point::end].x, entry.paths.front().points[segment_data::point::end].z, low, high), logtype::generic); + std::size_t raised{0}; + glm::dvec3 flatfrom{0.0}, flatto{0.0}; + double ramplength{0.0}; + if (std::abs(lift) > 0.002) + { + auto route{editor_track::run_route(*outertrack, 3000.0)}; + auto const samples{editor_track::sample_route(route, 1.0)}; + if (samples.size() < 2) + return fail(STR("The outer track can't be followed")); + // the chainage of the point projected onto the line between the samples + auto const chainage_of = [&](glm::dvec3 const &Point) { + double best{std::numeric_limits::max()}; + double chainage{0.0}; + for (std::size_t i = 0; i + 1 < samples.size(); ++i) + { + auto const a{plan_of(samples[i].position)}; + auto const run{plan_of(samples[i + 1].position) - a}; + auto const t{std::clamp(glm::dot(plan_of(Point) - a, run) / std::max(1e-12, glm::dot(run, run)), 0.0, 1.0)}; + if (auto const distance{glm::length(plan_of(Point) - (a + run * t))}; distance < best) + { + best = distance; + chainage = samples[i].chainage + (samples[i + 1].chainage - samples[i].chainage) * t; + } + } + return chainage; + }; + auto const outerx{swapped ? 0.0 : -offset}; + auto c1{chainage_of(mapping.point({outerx, 0.0, low}))}; + auto c2{chainage_of(mapping.point({outerx, 0.0, high}))}; + // the top of the ramps where the switch on the outer track ends: no short paths left between the two + if (c1 > c2) + std::swap(c1, c2); + auto const ramp{std::abs(lift) / (kRampShare * m_profile.line.norms.curve_threshold / 1000.0)}; + flatfrom = editor_track::sampled_position(samples, c1); + flatto = editor_track::sampled_position(samples, c2); + ramplength = ramp; + auto const share = [&](double const Chainage) { + if (Chainage <= c1 - ramp || Chainage >= c2 + ramp) + return 0.0; + if (Chainage < c1) + return (Chainage - (c1 - ramp)) / ramp; + if (Chainage > c2) + return ((c2 + ramp) - Chainage) / ramp; + return 1.0; + }; + auto const slope = [&](double const Chainage) { + if (Chainage > c1 - ramp && Chainage < c1) + return 1.0 / ramp; + if (Chainage > c2 && Chainage < c2 + ramp) + return -1.0 / ramp; + return 0.0; + }; + std::vector breaks; + for (auto const chainage : {c1 - ramp, c1, c2, c2 + ramp}) + if (chainage > 0.5 && chainage < route.length - 0.5) + breaks.push_back(chainage); + // the design of the outer track before, kept or read from the track, to go up with it + auto *first{route.spans.front().track}; + auto *last{route.spans.back().track}; + profile_take(route, first, last); + auto const before{m_profile.line}; + std::vector> raisestates; + std::vector raisecreated; + editor_track::apply_profile( + route, [&](double const Chainage) { return editor_track::sampled_elevation(samples, Chainage) + lift * share(Chainage); }, + [&](double const Chainage) { return editor_track::sampled_grade(samples, Chainage) + lift * slope(Chainage); }, breaks, raisestates, raisecreated); + raised = raisestates.size() + raisecreated.size(); + states.insert(states.end(), raisestates.begin(), raisestates.end()); + created.insert(created.end(), raisecreated.begin(), raisecreated.end()); + // the raised grade line kept as the design of the outer track, in place of the one it had: its points gone up with it + // and the corners of the ramps, straight between them as the ramps are + if (false == route.spans.empty()) + { + profile_take(route, route.spans.front().track, route.spans.back().track); + std::vector points; + for (auto point : before.points) + { + point.elevation += lift * share(point.chainage); + points.push_back(point); + } + for (auto const chainage : breaks) + { + profile::pvi corner; + corner.chainage = chainage; + corner.elevation = profile::elevation(before, chainage) + lift * share(chainage); + corner.automatic = false; + points.push_back(corner); + } + std::sort(points.begin(), points.end(), [](auto const &Left, auto const &Right) { return Left.chainage < Right.chainage; }); + std::vector line; + for (auto const &point : points) + { + if (false == line.empty() && point.chainage - line.back().chainage < 0.5) + { + // the corner of a ramp before a point of the design at the same place + if (false == point.automatic && point.radius == 0.0) + line.back() = point; + continue; + } + line.push_back(point); + } + if (line.size() >= 2) + { + m_profile.line = before; + m_profile.line.points = std::move(line); + m_profile.selected = -1; + profile_check(); + profile_store(); + } + } + WriteLog(format("Editor: curved crossover - the outer track %s raised by %.3f m from %.1f to %.1f m of its line, ramps %.1f m", outertrack->name().c_str(), lift, c1, c2, ramp), logtype::generic); + } + // the outer curve again, as it lies now + std::vector outerframe; + std::vector outertracks; + { + auto const probe{mapping.point({swapped ? 0.0 : -offset, 0.0, 0.5 * (low + high)})}; + TTrack *closest{nullptr}; + auto nearest{1.0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const distance{plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, probe)), probe)}; + if (distance < nearest) + { + nearest = distance; + closest = track; + } + } + if (closest == nullptr || false == curve_of(*closest, m_straights.tolerance, m_route.design.norms.gauge, outerframe, outertracks)) + { + undo_all(); + return fail(STR("The outer track can't be followed after it went up")); + } + } + // the inner curve again: the outer track may be split where the ramps end + if (false == swapped) + innertracks = tool.frame_tracks; + + std::vector switches; + for (auto const &entry : plan.switches) + { + auto const &shape{entry.second ? request.second : request.first}; + std::vector paths; + for (auto const &local : entry.paths) + paths.push_back(mapping.path(local)); + auto const onouter{entry.line == outerline}; + auto &frame{onouter ? outerframe : innerframe}; + auto &candidates{onouter ? outertracks : innertracks}; + // the other track runs along at the distance the crossover was planned for, else its switch would miss it + for (auto const end : {segment_data::point::start, segment_data::point::end}) + { + auto const &point{paths.front().points[end]}; + if (plan_distance(sample_at(frame, nearest_station(frame, point)).position, point) > 0.1) + { + undo_all(); + return fail(STR("The other track doesn't run along at the same distance there")); + } + } + cut_curve(candidates, frame, paths.front().points[segment_data::point::start], paths.front().points[segment_data::point::end], states, created, removed); + // the ends of the main track of the switch onto the ends of the paths cut off, which the curve put a little apart + auto const snap = [&](glm::dvec3 const &Point) { + glm::dvec3 found{Point}; + auto nearest{0.1}; + for (auto *candidate : candidates) + { + if (candidate->m_editorremoved || candidate->m_paths.empty()) + continue; + for (auto const which : {segment_data::point::start, segment_data::point::end}) + if (auto const distance{glm::distance(candidate->m_paths.front().points[which], Point)}; distance < nearest) + { + nearest = distance; + found = candidate->m_paths.front().points[which]; + } + } + return found; + }; + auto const begin{paths.front().points[segment_data::point::start]}; + auto const finish{paths.front().points[segment_data::point::end]}; + auto const start{snap(begin)}; + auto const end{snap(finish)}; + for (auto &path : paths) + { + if (glm::distance(path.points[segment_data::point::start], begin) < 1e-6) + { + path.points[segment_data::point::start] = start; + if (false == path.pieces.empty()) + path.pieces.front().points[segment_data::point::start] = start; + } + } + paths.front().points[segment_data::point::end] = end; + if (false == paths.front().pieces.empty()) + paths.front().pieces.back().points[segment_data::point::end] = end; + auto const made{create_switch_set(shape, paths, *style, switch_name_for_new())}; + if (made.empty()) + { + undo_all(); + return fail(tool.status.empty() ? std::string{STR("A switch of the crossover couldn't be made")} : tool.status); + } + created.insert(created.end(), made.begin(), made.end()); + for (auto *track : made) + if (track->eType == tt_Switch) + switches.push_back(track); + advance_switch_name(); + } + std::vector inserts; + for (auto const &insert : plan.inserts) + if (auto *track{editor_track::create_path(*style, mapping.path(insert))}) + inserts.push_back(track); + editor_track::commit(inserts); + digitize_drawn(inserts); + created.insert(created.end(), inserts.begin(), inserts.end()); + if (switches.empty()) + { + undo_all(); + return fail(STR("The crossover couldn't be laid")); + } + record_connection(request.type, switches, plan, request.first); + // the lift kept with the crossover, so it can be taken back when the crossover moves or goes + if (std::abs(lift) > 0.002) + for (auto entry : switch_store::connections(*switches.front())) + if (entry.kind == "connection" && entry.name == switches.front()->name()) + { + entry.lift = lift; + entry.lift_from = flatfrom; + entry.lift_to = flatto; + entry.ramp = ramplength; + switch_store::put(entry, entry.layer); + } + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + first_step_done(first_step::turnout); + m_node = switches.front(); + ui()->set_node(m_node); + std::string names; + for (auto *track : switches) + { + if (place_switch_drive(*track)) + join_last_steps(); + names += (names.empty() ? "" : ", ") + track->name(); + } + tool.status = format(STR_C("Crossover laid along the curve, switches %s, the tracks %.2f m apart"), names.c_str(), std::abs(offset)); + if (std::abs(lift) > 0.002) + tool.status += format(STR_C("\nThe outer track went up %.3f m onto the plane of the cant of the inner one, with ramps of %.1f per mille"), std::abs(lift), kRampShare * m_profile.line.norms.curve_threshold); + tool.status += "\n" + connection_readout(plan); + ui()->set_status(tool.status, false); +} + +// the crossover laid along a curve the switch is a part of: its record, its two switches and the insert between them +std::optional editor_mode::crossover_of(TTrack const &Switch) const +{ + if (Switch.eType != tt_Switch || Switch.m_editorremoved) + return std::nullopt; + for (auto const &entry : switch_store::connections(Switch)) + { + if (entry.kind != "connection" || entry.type != connection::word(connection::kind::crossover) || entry.parts.size() != 2) + continue; + curved_crossover result; + result.record = entry; + result.first = simulation::Paths.find(entry.parts[0]); + result.second = simulation::Paths.find(entry.parts[1]); + if (result.first == nullptr || result.second == nullptr || result.first->m_editorremoved || result.second->m_editorremoved || result.first->m_paths.size() < 2 || result.second->m_paths.size() < 2) + continue; + auto const &end{result.first->m_paths[1].points[segment_data::point::end]}; + auto const &other{result.second->m_paths[1].points[segment_data::point::end]}; + for (auto const &[track, point] : editor_track::connected_points(*result.first, end)) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.size() != 1) + continue; + auto const &path{track->m_paths.front()}; + if (plan_distance(path.points[segment_data::point::start], other) < 0.05 || plan_distance(path.points[segment_data::point::end], other) < 0.05) + result.insert = track; + } + if (result.insert != nullptr) + return result; + } + return std::nullopt; +} + +// the crossover taken out: the main tracks of its switches plain paths again, of the same shape, the insert gone, the outer track +// down by the lift it went up by. Gives the plain paths in place of the first and the second switch +bool editor_mode::take_out_crossover(curved_crossover const &Crossover, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack *&First, TTrack *&Second) +{ + TTrack const *style{nullptr}; + for (auto const &[track, point] : editor_track::connected_points(*Crossover.first, Crossover.first->m_paths.front().points[segment_data::point::start])) + if (track != nullptr && track->eType == tt_Normal && false == track->m_editorremoved) + style = track; + if (style == nullptr) + style = Crossover.insert; + std::vector plains; + for (auto *track : {Crossover.first, Crossover.second}) + { + auto *plain{editor_track::create_path(*style, track->m_paths.front())}; + if (plain == nullptr) + return false; + plains.push_back(plain); + Created.push_back(plain); + } + for (auto *track : {Crossover.first, Crossover.second, Crossover.insert}) + { + editor_track::retire(*track); + Removed.push_back(track); + } + editor_track::commit(plains); + switch_store::forget(Crossover.first->name()); + switch_store::forget(Crossover.second->name()); + First = plains[0]; + Second = plains[1]; + auto const &record{Crossover.record}; + if (std::abs(record.lift) <= 0.002 || record.ramp <= 0.0) + return true; + // the outer track is the one the level stretch of the lift lies on + TTrack *outer{nullptr}; + for (auto *plain : plains) + if (plan_distance(editor_track::point_at(*plain, editor_track::nearest_parameter(*plain, record.lift_from)), record.lift_from) < 1.0 || + plan_distance(editor_track::point_at(*plain, editor_track::nearest_parameter(*plain, record.lift_to)), record.lift_to) < 1.0) + outer = plain; + if (outer == nullptr) + return true; + auto route{editor_track::run_route(*outer, 3000.0)}; + auto const samples{editor_track::sample_route(route, 1.0)}; + if (samples.size() < 2) + return true; + auto const chainage_of = [&](glm::dvec3 const &Point) { + double best{std::numeric_limits::max()}; + double chainage{0.0}; + for (std::size_t i = 0; i + 1 < samples.size(); ++i) + { + auto const a{plan_of(samples[i].position)}; + auto const run{plan_of(samples[i + 1].position) - a}; + auto const t{std::clamp(glm::dot(plan_of(Point) - a, run) / std::max(1e-12, glm::dot(run, run)), 0.0, 1.0)}; + if (auto const distance{glm::length(plan_of(Point) - (a + run * t))}; distance < best) + { + best = distance; + chainage = samples[i].chainage + (samples[i + 1].chainage - samples[i].chainage) * t; + } + } + return chainage; + }; + auto c1{chainage_of(record.lift_from)}; + auto c2{chainage_of(record.lift_to)}; + if (c1 > c2) + std::swap(c1, c2); + auto const ramp{record.ramp}; + auto const share = [&](double const Chainage) { + if (Chainage <= c1 - ramp || Chainage >= c2 + ramp) + return 0.0; + if (Chainage < c1) + return (Chainage - (c1 - ramp)) / ramp; + if (Chainage > c2) + return ((c2 + ramp) - Chainage) / ramp; + return 1.0; + }; + auto const slope = [&](double const Chainage) { + if (Chainage > c1 - ramp && Chainage < c1) + return 1.0 / ramp; + if (Chainage > c2 && Chainage < c2 + ramp) + return -1.0 / ramp; + return 0.0; + }; + std::vector breaks; + for (auto const chainage : {c1 - ramp, c1, c2, c2 + ramp}) + if (chainage > 0.5 && chainage < route.length - 0.5) + breaks.push_back(chainage); + profile_take(route, route.spans.front().track, route.spans.back().track); + auto const before{m_profile.line}; + editor_track::apply_profile( + route, [&](double const Chainage) { return editor_track::sampled_elevation(samples, Chainage) - record.lift * share(Chainage); }, + [&](double const Chainage) { return editor_track::sampled_grade(samples, Chainage) - record.lift * slope(Chainage); }, breaks, States, Created); + // the design of the outer track down with it, the corners of the ramps left out where the line runs on straight through them + profile_take(route, route.spans.front().track, route.spans.back().track); + std::vector points; + for (auto point : before.points) + { + point.elevation -= record.lift * share(point.chainage); + points.push_back(point); + } + for (std::size_t k = 1; k + 1 < points.size();) + { + auto const in{(points[k].elevation - points[k - 1].elevation) / std::max(1e-6, points[k].chainage - points[k - 1].chainage)}; + auto const out{(points[k + 1].elevation - points[k].elevation) / std::max(1e-6, points[k + 1].chainage - points[k].chainage)}; + if (std::abs(out - in) < 1e-7) + points.erase(points.begin() + static_cast(k)); + else + ++k; + } + if (points.size() >= 2) + { + m_profile.line = before; + m_profile.line.points = std::move(points); + m_profile.selected = -1; + profile_check(); + profile_store(); + } + WriteLog(format("Editor: curved crossover - the outer track %s down by %.3f m again", outer->name().c_str(), record.lift), logtype::generic); + return true; +} + +// the crossover along a curve moved along it by the distance, from its first switch towards the second: taken out and laid again +bool editor_mode::slide_curved_connection(TTrack &Switch, double const Distance) +{ + auto const crossover{crossover_of(Switch)}; + if (false == crossover.has_value()) + return false; + auto &tool{m_switch}; + auto const shape{crossover_template(crossover->record)}; + if (shape < 0) + { + tool.status = STR_C("The template the crossover was laid of isn't among the templates"); + ui()->set_status(tool.status, true); + return false; + } + std::vector const names{crossover->first->name(), crossover->second->name()}; + auto const firststart{crossover->first->m_paths.front().points[segment_data::point::start]}; + auto const firstend{crossover->first->m_paths.front().points[segment_data::point::end]}; + auto const secondstart{crossover->second->m_paths.front().points[segment_data::point::start]}; + std::vector> states; + std::vector created; + std::vector removed; + TTrack *first{nullptr}; + TTrack *second{nullptr}; + if (false == take_out_crossover(*crossover, states, created, removed, first, second)) + return false; + // the new places of the press and of the release, along the two curves, or the two straights + std::vector firstframe, secondframe; + std::vector firsttracks, secondtracks; + glm::dvec3 press, release; + if (curve_of(*first, m_straights.tolerance, m_route.design.norms.gauge, firstframe, firsttracks) && curve_of(*second, m_straights.tolerance, m_route.design.norms.gauge, secondframe, secondtracks)) + { + auto const towards{nearest_station(firstframe, firstend) >= nearest_station(firstframe, firststart) ? 1.0 : -1.0}; + press = sample_at(firstframe, nearest_station(firstframe, firststart) + towards * Distance).position; + auto const ahead{nearest_station(secondframe, secondstart) >= nearest_station(secondframe, firststart) ? 1.0 : -1.0}; + release = sample_at(secondframe, nearest_station(secondframe, secondstart) + ahead * Distance).position; + } + else + { + auto direction{plan_of(firstend - firststart)}; + direction = glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0}; + press = firststart + glm::dvec3{direction.x, 0.0, direction.y} * Distance; + release = secondstart + glm::dvec3{direction.x, 0.0, direction.y} * Distance; + firsttracks = {first}; + } + switch_store::follow(); + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + records_to_history(); + // laid again + if (false == lay_crossover(shape, press, release, 3.0, names)) + { + auto const reason{tool.status}; + undo_last(); + tool.status = reason.empty() ? std::string{STR_C("The crossover doesn't fit there")} : reason; + ui()->set_status(tool.status, true); + return false; + } + join_last_steps(); + tool.status = format(STR_C("Crossover moved %.2f m"), Distance) + "\n" + tool.status; + ui()->set_status(tool.status, false); + return true; +} + +std::size_t editor_mode::follow_crossovers(std::vector const &Before) +{ + auto &state{m_profile}; + auto const changedroute{state.route}; + auto *changedfrom{state.from}; + auto *changedto{state.to}; + auto const after{state.samples}; + auto const changedline{state.line}; + if (Before.size() < 2 || after.size() < 2) + return 0; + double constexpr follow{25.0}; + double constexpr leave{50.0}; + // how much the route went up at the point beside it, and how far the point is from it + auto const change_at = [&](glm::dvec3 const &Point, double &Distance, double &Chainage) { + Distance = std::numeric_limits::max(); + Chainage = 0.0; + for (std::size_t i = 0; i + 1 < after.size(); ++i) + { + auto const a{plan_of(after[i].position)}; + auto const run{plan_of(after[i + 1].position) - a}; + auto const t{std::clamp(glm::dot(plan_of(Point) - a, run) / std::max(1e-12, glm::dot(run, run)), 0.0, 1.0)}; + if (auto const distance{glm::length(plan_of(Point) - (a + run * t))}; distance < Distance) + { + Distance = distance; + Chainage = after[i].chainage + (after[i + 1].chainage - after[i].chainage) * t; + } + } + auto const weight{std::clamp((leave - Distance) / (leave - follow), 0.0, 1.0)}; + return weight * (editor_track::sampled_elevation(after, Chainage) - editor_track::sampled_elevation(Before, Chainage)); + }; + std::vector done; + std::size_t followed{0}; + for (auto const &span : changedroute.spans) + { + if (span.track == nullptr || span.track->eType != tt_Switch) + continue; + auto const crossover{crossover_of(*span.track)}; + if (false == crossover.has_value()) + continue; + auto *other{crossover->first == span.track ? crossover->second : crossover->first}; + if (std::any_of(changedroute.spans.begin(), changedroute.spans.end(), [&](auto const &Span) { return Span.track == other; }) || std::find(done.begin(), done.end(), other) != done.end()) + continue; + done.push_back(other); + auto route{editor_track::run_route(*other, 3000.0)}; + auto const samples{editor_track::sample_route(route, 1.0)}; + if (samples.size() < 2) + continue; + // the change of each point of the other line + std::vector changes(samples.size(), 0.0); + std::vector distances(samples.size(), 0.0); + bool any{false}; + for (std::size_t i = 0; i < samples.size(); ++i) + { + double chainage; + changes[i] = change_at(samples[i].position, distances[i], chainage); + any = any || std::abs(changes[i]) > 0.001; + } + if (false == any) + continue; + auto const change = [&](double const Chainage) { + auto const at{std::clamp(Chainage, samples.front().chainage, samples.back().chainage)}; + auto const next{std::lower_bound(samples.begin(), samples.end(), at, [](auto const &Sample, double const Value) { return Sample.chainage < Value; })}; + if (next == samples.begin()) + return changes.front(); + if (next == samples.end()) + return changes.back(); + auto const i{static_cast(std::distance(samples.begin(), next))}; + auto const f{(at - samples[i - 1].chainage) / std::max(1e-9, samples[i].chainage - samples[i - 1].chainage)}; + return changes[i - 1] + (changes[i] - changes[i - 1]) * f; + }; + // where the grade line of the route breaks, the other line breaks too + std::vector breaks; + for (auto const &point : changedline.points) + { + auto const at{editor_track::sampled_position(after, point.chainage)}; + double best{std::numeric_limits::max()}; + double chainage{0.0}; + for (auto const &sample : samples) + if (auto const distance{plan_distance(sample.position, at)}; distance < best) + { + best = distance; + chainage = sample.chainage; + } + if (best < follow && chainage > 0.5 && chainage < route.length - 0.5) + breaks.push_back(chainage); + } + // and where it starts and stops running beside the route + for (std::size_t i = 1; i < samples.size(); ++i) + for (auto const bound : {follow, leave}) + if ((distances[i - 1] < bound) != (distances[i] < bound) && samples[i].chainage > 0.5 && samples[i].chainage < route.length - 0.5) + breaks.push_back(samples[i].chainage); + std::sort(breaks.begin(), breaks.end()); + breaks.erase(std::unique(breaks.begin(), breaks.end(), [](double const A, double const B) { return std::abs(A - B) < 1.0; }), breaks.end()); + profile_take(route, route.spans.front().track, route.spans.back().track); + auto const designed{m_profile.line}; + std::vector> states; + std::vector created; + editor_track::apply_profile( + route, [&](double const Chainage) { return editor_track::sampled_elevation(samples, Chainage) + change(Chainage); }, + [&](double const Chainage) { return editor_track::sampled_grade(samples, Chainage) + (change(Chainage + 0.5) - change(Chainage - 0.5)); }, breaks, states, created); + profile_library_load(); + auto library{m_profiles}; + push_track_snapshot(std::move(states), std::move(created)); + m_history.back().profiles = true; + m_history.back().profile_library = std::move(library); + join_last_steps(); + // its design changed by as much + profile_take(route, route.spans.front().track, route.spans.back().track); + std::vector points; + for (auto point : designed.points) + { + point.elevation += change(point.chainage); + points.push_back(point); + } + for (auto const chainage : breaks) + { + profile::pvi point; + point.chainage = chainage; + point.elevation = profile::elevation(designed, chainage) + change(chainage); + points.push_back(point); + } + std::sort(points.begin(), points.end(), [](auto const &Left, auto const &Right) { return Left.chainage < Right.chainage; }); + std::vector line; + for (auto const &point : points) + if (line.empty() || point.chainage - line.back().chainage >= 0.5) + line.push_back(point); + if (line.size() >= 2) + { + m_profile.line = designed; + m_profile.line.points = std::move(line); + m_profile.selected = -1; + profile_check(); + profile_store(); + } + ++followed; + WriteLog(format("Editor: vertical profile - the track %s across the crossover %s followed the change", other->name().c_str(), crossover->first->name().c_str()), logtype::generic); + } + if (followed > 0) + { + // the route the user works on open again + profile_take(changedroute, changedfrom, changedto); + } + return followed; +} + +int editor_mode::crossover_template(switch_store::record const &Record) +{ + auto &tool{m_switch}; + show_track_tab(track_tab::turnout); + arm_switch(); + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + { + auto const &candidate{tool.templates[i]}; + if (candidate.source == nullptr && false == candidate.double_slip && false == candidate.twofold() && std::abs(candidate.radius - Record.radius) < 0.5 && std::abs(candidate.ratio - Record.ratio) < 0.01 && (Record.rail == "-" || candidate.rail == Record.rail)) + return i; + } + // switches of no template of the catalogue, as the digitized ones of the old sceneries: the first one is the template + if (Record.source != "measured" || Record.parts.empty()) + return -1; + auto own{editor_track::template_of_switch(simulation::Paths.find(Record.parts.front()))}; + if (false == own.has_value()) + return -1; + tool.templates.push_back(std::move(*own)); + return static_cast(tool.templates.size()) - 1; +} + +bool editor_mode::lay_crossover(int const Shape, glm::dvec3 const &Press, glm::dvec3 const &Release, double const Reach, std::vector const &Names) +{ + auto &tool{m_switch}; + tool.keep_names = Names; + struct forget_names + { + std::vector &names; + ~forget_names() { names.clear(); } + } const forget{tool.keep_names}; + // the plain path the press falls on + TTrack *closest{nullptr}; + auto nearest{Reach}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + if (auto const distance{plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, Press)), Press)}; distance < nearest) + { + nearest = distance; + closest = track; + } + } + if (closest == nullptr || Shape < 0) + return false; + show_track_tab(track_tab::turnout); + arm_switch(); + tool.armed = Shape; + tool.connection = static_cast(connection::kind::crossover); + tool.connection_second = -1; + m_node = closest; + ui()->set_node(m_node); + // the press onto the path, where the line went sideways + auto const press{editor_track::point_at(*closest, editor_track::nearest_parameter(*closest, Press))}; + m_cursor_override = press; + auto const started{start_switch_placement()}; + m_cursor_override.reset(); + if (false == started) + return false; + tool.mouse = {Release.x, tool.point.y, Release.z}; + auto const steps{m_history.size()}; + if (tool.curved) + insert_curved_connection(); + else + insert_connection(); + if (m_history.size() == steps) + return false; + // both switches joined to their tracks at the start and at the end of the main track, else the line under them moved + // and it's taken back + for (auto *track : std::vector{m_node != nullptr ? dynamic_cast(m_node) : nullptr}) + if (auto const crossover{track != nullptr ? crossover_of(*track) : std::nullopt}) + for (auto *part : {crossover->first, crossover->second}) + for (auto const which : {segment_data::point::start, segment_data::point::end}) + { + auto const joined{editor_track::connected_points(*part, part->m_paths.front().points[which])}; + if (std::none_of(joined.begin(), joined.end(), [&](auto const &Entry) { return Entry.first != part && Entry.first != nullptr && false == Entry.first->m_editorremoved; })) + { + undo_last(); + tool.status = STR_C("The crossover wouldn't join the tracks there"); + return false; + } + } + return true; +} diff --git a/application/editormodedigitize.cpp b/application/editormodedigitize.cpp new file mode 100644 index 00000000..9eb5f83e --- /dev/null +++ b/application/editormodedigitize.cpp @@ -0,0 +1,423 @@ +/* +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/. +*/ + +// the digitization of a scenery made without the editor: its lines kept with their designs, its switches with their types out of +// the catalogue, its crossovers as connections and the objects along the tracks bound to them, at one go + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorConnections.hpp" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorSwitchStore.hpp" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/translation.h" +#include "world/Track.h" +#include +#include +#include +#include + +namespace +{ + +using geometry::bezier; +using geometry::plan_of; +using geometry::signed_angle; + +// how far the size of a switch may be from the template to be of it: the length of the main and the diverging track, the angle +double constexpr kMainTolerance{0.10}; +double constexpr kBranchTolerance{0.15}; +double constexpr kAngleTolerance{0.0025}; +// a plain track this short between the ends of two switches is a part of a slip +double constexpr kSlipLink{6.0}; +// the longest insert of a crossover +double constexpr kInsertLongest{150.0}; +// the tracks a crossover joins lie this far apart +double constexpr kSpacingLeast{2.5}; +double constexpr kSpacingMost{15.0}; +// radii of the diverging track the switches are made with; a measured one this close to one of them is taken for it +double constexpr kNominalRadii[]{150.0, 190.0, 205.0, 230.0, 265.0, 300.0, 500.0, 760.0, 1200.0}; +double constexpr kNominalNear{0.04}; + +double nominal_radius(double const Radius) +{ + for (auto const nominal : kNominalRadii) + if (std::abs(Radius - nominal) <= kNominalNear * nominal) + return nominal; + return std::round(Radius); +} + +std::vector parts_of(segment_data const &Path) +{ + std::vector parts; + if (Path.pieces.empty()) + parts.push_back(&Path); + else + for (auto const &piece : Path.pieces) + parts.push_back(&piece); + return parts; +} + +double path_length(segment_data const &Path) +{ + double length{0.0}; + for (auto const *part : parts_of(Path)) + length += bezier{*part}.plan_length(); + return length; +} + +glm::dvec2 heading(segment_data const &Path, bool const End) +{ + auto const parts{parts_of(Path)}; + bezier const curve{End ? *parts.back() : *parts.front()}; + auto const direction{plan_of(curve.first(End ? 1.0 : 0.0))}; + if (glm::length(direction) > 1e-9) + return glm::normalize(direction); + auto const chord{plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])}; + return glm::length(chord) > 1e-9 ? glm::normalize(chord) : glm::dvec2{0.0, 1.0}; +} + +// in the plan, + to the left, at the middle of the longest part of the path +double curvature(segment_data const &Path) +{ + segment_data const *longest{nullptr}; + double length{-1.0}; + for (auto const *part : parts_of(Path)) + if (auto const l{bezier{*part}.plan_length()}; l > length) + { + length = l; + longest = part; + } + if (longest == nullptr || false == geometry::has_controls(*longest)) + return 0.0; + bezier const curve{*longest}; + auto const first{plan_of(curve.first(0.5))}; + auto const second{plan_of(curve.second(0.5))}; + auto const speed{glm::length(first)}; + return speed > 1e-9 ? (first.x * second.y - first.y * second.x) / (speed * speed * speed) : 0.0; +} + +struct switch_size +{ + double main{0.0}; + double branch{0.0}; + double angle{0.0}; // between the ends of the two tracks + double radius{0.0}; // of the diverging track of the straight switch the switch is made of +}; + +switch_size size_of(segment_data const &Main, segment_data const &Branch) +{ + switch_size size; + size.main = path_length(Main); + size.branch = path_length(Branch); + size.angle = std::abs(signed_angle(heading(Main, true), heading(Branch, true))); + auto const bend{std::abs(curvature(Branch) - curvature(Main))}; + size.radius = bend > 1e-6 ? 1.0 / bend : 0.0; + return size; +} + +// the template of the catalogue the size is of: the rail of the track and a movable nose decide between the same shapes +int matching(std::vector const &Templates, std::vector> const &Sizes, switch_size const &Size, std::string const &Rail, bool const Movable) +{ + int best{-1}; + auto score{std::numeric_limits::max()}; + for (auto const &[index, size] : Sizes) + { + auto const &shape{Templates[index]}; + auto const main{std::abs(size.main - Size.main)}; + auto const branch{std::abs(size.branch - Size.branch)}; + auto const angle{std::abs(size.angle - Size.angle)}; + if (main > kMainTolerance || branch > kBranchTolerance || angle > kAngleTolerance) + continue; + auto value{main / kMainTolerance + branch / kBranchTolerance + angle / kAngleTolerance}; + if (false == Rail.empty() && shape.rail != Rail) + value += 10.0; + if (shape.movable != Movable) + value += 5.0; + if (value < score) + { + score = value; + best = index; + } + } + return best; +} + +bool usable_switch(TTrack const *Track) +{ + return Track != nullptr && false == Track->m_editorremoved && Track->eType == tt_Switch && Track->m_paths.size() == 2; +} + +bool named(TTrack const &Track) +{ + return false == Track.name().empty() && Track.name() != "none"; +} + +glm::dvec3 far_end(segment_data const &Path, editor_track::point_kind const Kind) +{ + return Path.points[Kind == editor_track::point_kind::start ? segment_data::point::end : segment_data::point::start]; +} + +// the end of the path meets an end of another switch, right there or over a short plain track: the paths of a slip do +bool towards_switch(TTrack const &Track, glm::dvec3 const &Point) +{ + for (auto const &[other, point] : editor_track::connected_points(Track, Point)) + { + if (other == nullptr || other == &Track || other->m_editorremoved) + continue; + if (other->eType == tt_Switch && point.kind == editor_track::point_kind::end) + return true; + if (other->eType != tt_Normal || other->m_paths.size() != 1 || path_length(other->m_paths.front()) > kSlipLink) + continue; + for (auto const &[next, nextpoint] : editor_track::connected_points(*other, far_end(other->m_paths.front(), point.kind))) + if (next != nullptr && next != other && next != &Track && next->eType == tt_Switch && nextpoint.kind == editor_track::point_kind::end) + return true; + } + return false; +} + +bool slip_part(TTrack const &Switch) +{ + return towards_switch(Switch, Switch.m_paths[0].points[segment_data::point::end]) && towards_switch(Switch, Switch.m_paths[1].points[segment_data::point::end]); +} + +} // namespace + +void editor_mode::digitize_switches(digitize_report &Report, bool const Write) +{ + auto const templates{editor_track::standard_switch_templates()}; + std::vector> sizes; + for (int i = 0; i < static_cast(templates.size()); ++i) + { + auto const &shape{templates[i]}; + if (shape.source == nullptr && false == shape.double_slip && false == shape.twofold()) + sizes.emplace_back(i, size_of(shape.local[0], shape.local[1])); + } + for (auto *track : simulation::Paths.sequence()) + { + if (false == usable_switch(track)) + continue; + ++Report.switches; + if (switch_store::named(*track).has_value()) + { + ++Report.known; + continue; + } + if (slip_part(*track)) + { + ++Report.slips; + continue; + } + auto const &main{track->m_paths[0]}; + auto const &branch{track->m_paths[1]}; + if (geometry::plan_distance(main.points[segment_data::point::start], branch.points[segment_data::point::start]) > geometry::same_point) + { + ++Report.odd; + Report.notes.push_back(track->name() + ": " + STR("the tracks of the switch don't start at the same point")); + continue; + } + auto const size{size_of(main, branch)}; + auto const movable{track->SwitchExtension != nullptr && track->SwitchExtension->m_movablenose}; + auto const found{matching(templates, sizes, size, editor_track::rail_of(track), movable)}; + editor_track::switch_template shape; + if (found >= 0) + { + shape = templates[found]; + ++Report.designed; + ++Report.types[shape.label.substr(0, shape.label.find(" "))]; + } + else + { + shape.source = track; + shape.radius = nominal_radius(size.radius); + shape.ratio = size.angle > 1e-6 ? 1.0 / std::tan(size.angle) : 0.0; + ++Report.measured; + ++Report.shapes[format("L %.2f / %.2f 1:%.2f R %.0f", size.main, size.branch, shape.ratio, shape.radius)]; + } + if (false == named(*track)) + { + // the record finds the switch by its name; the names of the scenery stay as they are + ++Report.unnamed; + continue; + } + if (Write && found >= 0) + record_switch(*track, shape); + else if (Write) + { + switch_store::record entry; + entry.kind = "switch"; + entry.name = track->name(); + entry.parts = {entry.name}; + entry.radius = shape.radius; + entry.ratio = shape.ratio; + entry.source = "measured"; + auto const rail{editor_track::rail_of(track)}; + entry.rail = rail.empty() ? std::string{"-"} : rail; + switch_store::place(entry, *track); + switch_store::classify(entry, *track); + switch_store::put(entry, alignment_store::layer_of(*track)); + } + ++Report.recorded; + } +} + +void editor_mode::digitize_crossovers(digitize_report &Report, bool const Write) +{ + std::set> seen; + for (auto *first : simulation::Paths.sequence()) + { + if (false == usable_switch(first) || slip_part(*first)) + continue; + auto const &branch{first->m_paths[1]}; + auto const end{branch.points[segment_data::point::end]}; + // the insert: plain tracks from the end of the diverging track up to the end of the diverging track of another switch + std::vector insert; + TTrack *second{nullptr}; + auto *from{static_cast(first)}; + auto point{end}; + double length{0.0}; + bool direct{false}; + while (second == nullptr && length <= kInsertLongest) + { + TTrack *next{nullptr}; + editor_track::point_ref at; + for (auto const &[other, ref] : editor_track::connected_points(*from, point)) + { + if (other == nullptr || other == from || other->m_editorremoved || std::find(insert.begin(), insert.end(), other) != insert.end()) + continue; + if (usable_switch(other) && other != first && ref.path == 1 && ref.kind == editor_track::point_kind::end) + { + second = other; + direct = insert.empty(); + break; + } + if (other->eType == tt_Normal && other->m_paths.size() == 1) + { + next = other; + at = ref; + } + } + if (second != nullptr || next == nullptr) + break; + insert.push_back(next); + length += path_length(next->m_paths.front()); + point = far_end(next->m_paths.front(), at.kind); + from = next; + } + if (second == nullptr || slip_part(*second)) + continue; + if (seen.count({second, first}) > 0) + continue; + seen.insert({first, second}); + // the two switches face each other from two tracks side by side + auto const along{heading(first->m_paths[0], false)}; + auto const back{heading(second->m_paths[0], false)}; + if (glm::dot(along, back) > -0.9) + continue; + auto const spacing{std::abs(geometry::cross(along, plan_of(second->m_paths[0].points[segment_data::point::start] - first->m_paths[0].points[segment_data::point::start])))}; + if (spacing < kSpacingLeast || spacing > kSpacingMost) + continue; + ++Report.crossovers; + auto const name = [](TTrack const *Track) { return Track->name().empty() ? STR("(no name)") : Track->name(); }; + auto const note = [&](std::string const &Why) { Report.notes.push_back(name(first) + " + " + name(second) + ": " + Why); }; + if (false == switch_store::connections(*first).empty()) + { + ++Report.crossovers_known; + continue; + } + if (direct || insert.size() != 1) + { + ++Report.crossovers_odd; + note(direct ? STR("the diverging tracks meet without an insert between them") : format(STR_C("the insert is made of %zu paths"), insert.size())); + continue; + } + auto const one{switch_store::named(*first)}; + auto const other{switch_store::named(*second)}; + if (Write && (false == one.has_value() || false == other.has_value())) + { + ++Report.crossovers_odd; + note(STR("a switch of it has no type kept")); + continue; + } + if (one.has_value() && other.has_value() && (std::abs(one->radius - other->radius) > kNominalNear * one->radius || std::abs(one->ratio - other->ratio) > kNominalNear * one->ratio)) + { + ++Report.crossovers_odd; + note(format(STR_C("the switches differ: %s and %s"), switch_store::label(*one).c_str(), switch_store::label(*other).c_str())); + continue; + } + if (Write) + { + auto entry{*one}; + entry.kind = "connection"; + entry.name = first->name(); + entry.parts = {first->name(), second->name()}; + entry.number = "-"; + switch_store::place(entry, *first); + switch_store::classify(entry, *first); + entry.type = connection::word(connection::kind::crossover); + entry.a = spacing; + entry.b = length; + switch_store::put(entry, alignment_store::layer_of(*first)); + WriteLog(format("Editor: digitizing - crossover %s + %s kept, %s, spacing %.2f m, insert %.2f m", first->name().c_str(), second->name().c_str(), switch_store::label(entry).c_str(), spacing, length), logtype::generic); + } + ++Report.crossovers_recorded; + } +} + +std::string editor_mode::digitize_scenery(bool const Write) +{ + digitize_report report; + if (Write) + digitize_all(&report.lines, &report.lines_failed); + digitize_switches(report, Write); + digitize_crossovers(report, Write); + if (Write) + { + switch_store::settle(); + auto const scope{m_infra.scope}; + m_infra.scope = 3; + infra_recognize(); + report.found = static_cast(m_infra.candidates.size()); + auto const before{m_bindings.size()}; + infra_bind_chosen(); + report.bound = static_cast(m_bindings.size() - before); + m_infra.scope = scope; + } + auto text{format(STR_C("Switches: %d, kept already %d, of the catalogue %d, measured %d, parts of slips %d, odd %d"), report.switches, report.known, report.designed, report.measured, report.slips, report.odd)}; + if (report.unnamed > 0) + text += format(STR_C(", without a name, left out %d"), report.unnamed); + text += "\n" + format(STR_C("Crossovers: %d, kept already %d, recorded %d, left out %d"), report.crossovers, report.crossovers_known, report.crossovers_recorded, report.crossovers_odd); + if (Write) + { + text = format(STR_C("Lines digitized: %d, paths left out: %d"), report.lines, report.lines_failed) + "\n" + text; + text += "\n" + format(STR_C("Objects by the tracks: %d found, %d bound"), report.found, report.bound); + } + WriteLog("Editor: digitizing the scenery\n" + text, logtype::generic); + std::vector> types; + for (auto const &[label, count] : report.types) + types.emplace_back(count, label); + std::sort(types.rbegin(), types.rend()); + for (auto const &[count, label] : types) + WriteLog(format("Editor: digitizing - %4d x %s", count, label.c_str()), logtype::generic); + std::vector> shapes; + for (auto const &[label, count] : report.shapes) + shapes.emplace_back(count, label); + std::sort(shapes.rbegin(), shapes.rend()); + for (std::size_t i = 0; i < std::min(shapes.size(), 20); ++i) + WriteLog(format("Editor: digitizing - measured %4d x %s", shapes[i].first, shapes[i].second.c_str()), logtype::generic); + for (std::size_t i = 0; i < std::min(report.notes.size(), 30); ++i) + WriteLog("Editor: digitizing - " + report.notes[i], logtype::generic); + if (Write) + ui()->set_status(text, report.odd + report.crossovers_odd + report.lines_failed > 0); + return text; +} diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 8ccdb39f..84af9df3 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -438,7 +438,7 @@ bool edit_points(std::vector &Points) bool edited{false}; bool inserted{false}; int remove{-1}; - ImGui::Columns(3, STR_C("gaugeoutline"), false); + ImGui::Columns(3, "gaugeoutline", false); ImGui::SetColumnWidth(0, 110.0f); ImGui::SetColumnWidth(1, 110.0f); ImGui::TextDisabled(STR_C("Half-width")); @@ -483,6 +483,60 @@ bool edit_points(std::vector &Points) return edited; } +// the railway outline drawn across the track at the size of the panel, so the name in the list has a shape +void gauge_sketch(gauge::profile const &Profile) +{ + auto const width{ImGui::GetContentRegionAvail().x}; + auto const height{std::round(ImGui::GetFontSize() * 9.0f)}; + auto const low{ImGui::GetCursorScreenPos()}; + ImGui::Dummy(ImVec2(width, height)); + if (Profile.outline.size() < 2) + return; + double top{0.0}; + double half{0.0}; + for (auto const &point : Profile.outline) + { + top = std::max(top, point.height); + half = std::max(half, point.half_width); + } + if (top <= 0.0 || half <= 0.0) + return; + auto const pad{ImGui::GetFontSize() * 1.2f}; + auto const scale{static_cast(std::min((height - pad * 2.0f) / top, (width - pad * 2.0f) / (half * 2.0)))}; + ImVec2 const base{std::round(low.x + width * 0.5f), std::round(low.y + height - pad)}; + auto const at = [&](double const X, double const Y) { return ImVec2(base.x + static_cast(X) * scale, base.y - static_cast(Y) * scale); }; + auto *draw{ImGui::GetWindowDrawList()}; + draw->AddRectFilled(low, ImVec2(low.x + width, low.y + height), ImGui::GetColorU32(ImGuiCol_FrameBg), 4.0f); + // ballast, a sleeper and the two rails + ImVec2 const ballast[]{at(-2.1, -0.6), at(2.1, -0.6), at(1.55, -0.2), at(-1.55, -0.2)}; + draw->AddConvexPolyFilled(ballast, 4, IM_COL32(110, 100, 90, 255)); + draw->AddRectFilled(at(-1.3, -0.2), at(1.3, -0.13), IM_COL32(150, 150, 150, 255)); + for (double const side : {-0.7175, 0.7175}) + draw->AddRectFilled(at(side - 0.04, -0.13), at(side + 0.04, 0.0), IM_COL32(210, 210, 215, 255)); + // the outline, the right half from the bottom up and its mirror back down + std::vector points; + for (auto const &point : Profile.outline) + points.push_back(at(point.half_width, point.height)); + for (auto point{Profile.outline.rbegin()}; point != Profile.outline.rend(); ++point) + points.push_back(at(-point->half_width, point->height)); + auto const accent{IM_COL32(120, 200, 255, 255)}; + auto const flags{draw->Flags}; + draw->Flags &= ~ImDrawListFlags_AntiAliasedFill; + draw->AddConcavePolyFilled(points.data(), static_cast(points.size()), IM_COL32(120, 200, 255, 40)); + draw->Flags = flags; + draw->AddPolyline(points.data(), static_cast(points.size()), accent, ImDrawFlags_Closed, 2.0f); + // the height over the top and the widest half at its height + auto const widest{std::max_element(Profile.outline.begin(), Profile.outline.end(), [](auto const &A, auto const &B) { return A.half_width < B.half_width; })}; + auto const tall{format("%.2f m", top)}; + auto const size{ImGui::CalcTextSize(tall.c_str())}; + draw->AddText(ImVec2(std::round(base.x - size.x * 0.5f), std::round(at(0.0, top).y - size.y - 2.0f)), accent, tall.c_str()); + auto const wide{format("%.2f m", widest->half_width * 2.0)}; + auto const y{at(0.0, widest->height).y}; + draw->AddLine(ImVec2(at(-widest->half_width, 0.0).x, y), ImVec2(at(widest->half_width, 0.0).x, y), IM_COL32(255, 220, 120, 160), 1.0f); + auto const wsize{ImGui::CalcTextSize(wide.c_str())}; + draw->AddText(ImVec2(std::round(base.x - wsize.x * 0.5f), std::round(y - wsize.y - 1.0f)), IM_COL32(255, 220, 120, 255), wide.c_str()); +} + } // namespace struct gauge_scan @@ -715,13 +769,15 @@ void editor_mode::render_gauge_window() bool changed{render_gauge_choice("Railway", m_gauge.profile, false)}; if (ImGui::IsItemHovered()) ImGui::SetTooltip(STR_C("GPL: unified structure gauge, widened only in the curves under 250 m.\nG1, G2, GA, GB, GC: limit (GSZ) and nominal (NSZ) installation gauges, widened by 3750/R in all curves.")); + if (m_gauge.profile >= 0) + gauge_sketch(m_gauge.profiles[m_gauge.profile]); changed |= render_gauge_choice("Road", m_gauge.road, true); if (ImGui::IsItemHovered()) ImGui::SetTooltip(STR_C("Clearance over the roads, from 0.5 m beyond the edges of the carriageway")); if (m_gauge.scan) { - auto const label{format("Scanning the map %.0f%%, %zu found", m_gauge.scan->progress * 100.f, m_gauge.map.hits.size())}; + auto const label{format(STR_C("Scanning the scenery %.0f%%, %zu found"), m_gauge.scan->progress * 100.f, m_gauge.map.hits.size())}; ImGui::ProgressBar(m_gauge.scan->progress, ImVec2(-70.0f, 0.0f), label.c_str()); ImGui::SameLine(); if (ImGui::SmallButton(STR_C("Cancel"))) @@ -729,7 +785,7 @@ void editor_mode::render_gauge_window() if (ImGui::IsItemHovered()) ImGui::SetTooltip(STR_C("Stops the scan and keeps the models found so far")); } - else if (ImGui::Button(STR_C("Find the violations on the whole map"))) + else if (ImGui::Button(STR_C("Find the violations in the whole scenery"))) scan_gauge_map(); else if (ImGui::IsItemHovered()) ImGui::SetTooltip(STR_C("Checks the models along all tracks of the scenery against the chosen gauge\nand lists them, the deepest first.\nRuns in the background, the list fills up as it goes")); @@ -760,10 +816,10 @@ void editor_mode::render_gauge_hits() return; if (hits.empty()) { - ImGui::TextDisabled(m_gauge.map.scanned ? STR_C("No model enters the gauge on the map") : STR_C("The gauge is clear along this line")); + ImGui::TextDisabled("%s", m_gauge.map.scanned ? STR_C("No model in the scenery enters the gauge") : STR_C("The gauge is clear along this line")); return; } - ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), STR_C("%zu models enter the gauge%s"), hits.size(), m_gauge.map.scanned ? STR_C(" on the map") : ""); + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), STR_C("%zu models enter the gauge%s"), hits.size(), m_gauge.map.scanned ? STR_C(" in the whole scenery") : ""); if (ImGui::ArrowButton("##gaugeprevious", ImGuiDir_Left)) gauge_focus(m_gauge.current - 1); ImGui::SameLine(); @@ -778,7 +834,7 @@ void editor_mode::render_gauge_hits() for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) { auto const &hit{hits[i]}; - auto const label{(hit.model.empty() ? std::string{"(unnamed)"} : hit.model) + " " + std::to_string(static_cast(std::lround(hit.depth * 100.0))) + " cm##" + std::to_string(i)}; + auto const label{(hit.model.empty() ? STR("(no name)") : hit.model) + " " + std::to_string(static_cast(std::lround(hit.depth * 100.0))) + " cm##" + std::to_string(i)}; if (ImGui::Selectable(label.c_str(), m_gauge.current == i)) gauge_focus(i); } @@ -788,8 +844,12 @@ void editor_mode::render_gauge_hits() bool editor_mode::render_gauge_choice(char const *Label, int &Index, bool const Road) { bool changed{false}; - ImGui::SetNextItemWidth(-60.0f); - if (ImGui::BeginCombo(Label, Index >= 0 ? m_gauge.profiles[Index].name.c_str() : STR_C("none"))) + ImGui::PushID(Label); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(STR_C(Label)); + ImGui::SameLine(std::round(ImGui::GetCursorStartPos().x + std::max(ImGui::CalcTextSize(STR_C("Railway")).x, ImGui::CalcTextSize(STR_C("Road")).x) + ImGui::GetStyle().ItemSpacing.x)); + ImGui::SetNextItemWidth(-FLT_MIN); + if (ImGui::BeginCombo("##profile", Index >= 0 ? m_gauge.profiles[Index].name.c_str() : STR_C("(none)"))) { for (int i = 0; i < static_cast(m_gauge.profiles.size()); ++i) if ((m_gauge.profiles[i].kind == gauge::kind::road) == Road && ImGui::Selectable(m_gauge.profiles[i].name.c_str(), i == Index)) @@ -799,6 +859,7 @@ bool editor_mode::render_gauge_choice(char const *Label, int &Index, bool const } ImGui::EndCombo(); } + ImGui::PopID(); return changed; } @@ -810,7 +871,7 @@ bool editor_mode::render_gauge_outline(int const Index) if (false == ImGui::TreeNode(&profile, STR_C("Outline of %s (right half)"), profile.name.c_str())) return false; ImGui::TextDisabled(profile.kind == gauge::kind::road ? STR_C("Metres from the edge of the carriageway and above the road, bottom up.") - : "PKP PLK standard, volume II; below 1170 mm the limit installation gauge.\nMetres from the track axis and above the rail top, bottom up."); + : STR_C("PKP PLK standard, volume II; below 1170 mm the limit installation gauge.\nMetres from the track axis and above the rail top, bottom up.")); bool edited{edit_points(profile.outline)}; if (ImGui::SmallButton(STR_C("Restore the starting outlines"))) { diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index 34a36a95..1840ffe7 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" #include "application/editorprojection.h" +#include "application/editormodeshared.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" @@ -65,6 +66,8 @@ ImU32 colour(infra::category const Category, int const Alpha = 255) case infra::category::catenary: return IM_COL32(80, 200, 255, Alpha); case infra::category::logic: return IM_COL32(220, 120, 255, Alpha); case infra::category::other: return IM_COL32(200, 200, 200, Alpha); + case infra::category::equipment: return IM_COL32(255, 150, 60, Alpha); + case infra::category::environment: return IM_COL32(110, 190, 110, Alpha); } return IM_COL32(200, 200, 200, Alpha); } @@ -123,6 +126,7 @@ struct include_place int yaw{-1}; glm::dvec3 offset{0.0}; std::string description; // name and category of the template + std::string category; // of the description of the template }; bool numeric(std::string const &Text, double &Value) @@ -164,6 +168,7 @@ bool include_place_of(scene::instance_handle const Instance, include_place &Plac Place.yaw = index("rot.y"); Place.offset = included.context.offset; Place.description = info.name + ' ' + info.category; + Place.category = info.category; return Place.position[0] >= 0 && Place.position[1] >= 0 && Place.position[2] >= 0; } @@ -669,7 +674,7 @@ std::optional read_binding(std::string const &Text, scene::layer return std::nullopt; location = Context.to_world(location); infra::binding binding; - binding.group = static_cast(std::clamp(group, 0, static_cast(infra::category::other))); + binding.group = static_cast(std::clamp(group, 0, static_cast(infra::category::environment))); binding.turns = (turns != 0); auto found{false}; if (kindname == infra::name(infra::kind::model)) @@ -963,14 +968,13 @@ void editor_mode::infra_recognize() candidate.offset = Binding.anchors.front().offset; candidate.reason = Reason; candidate.bound = (infra_find(Binding) != nullptr); - candidate.chosen = (false == candidate.bound && Binding.group != infra::category::other); + candidate.chosen = (false == candidate.bound && Binding.group != infra::category::other && Binding.group != infra::category::environment); candidate.binding = std::move(Binding); state.read += candidate.reader != nullptr ? 1 : 0; state.overruled += candidate.nearer != nullptr ? 1 : 0; state.unread += candidate.unread ? 1 : 0; state.candidates.push_back(std::move(candidate)); }; - auto const rule_reason = [](infra::category const Group, char const *What) { return Group == infra::category::other ? std::string{"nearby, no rule matches the "} + What : std::string{"the "} + What + " matches a rule"; }; for (auto *model : simulation::Instances.sequence()) { @@ -981,7 +985,7 @@ void editor_mode::infra_recognize() binding.type = infra::kind::model; binding.node = model; binding.group = infra::classify(model->Model()->NameGet() + ' ' + model->name(), state.rules); - add(binding, rule_reason(binding.group, "file name"), corridor); + add(binding, binding.group == infra::category::other ? STR("nearby, no rule matches the file name") : STR("the file name matches a rule"), corridor); } for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) { @@ -991,8 +995,15 @@ void editor_mode::infra_recognize() infra::binding binding; binding.type = infra::kind::include; binding.include = handle; + // the kind the description of the template names, else by the rules + if (auto const named{infra::category_named(place.category)}) + { + binding.group = *named; + add(binding, STR("the description of the template says what it is"), corridor); + continue; + } binding.group = infra::classify(place.file + ' ' + place.description, state.rules); - add(binding, rule_reason(binding.group, "template or its description"), corridor); + add(binding, binding.group == infra::category::other ? STR("nearby, no rule matches the template or its description") : STR("the template or its description matches a rule"), corridor); } for (auto *piece : simulation::Traction.sequence()) { @@ -1002,7 +1013,7 @@ void editor_mode::infra_recognize() binding.type = infra::kind::traction; binding.node = piece; binding.group = infra::category::catenary; - add(binding, "wire over the track", corridor); + add(binding, STR("wire over the track"), corridor); } for (auto *memcell : simulation::Memory.sequence()) { @@ -1013,7 +1024,7 @@ void editor_mode::infra_recognize() binding.node = memcell; binding.group = infra::category::logic; auto const named{false == memcell->asTrackName.empty() && names.count(memcell->asTrackName) > 0}; - add(binding, named ? "names the path " + memcell->asTrackName : std::string{"nearby"}, named ? std::max(corridor, 50.0) : corridor); + add(binding, named ? format(STR_C("names the path %s"), memcell->asTrackName.c_str()) : STR("nearby"), named ? std::max(corridor, 50.0) : corridor); } for (auto *launcher : simulation::Events.launchers()) { @@ -1023,7 +1034,7 @@ void editor_mode::infra_recognize() binding.type = infra::kind::launcher; binding.node = launcher; binding.group = infra::category::logic; - add(binding, "nearby", corridor); + add(binding, STR("nearby"), corridor); } std::stable_sort(state.candidates.begin(), state.candidates.end(), [](infra_candidate const &A, infra_candidate const &B) { return A.binding.group < B.binding.group; }); state.status = format(STR_C("%d object(s) found along %d path(s)"), static_cast(state.candidates.size()), static_cast(state.tracks.size())); @@ -1417,31 +1428,35 @@ void editor_mode::render_infra_search() char const *const scopes[] = {STR_C("Selected path"), STR_C("Line through the selected path"), STR_C("Route of the vertical profile"), STR_C("Whole scenery")}; ImGui::SetNextItemWidth(260.f); ImGui::Combo(STR_C("Look along"), &state.scope, scopes, IM_ARRAYSIZE(scopes)); - if (state.scope == 1) + if (ImGui::TreeNode(STR_C("More settings"))) { - ImGui::SetNextItemWidth(120.f); - ImGui::InputFloat(STR_C("Reach each way, m"), &state.reach, 100.f, 500.f, "%.0f"); - state.reach = std::clamp(state.reach, 10.f, 20000.f); - } - ImGui::SetNextItemWidth(120.f); - ImGui::InputFloat(STR_C("Corridor each side of the axis, m"), &state.corridor, 0.5f, 2.f, "%.1f"); - state.corridor = std::clamp(state.corridor, 0.5f, 50.f); - if (ImGui::TreeNode(STR_C("Rules for names of the files"))) - { - ImGui::TextDisabled(STR_C("Parts of the names, separated by spaces; the first rule which matches decides")); - for (std::size_t i = 0; i < state.rules.size(); ++i) + if (state.scope == 1) { - auto &rule{state.rules[i]}; - char buffer[512]; - std::snprintf(buffer, sizeof(buffer), "%s", rule.keywords.c_str()); - ImGui::SetNextItemWidth(360.f); - if (ImGui::InputText((std::string{infra::name(rule.group)} + "##rule" + std::to_string(i)).c_str(), buffer, sizeof(buffer))) - rule.keywords = buffer; + ImGui::SetNextItemWidth(120.f); + ImGui::InputFloat(STR_C("Reach each way, m"), &state.reach, 100.f, 500.f, "%.0f"); + state.reach = std::clamp(state.reach, 10.f, 20000.f); + } + ImGui::SetNextItemWidth(120.f); + ImGui::InputFloat(STR_C("Corridor each side of the axis, m"), &state.corridor, 0.5f, 2.f, "%.1f"); + state.corridor = std::clamp(state.corridor, 0.5f, 50.f); + if (ImGui::TreeNode(STR_C("Rules for names of the files"))) + { + ImGui::TextDisabled("%s", STR_C("Parts of the names, separated by spaces; the first rule which matches decides")); + for (std::size_t i = 0; i < state.rules.size(); ++i) + { + auto &rule{state.rules[i]}; + char buffer[512]; + std::snprintf(buffer, sizeof(buffer), "%s", rule.keywords.c_str()); + ImGui::SetNextItemWidth(360.f); + if (ImGui::InputText((std::string{STR_C(infra::name(rule.group))} + "##rule" + std::to_string(i)).c_str(), buffer, sizeof(buffer))) + rule.keywords = buffer; + } + if (ImGui::SmallButton(STR_C("Default rules"))) + state.rules = infra::default_rules(); + ImGui::TextDisabled("%s", STR_C("Includes are matched by the template name and its //$e description, models by the file and node name;\n" + "traction is always the catenary, memory cells and event launchers the logic")); + ImGui::TreePop(); } - if (ImGui::SmallButton(STR_C("Default rules"))) - state.rules = infra::default_rules(); - ImGui::TextDisabled(STR_C("Includes are matched by the template name and its //$e description, models by the file and node name;\n" - "traction is always the catenary, memory cells and event launchers the logic")); ImGui::TreePop(); } if (ImGui::Button(STR_C("Find objects"))) @@ -1487,31 +1502,15 @@ void editor_mode::render_infra_candidates() if (state.read > 0 || state.unread > 0) { - ImGui::TextColored(readcolour, STR_C("%d read by the paths"), state.read); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip(STR_C("The memory cell of the object is read by a getvalues event of a path, listed by the path, inside a multiple\n" - "event or written loose as :event. The object is bound to the thread of that path, not to the nearest one")); - if (state.overruled > 0) - { - ImGui::SameLine(); - ImGui::TextColored(warncolour, STR_C("%d of them nearer to another path"), state.overruled); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip(STR_C("By the distance alone these would go to the wrong thread. Check them in 3D: the dashed line goes to the nearer path")); - } - if (state.unread > 0) - { - ImGui::SameLine(); - ImGui::TextColored(warncolour, STR_C("%d signal(s) not read by any path"), state.unread); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip(STR_C("The signal has a memory cell, but no event of a path reads it: the vehicles won't see it.\nBound to the nearest path")); - } - static char const *const filters[] = {"All", "Read by a path", "Nearer to another path", "Not read"}; - for (int i = 0; i < IM_ARRAYSIZE(filters); ++i) - { - if (i > 0) - ImGui::SameLine(); - ImGui::RadioButton(STR_C(filters[i]), &state.filter, i); - } + std::string const labels[]{format("%s (%d)", STR_C("All"), static_cast(candidates.size())), format("%s (%d)", STR_C("Read by a path"), state.read), + format("%s (%d)", STR_C("Nearer to another path"), state.overruled), format("%s (%d)", STR_C("Not read"), state.unread)}; + choice_chips("infrafilter", state.filter, + {{0, labels[0].c_str()}, + {1, labels[1].c_str(), + "The memory cell of the object is read by a getvalues event of a path, listed by the path, inside a multiple\n" + "event or written loose as :event. The object is bound to the thread of that path, not to the nearest one"}, + {2, labels[2].c_str(), "By the distance alone these would go to the wrong thread. Check them in 3D: the dashed line goes to the nearer path"}, + {3, labels[3].c_str(), "The signal has a memory cell, but no event of a path reads it: the vehicles won't see it.\nBound to the nearest path"}}); } ImGui::BeginChild("##infracandidates", ImVec2(0.f, 260.f), true); @@ -1525,7 +1524,7 @@ void editor_mode::render_infra_candidates() if (rows.empty()) continue; ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(colour(group))); - auto const open{ImGui::TreeNodeEx(format("%s (%d)", infra::name(group), static_cast(rows.size())).c_str(), group == infra::category::other ? 0 : ImGuiTreeNodeFlags_DefaultOpen)}; + auto const open{ImGui::TreeNodeEx(format("%s (%d)", STR_C(infra::name(group)), static_cast(rows.size())).c_str(), group == infra::category::other || group == infra::category::environment ? 0 : ImGuiTreeNodeFlags_DefaultOpen)}; ImGui::PopStyleColor(); ImGui::SameLine(); if (ImGui::SmallButton((std::string{STR_C("all##g")} + infra::name(group)).c_str())) @@ -1556,7 +1555,7 @@ void editor_mode::render_infra_candidates() { state.hovered = i; std::string text{format(STR_C("%s, %s\n%.2f m %s of the axis, %.2f m above the rail top\n%s"), infra::name(candidate.binding.type), anchor.at.track != nullptr ? anchor.at.track->name().c_str() : "-", std::abs(candidate.offset), - candidate.offset >= 0.0 ? "right" : "left", anchor.height, candidate.reason.c_str())}; + candidate.offset >= 0.0 ? STR_C("on the right") : STR_C("on the left"), anchor.height, candidate.reason.c_str())}; if (candidate.reader != nullptr) text += format(STR_C("\nRead by the path %s: event %s, getvalues of the memory cell %s"), candidate.reader->name().c_str(), candidate.event.c_str(), candidate.cell.c_str()) + (candidate.loose ? STR(" (written loose, the path takes it by the name)") : std::string{}); diff --git a/application/editormodejoints.cpp b/application/editormodejoints.cpp index c50d9d82..da101b50 100644 --- a/application/editormodejoints.cpp +++ b/application/editormodejoints.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" +#include "application/editormodeshared.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "editor/editorFormat.hpp" @@ -107,9 +108,9 @@ std::string value_text(editor_mode::joint_issue const &Issue) { case kind::gap: return format("%.3f m", Issue.value); case kind::step: return format("%.1f mm", Issue.value * 1000.0); - case kind::kink: return format("%.3f deg", Issue.value); + case kind::kink: return format("%.3f\xc2\xb0", Issue.value); case kind::cant: return format("%.1f mm", Issue.value); - case kind::grade: return format("%.1f %%o", Issue.value); + case kind::grade: return format("%.1f \xe2\x80\xb0", Issue.value); default: return "-"; } } @@ -131,7 +132,7 @@ std::string name_of(TTrack const *Track) { if (Track == nullptr) return "-"; - return Track->name().empty() ? std::string{"(noname)"} : Track->name(); + return Track->name().empty() ? STR("(no name)") : Track->name(); } } // namespace @@ -387,10 +388,10 @@ void editor_mode::joints_fix(std::vector const &Indices) void editor_mode::render_joints_body() { auto &state{m_joints}; - ImGui::TextDisabled("%s", STR_C("Ends of the paths which almost meet, steps, kinks,\njumps of the cant and of the grade at the joints")); - ImGui::RadioButton(STR_C("Around the camera"), &state.scope, 0); - ImGui::SameLine(); - ImGui::RadioButton(STR_C("Whole scenery"), &state.scope, 1); + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", STR_C("Looks for faults at the joints of the tracks")); + ImGui::PopTextWrapPos(); + segmented("jointscope", state.scope, {{0, STR_C("Around the camera")}, {1, STR_C("Whole scenery")}}); bool const running{state.next < state.queue.size()}; if (running) { @@ -400,31 +401,42 @@ void editor_mode::render_joints_body() if (ImGui::SmallButton(STR_C("Cancel"))) state.queue.resize(state.next); } - else if (ImGui::Button(STR_C("Check"))) + else { - joints_start(); + ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(family_colour(2, 0.55f))); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(family_colour(2, 0.75f))); + if (ImGui::Button(STR_C("Check"), ImVec2(-1.0f, std::round(ImGui::GetFrameHeight() * 1.3f)))) + joints_start(); + ImGui::PopStyleColor(2); + item_tooltip("Finds the gaps between the ends of the tracks, the steps, the kinks and the jumps of the cant and of the grade"); } - if (ImGui::TreeNode(STR_C("Limits"))) + if (ImGui::TreeNode(STR_C("Thresholds"))) { - ImGui::PushItemWidth(100.0f); - if (state.scope == 0) - ImGui::InputDouble(STR_C("Around the camera (m)"), &state.reach, 0.0, 0.0, "%.0f"); - ImGui::InputDouble(STR_C("Gap up to (m)"), &state.gap, 0.0, 0.0, "%.2f"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("A free end closer than this to the end of another path is a gap: the trains don't go across it.\nThe simulation joins the ends no more than 2 cm apart")); - ImGui::InputDouble(STR_C("Step (m)"), &state.step, 0.0, 0.0, "%.3f"); - ImGui::InputDouble(STR_C("Kink (deg)"), &state.angle, 0.0, 0.0, "%.2f"); - ImGui::InputDouble(STR_C("Cant jump (mm)"), &state.cant, 0.0, 0.0, "%.1f"); - ImGui::InputDouble(STR_C("Grade break (per mille)"), &state.grade, 0.0, 0.0, "%.1f"); - ImGui::Checkbox(STR_C("Free ends too"), &state.free_ends); - ImGui::PopItemWidth(); + if (begin_fields("##jointlimits", 0.55f)) + { + auto const limit = [](char const *Label, char const *Id, double &Value, char const *Format) { + field_row(Label); + ImGui::InputDouble(Id, &Value, 0.0, 0.0, Format); + }; + if (state.scope == 0) + limit("Around the camera (m)", "##jointreach", state.reach, "%.0f"); + limit("Gap up to (m)", "##jointgap", state.gap, "%.2f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("A free end closer than this to the end of another path is a gap: the trains don't go across it.\nThe simulation joins the ends no more than 2 cm apart")); + limit("Step (m)", "##jointstep", state.step, "%.3f"); + limit("Kink (deg)", "##jointkink", state.angle, "%.2f"); + limit("Cant jump (mm)", "##jointcant", state.cant, "%.1f"); + limit("Grade break (per mille)", "##jointgrade", state.grade, "%.1f"); + ImGui::EndTable(); + } + toggle_chips("jointflags", {{STR_C("Free ends too"), &state.free_ends}}); state.reach = std::clamp(state.reach, 50.0, 50000.0); state.gap = std::clamp(state.gap, 0.03, 20.0); state.step = std::max(0.001, state.step); state.angle = std::max(0.01, state.angle); state.cant = std::max(0.5, state.cant); state.grade = std::max(0.5, state.grade); - ImGui::TextDisabled("%s", STR_C("Change the limits, then Check again")); + ImGui::TextDisabled("%s", STR_C("Change the thresholds, then Check again")); ImGui::TreePop(); } if (false == state.status.empty()) diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index cf14ffb3..b6aa4a3d 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -223,7 +223,7 @@ void editor_mode::draw_free_ends(TTrack const *Self, int const Category, std::ve screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; auto const &display{ImGui::GetIO().DisplaySize}; - auto const reach{Global.EditorOrtho ? std::max(kJointRange, static_cast(Global.EditorOrthoExtent) * 2.0) : kJointRange}; + auto const reach{Global.EditorOrtho ? std::max(kJointRange, static_cast(Global.EditorOrthoExtent) * 2.0) : std::min(kJointRange, kDotsRange)}; for (auto const &candidate : editor_track::free_ends(Self, Category, glm::dvec3{Global.pCamera.Pos}, reach, Exclude)) { ImVec2 screen; @@ -484,7 +484,7 @@ std::vector editor_mode::join_pieces(TTrack const *From, editor_tr Length = result.length; std::vector notes; for (auto const &warning : result.warnings) - if (m_join_radius > 0.0 || warning.find("R reduced") == std::string::npos) + if (m_join_radius > 0.0 || std::find(result.radius_warnings.begin(), result.radius_warnings.end(), warning) == result.radius_warnings.end()) notes.push_back(warning); if (result.kind == alignment::join_kind::bezier) notes.insert(notes.begin(), STR_C("No curve fits between the ends, they are joined with a free curve")); @@ -597,42 +597,102 @@ void editor_mode::render_extend_ui() extend_tool tool; if (false == chosen_free_end(tool)) return; - if (false == ImGui::CollapsingHeader(STR_C("Continue from the free end"), ImGuiTreeNodeFlags_DefaultOpen)) + if (false == ImGui::CollapsingHeader(STR_C("Continue from the free end"))) return; - ImGui::PushItemWidth(100.0f); - ImGui::DragScalar(STR_C("Length (m)##extend"), ImGuiDataType_Double, &m_extend_length, 0.5f, nullptr, nullptr, "%.1f"); - item_tooltip("Length of the next piece, along the curve for the curves"); - ImGui::SameLine(); - ImGui::DragScalar("R (m)##extend", ImGuiDataType_Double, &m_extend_radius, 5.0f, nullptr, nullptr, "%.0f"); - item_tooltip("Radius of the curves added"); - ImGui::PopItemWidth(); + if (begin_fields("##extendfields", 0.3f)) + { + field_row("Length (m)##extend"); + ImGui::DragScalar("##extendlength", ImGuiDataType_Double, &m_extend_length, 0.5f, nullptr, nullptr, "%.1f"); + item_tooltip("Length of the next piece, along the curve for the curves"); + field_row("R (m)##extend"); + ImGui::DragScalar("##extendradius", ImGuiDataType_Double, &m_extend_radius, 5.0f, nullptr, nullptr, "%.0f"); + item_tooltip("Radius of the curves added"); + ImGui::EndTable(); + } m_extend_length = std::clamp(m_extend_length, 0.5, 5000.0); m_extend_radius = std::clamp(m_extend_radius, 10.0, 100000.0); - if (ImGui::Button(STR_C("Straight##extend"), ImVec2(100.0f, 0.0f))) + auto const gap{std::round(ImGui::GetFontSize() * 0.3f)}; + auto const width{std::floor((ImGui::GetContentRegionAvail().x - 2.0f * gap) / 3.0f)}; + auto const at = [](ImVec2 const Low, ImVec2 const High, float const X, float const Y) { return ImVec2(Low.x + (High.x - Low.x) * X, Low.y + (High.y - Low.y) * Y); }; + auto const piece = [&](char const *Id, char const *Label, int const Bend) { + return sketch_tile(Id, Label, false, width, [&](ImDrawList &List, ImVec2 Low, ImVec2 High, ImU32 Line, ImU32) { + List.AddLine(at(Low, High, 0.0f, 0.5f), at(Low, High, 0.4f, 0.5f), Line, 2.0f); + List.AddCircleFilled(at(Low, High, 0.4f, 0.5f), 2.5f, IM_COL32(230, 90, 70, 255)); + if (Bend == 0) + List.AddLine(at(Low, High, 0.4f, 0.5f), at(Low, High, 1.0f, 0.5f), IM_COL32(255, 210, 60, 255), 2.0f); + else + List.AddBezierQuadratic(at(Low, High, 0.4f, 0.5f), at(Low, High, 0.75f, 0.5f), at(Low, High, 0.95f, Bend > 0 ? 0.08f : 0.92f), IM_COL32(255, 210, 60, 255), 2.0f); + }); + }; + if (piece("##extendstraight", STR_C("Straight"), 0)) extend_add(0); item_tooltip("Adds a straight at the free end; the new piece gets selected, so the next click goes on from it"); - ImGui::SameLine(); - if (ImGui::Button(STR_C("Curve left##extend"), ImVec2(100.0f, 0.0f))) + ImGui::SameLine(0.0f, gap); + if (piece("##extendleft", STR_C("Curve left"), 1)) extend_add(1); - ImGui::SameLine(); - if (ImGui::Button(STR_C("Curve right##extend"), ImVec2(100.0f, 0.0f))) + ImGui::SameLine(0.0f, gap); + if (piece("##extendright", STR_C("Curve right"), -1)) extend_add(-1); } +// the shapes laid from a free end as four tiles, each with a sketch: the end of the track, what's laid from it, the cursor void editor_mode::render_layout_combo() { char const *const names[]{"Arc, then straight", "Straight, then arc", "Arc through the cursor", "Straight"}; - auto current{static_cast(m_track_layout)}; - ImGui::PushItemWidth(200.0f); - if (ImGui::BeginCombo(STR_C("Shape from a free end"), STR_C(names[current]))) + auto const current{static_cast(m_track_layout)}; + ImGui::TextDisabled("%s", STR_C("Shape from a free end")); + auto const font{ImGui::GetFontSize()}; + auto const gap{std::round(font * 0.3f)}; + auto const width{std::floor((ImGui::GetContentRegionAvail().x - 3.0f * gap) * 0.25f)}; + auto const height{std::round(font * 2.4f)}; + auto *list{ImGui::GetWindowDrawList()}; + auto const accent{family_colour(0)}; + for (int i = 0; i < 4; ++i) { - for (int i = 0; i < 4; ++i) - if (ImGui::Selectable(STR_C(names[i]), i == current)) - m_track_layout = static_cast(i); - ImGui::EndCombo(); + if (i > 0) + ImGui::SameLine(0.0f, gap); + ImGui::PushID(i); + auto const top{ImGui::GetCursorScreenPos()}; + if (ImGui::InvisibleButton("##shape", ImVec2(width, height))) + m_track_layout = static_cast(i); + auto const hovered{ImGui::IsItemHovered()}; + if (hovered) + ImGui::SetTooltip("%s\n%s", STR_C(names[i]), STR_C("While laying from a free end: Shift+wheel")); + ImGui::PopID(); + auto const chosen{i == current}; + ImVec2 const bottom{top.x + width, top.y + height}; + auto const rounding{std::round(font * 0.35f)}; + list->AddRectFilled(top, bottom, chosen ? (accent & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 46) : ImGui::GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), rounding); + list->AddRect(top, bottom, chosen ? accent : ImGui::GetColorU32(ImGuiCol_Border), rounding, 0, chosen ? 2.0f : 1.0f); + auto const pad{std::round(font * 0.45f)}; + auto const at = [&](float const X, float const Y) { return ImVec2(top.x + pad + X * (width - 2.0f * pad), top.y + pad + Y * (height - 2.0f * pad)); }; + auto const laid{chosen ? accent : ImGui::GetColorU32(ImGuiCol_Text, 0.7f)}; + list->AddLine(at(0.0f, 0.85f), at(0.18f, 0.85f), ImGui::GetColorU32(ImGuiCol_TextDisabled), 3.0f); + switch (i) + { + case 0: + list->AddBezierQuadratic(at(0.18f, 0.85f), at(0.4f, 0.85f), at(0.61f, 0.6f), laid, 2.0f); + list->AddLine(at(0.61f, 0.6f), at(1.0f, 0.1f), laid, 2.0f); + break; + case 1: + list->AddLine(at(0.18f, 0.85f), at(0.45f, 0.85f), laid, 2.0f); + list->AddBezierQuadratic(at(0.45f, 0.85f), at(0.85f, 0.85f), at(1.0f, 0.1f), laid, 2.0f); + break; + case 2: list->AddBezierQuadratic(at(0.18f, 0.85f), at(0.7f, 0.85f), at(1.0f, 0.1f), laid, 2.0f); break; + default: + list->AddLine(at(0.18f, 0.85f), at(1.0f, 0.85f), laid, 2.0f); + for (float y{0.15f}; y < 0.8f; y += 0.2f) + list->AddLine(at(1.0f, y), at(1.0f, y + 0.1f), IM_COL32(255, 210, 60, 160), 1.0f); + break; + } + list->AddCircleFilled(at(0.18f, 0.85f), 2.5f, IM_COL32(230, 80, 230, 255)); + auto const cursor{at(1.0f, 0.1f)}; + list->AddLine(ImVec2(cursor.x - 4.0f, cursor.y), ImVec2(cursor.x + 4.0f, cursor.y), IM_COL32(255, 210, 60, 255), 1.5f); + list->AddLine(ImVec2(cursor.x, cursor.y - 4.0f), ImVec2(cursor.x, cursor.y + 4.0f), IM_COL32(255, 210, 60, 255), 1.5f); } - ImGui::PopItemWidth(); - item_tooltip("What a drag or a click from a free end lays towards the cursor; Shift+wheel changes it, Ctrl+wheel the radius"); + ImGui::TextUnformatted(STR_C(names[current])); + ImGui::SameLine(); + ImGui::TextDisabled("%s", STR_C("(Shift+wheel)")); } bool editor_mode::track_wheel(double const Offset) @@ -927,7 +987,7 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) first_step_done(first_step::laid); if (End.track != nullptr) first_step_done(first_step::join); - tool.status = format(STR_C("Laid %zu paths, %.1f m"), created.size(), length); + tool.status = format(STR_P("Laid %zu track, %.1f m, Ctrl+Z takes it back", "Laid %zu tracks, %.1f m, Ctrl+Z takes them back", static_cast(created.size())), created.size(), length); if (false == error.empty()) tool.status += "\n" + error; { @@ -948,7 +1008,7 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) } } if (scene::Layers.active() == null_handle && style == nullptr) - tool.status += "\nThe scenery isn't opened for editing, the new track won't be saved"; + tool.status += "\n" + STR("The scenery isn't opened for editing: new track won't be saved"); WriteLog("Editor: " + tool.status, logtype::generic); tool.points.clear(); tool.start = {}; @@ -1062,29 +1122,133 @@ alignment::limits editor_mode::laying_norms() const return norms; } +namespace +{ + +// a card of the geometry of the new track: its plan, and under it its curvature along the length, a point running along both; +// a plain arc jumps to its curvature, a railway curve leads into it along a transition, the cant with it +float geometry_card_height(char const *Title, char const *Text, float const Width) +{ + auto const font{ImGui::GetFontSize()}; + auto const pad{std::round(font * 0.5f)}; + auto const wrap{Width - 2.0f * pad}; + return pad + ImGui::CalcTextSize(Title, nullptr, false, wrap).y + pad * 0.5f + std::round(font * 2.6f) + pad * 0.5f + ImGui::CalcTextSize(Text, nullptr, false, wrap).y + pad; +} + +bool geometry_card(char const *Id, char const *Title, char const *Text, bool const Railway, bool const Chosen, float const Width, float const Height) +{ + auto const font{ImGui::GetFontSize()}; + auto const pad{std::round(font * 0.5f)}; + auto const sketch{std::round(font * 2.6f)}; + auto const wrap{Width - 2.0f * pad}; + auto const title{ImGui::CalcTextSize(Title, nullptr, false, wrap)}; + auto const height{Height}; + auto const top{ImGui::GetCursorScreenPos()}; + auto const clicked{ImGui::InvisibleButton(Id, ImVec2(Width, height))}; + auto const hovered{ImGui::IsItemHovered()}; + auto *list{ImGui::GetWindowDrawList()}; + auto const accent{family_colour(0)}; + ImVec2 const bottom{top.x + Width, top.y + height}; + auto const rounding{std::round(font * 0.35f)}; + list->AddRectFilled(top, bottom, Chosen ? (accent & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 46) : ImGui::GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), rounding); + list->AddRect(top, bottom, Chosen ? accent : ImGui::GetColorU32(ImGuiCol_Border), rounding, 0, Chosen ? 2.0f : 1.0f); + auto y{top.y + pad}; + list->AddText(nullptr, 0.0f, ImVec2(top.x + pad, y), ImGui::GetColorU32(ImGuiCol_Text), Title, nullptr, wrap); + y += title.y + pad * 0.5f; + // the curvature along the length, as a share of the curvature of the arc + auto const curvature = [Railway](double const Share) { + if (Railway) + return std::clamp((Share - 0.22) / 0.3, 0.0, 1.0); + return Share < 0.37 ? 0.0 : 1.0; + }; + int constexpr steps{80}; + std::vector plan; + plan.reserve(steps + 1); + double heading{0.0}, px{0.0}, py{0.0}; + auto const turn{1.45 / (Railway ? 0.63 - 0.15 : 0.63)}; + for (int i = 0; i <= steps; ++i) + { + plan.emplace_back(static_cast(px), static_cast(py)); + auto const share{(i + 0.5) / steps}; + heading += curvature(share) * turn / steps; + px += std::cos(heading) / steps; + py -= std::sin(heading) / steps; + } + ImVec2 low{plan.front()}, high{plan.front()}; + for (auto const &point : plan) + { + low = ImVec2(std::min(low.x, point.x), std::min(low.y, point.y)); + high = ImVec2(std::max(high.x, point.x), std::max(high.y, point.y)); + } + // the plan on the left, the curvature along the length on the right + auto const half{std::floor((wrap - pad) * 0.5f)}; + auto const scale{std::min(half / std::max(1e-3f, high.x - low.x), sketch / std::max(1e-3f, high.y - low.y))}; + ImVec2 const origin{top.x + pad + (half - (high.x - low.x) * scale) * 0.5f, y + sketch - (high.y - low.y) * scale}; + for (auto &point : plan) + point = ImVec2(origin.x + (point.x - low.x) * scale, origin.y + (point.y - low.y) * scale); + auto const line{Chosen ? accent : ImGui::GetColorU32(ImGuiCol_Text, 0.75f)}; + list->AddPolyline(plan.data(), static_cast(plan.size()), line, 0, 2.0f); + auto const label{ImGui::CalcTextSize("1/R")}; + auto const graph_left{top.x + pad + half + pad * 0.5f + label.x + 2.0f}; + auto const graph_width{top.x + pad + wrap - graph_left}; + auto const graph_top{y}; + auto const graph{sketch}; + auto const axis{ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.6f)}; + list->AddLine(ImVec2(graph_left, graph_top + graph), ImVec2(graph_left + graph_width, graph_top + graph), axis); + list->AddLine(ImVec2(graph_left, graph_top), ImVec2(graph_left, graph_top + graph), axis); + list->AddText(ImVec2(graph_left - label.x - 2.0f, graph_top + graph * 0.2f - label.y * 0.5f), axis, "1/R"); + std::vector curve; + for (int i = 0; i <= steps; ++i) + { + auto const share{static_cast(i) / steps}; + curve.emplace_back(graph_left + static_cast(share) * graph_width, graph_top + graph - static_cast(curvature(share)) * graph * 0.8f); + } + list->AddPolyline(curve.data(), static_cast(curve.size()), line, 0, 1.5f); + if (Railway) + for (int i = 0; i < steps; i += 4) + { + auto const share{static_cast(i) / steps}; + auto const next{static_cast(i + 2) / steps}; + list->AddLine(ImVec2(graph_left + static_cast(share) * graph_width, graph_top + graph - static_cast(curvature(share)) * graph * 0.45f), + ImVec2(graph_left + static_cast(next) * graph_width, graph_top + graph - static_cast(curvature(next)) * graph * 0.45f), IM_COL32(240, 170, 60, 220), 1.0f); + } + auto const running{std::fmod(ImGui::GetTime() * 0.22, 1.0)}; + auto const index{std::clamp(static_cast(running * steps), 0, steps)}; + list->AddCircleFilled(plan[index], 3.0f, IM_COL32(255, 255, 255, 230)); + list->AddLine(ImVec2(curve[index].x, graph_top), ImVec2(curve[index].x, graph_top + graph), IM_COL32(255, 255, 255, 70)); + list->AddCircleFilled(curve[index], 2.5f, IM_COL32(255, 255, 255, 230)); + y += sketch + pad * 0.5f; + list->AddText(nullptr, 0.0f, ImVec2(top.x + pad, y), ImGui::GetColorU32(ImGuiCol_TextDisabled), Text, nullptr, wrap); + return clicked; +} + +} // namespace + bool editor_mode::render_norms_choice() { - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(STR_C("Norms:")); auto changed{false}; - char const *names[]{STR_C("free"), STR_C("PKP PLK")}; + auto const gap{std::round(ImGui::GetFontSize() * 0.5f)}; + auto const width{std::floor((ImGui::GetContentRegionAvail().x - gap) * 0.5f)}; + char const *const titles[]{STR_C("Free laying"), STR_C("To the PKP PLK norms")}; + char const *const texts[]{STR_C("Plain arcs, no transition curves nor cant"), STR_C("Transition curves and cant for the speed, checked against the norms")}; + auto const height{std::max(geometry_card_height(titles[0], texts[0], width), geometry_card_height(titles[1], texts[1], width))}; + ImGui::PushID("geometry"); for (int i = 0; i < 2; ++i) { - ImGui::SameLine(); + if (i > 0) + ImGui::SameLine(0.0f, gap); auto const chosen{m_norms_free == (i == 0)}; - if (chosen) - ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); - if (ImGui::Button(names[i], ImVec2(90.0f, 0.0f)) && false == chosen) + auto const clicked{geometry_card(i == 0 ? "##free" : "##norms", titles[i], texts[i], i == 1, chosen, width, height)}; + item_tooltip(i == 0 ? "Plain arcs with no transition curves and no cant, no warnings about the norms: the track goes where it's clicked" + : "Transition curves and cant for the design speed, warnings when the radius, the cant or the transitions don't meet the norms"); + if (clicked && false == chosen) { m_norms_free = i == 0; m_lay.transitions = false == m_norms_free; changed = true; } - if (chosen) - ImGui::PopStyleColor(); - item_tooltip(i == 0 ? "Plain arcs with no transition curves and no cant, no warnings about the norms: the track goes where it's clicked" - : "Transition curves and cant for the design speed, warnings when the radius, the cant or the transitions don't meet the norms"); } + ImGui::PopID(); if (changed && false == m_route.chain.tracks.empty()) route_update(); return changed; @@ -1098,13 +1262,15 @@ void editor_mode::render_lay_ui() ImGui::PushItemWidth(120.0f); ImGui::DragScalar(STR_C("Curve radius (m)"), ImGuiDataType_Double, &tool.radius, 5.0f, nullptr, nullptr, "%.0f"); item_tooltip("Drag to change, Ctrl+click to type the value. While laying: Ctrl+wheel"); + value_chips("radii", tool.radius, {190.0, 300.0, 500.0, 800.0, 1200.0, 2000.0}, "%.0f"); tool.radius = std::max(10.0, tool.radius); ImGui::DragScalar(STR_C("Speed (km/h)"), ImGuiDataType_Double, &tool.style.velocity, 1.0f, nullptr, nullptr, "%.0f"); - tool.style.velocity = std::clamp(tool.style.velocity, 0.0, 400.0); item_tooltip(m_norms_free ? "Speed limit of the new paths; the speed of the track joined comes first" : "Speed limit of the new paths, and the design speed of the transition curves and of the cant; the speed of the track joined comes first, and none higher than the radius allows"); + if (false == m_norms_free) + value_chips("speeds", tool.style.velocity, {40.0, 60.0, 80.0, 100.0, 120.0, 160.0}, "%.0f"); + tool.style.velocity = std::clamp(tool.style.velocity, 0.0, 400.0); ImGui::PopItemWidth(); - render_layout_combo(); if (auto *track{selected_track()}; track != nullptr) { std::string reason; @@ -1116,25 +1282,67 @@ void editor_mode::render_lay_ui() render_lay_status(); return; } + render_layout_combo(); + ImGui::Spacing(); ImGui::PushItemWidth(120.0f); ImGui::DragScalar(STR_C("Longest path (m)"), ImGuiDataType_Double, &tool.piece_length, 1.0f, nullptr, nullptr, "%.0f"); tool.piece_length = std::clamp(tool.piece_length, 5.0, 1000.0); ImGui::PopItemWidth(); - ImGui::Checkbox(STR_C("Parameters of the selected track"), &tool.copy_selected); - item_tooltip("Takes the textures and the other parameters from the selected track.\nA track started or finished at a free end always takes them from the track it joins"); + ImGui::TextDisabled("%s", STR_C("Look of the new track")); + int source{tool.copy_selected ? 1 : 0}; + if (segmented("laylook", source, + {{0, STR_C("These textures"), "The rails and the trackbed chosen below.\nA track started or finished at a free end always takes them from the track it joins"}, + {1, STR_C("Like the selected one"), "Takes the textures and the other parameters from the selected track.\nA track started or finished at a free end always takes them from the track it joins"}})) + tool.copy_selected = source == 1; if (false == tool.copy_selected) { - auto const text_field = [](char const *Label, std::string &Value, char const *Tooltip) { - char buffer[256]; - std::snprintf(buffer, sizeof(buffer), "%s", Value.c_str()); - if (ImGui::InputText(Label, buffer, sizeof(buffer))) - Value = buffer; - item_tooltip(Tooltip); + m_textures.loads = 0; + auto const column{std::round(ImGui::GetCursorPosX() + std::max(ImGui::CalcTextSize(STR_C("Rails")).x, ImGui::CalcTextSize(STR_C("Trackbed")).x) + ImGui::GetStyle().ItemSpacing.x)}; + auto const texture_row = [&](int const Kind, char const *Label, std::string &Value) { + ImGui::PushID(Kind); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(Label); + ImGui::SameLine(column); + auto const side{ImGui::GetFrameHeight()}; + auto const corner{ImGui::GetCursorScreenPos()}; + if (auto const image{Value.empty() || Value == "none" ? 0 : track_texture_image(Kind, Value)}; image != 0) + { + ImGui::Image(static_cast(image), ImVec2(side, side), ImVec2(0, 1), ImVec2(1, 0)); + if (ImGui::IsItemHovered()) + { + auto const big{std::round(ImGui::GetFontSize() * 14.0f)}; + ImGui::BeginTooltip(); + ImGui::Image(static_cast(image), ImVec2(big, big), ImVec2(0, 1), ImVec2(1, 0)); + ImGui::TextUnformatted(Value.c_str()); + ImGui::EndTooltip(); + } + } + else + { + ImGui::Dummy(ImVec2(side, side)); + ImGui::GetWindowDrawList()->AddRect(corner, ImVec2(corner.x + side, corner.y + side), ImGui::GetColorU32(ImGuiCol_Border)); + } + ImGui::SameLine(0.0f, 4.0f); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::SetNextWindowSizeConstraints(ImVec2(ImGui::GetFontSize() * 36.0f, 0.0f), ImVec2(FLT_MAX, FLT_MAX)); + auto const slash{Value.find_last_of('/')}; + auto const preview{Value.empty() || Value == "none" ? std::string{STR_C("No texture")} : slash == std::string::npos ? Value : Value.substr(slash + 1)}; + if (ImGui::BeginCombo("##texture", preview.c_str(), ImGuiComboFlags_HeightLargest)) + { + if (std::string chosen; track_texture_picker(Kind, Value, false, chosen)) + { + Value = chosen; + ImGui::CloseCurrentPopup(); + } + ImGui::EndCombo(); + } + item_tooltip(Kind == 0 ? "Texture of the rails.\nA material dragged from the texture browser can be dropped here" : "Texture of the trackbed (ballast) under the rails.\nA material dragged from the texture browser can be dropped here"); + if (std::string dropped; material_drop(dropped)) + Value = dropped; + ImGui::PopID(); }; - ImGui::PushItemWidth(-1.0f); - text_field("##layrail", tool.style.rail, STR_C("Texture of the rails")); - text_field("##layballast", tool.style.ballast, STR_C("Texture of the ballast")); - ImGui::PopItemWidth(); + texture_row(0, STR_C("Rails"), tool.style.rail); + texture_row(1, STR_C("Trackbed"), tool.style.ballast); } ImGui::TreePop(); render_lay_status(); diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp index 500d5018..e96858d8 100644 --- a/application/editormodelineside.cpp +++ b/application/editormodelineside.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" +#include "application/editormodeshared.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "editor/editorFormat.hpp" @@ -525,7 +526,7 @@ void editor_mode::hekto_remove_duplicates() if (found.duplicate) instances.push_back(found.post.instance); auto const removed{remove_templates(instances)}; - tool.status = format(STR_C("%zu duplicate hectometre posts removed, Ctrl+Z brings them back"), removed); + tool.status = format(STR_P("%zu duplicate hectometre post removed, Ctrl+Z brings it back", "%zu duplicate hectometre posts removed, Ctrl+Z brings them back", static_cast(removed)), removed); WriteLog("Editor: " + tool.status); tool.dirty = true; } @@ -548,7 +549,9 @@ void editor_mode::hekto_place() } std::string error; auto const placed{place_templates(items, error)}; - tool.status = placed > 0 ? format(STR_C("%zu hectometre posts placed, Ctrl+Z takes them back"), placed) : error; + tool.status = placed > 0 ? format(STR_P("%zu hectometre post placed, Ctrl+Z takes it back", "%zu hectometre posts placed, Ctrl+Z takes them back", static_cast(placed)), placed) : error; + if (placed > 0) + first_step_done(first_step::posts); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } @@ -568,7 +571,7 @@ void editor_mode::hekto_fix() moves.emplace_back(tool.found[post.standing].post, item); } auto const moved{move_templates(moves)}; - tool.status = format(STR_C("%zu hectometre posts moved to their places, Ctrl+Z takes it back"), moved); + tool.status = format(STR_P("%zu hectometre post moved to its place, Ctrl+Z takes it back", "%zu hectometre posts moved to their places, Ctrl+Z takes it back", static_cast(moved)), moved); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } @@ -590,44 +593,46 @@ void editor_mode::render_hekto_ui() if (Changed) tool.dirty = true; }; - ImGui::PushItemWidth(110.0f); - changed(ImGui::InputDouble(STR_C("Kilometre at the point (km)"), &tool.km, 0.0, 0.0, "%.3f")); - ImGui::SameLine(); - if (ImGui::SmallButton(STR_C("Read from the posts"))) + if (begin_fields("##hektokm", 0.45f)) { - tool.km_read = false; - tool.dirty = true; + field_row("Kilometre at the point (km)"); + changed(ImGui::InputDouble("##hektokilometre", &tool.km, 0.0, 0.0, "%.3f")); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + if (ImGui::Button(STR_C("Read from the posts"), ImVec2(-FLT_MIN, 0.0f))) + { + tool.km_read = false; + tool.dirty = true; + } + ImGui::EndTable(); } ImGui::TextDisabled("%s", STR_C("The kilometrage grows")); - ImGui::SameLine(); - changed(ImGui::RadioButton(STR_C("along the line"), &tool.growth, 1)); - ImGui::SameLine(); - changed(ImGui::RadioButton(STR_C("against it"), &tool.growth, -1)); + changed(segmented("growth", tool.growth, {{1, STR_C("along the line")}, {-1, STR_C("against it")}})); ImGui::TextDisabled("%s", STR_C("Side, looking where it grows")); - ImGui::SameLine(); - changed(ImGui::RadioButton(STR_C("right"), &tool.side, 1)); - ImGui::SameLine(); - changed(ImGui::RadioButton(STR_C("left"), &tool.side, -1)); - ImGui::SameLine(); - changed(ImGui::RadioButton(STR_C("alternately"), &tool.side, 0)); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("The even hectometres on the right, the odd ones on the left")); - changed(ImGui::InputDouble(STR_C("From the axis (m)"), &tool.offset, 0.0, 0.0, "%.2f")); - tool.offset = std::clamp(tool.offset, 1.5, 30.0); - char const *styles[] = {"nowy", "stary", "zgnity", "mixed"}; - ImGui::Combo(STR_C("Posts"), &tool.style, styles, IM_ARRAYSIZE(styles)); - char const *turns[] = {"0", "90", "180", "270"}; - int turn{tool.turn / 90}; - if (ImGui::Combo(STR_C("Turned by (deg)"), &turn, turns, IM_ARRAYSIZE(turns))) + changed(segmented("side", tool.side, {{1, STR_C("on the right")}, {-1, STR_C("on the left")}, {0, STR_C("alternately"), "The even hectometres on the right, the odd ones on the left"}})); + if (ImGui::TreeNode(STR_C("More settings"))) { - tool.turn = turn * 90; - tool.dirty = true; + ImGui::TextDisabled("%s", STR_C("Look")); + segmented("hektostyle", tool.style, {{0, STR_C("new")}, {1, STR_C("old")}, {2, STR_C("rotten")}, {3, STR_C("mixed"), "New, old and rotten ones at random, mostly new"}}); + ImGui::TextDisabled("%s", STR_C("Turned by (deg)")); + if (segmented("hektoturn", tool.turn, {{0, "0"}, {90, "90"}, {180, "180"}, {270, "270"}})) + tool.dirty = true; + if (begin_fields("##hektofields", 0.6f)) + { + field_row("From the axis (m)"); + changed(ImGui::InputDouble("##hektooffset", &tool.offset, 0.0, 0.0, "%.2f")); + field_row("Along the line each way (m)"); + changed(ImGui::InputDouble("##hektoreach", &tool.reach, 0.0, 0.0, "%.0f")); + field_row("Tolerance (m)"); + changed(ImGui::InputDouble("##hektotolerance", &tool.tolerance, 0.0, 0.0, "%.1f")); + item_tooltip("A post standing farther than this from its place counts as wrong"); + ImGui::EndTable(); + } + ImGui::TreePop(); } - changed(ImGui::InputDouble(STR_C("Along the line each way (m)"), &tool.reach, 0.0, 0.0, "%.0f")); + tool.offset = std::clamp(tool.offset, 1.5, 30.0); tool.reach = std::clamp(tool.reach, 100.0, 100000.0); - changed(ImGui::InputDouble(STR_C("A post standing off by more (m) is wrong"), &tool.tolerance, 0.0, 0.0, "%.1f")); tool.tolerance = std::clamp(tool.tolerance, 0.5, 50.0); - ImGui::PopItemWidth(); hekto_update(); if (false == tool.error.empty()) { @@ -637,15 +642,21 @@ void editor_mode::render_hekto_ui() auto const missing{std::count_if(tool.posts.begin(), tool.posts.end(), [](hekto_post const &Post) { return Post.standing < 0; })}; auto const wrong{std::count_if(tool.found.begin(), tool.found.end(), [](hekto_found const &Post) { return Post.wrong; })}; auto const duplicates{std::count_if(tool.found.begin(), tool.found.end(), [](hekto_found const &Post) { return Post.duplicate; })}; - ImGui::Text(STR_C("Line %.0f m, km %s - %s: %d posts to place, %zu standing, %d of them wrong"), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, tool.posts.empty() ? "-" : km_text(std::min(tool.posts.front().hectometres, tool.posts.back().hectometres)).c_str(), - tool.posts.empty() ? "-" : km_text(std::max(tool.posts.front().hectometres, tool.posts.back().hectometres)).c_str(), static_cast(missing), tool.found.size(), static_cast(wrong)); - if (missing > 0 && ImGui::Button(format(STR_C("Place %d posts"), static_cast(missing)).c_str())) - hekto_place(); + ImGui::Text(STR_C("To place: %d"), static_cast(missing)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Line %.0f m, km %s - %s. Standing: %zu, %d of them wrong"), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, tool.posts.empty() ? "-" : km_text(std::min(tool.posts.front().hectometres, tool.posts.back().hectometres)).c_str(), + tool.posts.empty() ? "-" : km_text(std::max(tool.posts.front().hectometres, tool.posts.back().hectometres)).c_str(), tool.found.size(), static_cast(wrong)); + if (missing > 0) + { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); + if (ImGui::Button(format(STR_P("Place %d post", "Place %d posts", static_cast(missing)), static_cast(missing)).c_str(), ImVec2(-1.0f, std::round(ImGui::GetFrameHeight() * 1.3f)))) + hekto_place(); + ImGui::PopStyleColor(2); + } if (duplicates > 0) { - if (missing > 0) - ImGui::SameLine(); - if (ImGui::Button(format(STR_C("Remove %d duplicate posts"), static_cast(duplicates)).c_str())) + if (ImGui::Button(format(STR_P("Remove %d duplicate post", "Remove %d duplicate posts", static_cast(duplicates)), static_cast(duplicates)).c_str())) hekto_remove_duplicates(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Posts with a number another post nearer to its place already has")); @@ -654,9 +665,9 @@ void editor_mode::render_hekto_ui() { if (wrong > 0) { - if (missing > 0 || duplicates > 0) + if (duplicates > 0) ImGui::SameLine(); - if (ImGui::Button(format(STR_C("Move %d wrong posts to their places"), static_cast(wrong)).c_str())) + if (ImGui::Button(format(STR_P("Move %d wrong post to its place", "Move %d wrong posts to their places", static_cast(wrong)), static_cast(wrong)).c_str())) hekto_fix(); } ImGui::BeginChild("##hektowrong", ImVec2(0.0f, std::min(6, static_cast(wrong + duplicates)) * ImGui::GetTextLineHeightWithSpacing() + 6.0f), true); @@ -846,7 +857,7 @@ void editor_mode::fouling_place(int const Only) } std::string error; auto const placed{place_templates(items, error)}; - tool.status = placed > 0 ? format(STR_C("%zu fouling point markers placed, Ctrl+Z takes them back"), placed) : error; + tool.status = placed > 0 ? format(STR_P("%zu fouling point marker placed, Ctrl+Z takes it back", "%zu fouling point markers placed, Ctrl+Z takes them back", static_cast(placed)), placed) : error; WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } @@ -877,7 +888,7 @@ void editor_mode::fouling_fix(int const Only) moves.emplace_back(found.marker, item); } auto const moved{move_templates(moves)}; - tool.status = format(STR_C("%zu fouling point markers moved to their places, Ctrl+Z takes it back"), moved); + tool.status = format(STR_P("%zu fouling point marker moved to its place, Ctrl+Z takes it back", "%zu fouling point markers moved to their places, Ctrl+Z takes it back", static_cast(moved)), moved); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } @@ -917,11 +928,7 @@ void editor_mode::render_fouling_ui() if (Changed) tool.dirty = true; }; - changed(ImGui::RadioButton(STR_C("The selected switch"), &tool.scope, 0)); - ImGui::SameLine(); - changed(ImGui::RadioButton(STR_C("Around the camera"), &tool.scope, 1)); - ImGui::SameLine(); - changed(ImGui::RadioButton(STR_C("Whole scenery"), &tool.scope, 2)); + changed(segmented("foulingscope", tool.scope, {{0, STR_C("The selected switch")}, {1, STR_C("Around the camera")}, {2, STR_C("Whole scenery")}})); fouling_update(); if (false == tool.error.empty()) ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", tool.error.c_str()); @@ -930,14 +937,22 @@ void editor_mode::render_fouling_ui() ImGui::TextWrapped("%s", tool.status.c_str()); auto const missing{std::count_if(tool.points.begin(), tool.points.end(), [](fouling_point const &Point) { return Point.standing < 0; })}; auto const wrong{std::count_if(tool.found.begin(), tool.found.end(), [](fouling_found const &Found) { return Found.wrong; })}; - if (missing > 0 && ImGui::Button(format(STR_C("Place %d markers"), static_cast(missing)).c_str())) - fouling_place(); + auto const tall{std::round(ImGui::GetFrameHeight() * 1.3f)}; + if (missing > 0) + { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); + if (ImGui::Button(format(STR_P("Place %d marker", "Place %d markers", static_cast(missing)), static_cast(missing)).c_str(), ImVec2(-1.0f, tall))) + fouling_place(); + ImGui::PopStyleColor(2); + } if (wrong > 0) { - if (missing > 0) - ImGui::SameLine(); - if (ImGui::Button(format(STR_C("Move %d wrong markers to their places"), static_cast(wrong)).c_str())) + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.4f, 0.1f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.48f, 0.15f, 1.0f)); + if (ImGui::Button(format(STR_P("Move %d wrong marker to its place", "Move %d wrong markers to their places", static_cast(wrong)), static_cast(wrong)).c_str(), ImVec2(-1.0f, tall))) fouling_fix(); + ImGui::PopStyleColor(2); } if (missing + wrong > 0) { @@ -974,36 +989,42 @@ void editor_mode::render_fouling_ui() ImGui::EndChild(); } } - if (ImGui::CollapsingHeader(STR_C("Settings##fouling"))) + if (ImGui::TreeNode(STR_C("More settings"))) { - ImGui::TextDisabled("%s", STR_C("W17 where the axes of the tracks meeting at a switch are the spacing apart,\nhalf way between them")); - ImGui::PushItemWidth(110.0f); - if (tool.scope == 1) - changed(ImGui::InputDouble(STR_C("Around the camera (m)"), &tool.reach, 0.0, 0.0, "%.0f")); - changed(ImGui::InputDouble(STR_C("Spacing of the axes (m)"), &tool.spacing, 0.0, 0.0, "%.2f")); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("3.5 m on the straight track; more on the curves, where the cars lean out")); - changed(ImGui::InputDouble(STR_C("A marker off by more (m) is wrong"), &tool.tolerance, 0.0, 0.0, "%.1f")); - changed(ImGui::RadioButton(STR_C("On the ground"), &tool.height_mode, 0)); - ImGui::SameLine(); - changed(ImGui::RadioButton(STR_C("Over the rail heads"), &tool.height_mode, 1)); - if (tool.height_mode == 1) + if (begin_fields("##foulingfields", 0.6f)) { - ImGui::SameLine(); - changed(ImGui::InputDouble(STR_C("by (m)"), &tool.height, 0.0, 0.0, "%.3f")); - changed(ImGui::InputDouble(STR_C("A marker higher or lower by more (m) is wrong"), &tool.height_tolerance, 0.0, 0.0, "%.3f")); + if (tool.scope == 1) + { + field_row("Around the camera (m)"); + changed(ImGui::InputDouble("##foulingreach", &tool.reach, 0.0, 0.0, "%.0f")); + } + field_row("Spacing of the axes (m)"); + changed(ImGui::InputDouble("##foulingspacing", &tool.spacing, 0.0, 0.0, "%.2f")); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s\n%s", STR_C("W17 where the axes of the tracks meeting at a switch are the spacing apart,\nhalf way between them"), STR_C("3.5 m on the straight track; more on the curves, where the cars lean out")); + field_row("Tolerance (m)"); + changed(ImGui::InputDouble("##foulingtolerance", &tool.tolerance, 0.0, 0.0, "%.1f")); + item_tooltip("A marker standing farther than this from its place counts as wrong"); + ImGui::EndTable(); + } + changed(segmented("foulingheight", tool.height_mode, {{0, STR_C("On the ground")}, {1, STR_C("Over the rail heads")}})); + if (tool.height_mode == 1 && begin_fields("##foulingheights", 0.6f)) + { + field_row("by (m)"); + changed(ImGui::InputDouble("##foulingby", &tool.height, 0.0, 0.0, "%.3f")); + field_row("Height tolerance (m)"); + changed(ImGui::InputDouble("##foulingheighttolerance", &tool.height_tolerance, 0.0, 0.0, "%.3f")); + item_tooltip("A marker higher or lower than its place by more than this counts as wrong"); tool.height_tolerance = std::clamp(tool.height_tolerance, 0.005, 1.0); + ImGui::EndTable(); } - char const *models[] = {"w17", "w17_new", "w17_old", "w17_dwuteo"}; - ImGui::Combo(STR_C("Marker"), &tool.model, models, IM_ARRAYSIZE(models)); - char const *turns[] = {"0", "90", "180", "270"}; - int turn{tool.turn / 90}; - if (ImGui::Combo(STR_C("Turned by (deg)"), &turn, turns, IM_ARRAYSIZE(turns))) - { - tool.turn = turn * 90; + ImGui::TextDisabled("%s", STR_C("Marker")); + segmented("foulingmodel", tool.model, + {{0, "w17", "Round post (ip/pkp/w17.inc)"}, {1, "w17_new", "Round post, taller than w17 (ip/pkp/w17_new.inc)"}, {2, "w17_old", "Square post (ip/pkp/w17_old.inc)"}, {3, STR_C("I-beam"), "Post of an HEB I-beam (ip/pkp/w17_dwuteo.inc)"}}); + ImGui::TextDisabled("%s", STR_C("Turned by (deg)")); + if (segmented("foulingturn", tool.turn, {{0, "0"}, {90, "90"}, {180, "180"}, {270, "270"}})) tool.dirty = true; - } - ImGui::PopItemWidth(); + ImGui::TreePop(); } tool.reach = std::clamp(tool.reach, 50.0, 50000.0); tool.spacing = std::clamp(tool.spacing, 2.0, 10.0); diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index 2dfe9fa2..26b6feab 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorSettings.hpp" +#include "model/AnimModel.h" #include "utilities/Globals.h" #include "simulation/simulation.h" #include "utilities/Logs.h" @@ -25,6 +26,7 @@ http://mozilla.org/MPL/2.0/. #include "imgui/imgui.h" #include +#include #include #include @@ -33,10 +35,108 @@ namespace // how many places of the tracks the offset of the model is worked out from at most, each needing its square of the model std::size_t constexpr fit_places{48}; +// the terrain under the tracks: places along them every so many m, the squares they're kept in, how far from the tracks the +// difference at them draws the model; past that the offset of the whole scenery +double constexpr anchor_step{25.0}; +double constexpr anchor_cell{100.0}; +double constexpr anchor_reach{300.0}; +double constexpr anchor_near{5.0}; + +std::pair anchor_cell_of(double const X, double const Z) +{ + return {static_cast(std::floor(X / anchor_cell)), static_cast(std::floor(Z / anchor_cell))}; +} + +// a path whose ends lie this far apart is an error of the scenery, as a point typed wrong: no grade line, no formation and no +// terrain along it +double constexpr broken_path{5000.0}; bool rail(TTrack const &Track) { - return (Track.iCategoryFlag & 15) == 1 && false == Track.m_editorremoved && false == Track.m_paths.empty(); + if ((Track.iCategoryFlag & 15) != 1 || Track.m_editorremoved || Track.m_paths.empty()) + return false; + auto const &path{Track.m_paths.front()}; + return geometry::plan_distance(path.points[segment_data::point::start], path.points[segment_data::point::end]) <= broken_path; +} + +// a choice of the new scenery as a card: a sketch of the ground a track lies on, the name and what it means; true when clicked. +// the real terrain: hills under an aerial photo, the track along them; none: the flat plane at 0 drawn as a grid +bool ground_card(char const *Id, char const *Title, char const *Text, bool const Hills, bool const Enabled, float const Width) +{ + auto const font{ImGui::GetFontSize()}; + auto const pad{std::round(font * 0.6f)}; + auto const sketch{std::round(font * 4.0f)}; + auto const wrap{Width - 2.0f * pad}; + auto const title{ImGui::CalcTextSize(Title, nullptr, false, wrap)}; + auto const text{ImGui::CalcTextSize(Text, nullptr, false, wrap)}; + auto const height{pad + sketch + pad * 0.6f + title.y + pad * 0.3f + text.y + pad}; + auto const top{ImGui::GetCursorScreenPos()}; + ImGui::BeginDisabled(false == Enabled); + auto const clicked{ImGui::InvisibleButton(Id, ImVec2(Width, height))}; + ImGui::EndDisabled(); + auto const hovered{Enabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)}; + auto *list{ImGui::GetWindowDrawList()}; + ImVec2 const bottom{top.x + Width, top.y + height}; + auto const rounding{std::round(font * 0.4f)}; + auto const accent{family_colour(0)}; + list->AddRectFilled(top, bottom, ImGui::GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), rounding); + list->AddRect(top, bottom, hovered ? accent : ImGui::GetColorU32(ImGuiCol_Border), rounding, 0, hovered ? 2.0f : 1.0f); + auto const alpha{Enabled ? 1.0f : 0.35f}; + auto const tint = [&](ImU32 const Colour) { return (Colour & ~IM_COL32_A_MASK) | (static_cast(((Colour >> IM_COL32_A_SHIFT) & 0xFF) * alpha) << IM_COL32_A_SHIFT); }; + ImVec2 const low{top.x + pad, top.y + pad}; + ImVec2 const high{top.x + Width - pad, top.y + pad + sketch}; + list->AddRectFilled(low, high, tint(IM_COL32(20, 26, 34, 255)), rounding * 0.6f); + list->PushClipRect(low, high, true); + int constexpr steps{48}; + auto const x_of = [&](float const Share) { return low.x + (high.x - low.x) * Share; }; + if (Hills) + { + auto const ground = [](float const Share) { return 0.45f + 0.18f * std::sin(Share * 7.0f + 0.4f) + 0.08f * std::sin(Share * 17.0f); }; + std::vector line; + for (int i = 0; i <= steps; ++i) + { + auto const share{static_cast(i) / steps}; + line.emplace_back(x_of(share), low.y + (high.y - low.y) * ground(share)); + } + // the strips under the line drawn without the soft edges, else seams show between them + auto const flags{list->Flags}; + list->Flags &= ~ImDrawListFlags_AntiAliasedFill; + for (int i = 0; i < steps; ++i) + list->AddQuadFilled(line[i], line[i + 1], ImVec2(line[i + 1].x, high.y), ImVec2(line[i].x, high.y), tint(IM_COL32(70, 110, 55, 200))); + list->Flags = flags; + list->AddPolyline(line.data(), static_cast(line.size()), tint(IM_COL32(150, 200, 110, 255)), 0, 1.5f); + // the track goes along the ground, a little above it on embankments, below it in cuttings + std::vector track; + for (int i = 0; i <= steps; ++i) + { + auto const share{static_cast(i) / steps}; + auto const smooth{0.45f + 0.09f * std::sin(share * 7.0f + 0.4f)}; + track.emplace_back(x_of(share), low.y + (high.y - low.y) * smooth - 2.0f); + } + list->AddPolyline(track.data(), static_cast(track.size()), tint(IM_COL32(235, 235, 235, 255)), 0, 2.5f); + } + else + { + auto const horizon{low.y + (high.y - low.y) * 0.35f}; + auto const grid{tint(IM_COL32(120, 130, 150, 120))}; + for (int i = 0; i <= 4; ++i) + { + auto const t{static_cast(i) / 4.0f}; + auto const y{horizon + (high.y - horizon) * t * t}; + list->AddLine(ImVec2(low.x, y), ImVec2(high.x, y), grid); + } + auto const middle{(low.x + high.x) * 0.5f}; + for (int i = -6; i <= 6; ++i) + list->AddLine(ImVec2(middle + i * (high.x - low.x) * 0.06f, horizon), ImVec2(middle + i * (high.x - low.x) * 0.22f, high.y), grid); + auto const level{horizon + (high.y - horizon) * 0.45f}; + list->AddLine(ImVec2(low.x, level), ImVec2(high.x, level), tint(IM_COL32(235, 235, 235, 255)), 2.5f); + } + list->PopClipRect(); + auto y{high.y + pad * 0.6f}; + list->AddText(nullptr, 0.0f, ImVec2(low.x, y), tint(ImGui::GetColorU32(ImGuiCol_Text)), Title, nullptr, wrap); + y += title.y + pad * 0.3f; + list->AddText(nullptr, 0.0f, ImVec2(low.x, y), tint(ImGui::GetColorU32(ImGuiCol_TextDisabled)), Text, nullptr, wrap); + return clicked && Enabled; } // a bridge or a tunnel keeps the model under it as it is @@ -50,13 +150,32 @@ bool shapes_ground(TTrack const &Track) std::vector editor_mode::nmt_track_places(double const Step) const { std::vector result; + // a track with no trackbed of its own, as the switches of the older sceneries whose bed is an include, lies in the bed of + // the others + std::vector depths; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && rail(*track) && profile_own_bed(*track)) + depths.push_back(profile_ballast(*track).first); + std::nth_element(depths.begin(), depths.begin() + depths.size() / 2, depths.end()); + auto const usual{depths.empty() ? 0.0 : depths[depths.size() / 2]}; for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || false == rail(*track)) continue; - auto const ballast{profile_ballast(*track)}; + auto const rails{static_cast(track->rail_height())}; for (std::size_t path = 0; path < track->m_paths.size(); ++path) { + // the ballast drawn under the path: a switch has it of the plain track it goes on into, as the renderer lays it + auto const *source{track}; + if (track->eType == tt_Switch && track->SwitchExtension != nullptr && path < 2) + { + auto const *previous{track->SwitchExtension->pPrevs[path]}; + auto const *next{track->SwitchExtension->pNexts[path]}; + source = previous != nullptr && previous->eType != tt_Switch ? previous : next != nullptr && next->eType != tt_Switch ? next : track; + } + auto ballast{profile_ballast(*source)}; + if (false == profile_own_bed(*source)) + ballast.first = usual; geometry::bezier const curve{track->m_paths[path]}; auto const &points{track->m_paths[path].points}; auto const chord{glm::distance(points[segment_data::point::start], points[segment_data::point::end])}; @@ -64,10 +183,62 @@ std::vector editor_mode::nmt_track_places(double const S for (int i = 0; i <= count; ++i) { auto const at{curve.point(static_cast(i) / count)}; - result.push_back({at, at.y - ballast.first, ballast.second, track, static_cast(path), shapes_ground(*track)}); + result.push_back({at, at.y - ballast.first, at.y - rails, ballast.second, track, static_cast(path), shapes_ground(*track)}); } } } + // a track passing over another one by more than the clearance, as at a flyover whose bridge is a model: where the slopes of its + // embankment would bury the track below, it stands on a bridge, and the ground there is the lower track's + double constexpr clearance{3.0}; + double constexpr cell{20.0}; + auto const key_of = [&](double const X, double const Z) { return (static_cast(std::floor(X / cell)) << 32) ^ (static_cast(std::floor(Z / cell)) & 0xffffffff); }; + std::unordered_map> grid; + double widest{0.0}; + double lowest{std::numeric_limits::max()}, highest{std::numeric_limits::lowest()}; + for (std::size_t i = 0; i < result.size(); ++i) + { + grid[key_of(result[i].position.x, result[i].position.z)].push_back(i); + widest = std::max(widest, result[i].half_width); + lowest = std::min(lowest, result[i].crown); + highest = std::max(highest, result[i].formation); + } + // the farthest the slopes of an embankment over a track can reach + auto const reach{2.0 * widest + m_nmt_state.slope * std::max(0.0, highest - lowest)}; + auto const span{static_cast(std::ceil(std::min(reach, 60.0) / cell))}; + std::vector over(result.size(), 0); + for (std::size_t i = 0; i < result.size(); ++i) + { + auto const &place{result[i]}; + if (false == place.shapes) + continue; + auto const cx{static_cast(std::floor(place.position.x / cell))}; + auto const cz{static_cast(std::floor(place.position.z / cell))}; + for (auto dz = -span; dz <= span && 0 == over[i]; ++dz) + for (auto dx = -span; dx <= span && 0 == over[i]; ++dx) + { + auto const found{grid.find(((cx + dx) << 32) ^ ((cz + dz) & 0xffffffff))}; + if (found == grid.end()) + continue; + for (auto const j : found->second) + { + auto const &other{result[j]}; + if (other.track == place.track) + continue; + auto const height{place.formation - other.crown}; + if (height < clearance) + continue; + auto const distance{glm::distance(glm::dvec2{place.position.x, place.position.z}, glm::dvec2{other.position.x, other.position.z})}; + if (distance <= place.half_width + other.half_width + m_nmt_state.slope * height) + { + over[i] = 1; + break; + } + } + } + } + for (std::size_t i = 0; i < result.size(); ++i) + if (over[i] != 0) + result[i].shapes = false; return result; } @@ -81,6 +252,7 @@ void editor_mode::nmt_fit() { nmt_locate(); auto &state{m_nmt_state}; + state.under = false; state.fit.clear(); auto const places{nmt_track_places(25.0)}; std::vector shaped; @@ -100,13 +272,94 @@ void editor_mode::nmt_fit() state.status = STR("Fetching the terrain model under the tracks..."); } +void editor_mode::nmt_under_start() +{ + nmt_locate(); + auto &state{m_nmt_state}; + state.fit.clear(); + state.anchors.clear(); + state.anchor_cells.clear(); + for (auto const &place : nmt_track_places(anchor_step)) + if (place.shapes) + state.fit.push_back(place); + if (state.fit.empty()) + { + state.status = STR("No tracks to make the terrain under"); + return; + } + state.under = true; + state.fitting = true; + state.status = STR("Fetching the terrain model under the tracks..."); +} + +void editor_mode::nmt_under_anchors() +{ + auto &state{m_nmt_state}; + struct found + { + glm::dvec2 at; + double difference; + TTrack const *track; + int path; + }; + std::vector places; + for (auto const &place : state.fit) + if (double height; m_nmt.height(place.position.x, place.position.z, height)) + places.push_back({{place.position.x, place.position.z}, place.crown - height, place.track, place.path}); + state.anchors.clear(); + state.anchor_cells.clear(); + for (std::size_t i = 0; i < places.size(); ++i) + { + // a place where the model is off, as a bridge it doesn't know of or a cutting dug since, doesn't pull the terrain around + // it: the median of the places around it along the same path + std::vector around; + for (auto j{i >= 4 ? i - 4 : 0}; j < std::min(places.size(), i + 5); ++j) + if (places[j].track == places[i].track && places[j].path == places[i].path) + around.push_back(places[j].difference); + std::nth_element(around.begin(), around.begin() + around.size() / 2, around.end()); + state.anchor_cells[anchor_cell_of(places[i].at.x, places[i].at.y)].push_back(state.anchors.size()); + state.anchors.emplace_back(places[i].at.x, places[i].at.y, around[around.size() / 2]); + } +} + +double editor_mode::nmt_offset_at(double const X, double const Z) const +{ + auto const &state{m_nmt_state}; + if (false == state.under || state.anchors.empty()) + return state.offset; + // the places near weigh the most, down to nothing at the reach; the offset of the scenery weighs as one place at the reach + auto const cell{anchor_cell_of(X, Z)}; + auto const span{static_cast(std::ceil(anchor_reach / anchor_cell))}; + auto const outermost{1.0 / (anchor_reach * anchor_reach + anchor_near * anchor_near)}; + double sum{state.offset * outermost}; + double weights{outermost}; + for (int dz = -span; dz <= span; ++dz) + for (int dx = -span; dx <= span; ++dx) + { + auto const found{state.anchor_cells.find({cell.first + dx, cell.second + dz})}; + if (found == state.anchor_cells.end()) + continue; + for (auto const index : found->second) + { + auto const &anchor{state.anchors[index]}; + auto const squared{(anchor.x - X) * (anchor.x - X) + (anchor.y - Z) * (anchor.y - Z)}; + if (squared >= anchor_reach * anchor_reach) + continue; + auto const weight{1.0 / (squared + anchor_near * anchor_near) - outermost}; + sum += anchor.z * weight; + weights += weight; + } + } + return sum / weights; +} + void editor_mode::nmt_finish_fit() { auto &state{m_nmt_state}; std::vector differences; for (auto const &place : state.fit) if (double height; m_nmt.height(place.position.x, place.position.z, height)) - differences.push_back(place.formation - height); + differences.push_back(place.crown - height); state.fitting = false; if (differences.empty()) { @@ -122,10 +375,17 @@ void editor_mode::nmt_finish_fit() state.spread = deviations[deviations.size() / 2]; state.fitted = true; state.samples = differences.size(); - state.status = format(STR_C("The tracks lie %+.2f m against the terrain model (%zu places, they differ by %.2f m around it)"), state.offset, state.samples, state.spread); + state.status = format(STR_P("The tracks lie %+.2f m against the terrain model (%zu place, they differ by %.2f m around it)", "The tracks lie %+.2f m against the terrain model (%zu places, they differ by %.2f m around it)", state.samples), state.offset, state.samples, state.spread); if (state.spread > 10.0) state.status += STR(". That's far too much: the place of the scenery is likely wrong, check the orthophoto"); WriteLog("Editor: NMT - " + state.status, logtype::generic); + if (false == state.under) + return; + // the terrain under the tracks: drawn to them place by place, then made along them + nmt_under_anchors(); + auto const status{state.status}; + nmt_make(nmt_keys_along_tracks(), true); + state.status = status + "\n" + state.status; } std::vector editor_mode::nmt_keys_around() const @@ -160,7 +420,7 @@ std::vector editor_mode::nmt_keys_along_tracks() const return {keys.begin(), keys.end()}; } -void editor_mode::nmt_make(std::vector const &Keys) +void editor_mode::nmt_make(std::vector const &Keys, bool const Rebuild) { auto &state{m_nmt_state}; if (false == ensure_terrain()) @@ -169,6 +429,21 @@ void editor_mode::nmt_make(std::vector const &Keys) return; } nmt_locate(); + if (Rebuild) + { + // the terrain made for the tracks before, as they lay then: the formation of the earlier runs, and the chunks along them, + // whose heights went with it + auto const dropped{m_streamer.drop_modifiers("nmt ")}; + std::size_t removed{0}; + for (auto const &key : Keys) + if (m_streamer.exists(key)) + { + m_streamer.remove_chunk(key.first, key.second); + ++removed; + } + forget_ground(); + WriteLog(format("Editor: NMT - made anew: %zu chunks, %zu formations of the tracks forgotten", removed, dropped), logtype::generic); + } std::size_t queued{0}; for (auto const &key : Keys) if (false == m_streamer.exists(key) && std::find(state.pending.begin(), state.pending.end(), key) == state.pending.end()) @@ -180,7 +455,7 @@ void editor_mode::nmt_make(std::vector const &Keys) // the terrain comes with its photos if (queued > 0 && false == m_orthophoto.enabled()) m_orthophoto.enabled(true); - state.status = queued > 0 ? format(STR_C("%zu chunks to make from the terrain model"), queued) : STR("There's terrain there already"); + state.status = queued > 0 ? format(STR_P("%zu chunk to make from the terrain model", "%zu chunks to make from the terrain model", queued), queued) : STR("There's terrain there already"); } bool editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key) @@ -194,6 +469,22 @@ bool editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key) auto const step{heightmap::chunk_size / cells}; std::vector heights(side * side, m_terrain_baseheight); std::vector found(side * side, 0); + // the offset over the chunk, every sixteenth of its side and between them along straight lines: it changes slowly + int constexpr coarse{16}; + std::array offsets; + for (int iz = 0; iz <= coarse; ++iz) + for (int ix = 0; ix <= coarse; ++ix) + offsets[iz * (coarse + 1) + ix] = nmt_offset_at(low.x + ix * heightmap::chunk_size / coarse, low.y + iz * heightmap::chunk_size / coarse); + auto const offset_at = [&](double const X, double const Z) { + auto const u{std::clamp((X - low.x) / heightmap::chunk_size * coarse, 0.0, static_cast(coarse))}; + auto const v{std::clamp((Z - low.y) / heightmap::chunk_size * coarse, 0.0, static_cast(coarse))}; + auto const i{std::min(static_cast(u), coarse - 1)}; + auto const j{std::min(static_cast(v), coarse - 1)}; + auto const fu{u - i}; + auto const fv{v - j}; + auto const at = [&](int const I, int const J) { return offsets[J * (coarse + 1) + I]; }; + return (at(i, j) * (1.0 - fu) + at(i + 1, j) * fu) * (1.0 - fv) + (at(i, j + 1) * (1.0 - fu) + at(i + 1, j + 1) * fu) * fv; + }; bool any{false}; for (std::size_t iz = 0; iz < side; ++iz) for (std::size_t ix = 0; ix < side; ++ix) @@ -201,7 +492,7 @@ bool editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key) auto const index{iz * side + ix}; if (double height; m_nmt.height(low.x + ix * step, low.y + iz * step, height)) { - heights[index] = height + m_nmt_state.offset; + heights[index] = height + offset_at(low.x + ix * step, low.y + iz * step); found[index] = 1; any = true; } @@ -225,24 +516,43 @@ void editor_mode::nmt_shape_tracks() // one modifier per path: the formation under its axis and the slopes from its edges to the ground. laid over each other the // modifiers keep the highest of the slopes going down and the lowest of those going up, so tracks side by side share one // formation instead of a saw of their own ones - std::map, heightmap::modifier> modifiers; + // a path broken by a bridge, as over another track, gets a modifier for each piece on the ground + std::map, heightmap::modifier> modifiers; + std::map, int> pieces; double constexpr step{5.0}; + std::pair previous{nullptr, -1}; + bool broken{false}; for (auto const &place : nmt_track_places(step)) { + std::pair const path{place.track, place.path}; + if (path != previous) + broken = false; + previous = path; if (false == place.shapes) + { + broken = true; continue; - auto &modifier{modifiers[{place.track, place.path}]}; + } + auto &piece{pieces[path]}; + if (broken) + { + ++piece; + broken = false; + } + auto &modifier{modifiers[{place.track, place.path, piece}]}; if (modifier.name.empty()) { - modifier.name = "nmt " + (place.track->name().empty() ? std::string{"track"} : place.track->name()) + (place.track->m_paths.size() > 1 ? " " + std::to_string(place.path) : std::string{}); + modifier.name = "nmt " + (place.track->name().empty() ? std::string{"track"} : place.track->name()) + (place.track->m_paths.size() > 1 ? " " + std::to_string(place.path) : std::string{}) + (piece > 0 ? " #" + std::to_string(piece) : std::string{}); modifier.slope = state.slope; modifier.reach = state.reach; } modifier.points.push_back({{place.position.x, place.position.z}, place.formation, place.half_width}); } + std::vector laid; for (auto &[path, modifier] : modifiers) if (modifier.points.size() >= 2) - m_streamer.modifier(std::move(modifier)); + laid.push_back(std::move(modifier)); + m_streamer.modifiers(std::move(laid)); forget_ground(); state.shaped = modifiers.size(); } @@ -251,6 +561,11 @@ void editor_mode::nmt_update() { auto &state{m_nmt_state}; nmt_offer(); + if (state.stage != nmt_state::automatic::idle) + { + nmt_auto_step(); + return; + } if (state.fitting) { bool all{true}; @@ -289,54 +604,265 @@ void editor_mode::nmt_update() if (state.shape) { nmt_shape_tracks(); - state.status += format(STR_C("; the formation of %zu paths shaped"), state.shaped); + state.status += format(STR_P("; the formation of %zu path shaped", "; the formation of %zu paths shaped", state.shaped), state.shaped); } // the photos laid onto the new ground m_orthophoto.refit(); + first_step_done(first_step::nmt); } } if (auto const error{m_nmt.error()}; false == error.empty()) - state.error = error; + state.error = STR(error); } void editor_mode::render_nmt_ui() { auto &state{m_nmt_state}; nmt_locate(); - if (false == m_nmt.placed()) + auto const busy{state.fitting || state.stage != nmt_state::automatic::idle}; + step_heading(1, STR_C("Location")); + if (false == m_nmt.placed() || state.moving) { - ImGui::TextWrapped("%s", STR_C("The scenery isn't placed anywhere: give its place in the settings of the orthophoto first")); - if (ImGui::Button(STR_C("Orthophoto settings..."))) - m_orthophoto_window = true; - return; + // where the scenery is, right here: the terrain model and the orthophoto take it + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", STR_C("Where the point 0,0 of the scenery lies on the map:")); + ImGui::PopTextWrapPos(); + ImGui::SetNextItemWidth(120.0f); + ImGui::InputDouble("X##nmtplacenorth", &state.north, 0.0, 0.0, "%.0f"); + item_tooltip("X of the point 0,0 in the PUWG 1992 coordinates of the Polish maps, metres to the north, as geoportal.gov.pl gives it"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(120.0f); + ImGui::InputDouble("Y##nmtplaceeast", &state.east, 0.0, 0.0, "%.0f"); + item_tooltip("Y of the point 0,0 in the PUWG 1992 coordinates of the Polish maps, metres to the east, as geoportal.gov.pl gives it"); + ImGui::BeginDisabled(state.north < 100000.0 || state.east < 100000.0); + if (ImGui::Button(STR_C("The scenery is there"))) + { + nmt_place_scenery(); + state.moving = false; + } + ImGui::EndDisabled(); + if (state.moving) + { + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + state.moving = false; + } + if (false == m_nmt.placed()) + return; } - auto const &config{m_orthophoto.settings()}; - ImGui::TextDisabled(STR_C("Place: N %.0f, E %.0f (PUWG 1992), as the orthophoto"), config.north, config.east); - if (ImGui::Button(STR_C("Fit to the tracks"))) - nmt_fit(); - item_tooltip("Compares the formation of the tracks with the terrain model and moves the model up or down to meet them, as a scenery built at height 0 needs"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(110.0f); - ImGui::InputDouble(STR_C("Height offset (m)"), &state.offset, 0.0, 0.0, "%.2f"); - ImGui::SetNextItemWidth(110.0f); - ImGui::InputDouble(STR_C("Band along the tracks (m)"), &state.band, 0.0, 0.0, "%.0f"); - state.band = std::clamp(state.band, 50.0, 3000.0); - ImGui::Checkbox(STR_C("Formation and slopes along the tracks"), &state.shape); - item_tooltip("Each track gets its formation with slopes 1:1.5 down or up to the terrain, as a modifier of the terrain; bridges and tunnels keep the model under them"); - ImGui::BeginDisabled(state.fitting); - if (ImGui::Button(STR_C("Along the tracks"))) - nmt_make(nmt_keys_along_tracks()); - ImGui::SameLine(); - if (ImGui::Button(STR_C("Around the camera"))) - nmt_make(nmt_keys_around()); + else + { + auto const &config{m_orthophoto.settings()}; + ImGui::Text(STR_C("N %.0f, E %.0f"), config.north, config.east); + ImGui::SameLine(); + ImGui::TextDisabled("%s", STR_C("PUWG 1992")); + ImGui::SameLine(); + ImGui::BeginDisabled(busy); + if (ImGui::SmallButton(STR_C("Change..."))) + { + state.north = config.north; + state.east = config.east; + state.moving = true; + } + ImGui::EndDisabled(); + item_tooltip("The place is shared with the orthophoto"); + } + + auto const tracks{std::any_of(simulation::Paths.sequence().begin(), simulation::Paths.sequence().end(), [](TTrack const *Track) { return Track != nullptr && rail(*Track); })}; + step_heading(2, tracks ? STR_C("What gives way") : STR_C("Terrain")); + if (tracks) + { + auto const spacing{ImGui::GetStyle().ItemSpacing.x}; + auto const width{std::floor((ImGui::GetContentRegionAvail().x - spacing) * 0.5f)}; + char const *const titles[]{STR_C("Tracks onto the terrain"), STR_C("Terrain under the tracks")}; + char const *const texts[]{STR_C("The tracks go down or up onto the real ground: grade lines fitted to it within the limits, switches level with their tracks"), + STR_C("The tracks stay where they are; the real ground is drawn to them and keeps its relief farther away")}; + auto const wrap{width - 16.0f}; + auto const textheight{std::max(ImGui::CalcTextSize(texts[0], nullptr, false, wrap).y, ImGui::CalcTextSize(texts[1], nullptr, false, wrap).y)}; + for (int index = 0; index < 2; ++index) + { + if (index > 0) + ImGui::SameLine(); + if (nmt_choice_card(index, titles[index], texts[index], width, textheight, state.mode == index) && false == busy) + state.mode = index; + } + } + else + { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", STR_C("No tracks yet: the terrain is made around the camera, the tracks laid then go along it")); + ImGui::PopTextWrapPos(); + } + + step_heading(3, STR_C("Load")); + if (tracks && m_streamer.resident() > 0) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.95f, 0.75f, 0.3f, 1.0f)); + ImGui::TextWrapped("%s", STR_C("The scenery has terrain already: along the tracks it's made anew from the model, its painting too")); + ImGui::PopStyleColor(); + } + ImGui::BeginDisabled(busy); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.45f, 0.75f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.26f, 0.55f, 0.88f, 1.0f)); + char const *label{tracks ? (state.mode == 0 ? STR_C("Load the terrain, put the tracks onto it") : STR_C("Load the terrain under the tracks")) : STR_C("Load the terrain around the camera")}; + if (ImGui::Button(label, ImVec2(-1.0f, ImGui::GetFrameHeight() * 1.5f))) + { + if (false == tracks) + { + state.offset = 0.0; + nmt_make(nmt_keys_around()); + } + else if (state.mode == 0) + nmt_auto_start(); + else + nmt_under_start(); + } + ImGui::PopStyleColor(2); ImGui::EndDisabled(); - if (false == state.pending.empty() || m_nmt.waiting() > 0) - ImGui::TextDisabled(STR_C("Squares of the model being fetched: %zu"), m_nmt.waiting()); + if (ImGui::IsItemHovered()) + { + auto const *text{tracks ? (state.mode == 0 ? STR_C("The terrain model is loaded along the tracks and the tracks go onto it, wherever the scenery was built: the top of the ballast at the height of the model, the grade line of every line fitted to it within the limits of the line, switches and the tracks beside them on one grade line, the formation and the slopes made along them. Where the model is off the tracks keep their line and it's reported") : + STR_C("The tracks aren't touched: the terrain model is loaded along them and drawn to them, at each place by as much as the tracks lie above or below it there, fading out away from them, so its relief stays and the tracks don't end up in deep cuttings; then the formation and the slopes along them. For a scenery built level or at its own heights")) : + STR_C("Loads the terrain model, the real relief of the ground, around the camera")}; + ImGui::SetTooltip("%s\n%s", text, STR_C("Terrain model: GUGiK, geoportal.gov.pl")); + } + // how far it got + if (state.stage == nmt_state::automatic::fetching || state.fitting) + { + auto const waiting{m_nmt.waiting()}; + ImGui::ProgressBar(-1.0f * static_cast(ImGui::GetTime()), ImVec2(-1.0f, 0.0f), format(STR_P("Terrain model: %zu square left", "Terrain model: %zu squares left", waiting), waiting).c_str()); + } + else if (state.stage == nmt_state::automatic::fitting && state.tracks > 0) + { + auto const fraction{1.0f - static_cast(state.left.size()) / static_cast(std::max(state.tracks, state.left.size()))}; + ImGui::ProgressBar(fraction, ImVec2(-1.0f, 0.0f), format(STR_P("Grade lines: %zu fitted, %zu track left", "Grade lines: %zu fitted, %zu tracks left", state.left.size()), state.lines, state.left.size()).c_str()); + } + else if (false == state.pending.empty()) + { + auto const total{state.made + state.pending.size()}; + ImGui::ProgressBar(static_cast(state.made) / static_cast(std::max(1, total)), ImVec2(-1.0f, 0.0f), format(STR_C("Chunks: %zu of %zu"), state.made, total).c_str()); + } if (false == state.status.empty()) - ImGui::TextWrapped("%s", state.status.c_str()); + { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", state.status.c_str()); + ImGui::PopTextWrapPos(); + } if (false == state.error.empty()) + { + ImGui::PushTextWrapPos(0.0f); ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", state.error.c_str()); - ImGui::TextDisabled("%s", STR_C("Terrain model: GUGiK, geoportal.gov.pl")); + ImGui::PopTextWrapPos(); + } + ImGui::PushID("nmtmore"); + if (tracks && ImGui::TreeNode(STR_C("More settings"))) + { + if (begin_fields("##nmtband", 0.5f)) + { + field_row("Band along the tracks (m)"); + ImGui::InputDouble("##nmtbandwidth", &state.band, 50.0, 250.0, "%.0f"); + item_tooltip("How far from the tracks, on both sides, the terrain model is loaded"); + ImGui::EndTable(); + } + toggle_chips("nmtshape", {{STR_C("Formation and slopes along the tracks"), &state.shape, + "Each track gets its formation with slopes 1:1.5 down or up to the terrain, as a modifier of the terrain; bridges and tunnels keep the model under them"}}); + ImGui::TreePop(); + } + ImGui::PopID(); + state.band = std::clamp(state.band, 50.0, 3000.0); + if (tracks && ImGui::TreeNode(STR_C("By hand: the model as it is, the tracks stay"))) + { + if (begin_fields("##nmtoffset", 0.5f)) + { + field_row("Height offset (m)"); + ImGui::InputDouble("##nmtheightoffset", &state.offset, 0.1, 1.0, "%.2f"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + if (ImGui::Button(STR_C("Fit to the tracks"), ImVec2(-FLT_MIN, 0.0f))) + nmt_fit(); + item_tooltip("Compares the formation of the tracks with the terrain model and moves the model up or down to meet them, as a scenery built at height 0 needs"); + ImGui::EndTable(); + } + auto const half{std::floor((ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f)}; + ImGui::BeginDisabled(busy); + if (ImGui::Button(STR_C("Load along the tracks"), ImVec2(half, 0.0f))) + nmt_make(nmt_keys_along_tracks()); + item_tooltip("Loads the terrain model as it is along the tracks, with the height offset above; the tracks stay where they are"); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Load around the camera"), ImVec2(-FLT_MIN, 0.0f))) + nmt_make(nmt_keys_around()); + item_tooltip("Loads the terrain model as it is around the camera, with the height offset above; the tracks stay where they are"); + ImGui::EndDisabled(); + ImGui::TreePop(); + } +} + +bool editor_mode::nmt_choice_card(int const Index, char const *Title, char const *Text, float const Width, float const TextHeight, bool const Chosen) const +{ + auto const line{ImGui::GetTextLineHeight()}; + auto const picture{std::round(line * 3.2f)}; + auto const pad{8.0f}; + ImVec2 const size{Width, pad + picture + pad * 0.5f + line + 4.0f + TextHeight + pad}; + auto const at{ImGui::GetCursorScreenPos()}; + ImGui::PushID(Index); + auto const clicked{ImGui::InvisibleButton("##nmtcard", size)}; + ImGui::PopID(); + auto const hovered{ImGui::IsItemHovered()}; + auto *draw{ImGui::GetWindowDrawList()}; + ImVec2 const end{at.x + size.x, at.y + size.y}; + draw->AddRectFilled(at, end, ImGui::GetColorU32(Chosen ? ImGuiCol_Header : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), 6.0f); + draw->AddRect(at, end, Chosen ? ImGui::GetColorU32(ImGuiCol_CheckMark) : ImGui::GetColorU32(ImGuiCol_Border), 6.0f, 0, Chosen ? 2.0f : 1.0f); + // the cross-section: the ground and the track over it, the one giving way moving + ImVec2 const low{at.x + pad, at.y + pad}; + ImVec2 const high{end.x - pad, at.y + pad + picture}; + draw->AddRectFilled(low, high, IM_COL32(18, 22, 30, 255), 4.0f); + draw->PushClipRect(low, high, true); + auto const phase{0.5f - 0.5f * std::cos(static_cast(ImGui::GetTime()) * 1.6f)}; + auto const amount{Chosen || hovered ? phase : 1.0f}; + auto const steps{48}; + auto const ground = [&](float const U) { return 0.62f + 0.22f * std::sin(U * 6.2f + 0.6f) * std::sin(U * 2.3f + 1.1f); }; + auto const level{0.4f}; + std::vector terrain, rails; + for (int i = 0; i <= steps; ++i) + { + auto const u{static_cast(i) / steps}; + auto const x{low.x + (high.x - low.x) * u}; + auto const closeness{std::clamp(1.0f - std::abs(u - 0.5f) * 2.4f, 0.0f, 1.0f)}; + float soil{ground(u)}; + float track{level}; + if (Index == 0) + track = level + (ground(u) - 0.1f - level) * amount; + else + soil = ground(u) + (level + 0.1f - ground(u)) * amount * (0.35f + 0.65f * closeness); + terrain.emplace_back(x, low.y + (high.y - low.y) * soil); + rails.emplace_back(x, low.y + (high.y - low.y) * track); + } + for (int i = 0; i < steps; ++i) + draw->AddQuadFilled(terrain[i], terrain[i + 1], ImVec2(terrain[i + 1].x, high.y), ImVec2(terrain[i].x, high.y), IM_COL32(120, 85, 50, 255)); + draw->AddPolyline(terrain.data(), static_cast(terrain.size()), IM_COL32(170, 120, 70, 255), 0, 2.0f); + draw->AddPolyline(rails.data(), static_cast(rails.size()), IM_COL32(215, 220, 230, 255), 0, 3.0f); + for (int i = 2; i < steps; i += 4) + draw->AddLine(ImVec2(rails[i].x, rails[i].y - 3.0f), ImVec2(rails[i].x, rails[i].y + 3.0f), IM_COL32(150, 120, 90, 255), 2.0f); + draw->PopClipRect(); + auto *font{ImGui::GetFont()}; + auto const titley{high.y + pad * 0.5f}; + draw->AddText(font, ImGui::GetFontSize(), ImVec2(low.x, titley), ImGui::GetColorU32(Chosen ? ImGuiCol_Text : ImGuiCol_Text, Chosen ? 1.0f : 0.85f), Title); + if (Chosen) + draw->AddText(font, ImGui::GetFontSize(), ImVec2(low.x + 0.6f, titley), ImGui::GetColorU32(ImGuiCol_Text), Title); + draw->AddText(font, ImGui::GetFontSize(), ImVec2(low.x, titley + line + 4.0f), ImGui::GetColorU32(ImGuiCol_TextDisabled), Text, nullptr, Width - 2.0f * pad); + return clicked; +} + +void editor_mode::nmt_place_scenery() +{ + auto &state{m_nmt_state}; + auto config{m_orthophoto.settings()}; + config.north = state.north; + config.east = state.east; + m_orthophoto.settings(config); + m_orthophoto_origin_edit = {config.north, config.east}; + save_orthophoto_settings(); + nmt_locate(); } void editor_mode::nmt_offer() @@ -357,14 +883,7 @@ void editor_mode::nmt_accept_offer() { auto &state{m_nmt_state}; if (false == m_nmt.placed()) - { - auto config{m_orthophoto.settings()}; - config.north = state.north; - config.east = state.east; - m_orthophoto.settings(config); - m_orthophoto_origin_edit = {config.north, config.east}; - save_orthophoto_settings(); - } + nmt_place_scenery(); // no tracks to fit to: the heights of the model as they are state.offset = 0.0; nmt_make(nmt_keys_around()); @@ -394,19 +913,23 @@ void editor_mode::render_nmt_prompt() ImGui::InputDouble("Y##nmteast", &state.east, 0.0, 0.0, "%.0f"); } auto const known{placed || (state.north > 100000.0 && state.east > 100000.0)}; - ImGui::BeginDisabled(false == known); - if (ImGui::Button(STR_C("Load the terrain"))) + ImGui::Spacing(); + auto const gap{std::round(ImGui::GetFontSize() * 0.6f)}; + auto const width{std::floor((ImGui::GetContentRegionAvail().x - gap) * 0.5f)}; + if (ground_card("##terrain", STR_C("Load the terrain"), STR_C("The terrain model and the aerial photos of geoportal.gov.pl around here; the tracks go along the real ground"), true, known, width)) { nmt_accept_offer(); ImGui::CloseCurrentPopup(); } - ImGui::EndDisabled(); - ImGui::SameLine(); - if (ImGui::Button(STR_C("Without terrain"))) + if (false == known) + item_tooltip("Type where the scenery lies first"); + ImGui::SameLine(0.0f, gap); + if (ground_card("##flat", STR_C("Without terrain"), STR_C("The tracks lie on the flat plane at the height 0; the terrain can be loaded later in Terrain (F5)"), false, true, width)) { state.prompt = false; ImGui::CloseCurrentPopup(); } + ImGui::Spacing(); bool ask{EditorSettings.terrain_offer()}; if (ImGui::Checkbox(STR_C("Ask with every new scenery"), &ask)) { @@ -415,3 +938,543 @@ void editor_mode::render_nmt_prompt() } ImGui::EndPopup(); } + +void editor_mode::nmt_lift_scenery(double const Offset) +{ + glm::dvec3 const shift{0.0, Offset, 0.0}; + // the tracks, joined again as they were + std::vector> states; + std::vector tracks; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) + continue; + states.emplace_back(track, editor_track::capture(*track)); + editor_track::translate(*track, shift); + tracks.push_back(track); + } + editor_track::commit(tracks); + auto const lifted{states.size()}; + push_track_snapshot(std::move(states)); + // the includes, by the place in their parameters + std::vector> moves; + for (auto const &standing : standing_templates([](std::string const &) { return true; })) + { + template_item item; + item.file = standing.file; + item.location = standing.location + shift; + item.yaw = standing.yaw; + moves.emplace_back(standing, item); + } + if (move_templates(moves) > 0 && m_history.size() >= 2) + m_history.back().joined = true; + // the models, a group as a whole + std::set groups; + for (auto *instance : simulation::Instances.sequence()) + { + if (instance == nullptr) + continue; + if (instance->group() > 1 && false == groups.insert(instance->group()).second) + continue; + push_snapshot(instance, EditorSnapshot::Action::Move); + m_history.back().joined = true; + m_editor.translate(instance, instance->location() + shift, true); + } + WriteLog(format("Editor: NMT - the scenery moved %+.2f m: %zu tracks, %zu includes, the models", Offset, lifted, moves.size()), logtype::generic); +} + +std::size_t editor_mode::nmt_join_ends() +{ + // ends which meet in the plan, laid apart in height by hand, so the game doesn't join them, are one joint before the grade + // lines run over them; farther apart than a track over another can be, they lie on two levels + double constexpr kLevel{2.0}; // m + struct end + { + TTrack *track; + int path; + bool atend; + glm::dvec3 point; + glm::dvec2 outward; + }; + auto const linked = [](TTrack const &Track, int const Path, bool const Atend) { + if (Track.eType == tt_Switch && Track.SwitchExtension) + return (Atend ? Track.SwitchExtension->pNexts[Path & 1] : Track.SwitchExtension->pPrevs[Path & 1]) != nullptr; + return (Atend ? Track.trNext : Track.trPrev) != nullptr; + }; + std::map, std::vector> cells; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == rail(*track) || track->Length() < 0.01) + continue; + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + for (auto const atend : {false, true}) + { + if (linked(*track, i, atend)) + continue; + auto const &path{track->m_paths[i]}; + auto const &point{path.points[atend ? segment_data::point::end : segment_data::point::start]}; + auto const &control{path.points[atend ? segment_data::point::control2 : segment_data::point::control1]}; + auto const &other{path.points[atend ? segment_data::point::start : segment_data::point::end]}; + auto const outward{geometry::plan_of(control != glm::dvec3{} ? -control : point - other)}; + cells[{static_cast(std::floor(point.x)), static_cast(std::floor(point.z))}].push_back({track, i, atend, point, outward}); + } + } + std::vector> states; + std::vector changed; + std::set> taken; + auto const remember = [&](TTrack *Track) { + if (std::find(changed.begin(), changed.end(), Track) != changed.end()) + return; + states.emplace_back(Track, editor_track::capture(*Track)); + changed.push_back(Track); + }; + std::size_t joined{0}; + for (auto const &[cell, ends] : cells) + for (auto const &a : ends) + { + if (a.track->eType != tt_Normal || taken.contains({a.track, a.path, a.atend})) + continue; + end const *match{nullptr}; + for (int dx = -1; dx <= 1 && match == nullptr; ++dx) + for (int dz = -1; dz <= 1 && match == nullptr; ++dz) + { + auto const found{cells.find({cell.first + dx, cell.second + dz})}; + if (found == cells.end()) + continue; + for (auto const &b : found->second) + if (b.track != a.track && false == taken.contains({b.track, b.path, b.atend}) && geometry::plan_distance(a.point, b.point) <= geometry::same_point && + std::abs(a.point.y - b.point.y) <= kLevel && glm::dot(a.outward, b.outward) < -0.5 * glm::length(a.outward) * glm::length(b.outward)) + { + match = &b; + break; + } + } + if (match == nullptr || (match->track->eType != tt_Normal && false == editor_track::is_supported(*match->track))) + continue; + auto const height{match->track->eType == tt_Normal ? (a.point.y + match->point.y) * 0.5 : match->point.y}; + for (auto const *side : {&a, match}) + { + taken.insert({side->track, side->path, side->atend}); + if (side->track->eType != tt_Normal) + continue; + remember(side->track); + side->track->m_paths[side->path].points[side->atend ? segment_data::point::end : segment_data::point::start].y = height; + } + ++joined; + } + if (changed.empty()) + return 0; + editor_track::commit(changed); + push_track_snapshot(std::move(states)); + if (m_history.size() >= 2 && m_nmt_state.lifted != 0.0) + m_history.back().joined = true; + WriteLog(format("Editor: NMT - %zu joints of tracks laid apart in height joined", joined), logtype::generic); + return joined; +} + +void editor_mode::nmt_auto_start() +{ + auto &state{m_nmt_state}; + state.under = false; + nmt_locate(); + state.probes.clear(); + for (auto const &place : nmt_track_places(20.0)) + if (place.shapes) + state.probes.push_back(place); + if (state.probes.empty()) + { + state.status = STR("No tracks to put onto the terrain"); + return; + } + state.lines = 0; + state.refused = 0; + state.departures = 0; + state.lifted = 0.0; + state.stage = nmt_state::automatic::fetching; + state.status = STR("Fetching the terrain model under the tracks..."); +} + +bool editor_mode::nmt_fit_line(TTrack &Track) +{ + auto &profile{m_profile}; + auto const &done{m_nmt_state.done}; + auto const fitted = [&](TTrack const *Other) { return std::find(done.begin(), done.end(), Other) != done.end(); }; + // the line runs over the tracks not fitted yet around the track: the ones fitted stay as they are + auto route{editor_track::run_route(Track, profile.run_length)}; + auto const at{std::find_if(route.spans.begin(), route.spans.end(), [&](auto const &Span) { return Span.track == &Track; })}; + if (at == route.spans.end()) + return false; + auto first{at}; + while (first != route.spans.begin() && false == fitted(std::prev(first)->track)) + --first; + auto last{at}; + while (std::next(last) != route.spans.end() && false == fitted(std::next(last)->track)) + ++last; + route.spans = {first, std::next(last)}; + profile.open = true; + profile.status.clear(); + profile.error.clear(); + auto *from{route.spans.front().track}; + auto *to{route.spans.back().track}; + profile_take(std::move(route), from, to); + if (false == profile.error.empty() || profile.samples.empty()) + return false; + // an end which goes on into a line fitted already keeps its height and grade; any other end is free, the tracks past it are + // fitted later and meet this line then + auto const joint = [&](bool const Atend, double &Height) { + double grade; + TTrack *adjoining{nullptr}; + int path{0}; + if (false == editor_track::adjoining_grade(profile.route, Atend, grade, &adjoining, &path) || adjoining == nullptr || false == fitted(adjoining)) + return false; + auto const &end{Atend ? profile.samples.back().position : profile.samples.front().position}; + auto const &points{adjoining->m_paths[path].points}; + auto const &start{points[segment_data::point::start]}; + auto const &finish{points[segment_data::point::end]}; + Height = geometry::plan_distance(start, end) < geometry::plan_distance(finish, end) ? start.y : finish.y; + return true; + }; + profile.context.start_joined = profile.context.start_joined && joint(false, profile.context.start_elevation); + profile.context.end_joined = profile.context.end_joined && joint(true, profile.context.end_elevation); + // the ground of the line: the terrain model on its axis, the top of the ballast with the rails on it, and the rail tops of + // the lines fitted already beside it on the same level as the model here, which tell its height the more closely the + // nearer they are. What doesn't agree with the line, a bridge over it, the ground under a viaduct, the fit lets go of by + // itself + double constexpr kModel{0.25}; // m, the tolerance of the model + double constexpr kRail{0.02}; // m, of a rail top next to the line, growing by kSpread a metre of the distance + double constexpr kSpread{0.01}; + double constexpr kLevel{0.5}; // m, a rail top farther from the model or from the line lies on another level + double constexpr kReach{50.0}; + double constexpr kSteepest{40.0}; // per mille, the steepest grade the ground may give a line + double constexpr kCell{50.0}; + auto const nearest = [&](glm::dvec3 const &Point, double &Distance) { + Distance = std::numeric_limits::max(); + double height{0.0}; + auto const cx{static_cast(std::floor(Point.x / kCell))}; + auto const cz{static_cast(std::floor(Point.z / kCell))}; + for (int dx = -1; dx <= 1; ++dx) + for (int dz = -1; dz <= 1; ++dz) + { + auto const cell{m_nmt_state.laid.find({cx + dx, cz + dz})}; + if (cell == m_nmt_state.laid.end()) + continue; + for (auto const &laid : cell->second) + if (auto const distance{glm::length(glm::dvec2{laid.x - Point.x, laid.z - Point.z})}; distance < Distance) + { + Distance = distance; + height = laid.y; + } + } + return height; + }; + // an end of the line, or the other path of a switch of it, which leads through the tracks not fitted yet into a line fitted + // already, as at a crossover, ties the two where it leaves the line: there the line keeps to the height of the other, on + // along its grade over the tracks between, give or take what these can climb with curves of the least radius at both ends + double constexpr kJoin{0.002}; // m + double constexpr kFarthest{200.0}; + profile::ground_fit options; + auto const length_of = [](TTrack const &Track, int const Path) { return Track.eType == tt_Switch ? Track.SwitchExtension->Segments[Path]->GetLength() : Track.Segment->GetLength(); }; + auto const on_route = [&](TTrack const *Other) { return std::any_of(profile.route.spans.begin(), profile.route.spans.end(), [&](auto const &Span) { return Span.track == Other; }); }; + std::vector exits; + exits.push_back({profile.route.spans.front().track, profile.route.spans.front().path, false == profile.route.spans.front().forward}); + exits.push_back({profile.route.spans.back().track, profile.route.spans.back().path, profile.route.spans.back().forward}); + for (auto const &span : profile.route.spans) + if (span.track->eType == tt_Switch && span.track->m_paths.size() >= 2) + for (auto const outward : {true, false}) + exits.push_back({span.track, 1 - span.path, outward}); + for (auto const &exiting : exits) + { + auto const &exit{exiting.track->m_paths[exiting.path].points[exiting.forward ? segment_data::point::end : segment_data::point::start]}; + editor_track::route step; + step.spans.push_back(exiting); + double between{0.0}; + for (int hops = 0; hops < 50 && between < kFarthest; ++hops) + { + double grade; + TTrack *next{nullptr}; + int path{0}; + bool forward{true}; + if (false == editor_track::adjoining_grade(step, true, grade, &next, &path, &forward) || next == nullptr || on_route(next)) + break; + if (fitted(next)) + { + std::size_t nearest{0}; + for (std::size_t i = 1; i < profile.samples.size(); ++i) + if (geometry::plan_distance(profile.samples[i].position, exit) < geometry::plan_distance(profile.samples[nearest].position, exit)) + nearest = i; + auto const &points{next->m_paths[path].points}; + auto const height{points[forward ? segment_data::point::start : segment_data::point::end].y - grade * between}; + auto const leeway{kJoin + between * between / (4.0 * profile.line.norms.radius_min)}; + options.bounds.push_back({profile.samples[nearest].chainage, height - leeway, height + leeway}); + break; + } + step.spans = {{next, path, forward}}; + between += length_of(*next, path); + } + } + auto const count{profile.samples.size()}; + std::vector sampled(count), model(count, std::numeric_limits::quiet_NaN()); + std::vector chainages, ground, tolerances, reaches; + for (std::size_t i = 0; i < count; ++i) + { + auto const &sample{profile.samples[i]}; + auto const *track{sample.span < profile.route.spans.size() ? profile.route.spans[sample.span].track : nullptr}; + sampled[i] = sample.chainage; + if (double height; track != nullptr && m_nmt.height(sample.position.x, sample.position.z, height)) + model[i] = height + m_nmt_state.offset + track->rail_height(); + chainages.push_back(sample.chainage); + ground.push_back(model[i]); + tolerances.push_back(kModel); + reaches.push_back(kModel); + double distance; + auto const beside{nearest(sample.position, distance)}; + if (distance < kReach && (false == std::isfinite(model[i]) || std::abs(beside - model[i]) <= kLevel)) + { + chainages.push_back(sample.chainage); + ground.push_back(beside); + tolerances.push_back(std::max(kRail, kSpread * distance)); + reaches.push_back(kLevel); + } + } + if (std::none_of(ground.begin(), ground.end(), [](double const Height) { return std::isfinite(Height); })) + return false; + // the switches of the tracks beside, the ones of the crossovers above all, lie on bearers common with this track or close + // to it: no break of the grade on them either, so the tracks of the station stay in one plane + for (auto *other : simulation::Paths.sequence()) + { + if (other == nullptr || other->eType != tt_Switch || other->m_paths.empty() || false == rail(*other)) + continue; + if (std::any_of(profile.route.spans.begin(), profile.route.spans.end(), [&](auto const &Span) { return Span.track == other; })) + continue; + auto const &points{other->m_paths.front().points}; + double from{std::numeric_limits::max()}; + double to{std::numeric_limits::lowest()}; + for (auto const end : {segment_data::point::start, segment_data::point::end}) + { + auto const &point{points[end]}; + double best{25.0}; + double chainage{0.0}; + for (auto const &sample : profile.samples) + if (auto const distance{glm::length(glm::dvec2{sample.position.x - point.x, sample.position.z - point.z})}; distance < best) + { + best = distance; + chainage = sample.chainage; + } + if (best < 25.0) + { + from = std::min(from, chainage); + to = std::max(to, chainage); + } + } + if (from < to) + profile.context.switches.push_back({from, to, other->name()}); + } + std::sort(profile.context.switches.begin(), profile.context.switches.end(), [](auto const &Left, auto const &Right) { return Left.from < Right.from; }); + auto settings{profile.line}; + settings.join_start = true; + settings.join_end = true; + settings.speed = 0.0; + for (auto const &span : profile.route.spans) + settings.speed = std::max(settings.speed, editor_track::velocity(*span.track)); + if (settings.speed <= 0.0) + settings.speed = 100.0; + options.tolerance = kModel; + options.grade_max = kSteepest; + auto line{profile::fit_ground(chainages, ground, profile.context, settings, options, tolerances, reaches)}; + if (line.points.size() < 2) + return false; + profile.line = std::move(line); + profile.selected = -1; + profile_check(); + auto const fitted_line{profile.line}; + profile_apply(); + auto const applied{profile.error.empty()}; + // where the line parts from the model: under it a gap, a bridge or a viaduct, over it a wall or a structure above the track + auto const parted{profile::departures(fitted_line, sampled, model, 2.0 * kModel)}; + if (false == parted.empty()) + ++m_nmt_state.departures; + std::size_t agreeing{0}; + std::size_t modelled{0}; + for (std::size_t i = 0; i < count; ++i) + if (std::isfinite(model[i])) + { + ++modelled; + agreeing += std::abs(profile::elevation(fitted_line, sampled[i]) - model[i]) <= kModel ? 1 : 0; + } + // how soon past a switch or its shared timbers the grade changes, the nearest break or start of a curve of the line + if (applied) + { + auto const breaks{profile::breaks(profile.line)}; + double nearest{std::numeric_limits::max()}; + std::string where; + for (auto const &zone : profile.context.switches) + for (auto const at : breaks) + { + auto const gap{at >= zone.to ? at - zone.to : at <= zone.from ? zone.from - at : -1.0}; + if (gap >= 0.0 && gap < nearest) + { + nearest = gap; + where = format("%.1f m %s %s (%.1f..%.1f), the break at %.1f", gap, at >= zone.to ? "past" : "before", zone.name.c_str(), zone.from, zone.to, at); + } + } + if (false == where.empty()) + WriteLog(format("Editor: NMT line %s .. %s: the grade changes %s", from->name().c_str(), to->name().c_str(), where.c_str()), logtype::generic); + } + // where the paths didn't take the line + double off{0.0}; + std::size_t off_at{0}; + if (applied) + for (std::size_t i = 0; i < profile.samples.size(); ++i) + if (auto const departure{std::abs(profile.samples[i].position.y - profile::elevation(profile.line, profile.samples[i].chainage))}; departure > off) + { + off = departure; + off_at = i; + } + if (off > 0.05) + { + auto const &sample{profile.samples[off_at]}; + auto const *track{profile.route.spans[sample.span].track}; + WriteLog(format("Editor: NMT line %s: the path %s (%s, %.0f m) is off the line by %.3f m at %.0f m", Track.name().c_str(), track->name().c_str(), track->eType == tt_Switch ? "switch" : "track", track->Length(), off, sample.chainage), logtype::generic); + } + if (applied) + for (auto const &sample : profile.samples) + m_nmt_state.laid[{static_cast(std::floor(sample.position.x / kCell)), static_cast(std::floor(sample.position.z / kCell))}].push_back(sample.position); + // the line in the log: how much of it lies on the model, where it parts from it, its grades, why it wasn't applied, its + // course in the plan + std::string text{format("Editor: NMT line %s: %s .. %s, %.0f m, on the model within %.2f m over %.1f%%, parts from it", applied ? "applied" : "refused", from->name().c_str(), to->name().c_str(), profile.route.length, kModel, + 100.0 * static_cast(agreeing) / static_cast(std::max(1, modelled)))}; + for (auto const &stretch : parted) + text += format(" %s %.0f..%.0f %.1f m", stretch.depth < 0.0 ? "under" : "over", stretch.from, stretch.to, std::abs(stretch.depth)); + text += ", grades"; + for (std::size_t i = 1; i < fitted_line.points.size(); ++i) + { + auto const &a{fitted_line.points[i - 1]}; + auto const &b{fitted_line.points[i]}; + text += format(" %+.2f/%.0f", 1000.0 * (b.elevation - a.elevation) / std::max(1e-6, b.chainage - a.chainage), b.chainage - a.chainage); + } + if (false == applied) + { + text += "; " + profile.error; + for (auto const &issue : profile.issues) + if (issue.error) + text += " | " + issue.text; + } + text += "; plan"; + for (std::size_t i = 0; i < profile.samples.size(); i += 10) + text += format(" %.1f,%.1f", profile.samples[i].position.x, profile.samples[i].position.z); + text += format(" %.1f,%.1f", profile.samples.back().position.x, profile.samples.back().position.z); + WriteLog(text, logtype::generic); + return applied; +} + +void editor_mode::nmt_auto_step() +{ + auto &state{m_nmt_state}; + if (state.stage == nmt_state::automatic::fetching) + { + bool all{true}; + for (auto const &place : state.probes) + all = m_nmt.request(glm::dvec2{place.position.x, place.position.z} - glm::dvec2{1.0}, glm::dvec2{place.position.x, place.position.z} + glm::dvec2{1.0}) && all; + auto const waiting{m_nmt.waiting()}; + state.status = format(STR_P("Fetching the terrain model under the tracks: %zu square left", "Fetching the terrain model under the tracks: %zu squares left", waiting), waiting); + if (false == all) + return; + // the scenery goes to the height of the model as a whole first, so the lines fitted one after another meet at heights near + // their own; the grade lines then follow the model + std::vector differences; + for (auto const &place : state.probes) + if (double height; m_nmt.height(place.position.x, place.position.z, height)) + differences.push_back(height - place.crown); + if (differences.empty()) + { + state.stage = nmt_state::automatic::idle; + state.status = STR("The terrain model has no data under the tracks: is the scenery placed where it is?"); + return; + } + std::nth_element(differences.begin(), differences.begin() + differences.size() / 2, differences.end()); + auto const lift{differences[differences.size() / 2]}; + // tracks of files that aren't saved, as of shared includes, would stay where they were: the model comes to the scenery + std::vector scenery; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && false == track->m_paths.empty()) + scenery.push_back(track); + std::string reason; + state.lifted = 0.0; + state.offset = 0.0; + if (std::abs(lift) > 0.05 && tracks_editable(scenery, reason)) + { + nmt_lift_scenery(lift); + state.lifted = lift; + } + else if (std::abs(lift) > 0.05) + { + state.offset = -lift; + WriteLog(format("Editor: NMT - the terrain model offset %+.2f m to the scenery, which can't be moved: ", -lift) + reason, logtype::generic); + } + state.fitted = true; + nmt_join_ends(); + // the longest tracks first: the main lines lead, the sidings join them + state.left.clear(); + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && rail(*track)) + state.left.push_back(track); + std::sort(state.left.begin(), state.left.end(), [](TTrack *Left, TTrack *Right) { return Left->Length() > Right->Length(); }); + state.tracks = state.left.size(); + state.split = 0; + state.done.clear(); + state.laid.clear(); + state.stage = nmt_state::automatic::fitting; + return; + } + // a few lines a frame + for (int line = 0; line < 2 && false == state.left.empty(); ++line) + { + auto *track{state.left.front()}; + auto const name{track->name()}; + auto const known{simulation::Paths.sequence().size()}; + if (nmt_fit_line(*track)) + ++state.lines; + else + ++state.refused; + // the tracks of the line are done, whether it went or not; so is every path made meanwhile, split off by the grade line or + // by the lines following it over the crossovers: the next line over them would lay them again, and split them again + std::set done{track}; + state.done.push_back(track); + for (auto const &span : m_profile.route.spans) + if (done.insert(span.track).second) + state.done.push_back(span.track); + auto const &sequence{simulation::Paths.sequence()}; + std::string made; + for (auto i{known}; i < sequence.size(); ++i) + if (auto *piece{sequence[i]}; piece != nullptr && done.insert(piece).second) + { + state.done.push_back(piece); + if (made.size() < 300) + made += ' ' + piece->name(); + } + auto const added{sequence.size() - known}; + state.split += added; + if (added > 20) + WriteLog(format("Editor: NMT line %s made %zu paths:%s", name.c_str(), added, made.c_str()), logtype::generic); + if (state.split > state.tracks / 2 + 200) + { + // something lays the same tracks over and over: stopped before it takes all the memory + state.left.clear(); + state.error = format(STR_P("Stopped: the grade lines split off %zu path from %zu tracks, more than they should; Ctrl+Z takes the changes back", "Stopped: the grade lines split off %zu paths from %zu tracks, more than they should; Ctrl+Z takes the changes back", state.split), state.split, state.tracks); + WriteLog("Editor: NMT - " + state.error, logtype::generic); + break; + } + state.left.erase(std::remove_if(state.left.begin(), state.left.end(), [&](TTrack *Track) { return done.contains(Track); }), state.left.end()); + } + state.status = format(STR_C("Scenery moved %+.2f m; grade lines fitted to the terrain: %zu, refused by the checks: %zu, tracks left: %zu"), state.lifted, state.lines, state.refused, state.left.size()); + if (state.departures > 0) + state.status += format(STR_P("\n%zu line parts from the terrain model by over 0.5 m: the model or the track is off there, the terrain follows the tracks (the log names it)", "\n%zu lines part from the terrain model by over 0.5 m: the model or the track is off there, the terrain follows the tracks (the log names them)", state.departures), state.departures); + if (false == state.left.empty()) + return; + m_profile.open = false; + state.done.clear(); + state.laid.clear(); + state.stage = nmt_state::automatic::idle; + WriteLog("Editor: NMT - " + state.status, logtype::generic); + nmt_make(nmt_keys_along_tracks(), true); +} diff --git a/application/editormodeparallel.cpp b/application/editormodeparallel.cpp index 79840b9f..7fb5b893 100644 --- a/application/editormodeparallel.cpp +++ b/application/editormodeparallel.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormode.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" +#include "application/editormodeshared.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" @@ -36,9 +37,95 @@ using geometry::signed_angle; double const kParallelReach{5000.0}; // m each way along the line double const kSpacings[] = {3.5, 4.0, 4.5, 4.75, 5.0, 6.0}; +// the side of the new track as a picture: three rows, the new track on the left of the line, the line with its direction, +// the new track on the right; the side chosen drawn with the spacing marked, the other faint. a click on a row chooses its side +bool side_picker(int &Side, double const Spacing) +{ + auto const font{ImGui::GetFontSize()}; + auto const width{ImGui::GetContentRegionAvail().x}; + auto const row{std::round(font * 1.7f)}; + auto const height{row * 3.0f}; + auto const top{ImGui::GetCursorScreenPos()}; + ImGui::InvisibleButton("##side", ImVec2(width, height)); + auto const hovered{ImGui::IsItemHovered()}; + auto const mouse{ImGui::GetIO().MousePos}; + auto const middle{std::round(top.y + row * 1.5f)}; + auto const pointed{hovered ? (mouse.y < top.y + row ? -1 : mouse.y >= top.y + row * 2.0f ? 1 : 0) : 0}; + bool changed{false}; + if (ImGui::IsItemClicked() && pointed != 0 && pointed != Side) + { + Side = pointed; + changed = true; + } + auto *list{ImGui::GetWindowDrawList()}; + ImVec2 const bottom{top.x + width, top.y + height}; + auto const rounding{std::round(font * 0.35f)}; + list->AddRectFilled(top, bottom, ImGui::GetColorU32(ImGuiCol_FrameBg), rounding); + if (pointed != 0 && pointed != Side) + list->AddRectFilled(ImVec2(top.x, pointed < 0 ? top.y : top.y + row * 2.0f), ImVec2(bottom.x, pointed < 0 ? top.y + row : bottom.y), ImGui::GetColorU32(ImGuiCol_FrameBgHovered), rounding); + auto const pad{std::round(font * 0.6f)}; + char const *const labels[]{STR_C("Left"), STR_C("selected"), STR_C("Right")}; + float column{0.0f}; + for (auto const *label : labels) + column = std::max(column, ImGui::CalcTextSize(label).x); + auto const left{top.x + pad + column + pad}; + auto const right{bottom.x - pad * 2.0f}; + auto const gauge{std::round(font * 0.36f)}; + // a track as two rails, sleepers under the solid ones + auto const track = [&](float const Y, ImU32 const Colour, bool const Dashed) { + if (false == Dashed) + for (auto x{left + font * 0.3f}; x < right; x += std::round(font * 0.9f)) + list->AddLine(ImVec2(x, Y - gauge * 0.95f), ImVec2(x, Y + gauge * 0.95f), (Colour & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 90), 2.0f); + for (auto const rail : {Y - gauge * 0.5f, Y + gauge * 0.5f}) + { + if (false == Dashed) + { + list->AddLine(ImVec2(left, rail), ImVec2(right, rail), Colour, 1.5f); + continue; + } + for (auto x{left}; x < right; x += font) + list->AddLine(ImVec2(x, rail), ImVec2(std::min(right, x + font * 0.55f), rail), Colour, 1.0f); + } + }; + auto const line{ImGui::GetColorU32(ImGuiCol_Text, 0.85f)}; + track(middle, line, false); + auto const arrow{std::round(font * 0.45f)}; + list->AddTriangleFilled(ImVec2(right + pad * 1.2f, middle), ImVec2(right + pad * 0.2f - arrow * 0.4f, middle - arrow), ImVec2(right + pad * 0.2f - arrow * 0.4f, middle + arrow), line); + auto const label_at = [&](int const Row, char const *Label, bool const Lit) { + auto const size{ImGui::CalcTextSize(Label)}; + list->AddText(ImVec2(top.x + pad + column - size.x, std::round(top.y + row * (Row + 0.5f) - size.y * 0.5f)), ImGui::GetColorU32(Lit ? ImGuiCol_Text : ImGuiCol_TextDisabled), Label); + }; + label_at(1, labels[1], false); + auto const accent{family_colour(0)}; + for (auto const side : {-1, 1}) + { + auto const y{middle + side * row}; + auto const chosen{side == Side}; + track(y, chosen ? accent : ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.6f), false == chosen); + label_at(side < 0 ? 0 : 2, labels[side < 0 ? 0 : 2], chosen); + if (false == chosen) + continue; + // the spacing, axis to axis + auto const x{std::round(left + (right - left) * 0.62f)}; + ImU32 const dimension{IM_COL32(255, 210, 60, 255)}; + list->AddLine(ImVec2(x, middle), ImVec2(x, y), dimension, 1.5f); + list->AddTriangleFilled(ImVec2(x, middle), ImVec2(x - 3.0f, middle + side * 6.0f), ImVec2(x + 3.0f, middle + side * 6.0f), dimension); + list->AddTriangleFilled(ImVec2(x, y), ImVec2(x - 3.0f, y - side * 6.0f), ImVec2(x + 3.0f, y - side * 6.0f), dimension); + char text[32]; + std::snprintf(text, sizeof(text), "%.2f m", Spacing); + auto const size{ImGui::CalcTextSize(text)}; + ImVec2 const at{x + 6.0f, std::round((middle + y) * 0.5f - size.y * 0.5f)}; + list->AddRectFilled(ImVec2(at.x - 3.0f, at.y), ImVec2(at.x + size.x + 3.0f, at.y + size.y), ImGui::GetColorU32(ImGuiCol_FrameBg), 3.0f); + list->AddText(at, dimension, text); + } + if (hovered) + ImGui::SetTooltip("%s", pointed != 0 && pointed != Side ? STR_C("A click: the new track on this side") : STR_C("Left and right as seen along the line, the way the arrow points")); + return changed; +} + std::string name_of(TTrack const &Track) { - return Track.name().empty() ? std::string{"(noname)"} : Track.name(); + return Track.name().empty() ? STR("(no name)") : Track.name(); } segment_data reversed(segment_data const &Path) @@ -299,47 +386,41 @@ void editor_mode::render_parallel_controls(bool const Scope) { auto &tool{m_parallel}; if (Scope) - { - ImGui::RadioButton(STR_C("This path"), &tool.scope, 0); - ImGui::SameLine(); - ImGui::RadioButton(STR_C("The line, up to the switches"), &tool.scope, 1); - } - ImGui::RadioButton(STR_C("Left"), &tool.side, -1); - ImGui::SameLine(); - ImGui::RadioButton(STR_C("Right"), &tool.side, 1); - ImGui::SameLine(); - ImGui::TextDisabled("%s", STR_C("(of the direction of the line, see the preview)")); - ImGui::SetNextItemWidth(90.0f); - ImGui::InputDouble(STR_C("Spacing (m)"), &tool.spacing, 0.0, 0.0, "%.2f"); + segmented("scope", tool.scope, {{0, STR_C("This path"), "Only beside the selected path"}, {1, STR_C("The line, up to the switches"), "Beside the whole line of the selected path, up to the switches: its curves go along with R minus or plus the spacing"}}); + side_picker(tool.side, tool.spacing); + ImGui::SetNextItemWidth(std::round(ImGui::GetFontSize() * 6.0f)); + ImGui::InputDouble("##spacing", &tool.spacing, 0.0, 0.0, STR_C("%.2f m")); + item_tooltip("Spacing of the axes of the tracks; the usual ones beside it"); tool.spacing = std::clamp(tool.spacing, 1.5, 100.0); - for (auto const spacing : kSpacings) - { - ImGui::SameLine(); - if (ImGui::SmallButton(format("%.2f", spacing).c_str())) - tool.spacing = spacing; - } + ImGui::SameLine(); + value_chips("spacings", tool.spacing, {kSpacings[0], kSpacings[1], kSpacings[2], kSpacings[3], kSpacings[4], kSpacings[5]}, "%.2f"); } void editor_mode::render_parallel_ui() { auto &tool{m_parallel}; - ImGui::TextDisabled("%s", STR_C("First select a straight path (LMB). Then a second track is laid beside it.\nAlong the line the curves are included: R minus or plus the spacing, same transitions and cant.")); + ImGui::PushTextWrapPos(0.0f); if (parallel_target() != nullptr) - ImGui::TextDisabled("%s", STR_C("Two paths selected: set the spacing of the other in Select. The first must be a straight.")); - else if (auto *source{parallel_source()}; source == nullptr) - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("First select a straight path in the view.")); - else if (source->eType != tt_Normal || false == editor_track::is_straight(*source, m_straights.tolerance)) - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("The selected path isn't a straight. Select a straight first.")); + ImGui::TextDisabled("%s", STR_C("Two tracks selected: set the spacing of the other in the Select tool")); + else if (parallel_source() == nullptr) + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("Click a track in the view: the new one goes beside it.")); + ImGui::PopTextWrapPos(); render_parallel_controls(); parallel_update(); if (false == tool.error.empty()) + { + ImGui::PushTextWrapPos(0.0f); ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str()); + ImGui::PopTextWrapPos(); + } else if (false == tool.pieces.empty()) { - if (ImGui::Button(format(STR_C("Lay %zu paths, %.1f m"), tool.pieces.size(), tool.length).c_str())) + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); + if (ImGui::Button(format(STR_P("Lay %zu track, %.1f m", "Lay %zu tracks, %.1f m", static_cast(tool.pieces.size())), tool.pieces.size(), tool.length).c_str(), ImVec2(-1.0f, std::round(ImGui::GetFrameHeight() * 1.3f)))) parallel_build(); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("First select a straight. Creates a new track beside it; the selected path stays.")); + ImGui::PopStyleColor(2); + item_tooltip("A new track beside the one selected, as in the preview; the selected stays as it is. Ctrl+Z takes it back"); } if (false == tool.status.empty()) ImGui::TextWrapped("%s", tool.status.c_str()); @@ -354,8 +435,8 @@ void editor_mode::render_parallel_set_ui() return; if (false == ImGui::CollapsingHeader(STR_C("Make parallel"), ImGuiTreeNodeFlags_DefaultOpen)) return; - ImGui::TextWrapped("%s", STR_C("First selected must be a straight. It stays; the other is moved to this spacing.")); - ImGui::TextWrapped("%s", format(STR_C("%s stays. %s is moved to this spacing, taking its curves."), name_of(*source).c_str(), name_of(*target).c_str()).c_str()); + ImGui::TextWrapped("%s", STR_C("The first selected track must be a straight")); + ImGui::TextWrapped("%s", format(STR_C("%s stays, %s moves to this spacing with its curves"), name_of(*source).c_str(), name_of(*target).c_str()).c_str()); if (tool.measured > 0.05) ImGui::TextDisabled("%s", format(lateral_of(*source, *target) >= 0.0 ? STR_C("Now %.2f m on the right") : STR_C("Now %.2f m on the left"), tool.measured).c_str()); tool.scope = 0; diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 5f5c3496..fafb1524 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -10,6 +10,9 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" #include "application/editorprojection.h" +#include "application/editorgestures.h" +#include "application/editormodeshared.h" +#include "application/editoruilayer.h" #include "editor/editorFormat.hpp" #include "editor/editorAlignmentStore.hpp" #include "editor/editorGeometry.hpp" @@ -18,6 +21,8 @@ http://mozilla.org/MPL/2.0/. #include "world/Track.h" #include "scene/scenelayers.h" #include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "world/TrackSleepers.h" #include "utilities/Logs.h" #include "utilities/translation.h" @@ -40,6 +45,13 @@ ImU32 const kGradeLine{IM_COL32(90, 230, 110, 255)}; ImU32 const kTangents{IM_COL32(255, 210, 60, 130)}; ImU32 const kTrack{IM_COL32(190, 190, 190, 255)}; ImU32 const kTerrain{IM_COL32(170, 120, 70, 255)}; +ImU32 const kFormation{IM_COL32(215, 185, 140, 200)}; +double constexpr kNoBed{0.05}; // m, a trackbed shallower is none +// a design the track lay on when it was kept, within kApplied, was applied; where the track parts from it by more than kStale since, +// it was changed by other tools, and the line is read from the track there, over the stretch and kStaleMargin around it +double constexpr kApplied{0.01}; +double constexpr kStale{0.02}; +double constexpr kStaleMargin{15.0}; ImU32 const kGrid{IM_COL32(255, 255, 255, 22)}; ImU32 const kLabel{IM_COL32(200, 200, 200, 200)}; ImU32 const kSwitch{IM_COL32(160, 100, 220, 45)}; @@ -47,6 +59,8 @@ ImU32 const kError{IM_COL32(255, 80, 70, 255)}; ImU32 const kWarning{IM_COL32(255, 200, 60, 255)}; ImU32 const kPlan{IM_COL32(120, 190, 255, 255)}; ImU32 const kSwitchLabel{IM_COL32(200, 160, 255, 220)}; +ImU32 const kShared{IM_COL32(160, 100, 220, 110)}; // hatching, the colour of the switches +ImU32 const kSharedEdge{IM_COL32(200, 160, 255, 220)}; ImU32 const kChip{IM_COL32(14, 15, 19, 210)}; // text on a dark plate, readable over the lines of the plot @@ -99,6 +113,28 @@ bool has_errors(std::vector const &Issues) return std::any_of(Issues.begin(), Issues.end(), [](profile::issue const &Issue) { return Issue.error; }); } +// the chainage of the point on the route sampled, projected onto the stretch between the samples; false off the route +bool sampled_chainage(std::vector const &Samples, glm::dvec3 const &Point, double &Chainage) +{ + double best{0.3}; + bool found{false}; + for (std::size_t i = 0; i + 1 < Samples.size(); ++i) + { + glm::dvec2 const a{Samples[i].position.x, Samples[i].position.z}; + glm::dvec2 const b{Samples[i + 1].position.x, Samples[i + 1].position.z}; + glm::dvec2 const p{Point.x, Point.z}; + auto const run{b - a}; + auto const t{std::clamp(glm::dot(p - a, run) / std::max(1e-12, glm::dot(run, run)), 0.0, 1.0)}; + if (auto const distance{glm::length(p - (a + run * t))}; distance < best) + { + best = distance; + Chainage = Samples[i].chainage + (Samples[i + 1].chainage - Samples[i].chainage) * t; + found = true; + } + } + return found; +} + } // format of the lines, coordinates in the space of the file: @@ -159,6 +195,8 @@ void editor_mode::profile_library_load() auto &norms{entry.line.norms}; line >> norms.grade_max >> norms.grade_switch_max >> norms.radius_factor >> norms.radius_min >> norms.curve_threshold >> norms.element_min >> norms.fixed_tolerance; } + else if (keyword == "departure") + line >> entry.departure; else if (keyword == "point") { profile::pvi point; @@ -206,6 +244,8 @@ void editor_mode::profile_library_mark(std::vector Layers) lines.push_back(format("speed %.1f origin %.4f join %d %d", entry.line.speed, entry.origin, entry.line.join_start ? 1 : 0, entry.line.join_end ? 1 : 0)); auto const &norms{entry.line.norms}; lines.push_back(format("norms %.4f %.4f %.4f %.1f %.4f %.2f %.4f", norms.grade_max, norms.grade_switch_max, norms.radius_factor, norms.radius_min, norms.curve_threshold, norms.element_min, norms.fixed_tolerance)); + if (entry.departure >= 0.0) + lines.push_back(format("departure %.4f", entry.departure)); for (auto const &point : entry.line.points) lines.push_back(format("point %.4f %.4f %.1f %d", point.chainage, context.to_local({0.0, point.elevation, 0.0}).y, point.radius, point.automatic ? 1 : 0)); lines.emplace_back("end"); @@ -246,14 +286,208 @@ bool editor_mode::profile_restore() bool reversed; auto const index{profile_find(reversed)}; if (index < 0) - return false; + return profile_restore_parts(); auto const &entry{m_profiles[index]}; state.line = reversed ? reversed_line(entry.line, entry.length) : entry.line; state.origin = entry.origin; state.selected = -1; state.changed = false; profile_check(); - state.status = STR_C("Design read from the scenery file ") + scene::Layers.layer(entry.layer).name + format(STR_C(", largest departure from the track %.3f m"), state.departure); + if (entry.departure >= 0.0 && entry.departure <= kApplied && state.departure > kStale) + return profile_restore_parts(); + state.status = format(STR_C("Design read from the scenery file %s, largest departure from the track %.3f m"), scene::Layers.layer(entry.layer).name.c_str(), state.departure); + return true; +} + +// the designs kept for routes lying along this one, as the lines the tracks were fitted with one by one: their points carried +// over to the chainage of this route, straight between them where none lies, as over a switch none of them runs through +bool editor_mode::profile_restore_parts() +{ + auto &state{m_profile}; + auto const &samples{state.samples}; + if (samples.size() < 2) + return false; + auto const locate = [&](glm::dvec3 const &Point, double &Chainage) { return sampled_chainage(samples, Point, Chainage); }; + auto const length{state.route.length}; + // the direction of the route at the chainage, in the plan + auto const heading = [&](double const Chainage) { + auto const a{editor_track::sampled_position(samples, std::max(0.0, Chainage - 1.0))}; + auto const b{editor_track::sampled_position(samples, std::min(length, Chainage + 1.0))}; + return glm::dvec2{b.x - a.x, b.z - a.z}; + }; + std::vector points; + std::size_t parts{0}; + std::size_t retaken{0}; + // the ends of this route inside a kept design take their heights from it + std::optional start, end; + // the newer designs first: where two lie over the same stretch, the one kept later is the one meant + std::vector> covered; + auto const taken = [&](double const Chainage) { return std::any_of(covered.begin(), covered.end(), [&](auto const &Stretch) { return Chainage > Stretch.first + 0.25 && Chainage < Stretch.second - 0.25; }); }; + for (auto entries{m_profiles.rbegin()}; entries != m_profiles.rend(); ++entries) + { + auto const &entry{*entries}; + // the design laid over this route at least in part: one of its ends on it, the route running on towards the other + double a{0.0}, b{0.0}; + auto const first{locate(entry.start, a)}; + auto const last{locate(entry.end, b)}; + if (false == first && false == last) + continue; + double origin, direction; + double scale{1.0}; + if (first && last) + { + // the route between the ends of the design got longer or shorter, as when a track of it was moved: the points go along + scale = std::abs(b - a) / std::max(1.0, entry.length); + if (scale < 0.8 || scale > 1.25) + continue; + direction = b >= a ? 1.0 : -1.0; + origin = a; + } + else + { + auto const anchor{first ? a : b}; + auto const towards{first ? entry.end - entry.start : entry.start - entry.end}; + auto const along{glm::dot(heading(anchor), glm::dvec2{towards.x, towards.z})}; + if (std::abs(along) < 1e-9) + continue; + // the chainage of the design grows along the route or against it + direction = (along > 0.0) == first ? 1.0 : -1.0; + origin = first ? a : b - direction * entry.length; + } + auto const local = [&](double const Chainage) { return (Chainage - origin) * direction / scale; }; + // the stretch of this route over the paths of the design, from where it meets it: off it, as past the switch where this + // route turns away, the design says nothing + auto const extent_low{std::min(origin, origin + direction * entry.length * scale)}; + auto const extent_high{std::max(origin, origin + direction * entry.length * scale)}; + auto low{extent_low}; + auto high{extent_high}; + if (auto const own{profile_stored_route(entry)}) + { + std::set> paths; + for (auto const &span : own->spans) + paths.emplace(span.track, span.path); + auto const &spans{state.route.spans}; + auto const on = [&](std::size_t const Index) { return paths.count({spans[Index].track, spans[Index].path}) > 0; }; + auto const anchor{first ? a : b}; + std::optional at; + for (std::size_t i = 0; i < spans.size(); ++i) + if (on(i) && anchor >= spans[i].from - 0.3 && anchor <= spans[i].to + 0.3) + { + at = i; + break; + } + if (false == at.has_value()) + continue; + auto left{*at}; + auto right{*at}; + while (left > 0 && on(left - 1)) + --left; + while (right + 1 < spans.size() && on(right + 1)) + ++right; + low = std::max(low, spans[left].from); + high = std::min(high, spans[right].to); + } + auto const inside = [&](double const Chainage) { return Chainage > 0.25 && Chainage < length - 0.25 && Chainage >= low - 0.25 && Chainage <= high + 0.25; }; + // where the track was changed since the design was applied to it, by the gizmo or by dragging its points, the track holds: + // the design gives way there to the line read from the track + std::vector> stale; + if (entry.departure >= 0.0 && entry.departure <= kApplied) + { + for (auto const &sample : samples) + { + if (sample.chainage < low - 0.25 || sample.chainage > high + 0.25) + continue; + auto const designed{profile::elevation(entry.line, std::clamp(local(sample.chainage), 0.0, entry.length))}; + if (std::abs(designed - sample.position.y) <= kStale) + continue; + if (false == stale.empty() && sample.chainage - stale.back().second <= kStaleMargin) + stale.back().second = sample.chainage; + else + stale.emplace_back(sample.chainage, sample.chainage); + } + for (auto &stretch : stale) + { + stretch.first = std::max(low, stretch.first - kStaleMargin); + stretch.second = std::min(high, stretch.second + kStaleMargin); + } + } + auto const changed = [&](double const Chainage) { return std::any_of(stale.begin(), stale.end(), [&](auto const &Stretch) { return Chainage >= Stretch.first - 0.25 && Chainage <= Stretch.second + 0.25; }); }; + for (auto point : entry.line.points) + { + point.chainage = origin + direction * point.chainage * scale; + point.joint = false; + if (inside(point.chainage) && false == taken(point.chainage) && false == changed(point.chainage)) + points.push_back(point); + } + for (auto const &stretch : stale) + { + std::vector chainages, elevations, grades; + for (auto const &sample : samples) + if (sample.chainage >= stretch.first - 1e-6 && sample.chainage <= stretch.second + 1e-6) + { + chainages.push_back(sample.chainage); + elevations.push_back(sample.position.y); + grades.push_back(sample.grade); + } + if (chainages.size() < 2) + continue; + profile::recognition options; + options.minimum_length = 5.0; + options.detail = kStale; + for (auto point : profile::recognize(chainages, elevations, grades, options).points) + { + point.joint = false; + if (point.chainage >= 0.0 && point.chainage <= length && false == taken(point.chainage)) + points.push_back(point); + } + ++retaken; + } + // where this route turns off the design, its height there: between two designs the line goes straight, over the switches. + // where it runs on past the end of the design, onto track laid since, the break gets the vertical curve of the limits + for (auto const edge : {low, high}) + if (edge > 0.5 && edge < length - 0.5 && false == taken(edge) && false == changed(edge)) + { + profile::pvi point; + point.chainage = edge; + point.elevation = profile::elevation(entry.line, std::clamp(local(edge), 0.0, entry.length)); + point.automatic = std::abs(edge - (edge == low ? extent_low : extent_high)) < 0.5; + points.push_back(point); + } + if (false == start.has_value() && false == changed(0.0) && local(0.0) >= -0.25 && local(0.0) <= entry.length + 0.25) + start = profile::elevation(entry.line, std::clamp(local(0.0), 0.0, entry.length)); + if (false == end.has_value() && false == changed(length) && local(length) >= -0.25 && local(length) <= entry.length + 0.25) + end = profile::elevation(entry.line, std::clamp(local(length), 0.0, entry.length)); + covered.emplace_back(low, high); + ++parts; + } + if (parts == 0) + return false; + std::sort(points.begin(), points.end(), [](auto const &Left, auto const &Right) { return Left.chainage < Right.chainage; }); + std::vector line; + for (auto point : points) + { + point.chainage = std::clamp(point.chainage, 0.0, length); + if (false == line.empty() && point.chainage - line.back().chainage < 0.5) + continue; + line.push_back(point); + } + if (line.empty() || line.front().chainage > 0.5) + line.insert(line.begin(), profile::pvi{0.0, start.value_or(samples.front().position.y)}); + else + line.front().chainage = 0.0; + if (line.back().chainage < length - 0.5) + line.push_back(profile::pvi{length, end.value_or(samples.back().position.y)}); + else + line.back().chainage = length; + line.front().radius = 0.0; + line.back().radius = 0.0; + state.line.points = std::move(line); + state.selected = -1; + state.changed = false; + profile_check(); + state.status = format(STR_C("Design put together from %zu designs kept for the routes along this one, largest departure from the track %.3f m"), parts, state.departure); + if (retaken > 0) + state.status += format(STR_C("; %zu stretches changed on the track since read from it again"), retaken); return true; } @@ -275,9 +509,28 @@ void editor_mode::profile_store() entry.middle = editor_track::sampled_position(state.samples, state.route.length * 0.5); entry.origin = state.origin; entry.line = state.line; - auto &points{entry.line.points}; - points.erase(std::remove_if(points.begin(), points.end(), [](profile::pvi const &Point) { return Point.joint; }), points.end()); + entry.departure = state.departure; + // the joints are kept as points of their own: read back over a longer route, where the end is no longer joined, the line + // keeps the curve it was laid with + for (auto &point : entry.line.points) + if (point.joint) + { + point.joint = false; + point.automatic = false; + } + // the designs of shorter routes this one runs all along are taken over by it: kept, they'd be put together with it again + std::vector layers; + for (auto i{m_profiles.size()}; i-- > 0;) + { + auto const &old{m_profiles[i]}; + double a{0.0}, b{0.0}; + if (old.length + 0.5 < entry.length && sampled_chainage(state.samples, old.start, a) && sampled_chainage(state.samples, old.end, b) && std::abs(std::abs(b - a) - old.length) <= 1.5) + { + layers.push_back(old.layer); + m_profiles.erase(m_profiles.begin() + static_cast(i)); + } + } bool reversed; auto const index{profile_find(reversed)}; if (index >= 0) @@ -295,7 +548,8 @@ void editor_mode::profile_store() entry.layer = layer; m_profiles.push_back(entry); } - profile_library_mark({entry.layer}); + layers.push_back(entry.layer); + profile_library_mark(layers); state.changed = false; } @@ -310,15 +564,11 @@ void editor_mode::profile_forget() auto const layer{m_profiles[index].layer}; m_profiles.erase(m_profiles.begin() + index); profile_library_mark({layer}); - state.status = STR_C("The design will be removed from ") + scene::Layers.layer(layer).name + STR_C(" on save"); + state.status = format(STR_C("The design will be removed from %s on save"), scene::Layers.layer(layer).name.c_str()); } -void editor_mode::profile_open_stored(std::size_t const Index) +std::optional editor_mode::profile_stored_route(stored_profile const &Entry, TTrack **From, TTrack **To) const { - auto &state{m_profile}; - if (Index >= m_profiles.size()) - return; - auto const entry{m_profiles[Index]}; auto const touching = [](glm::dvec3 const &Point) { std::vector result; for (auto *track : simulation::Paths.sequence()) @@ -335,19 +585,38 @@ void editor_mode::profile_open_stored(std::size_t const Index) } return result; }; - for (auto *from : touching(entry.start)) - for (auto *to : touching(entry.end)) + for (auto *from : touching(Entry.start)) + for (auto *to : touching(Entry.end)) { editor_track::route route; std::string error; - if (false == editor_track::find_route(from, to, route, error) || std::abs(route.length - entry.length) > 0.5) + if (false == editor_track::find_route(from, to, route, error) || std::abs(route.length - Entry.length) > 0.5) continue; auto const samples{editor_track::sample_route(route, 1.0)}; - if (samples.empty() || plan_distance(samples.front().position, entry.start) > 0.1 || plan_distance(editor_track::sampled_position(samples, route.length * 0.5), entry.middle) > 0.5) + if (samples.empty() || plan_distance(samples.front().position, Entry.start) > 0.1 || plan_distance(editor_track::sampled_position(samples, route.length * 0.5), Entry.middle) > 0.5) continue; - profile_open(from, to); - return; + if (From != nullptr) + *From = from; + if (To != nullptr) + *To = to; + return route; } + return std::nullopt; +} + +void editor_mode::profile_open_stored(std::size_t const Index) +{ + auto &state{m_profile}; + if (Index >= m_profiles.size()) + return; + auto const entry{m_profiles[Index]}; + TTrack *from{nullptr}; + TTrack *to{nullptr}; + if (profile_stored_route(entry, &from, &to).has_value()) + { + profile_open(from, to); + return; + } state.open = true; state.error = STR_C("The route of the stored design isn't in the scenery any more (paths moved or removed)"); } @@ -356,6 +625,7 @@ void editor_mode::profile_edited() { m_profile.changed = true; profile_check(); + first_step_done(first_step::profile_point); } void editor_mode::profile_open(TTrack *From, TTrack *To) @@ -386,6 +656,56 @@ void editor_mode::profile_open_run(TTrack &Track) profile_take(std::move(route), from, to); } +// the route from one end of the selected tracks to the other: of the two of them farthest apart, through the switches between +void editor_mode::profile_open_selection(std::vector const &Tracks) +{ + auto const middle = [](TTrack const &Track) { return geometry::bezier{Track.m_paths.front()}.point(0.5); }; + TTrack *from{nullptr}; + TTrack *to{nullptr}; + double farthest{-1.0}; + for (std::size_t i = 0; i < Tracks.size(); ++i) + for (std::size_t j = i + 1; j < Tracks.size(); ++j) + if (auto const distance{plan_distance(middle(*Tracks[i]), middle(*Tracks[j]))}; distance > farthest) + { + farthest = distance; + from = Tracks[i]; + to = Tracks[j]; + } + if (from == nullptr || to == nullptr) + return; + profile_open(from, to); + auto &state{m_profile}; + if (state.route.spans.empty()) + return; + auto const off{std::count_if(Tracks.begin(), Tracks.end(), [&](TTrack const *Track) { + return std::none_of(state.route.spans.begin(), state.route.spans.end(), [&](editor_track::route_span const &Span) { return Span.track == Track; }); + })}; + if (off > 0) + state.status = format(STR_C("%d of the selected tracks are off the route from %s to %s"), static_cast(off), from->name().c_str(), to->name().c_str()); +} + +// the route follows the selection: one track gives its whole line unless it's on the route already, several give the route +// from one end of them to the other; the changes of the design left are kept for the save +void editor_mode::profile_follow_selection() +{ + auto &state{m_profile}; + auto const tracks{selected_tracks()}; + if (tracks == state.followed) + return; + state.followed = tracks; + if (tracks.empty()) + return; + auto const on_route{std::any_of(state.route.spans.begin(), state.route.spans.end(), [&](editor_track::route_span const &Span) { return Span.track == tracks.front(); })}; + if (tracks.size() == 1 && on_route) + return; + if (state.changed) + profile_store(); + if (tracks.size() == 1) + profile_open_run(*tracks.front()); + else + profile_open_selection(tracks); +} + void editor_mode::profile_take(editor_track::route Route, TTrack *From, TTrack *To) { auto &state{m_profile}; @@ -395,6 +715,8 @@ void editor_mode::profile_take(editor_track::route Route, TTrack *From, TTrack * state.to = state.picked_to; state.route = std::move(Route); state.selected = -1; + state.edits = m_track_edits; + first_step_done(first_step::profile); profile_resample(); if (false == profile_restore()) { @@ -425,6 +747,27 @@ void editor_mode::profile_resample() for (std::size_t i = 0; i < samples.size(); ++i) if (found[i] != 0) state.terrain[i] = heights[i]; + state.beds.assign(samples.size(), 0.0); + std::vector bedless(samples.size(), 0); + std::vector own; + for (std::size_t i = 0; i < samples.size(); ++i) + if (samples[i].span < state.route.spans.size()) + { + auto const &track{*state.route.spans[samples[i].span].track}; + state.beds[i] = profile_ballast(track).first; + bedless[i] = profile_own_bed(track) ? 0 : 1; + if (bedless[i] == 0) + own.push_back(state.beds[i]); + } + // a track with no trackbed of its own, as the switches of the older sceneries whose bed is an include, lies in the bed of + // the others + if (false == own.empty()) + { + std::nth_element(own.begin(), own.begin() + own.size() / 2, own.end()); + for (std::size_t i = 0; i < samples.size(); ++i) + if (bedless[i] != 0) + state.beds[i] = own[own.size() / 2]; + } auto &context{state.context}; context = {}; @@ -451,10 +794,46 @@ void editor_mode::profile_resample() open.reset(); } } + // where the rails of the route lie on timbers shared with another track, at a switch, a crossover, a crossing, the grade + // doesn't break nor does a vertical curve reach: the timbers carry both tracks in one plane + { + auto const before{context.switches.size()}; + for (auto const &span : state.route.spans) + { + auto const length{span.track->eType == tt_Switch ? span.track->SwitchExtension->Segments[span.path]->GetLength() : span.track->Segment->GetLength()}; + if (length < 1e-6) + continue; + auto const scale{(span.to - span.from) / length}; + for (auto const &zone : track_sleepers::shared_zones(*span.track, span.path)) + { + auto const a{span.forward ? span.from + zone.first * scale : span.to - zone.second * scale}; + auto const b{span.forward ? span.from + zone.second * scale : span.to - zone.first * scale}; + // the stretches of a switch on the route are its own zone already + auto const inside{std::any_of(context.switches.begin(), context.switches.begin() + static_cast(before), [&](auto const &Zone) { return a >= Zone.from - 0.5 && b <= Zone.to + 0.5; })}; + if (false == inside && b - a > 0.1) + context.switches.push_back({a, b, STR("shared timbers")}); + } + } + std::sort(context.switches.begin(), context.switches.end(), [](auto const &Left, auto const &Right) { return Left.from < Right.from; }); + } context.start_elevation = samples.front().position.y; context.end_elevation = samples.back().position.y; - context.start_joined = editor_track::adjoining_grade(state.route, false, context.start_grade); - context.end_joined = editor_track::adjoining_grade(state.route, true, context.end_grade); + TTrack *adjoining{nullptr}; + int path{0}; + bool entered{false}; + // a joined end on a switch inherits its height and grade from the switch, which lies in one plane + auto const inherits = [&](bool const Joined, double const Elevation, double const Grade) { + if (false == Joined || adjoining == nullptr || adjoining->eType != tt_Switch) + return std::string{}; + auto const found{switch_store::named(*adjoining)}; + auto const point{entered ? "P1" : path == 0 ? "P2" : "P4"}; + return format(STR_C("Grade line from the switch %s (%s): %.2f m, %+.2f per mille"), (found ? switch_store::label(*found) : adjoining->name()).c_str(), point, Elevation, Grade * 1000.0); + }; + context.start_joined = editor_track::adjoining_grade(state.route, false, context.start_grade, &adjoining, &path, &entered); + state.start_inherits = inherits(context.start_joined, context.start_elevation, context.start_grade); + adjoining = nullptr; + context.end_joined = editor_track::adjoining_grade(state.route, true, context.end_grade, &adjoining, &path, &entered); + state.end_inherits = inherits(context.end_joined, context.end_elevation, context.end_grade); profile_check(); } @@ -474,7 +853,7 @@ void editor_mode::profile_recognize() state.line.speed = route_speed(state.route); state.selected = -1; profile_check(); - state.status = format(STR_C("Grade line recognized from the track: %zu points, largest departure %.3f m"), state.line.points.size(), state.departure); + state.status = format(STR_P("Grade line recognized from the track: %zu point, largest departure %.3f m", "Grade line recognized from the track: %zu points, largest departure %.3f m", state.line.points.size()), state.line.points.size(), state.departure); } void editor_mode::profile_fit_ground() @@ -490,7 +869,11 @@ void editor_mode::profile_fit_ground() } auto settings{state.line}; settings.speed = route_speed(state.route); - auto line{profile::fit_ground(chainages, state.terrain, state.context, settings, state.fit)}; + // the grade line is the rail top: it's fitted so the formation under it, not the rails, follows the ground + auto ground{state.terrain}; + for (std::size_t i = 0; i < ground.size() && i < state.beds.size(); ++i) + ground[i] += state.beds[i]; + auto line{profile::fit_ground(chainages, ground, state.context, settings, state.fit)}; if (line.points.size() < 2) return; state.line = std::move(line); @@ -503,20 +886,26 @@ void editor_mode::profile_fit_ground() { if (false == std::isfinite(state.terrain[i])) continue; - auto const difference{profile::elevation(state.line, state.samples[i].chainage) - state.terrain[i]}; + auto const difference{profile::elevation(state.line, state.samples[i].chainage) - (i < state.beds.size() ? state.beds[i] : 0.0) - state.terrain[i]}; fill = std::max(fill, difference); cut = std::max(cut, -difference); } - state.status = format(STR_C("Grade line fitted to the ground: %zu points, embankment up to %.2f m, cutting up to %.2f m"), state.line.points.size(), fill, cut); + state.status = format(STR_P("Grade line fitted to the ground: %zu point, embankment up to %.2f m, cutting up to %.2f m", "Grade line fitted to the ground: %zu points, embankment up to %.2f m, cutting up to %.2f m", state.line.points.size()), state.line.points.size(), fill, cut); } std::pair editor_mode::profile_ballast(TTrack const &Track) { - auto const depth{std::max(0.0, static_cast(Track.fTexHeight1 - Track.fTexHeightOffset))}; + // the ballast is laid from the foot of the rails down, the rail top being the grade line + auto const depth{std::max(0.0, static_cast(Track.fTexHeight1 - Track.fTexHeightOffset)) + Track.rail_height()}; auto const foot{0.5 * std::abs(Track.fTrackWidth) + std::abs(Track.fTexWidth) + std::abs(Track.fTexSlope)}; return {depth, std::max(0.5, foot)}; } +bool editor_mode::profile_own_bed(TTrack const &Track) +{ + return Track.fTexHeight1 - Track.fTexHeightOffset > kNoBed; +} + std::pair editor_mode::profile_generate_ground(std::vector> const &Beds, double const Reach) { auto &state{m_profile}; @@ -677,11 +1066,11 @@ void editor_mode::profile_shape_ground() m_streamer.modifier(std::move(modifier)); forget_ground(); - auto status{format(STR_C("Terrain shaped by the modifier \"%s\" (%zu points): embankment up to %.2f m, cutting up to %.2f m"), works.modifier.c_str(), count, fill, cut)}; + auto status{format(STR_P("Terrain shaped by the modifier \"%s\" (%zu point): embankment up to %.2f m, cutting up to %.2f m", "Terrain shaped by the modifier \"%s\" (%zu points): embankment up to %.2f m, cutting up to %.2f m", count), works.modifier.c_str(), count, fill, cut)}; if (generated.first > 0) - status += format(STR_C("; %d chunk(s) of terrain made under the route"), generated.first); + status += format(STR_P("; %d chunk of terrain made under the route", "; %d chunks of terrain made under the route", generated.first), generated.first); if (generated.second > 0) - status += format(STR_C("; %d chunk(s) left out: the scenery has its own ground there, convert it to shape it"), generated.second); + status += format(STR_P("; %d chunk left out: the scenery has its own ground there, convert it to shape it", "; %d chunks left out: the scenery has its own ground there, convert it to shape it", generated.second), generated.second); state.error.clear(); profile_resample(); state.status = status; @@ -796,16 +1185,19 @@ void editor_mode::render_profile_earthworks() auto &works{state.earthworks}; if (false == ImGui::TreeNode(STR_C("Shape the terrain to the grade line"))) return; - ImGui::TextWrapped("%s", STR_C("Only the heightmap terrain changes: a formation under the track and slopes to the ground, kept as a modifier of the terrain which follows the grade line when it's shaped again; the triangles and models of the scenery stay as they are.")); + ImGui::TextWrapped("%s", STR_C("Only the heightmap terrain changes: a formation and slopes are made, the models of the scenery stay as they are")); render_ground_conversion(); - auto const drag = [](char const *Label, double &Value, float const Speed, char const *Format) { return ImGui::DragScalar(STR_C(Label), ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format); }; - ImGui::PushItemWidth(90.0f); - ImGui::Checkbox(STR_C("Make terrain where the route has none"), &works.generate); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Chunks of heightmap terrain are made along the route where there's none, from the ground around,\nthen shaped. Where the scenery has its own ground triangles the chunk is left out: convert them instead")); - ImGui::Checkbox(STR_C("Formation under the ballast of the track"), &works.from_ballast); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("The terrain goes as deep under the track as its ballast reaches and as wide as its foot,\nread from each path of the route; off: the values given below")); + auto const drag = [](char const *Label, double &Value, float const Speed, char const *Format) { + field_row(Label); + ImGui::PushID(Label); + auto const edited{ImGui::DragScalar("##value", ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format)}; + ImGui::PopID(); + return edited; + }; + toggle_chips("earthworks", {{STR_C("Make terrain where the route has none"), &works.generate, + "Chunks of heightmap terrain are made along the route where there's none, from the ground around,\nthen shaped. Where the scenery has its own ground triangles the chunk is left out: convert them instead"}, + {STR_C("Formation under the ballast of the track"), &works.from_ballast, + "The terrain goes as deep under the track as its ballast reaches and as wide as its foot,\nread from each path of the route; off: the values given below"}}); if (works.from_ballast) { if (false == state.route.spans.empty() && state.route.spans.front().track != nullptr) @@ -814,19 +1206,29 @@ void editor_mode::render_profile_earthworks() ImGui::TextDisabled(STR_C("Ballast %.2f m deep, its foot %.2f m from the axis"), bed.first, bed.second); } } - else + if (false == works.from_ballast && begin_fields("##earthworkfields", 0.55f)) { drag("Formation below the track (m)", works.depth, 0.01f, "%.2f"); drag("Half width of the formation (m)", works.half_width, 0.05f, "%.2f"); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("From the axis of the route to the edge of the formation;\nfor two tracks add the distance between them")); + ImGui::EndTable(); } - drag("Slopes 1 : n, n", works.slope, 0.05f, "%.2f"); - drag("Slopes reach at most (m)", works.reach, 1.0f, "%.0f"); - drag("Rounding of the edges (m)", works.rounding, 0.1f, "%.1f"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Length over which the slope bends into the formation and into the ground; 0: sharp edges")); - ImGui::PopItemWidth(); + ImGui::PushID("earthworkmore"); + if (ImGui::TreeNode(STR_C("More settings"))) + { + if (begin_fields("##earthworkslopes", 0.55f)) + { + drag("Slopes 1 : n, n", works.slope, 0.05f, "%.2f"); + drag("Slopes reach at most (m)", works.reach, 1.0f, "%.0f"); + drag("Rounding of the edges (m)", works.rounding, 0.1f, "%.1f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Length over which the slope bends into the formation and into the ground; 0: sharp edges")); + ImGui::EndTable(); + } + ImGui::TreePop(); + } + ImGui::PopID(); works.rounding = std::clamp(works.rounding, 0.0, 20.0); works.depth = std::clamp(works.depth, 0.0, 5.0); works.half_width = std::clamp(works.half_width, 0.5, 30.0); @@ -834,12 +1236,18 @@ void editor_mode::render_profile_earthworks() works.reach = std::clamp(works.reach, 1.0, 200.0); if (has_errors(state.issues)) ImGui::TextDisabled("%s", STR_C("Correct the errors of the grade line first")); - else if (ImGui::Button(STR_C("Shape the terrain"))) - profile_shape_ground(); + else + { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.42f, 0.24f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.22f, 0.52f, 0.30f, 1.0f)); + if (ImGui::Button(STR_C("Shape the terrain"), ImVec2(-FLT_MIN, ImGui::GetFrameHeight() * 1.3f))) + profile_shape_ground(); + item_tooltip("Makes the formation and the slopes along the route in the heightmap terrain.\nThey're kept as a modifier of the terrain, which follows the grade line when you shape it again; the triangles and models of the scenery stay as they are"); + ImGui::PopStyleColor(2); + } if (false == works.modifier.empty() || false == works.generated.empty()) { - ImGui::SameLine(); - if (ImGui::Button(STR_C("Restore the terrain"))) + if (ImGui::Button(STR_C("Restore the terrain"), ImVec2(-FLT_MIN, 0.0f))) profile_restore_ground(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Removes the modifier of the route, and takes away the terrain the shaping made")); @@ -919,17 +1327,21 @@ void editor_mode::profile_apply() return; } auto const line{state.line}; + auto const before{state.samples}; std::vector> states; std::vector created; auto const gaps{editor_track::apply_profile( state.route, [&](double const Chainage) { return profile::elevation(line, Chainage); }, [&](double const Chainage) { return profile::grade(line, Chainage); }, profile::breaks(line), states, created)}; auto const changed{states.size()}; auto const added{created.size()}; + state.created = created; // the stored design changes with the paths, an undo takes both back profile_library_load(); auto library{m_profiles}; bool const pushed{false == states.empty() || false == created.empty()}; push_track_snapshot(std::move(states), std::move(created)); + state.edits = m_track_edits; + first_step_done(first_step::profile_apply); if (pushed) { m_history.back().profiles = true; @@ -939,21 +1351,25 @@ void editor_mode::profile_apply() state.to = state.route.spans.back().track; profile_resample(); profile_store(); - state.status = format(STR_C("Grade line applied: %zu paths changed, %zu added, largest departure %.3f m"), changed, added, state.departure); + state.status = format(STR_P("Grade line applied: %zu path changed, %zu added, largest departure %.3f m", "Grade line applied: %zu paths changed, %zu added, largest departure %.3f m", changed), changed, added, state.departure); if (false == scene::Layers.empty()) - state.status += "\nThe paths and the design (//$p lines) go to the scenery files on save"; - auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; + state.status += STR_C("\nThe paths and the design (//$p lines) go to the scenery files on save"); + auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{STR_C("(no name)")} : Track->name(); }; auto const closed{std::count_if(gaps.begin(), gaps.end(), [](auto const &Gap) { return Gap.closed; })}; if (closed > 0) - state.status += "\nPaths off the route brought to the height of the switches:"; + state.status += STR_C("\nPaths off the route brought to its height:"); for (auto const &gap : gaps) if (gap.closed) - state.status += format("\n %s at %s: %+.3f m", name(gap.neighbour).c_str(), name(gap.track).c_str(), -gap.gap); + state.status += format(STR_C("\n %s at %s: %+.3f m"), name(gap.neighbour).c_str(), name(gap.track).c_str(), -gap.gap); if (closed < static_cast(gaps.size())) - state.status += "\nSwitch ends off the adjoining paths, which can't be changed here:"; + state.status += STR_C("\nSwitch ends off the adjoining paths, which can't be changed here:"); for (auto const &gap : gaps) if (false == gap.closed) state.status += format("\n %s -> %s: %+.3f m", name(gap.track).c_str(), name(gap.neighbour).c_str(), gap.gap); + // the tracks on the other side of the crossovers of the route go up and down with it + auto const followed{follow_crossovers(before)}; + if (followed > 0) + state.status += format(STR_C("\nThe tracks across %zu crossover(s) followed the change"), followed); WriteLog("Editor: vertical profile - " + state.status, logtype::generic); } @@ -981,21 +1397,35 @@ void editor_mode::profile_after_undo() profile_resample(); } +// the tracks of the route changed by another tool or by an undo: the route read again, the design kept in the scenery read +// again over it unless it's being changed +void editor_mode::profile_follow_edits() +{ + auto &state{m_profile}; + if (state.edits == m_track_edits) + return; + state.edits = m_track_edits; + if (state.route.spans.empty() || state.dragging >= 0) + return; + auto const kept{false == state.changed}; + profile_after_undo(); + if (kept && false == state.route.spans.empty()) + profile_restore(); +} + void editor_mode::render_profile_body() { auto &state{m_profile}; + profile_follow_selection(); + step_heading(1, STR_C("Route")); render_profile_source(); if (state.route.spans.empty()) { - ImGui::TextWrapped("%s", STR_C("Select a path in the view, then build the route along the line, or set its start and end paths (switches are passed through)")); - if (state.picked_from != nullptr) - ImGui::Text(STR_C("Start: %s"), state.picked_from->name().c_str()); if (false == state.error.empty()) ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", state.error.c_str()); return; } - ImGui::Separator(); - ImGui::TextWrapped(STR_C("Route: %zu paths, %.2f m, %s ... %s"), state.route.spans.size(), state.route.length, state.route.spans.front().track->name().c_str(), state.route.spans.back().track->name().c_str()); + ImGui::TextWrapped(STR_P("%zu path, %.2f m: %s ... %s", "%zu paths, %.2f m: %s ... %s", state.route.spans.size()), state.route.spans.size(), state.route.length, state.route.spans.front().track->name().c_str(), state.route.spans.back().track->name().c_str()); { bool reversed; auto const stored{profile_find(reversed)}; @@ -1010,77 +1440,210 @@ void editor_mode::render_profile_body() ImGui::TextDisabled(STR_C("The design goes to the scenery file of the route on save")); } + step_heading(2, STR_C("Grade line")); if (render_profile_parameters()) profile_edited(); + step_heading(3, STR_C("Terrain")); + render_profile_earthworks(); } void editor_mode::render_profile_strip() { + profile_follow_edits(); auto &state{m_profile}; auto const &io{ImGui::GetIO()}; - ImGui::SetNextWindowPos(ImVec2(8.0f, io.DisplaySize.y - 360.0f), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2(std::max(600.0f, io.DisplaySize.x - 500.0f), 320.0f), ImGuiCond_FirstUseEver); - auto const title{std::string{STR_C("Vertical profile")} + format(" %.2f m###profilestrip", state.route.length)}; + // along the bottom of the 3d view, as wide as it; each time the profile opens, wherever it was left before + auto const low{editor_ui::view_min()}; + auto const high{editor_ui::view_max()}; + auto const width{std::max(480.0f, high.x - low.x)}; + auto const height{std::clamp((high.y - low.y) * 0.5f, 260.0f, std::max(260.0f, high.y - low.y - 80.0f))}; + ImGui::SetNextWindowPos(ImVec2(low.x, high.y - height), ImGuiCond_Appearing); + ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Appearing); + auto title{std::string{STR_C("Vertical profile")}}; + if (false == state.route.spans.empty()) + title += format(STR_P(" %s > %s, %zu path, %.2f m", " %s > %s, %zu paths, %.2f m", state.route.spans.size()), state.route.spans.front().track->name().c_str(), state.route.spans.back().track->name().c_str(), state.route.spans.size(), state.route.length); + title += "###profilestrip"; if (ImGui::Begin(title.c_str(), nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { - auto const side{std::clamp(ImGui::GetContentRegionAvail().x * 0.28f, 280.0f, 420.0f)}; - ImGui::BeginChild("##profileplot", ImVec2(-side, 0.0f), false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); - render_profile_toolbar(); - render_profile_canvas(ImGui::GetContentRegionAvail().y); - ImGui::EndChild(); - ImGui::SameLine(); - ImGui::BeginChild("##profileside", ImVec2(0.0f, 0.0f), false); - if (ImGui::BeginTabBar("##profiletabs")) + // a window left past the edge of the screen comes back into it + auto const position{ImGui::GetWindowPos()}; + auto const size{ImGui::GetWindowSize()}; + state.strip_top = ImGui::IsWindowDocked() ? -1.0f : position.y; + if (false == ImGui::IsWindowDocked() && (position.y + size.y > io.DisplaySize.y + 1.0f || position.x + 80.0f > io.DisplaySize.x || position.x + size.x < 80.0f)) + ImGui::SetWindowPos(ImVec2(std::clamp(position.x, 0.0f, std::max(0.0f, io.DisplaySize.x - size.x)), std::max(0.0f, std::min(position.y, io.DisplaySize.y - size.y)))); + if (state.route.spans.empty()) + render_profile_empty(); + else { - // a click on a point in the plot brings its tab forward - bool const picked{state.selected >= 0 && state.selected != state.shown}; - state.shown = state.selected; - if (ImGui::BeginTabItem(STR_C("Point"), nullptr, picked ? ImGuiTabItemFlags_SetSelected : 0)) + render_profile_header(); + auto const side{std::clamp(ImGui::GetContentRegionAvail().x * 0.3f, 250.0f, 400.0f)}; + ImGui::BeginChild("##profileplot", ImVec2(-side, 0.0f), false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + render_profile_toolbar(); + render_profile_canvas(ImGui::GetContentRegionAvail().y); + ImGui::EndChild(); + ImGui::SameLine(); + ImGui::BeginChild("##profileside", ImVec2(0.0f, 0.0f), false); + if (ImGui::BeginTabBar("##profiletabs")) { - render_profile_point(); - ImGui::EndTabItem(); + // a click on a point in the plot brings its tab forward, a click on the state of the grade line the remarks + bool const picked{state.selected >= 0 && state.selected != state.shown}; + state.shown = state.selected; + if (ImGui::BeginTabItem(STR_C("Point"), nullptr, picked ? ImGuiTabItemFlags_SetSelected : 0)) + { + render_profile_point(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem(format("%s (%zu)###profilepoints", STR_C("Points"), state.line.points.size()).c_str())) + { + ImGui::BeginChild("##profilepointlist"); + render_profile_points(); + ImGui::EndChild(); + ImGui::EndTabItem(); + } + bool const errors{has_errors(state.issues)}; + if (false == state.issues.empty()) + ImGui::PushStyleColor(ImGuiCol_Text, errors ? ImVec4(1.0f, 0.45f, 0.4f, 1.0f) : ImVec4(1.0f, 0.8f, 0.3f, 1.0f)); + bool const remarks{ImGui::BeginTabItem(format("%s (%zu)###profileremarks", STR_C("Remarks"), state.issues.size()).c_str(), nullptr, state.show_remarks ? ImGuiTabItemFlags_SetSelected : 0)}; + state.show_remarks = false; + if (false == state.issues.empty()) + ImGui::PopStyleColor(); + if (remarks) + { + render_profile_issues(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); } - if (ImGui::BeginTabItem(format("%s (%zu)###profilepoints", STR_C("Points"), state.line.points.size()).c_str())) - { - ImGui::BeginChild("##profilepointlist"); - render_profile_points(); - ImGui::EndChild(); - ImGui::EndTabItem(); - } - bool const errors{has_errors(state.issues)}; - if (false == state.issues.empty()) - ImGui::PushStyleColor(ImGuiCol_Text, errors ? ImVec4(1.0f, 0.45f, 0.4f, 1.0f) : ImVec4(1.0f, 0.8f, 0.3f, 1.0f)); - bool const remarks{ImGui::BeginTabItem(format("%s (%zu)###profileremarks", STR_C("Remarks"), state.issues.size()).c_str())}; - if (false == state.issues.empty()) - ImGui::PopStyleColor(); - if (remarks) - { - render_profile_issues(); - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); + ImGui::EndChild(); } - ImGui::EndChild(); } ImGui::End(); } -void editor_mode::render_profile_source() +void editor_mode::render_profile_header() +{ + auto &state{m_profile}; + auto const errors{std::count_if(state.issues.begin(), state.issues.end(), [](profile::issue const &Issue) { return Issue.error; })}; + auto const warnings{static_cast(state.issues.size()) - errors}; + if (ImGui::Button(STR_C("From the track"))) + { + profile_recognize(); + state.changed = true; + } + item_tooltip("The points of the grade line read from the track as it lies now"); + ImGui::SameLine(); + if (ImGui::Button(STR_C("To the ground"))) + { + profile_fit_ground(); + state.changed = true; + } + item_tooltip("Places the breaks of the grade line along the ground under the route,\nwithin the limits and with the least earthworks; the ground itself stays as it is"); + ImGui::SameLine(0.0f, 2.0f); + ImGui::SetNextItemWidth(ImGui::CalcTextSize("+00.00 m").x + ImGui::GetStyle().FramePadding.x * 2.0f); + ImGui::DragScalar("##fittolerance", ImGuiDataType_Double, &state.fit.tolerance, 0.05f, nullptr, nullptr, STR_C("+-%.2f m")); + state.fit.tolerance = std::clamp(state.fit.tolerance, 0.05, 50.0); + item_tooltip("How far the grade line may depart from the ground before it breaks;\nmore gives fewer breaks and higher embankments and deeper cuttings"); + ImGui::SameLine(0.0f, ImGui::GetFontSize()); + // the state of the grade line against the limits; a click lists the remarks + std::string chip; + ImVec4 colour; + if (errors > 0) + { + chip = warnings > 0 ? format(STR_C("%d errors, %d warnings"), static_cast(errors), static_cast(warnings)) : format(STR_C("%d errors"), static_cast(errors)); + colour = ImVec4(1.0f, 0.45f, 0.38f, 1.0f); + } + else if (warnings > 0) + { + chip = format(STR_C("%d warnings"), static_cast(warnings)); + colour = ImVec4(1.0f, 0.8f, 0.3f, 1.0f); + } + else + { + chip = STR_C("Within the limits"); + colour = ImVec4(0.45f, 0.9f, 0.45f, 1.0f); + } + if (state_chip((chip + "###profilestate").c_str(), colour)) + state.show_remarks = true; + item_tooltip(state.issues.empty() ? "The grade line meets the limits of the line" : "Click: the list of the remarks; a click on a remark shows its place"); + // apply at the right end, as far as there's room + auto const label{std::string{STR_C("Apply to the scenery")} + (state.departure > 0.0005 ? " *" : "")}; + auto const button{ImGui::CalcTextSize(label.c_str()).x + ImGui::GetStyle().FramePadding.x * 2.0f}; + auto const right{ImGui::GetWindowContentRegionMax().x - button}; + ImGui::SameLine(0.0f, ImGui::GetFontSize()); + if (ImGui::GetCursorPosX() < right) + ImGui::SetCursorPosX(right); + ImGui::BeginDisabled(errors > 0); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.68f, 0.3f, 1.0f)); + if (ImGui::Button((label + "###profileapply").c_str())) + profile_apply(); + ImGui::PopStyleColor(2); + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + { + if (errors > 0) + ImGui::SetTooltip("%s", STR_C("Correct the errors first: the red remarks, a click on the state shows them")); + else if (state.departure > 0.0005) + ImGui::SetTooltip(STR_C("Sets the heights of the track along the route to the grade line, the track moves by up to %.3f m; Ctrl+Z takes it back"), state.departure); + else + ImGui::SetTooltip("%s", STR_C("The track lies on the grade line already")); + } + if (false == state.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", state.error.c_str()); +} + +void editor_mode::render_profile_empty() { auto &state{m_profile}; auto *selected{selected_track()}; - if (ImGui::Button(STR_C("Along the line from the selected path")) && selected != nullptr) - profile_open_run(*selected); - ImGui::SameLine(); - ImGui::PushItemWidth(70.0f); - ImGui::DragScalar(STR_C("m each way"), ImGuiDataType_Double, &state.run_length, 10.0f, nullptr, nullptr, "%.0f"); - ImGui::PopItemWidth(); - state.run_length = std::clamp(state.run_length, 10.0, 50000.0); - if (ImGui::Button(STR_C("Start: selected path")) && selected != nullptr) - state.picked_from = selected; - ImGui::SameLine(); - if (ImGui::Button(STR_C("End: selected path")) && selected != nullptr && state.picked_from != nullptr) - profile_open(state.picked_from, selected); + auto const available{ImGui::GetContentRegionAvail()}; + auto *draw{ImGui::GetWindowDrawList()}; + auto const line{ImGui::GetTextLineHeight()}; + auto const mouse{std::round(line * 2.6f)}; + auto const headline{std::string{STR_C("Click a track in the view")}}; + auto const detail{std::string{STR_C("The grade line of its whole line shows here; Shift+LMB on more tracks or Shift+drag a box: the route from one end of them to the other. Drag the points of the line up and down, then apply it to the scenery.")}}; + auto const wrap{std::min(available.x - 40.0f, 560.0f)}; + auto const top{ImGui::GetCursorScreenPos().y + std::max(0.0f, available.y * 0.5f - mouse - line * 2.0f)}; + auto const left{ImGui::GetCursorScreenPos().x + std::max(0.0f, (available.x - wrap) * 0.5f)}; + gesture_mouse(*draw, ImVec2(left, top), mouse, "LMB"); + auto *font{ui_layer::font_bold != nullptr ? ui_layer::font_bold : ImGui::GetFont()}; + auto const big{ui_layer::font_bold != nullptr ? ui_layer::font_bold->FontSize : ImGui::GetFontSize() * 1.2f}; + auto const textleft{left + mouse * 0.68f + line}; + draw->AddText(font, big, ImVec2(textleft, top + line * 0.2f), ImGui::GetColorU32(ImGuiCol_Text), headline.c_str()); + draw->AddText(ImGui::GetFont(), ImGui::GetFontSize(), ImVec2(textleft, top + big + line * 0.6f), ImGui::GetColorU32(ImGuiCol_TextDisabled), detail.c_str(), nullptr, wrap - (textleft - left)); + auto const detailsize{ImGui::CalcTextSize(detail.c_str(), nullptr, false, wrap - (textleft - left))}; + ImGui::SetCursorScreenPos(ImVec2(textleft, std::max(top + mouse, top + big + line * 0.6f + detailsize.y) + line)); + if (selected != nullptr) + { + if (ImGui::Button(format(STR_C("Along the line of %s"), selected->name().c_str()).c_str())) + profile_open_run(*selected); + ImGui::SameLine(); + } + profile_library_load(); + if (false == m_profiles.empty() && ImGui::Button(format(STR_C("Designs kept in the scenery (%zu)..."), m_profiles.size()).c_str())) + ImGui::OpenPopup("##profilekept"); + if (ImGui::BeginPopup("##profilekept")) + { + for (std::size_t i = 0; i < m_profiles.size(); ++i) + { + auto const &entry{m_profiles[i]}; + ImGui::PushID(static_cast(i)); + if (ImGui::Selectable(format(STR_P("%s %.1f m, %zu point", "%s %.1f m, %zu points", entry.line.points.size()), scene::Layers.layer(entry.layer).name.c_str(), entry.length, entry.line.points.size()).c_str())) + profile_open_stored(i); + ImGui::PopID(); + } + ImGui::EndPopup(); + } + if (false == state.error.empty()) + { + ImGui::SetCursorScreenPos(ImVec2(textleft, ImGui::GetCursorScreenPos().y)); + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", state.error.c_str()); + } +} + +void editor_mode::render_profile_source() +{ + gesture_rows({{"LMB on a track", "its whole line"}, {"Shift+LMB on more tracks", "the route between the outermost ones"}, {"Shift+drag a box", "the route over the tracks in it"}}); profile_library_load(); if (false == m_profiles.empty() && ImGui::TreeNode("##profilestored", STR_C("Designs kept in the scenery (%zu)"), m_profiles.size())) { @@ -1091,7 +1654,7 @@ void editor_mode::render_profile_source() if (ImGui::SmallButton(STR_C("Open"))) profile_open_stored(i); ImGui::SameLine(); - ImGui::Text(STR_C("%s %.1f m, %zu points, from (%.1f, %.1f) to (%.1f, %.1f)"), scene::Layers.layer(entry.layer).name.c_str(), entry.length, entry.line.points.size(), entry.start.x, entry.start.z, entry.end.x, entry.end.z); + ImGui::Text(STR_P("%s %.1f m, %zu point, from (%.1f, %.1f) to (%.1f, %.1f)", "%s %.1f m, %zu points, from (%.1f, %.1f) to (%.1f, %.1f)", entry.line.points.size()), scene::Layers.layer(entry.layer).name.c_str(), entry.length, entry.line.points.size(), entry.start.x, entry.start.z, entry.end.x, entry.end.z); ImGui::PopID(); } ImGui::TreePop(); @@ -1103,11 +1666,19 @@ bool editor_mode::render_profile_parameters() auto &state{m_profile}; auto &line{state.line}; bool changed{false}; - auto const drag = [](char const *Label, double &Value, float const Speed, char const *Format) { return ImGui::DragScalar(STR_C(Label), ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format); }; - ImGui::PushItemWidth(120.0f); - drag("Chainage of the start (m)", state.origin, 1.0f, "%.2f"); - changed |= drag("Design speed (km/h)", line.speed, 1.0f, "%.0f"); - ImGui::PopItemWidth(); + auto const drag = [](char const *Label, double &Value, float const Speed, char const *Format) { + field_row(Label); + ImGui::PushID(Label); + auto const edited{ImGui::DragScalar("##value", ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format)}; + ImGui::PopID(); + return edited; + }; + if (begin_fields("##profileline", 0.5f)) + { + drag("Chainage of the start (m)", state.origin, 1.0f, "%.2f"); + changed |= drag("Design speed (km/h)", line.speed, 1.0f, "%.0f"); + ImGui::EndTable(); + } line.speed = std::max(1.0, line.speed); auto const join_tooltip = [] { if (ImGui::IsItemHovered()) @@ -1123,18 +1694,29 @@ bool editor_mode::render_profile_parameters() changed |= ImGui::Checkbox(STR_C("Join the adjoining track at the end"), &line.join_end); join_tooltip(); } - if (ImGui::TreeNode(STR_C("Limits"))) + if (ImGui::TreeNode(STR_C("Norms"))) { auto &norms{line.norms}; - ImGui::PushItemWidth(90.0f); - changed |= drag("Maximum grade (per mille)", norms.grade_max, 0.1f, "%.2f"); - changed |= drag("Maximum grade over switches (per mille)", norms.grade_switch_max, 0.1f, "%.2f"); - changed |= drag("Vertical curve R >= k*V^2, k", norms.radius_factor, 0.01f, "%.2f"); - changed |= drag("Vertical curve R min (m)", norms.radius_min, 10.0f, "%.0f"); - changed |= drag("Vertical curve needed above a change of (per mille)", norms.curve_threshold, 0.1f, "%.2f"); - changed |= drag("Constant grade between curves at least (m)", norms.element_min, 1.0f, "%.0f"); - changed |= drag("Tolerance at fixed points (m)", norms.fixed_tolerance, 0.001f, "%.3f"); - ImGui::PopItemWidth(); + auto const table{begin_fields("##profilelimits", 0.6f)}; + changed |= table && drag("Maximum grade (per mille)", norms.grade_max, 0.1f, "%.2f"); + if (table) + { + changed |= drag("Maximum grade over switches (per mille)", norms.grade_switch_max, 0.1f, norms.grade_switch_max > 0.0 ? "%.2f" : STR_C("as the line")); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("0: the limit of the line, a switch may lie on its grade.\nA lower one for the tracks of a station where the vehicles stand, e.g. 2.5")); + changed |= drag("Vertical curve R >= k*V^2, k", norms.radius_factor, 0.01f, "%.2f"); + item_tooltip("The required radius of a vertical curve grows with the square of the design speed: R = k * V^2, V in km/h"); + changed |= drag("Vertical curve R min (m)", norms.radius_min, 10.0f, "%.0f"); + item_tooltip("A vertical curve is never smaller than this, even at a low speed"); + changed |= drag("Vertical curve needed above a change of (per mille)", norms.curve_threshold, 0.1f, "%.2f"); + item_tooltip("A smaller change of grade may stay a sharp break, without a curve"); + changed |= drag("Constant grade between curves at least (m)", norms.element_min, 1.0f, "%.0f"); + item_tooltip("The shortest stretch of one grade between two vertical curves; 0: not checked"); + changed |= drag("Tolerance at fixed points (m)", norms.fixed_tolerance, 0.001f, "%.3f"); + item_tooltip("How far the grade line may pass from a fixed point, e.g. the joint with the adjoining track, before it's reported"); + ImGui::EndTable(); + } + norms.grade_switch_max = std::max(0.0, norms.grade_switch_max); if (ImGui::SmallButton(STR_C("Automatic R at all points"))) { for (auto &point : line.points) @@ -1144,48 +1726,21 @@ bool editor_mode::render_profile_parameters() ImGui::TextDisabled(STR_C("Required R for %.0f km/h: %.0f m"), line.speed, profile::required_radius(line)); ImGui::TreePop(); } - - ImGui::Separator(); auto const errors{std::count_if(state.issues.begin(), state.issues.end(), [](profile::issue const &Issue) { return Issue.error; })}; auto const warnings{static_cast(state.issues.size()) - errors}; - if (errors > 0) - ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), STR_C("%d errors, %d warnings: see the Remarks in the strip"), static_cast(errors), static_cast(warnings)); - else if (warnings > 0) - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), STR_C("%d warnings: see the Remarks in the strip"), static_cast(warnings)); + ImGui::PushTextWrapPos(0.0f); + auto const good{IM_COL32(100, 230, 100, 255)}; + ImGui::BeginGroup(); + metric_tiles({{format("%d", static_cast(errors)), STR_C("errors"), errors > 0 ? IM_COL32(255, 100, 80, 255) : good}, + {format("%d", static_cast(warnings)), STR_C("warnings"), warnings > 0 ? IM_COL32(255, 200, 80, 255) : good}, + {format("%.3f m", state.departure), STR_C("off the track"), ImGui::GetColorU32(ImGuiCol_Text)}}); + ImGui::EndGroup(); + item_tooltip("From the track, To the ground and Apply to the scenery are over the plot of the strip"); + if (errors + warnings > 0) + ImGui::TextDisabled("%s", STR_C("The list of them: Remarks in the strip under the view")); else ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "%s", STR_C("The grade line meets the limits")); - ImGui::TextDisabled(STR_C("Largest departure from the track: %.3f m"), state.departure); - if (ImGui::Button(STR_C("Recognize from the track"))) - { - profile_recognize(); - state.changed = true; - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Sets the points of the grade line from the existing track")); - ImGui::SameLine(); - if (ImGui::Button(STR_C("Fit to the ground"))) - { - profile_fit_ground(); - state.changed = true; - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Places the breaks of the grade line along the ground under the route,\nwithin the limits and with the least earthworks; the ground itself stays as it is")); - ImGui::PushItemWidth(70.0f); - ImGui::DragScalar(STR_C("m off the ground##fittolerance"), ImGuiDataType_Double, &state.fit.tolerance, 0.05f, nullptr, nullptr, "%.2f"); - ImGui::PopItemWidth(); - state.fit.tolerance = std::clamp(state.fit.tolerance, 0.05, 50.0); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("How far the grade line may depart from the ground before it breaks;\nmore gives fewer breaks and higher embankments and deeper cuttings")); - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.68f, 0.3f, 1.0f)); - if (ImGui::Button(STR_C("Apply to the scenery"), ImVec2(-1.0f, 0.0f))) - profile_apply(); - ImGui::PopStyleColor(2); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Sets the heights of the track along the route to the grade line, Ctrl+Z takes it back")); - ImGui::Separator(); - render_profile_earthworks(); + ImGui::PopTextWrapPos(); return changed; } @@ -1229,7 +1784,13 @@ void editor_mode::render_profile_point() auto const count{static_cast(points.size())}; if (state.selected >= count) state.selected = -1; - auto const drag = [](char const *Label, double &Value, float const Speed, char const *Format) { return ImGui::DragScalar(STR_C(Label), ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format); }; + auto const drag = [](char const *Label, double &Value, float const Speed, char const *Format) { + field_row(Label); + ImGui::PushID(Label); + auto const edited{ImGui::DragScalar("##value", ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format)}; + ImGui::PopID(); + return edited; + }; auto const kind_of = [](double const Change) { return Change > 0.0 ? STR_C("sag") : STR_C("crest"); }; if (state.selected < 0) { @@ -1243,7 +1804,7 @@ void editor_mode::render_profile_point() ImGui::Text(STR_C("Point %d: joint with the adjoining track"), index + 1); ImGui::Text(STR_C("km %s H %.3f m R %.0f m"), profile::format_chainage(point.chainage + state.origin).c_str(), point.elevation, point.radius); ImGui::Text(STR_C("Grade %+.2f -> %+.2f per mille (%s)"), profile::grade_after(state.line, index - 1) * 1000.0, profile::grade_after(state.line, index) * 1000.0, kind_of(change)); - ImGui::TextWrapped("%s", STR_C("Placed automatically, moves with the neighbouring point; untick the joining in the inspector to edit the end freely")); + ImGui::TextWrapped("%s", STR_C("Placed automatically, moves with the neighbouring point; untick the joining in the tool options to edit the end freely")); } else { @@ -1254,54 +1815,66 @@ void editor_mode::render_profile_point() ImGui::Text(STR_C("Point %d"), index + 1); ImGui::SameLine(); ImGui::TextDisabled(STR_C("km %s%s"), profile::format_chainage(point.chainage + state.origin).c_str(), locked ? STR_C(", joined to the adjoining track") : end ? STR_C(", end of the route") : ""); - ImGui::PushItemWidth(std::max(90.0f, ImGui::GetContentRegionAvail().x * 0.4f)); - auto chainage{point.chainage + state.origin}; - if (false == end && drag("Chainage (m)", chainage, 0.5f, "%.2f")) + if (begin_fields("##profilepoint", 0.45f)) { - point.chainage = std::clamp(chainage - state.origin, points[index - 1].chainage + 0.5, points[index + 1].chainage - 0.5); - changed = true; - } - if (locked) - ImGui::Text(STR_C("Elevation %.3f m"), point.elevation); - else - changed |= drag("Elevation (m)", point.elevation, 0.01f, "%.3f"); - if (index > 0 && false == locked) - { - auto grade{profile::grade_after(state.line, index - 1) * 1000.0}; - if (drag("Grade before (per mille)", grade, 0.05f, "%+.2f")) + auto chainage{point.chainage + state.origin}; + if (false == end && drag("Chainage (m)", chainage, 0.5f, "%.2f")) { - point.elevation = points[index - 1].elevation + grade / 1000.0 * (point.chainage - points[index - 1].chainage); + point.chainage = std::clamp(chainage - state.origin, points[index - 1].chainage + 0.5, points[index + 1].chainage - 0.5); changed = true; } - } - if (index + 1 < count) - { - auto grade{profile::grade_after(state.line, index) * 1000.0}; - if (drag("Grade after (per mille)", grade, 0.05f, "%+.2f")) + if (locked) { - points[index + 1].elevation = point.elevation + grade / 1000.0 * (points[index + 1].chainage - point.chainage); - changed = true; + field_row("Elevation (m)"); + ImGui::Text("%.3f", point.elevation); } - } - if (false == end) - { - if (drag("Vertical curve R (m)", point.radius, 10.0f, "%.0f")) + else + changed |= drag("Elevation (m)", point.elevation, 0.01f, "%.3f"); + if (index > 0 && false == locked) { - point.automatic = false; - changed = true; + auto grade{profile::grade_after(state.line, index - 1) * 1000.0}; + if (drag("Grade before (per mille)", grade, 0.05f, "%+.2f")) + { + point.elevation = points[index - 1].elevation + grade / 1000.0 * (point.chainage - points[index - 1].chainage); + changed = true; + } } - point.radius = std::max(0.0, point.radius); - ImGui::SameLine(); - changed |= ImGui::Checkbox(STR_C("auto"), &point.automatic); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("R follows the limits: the required radius where the change of grade needs a curve; 0: no curve")); - auto const change{(profile::grade_after(state.line, index) - profile::grade_after(state.line, index - 1)) * 1000.0}; - auto const tangent{point.radius * std::abs(change) / 2000.0}; - ImGui::TextDisabled(STR_C("Change of grade %+.2f per mille, %s"), change, kind_of(change)); - if (point.radius > 0.0) - ImGui::TextDisabled(STR_C("Tangent T %.2f m, sagitta f %.3f m"), tangent, tangent * tangent / (2.0 * point.radius)); + if (index + 1 < count) + { + auto grade{profile::grade_after(state.line, index) * 1000.0}; + if (drag("Grade after (per mille)", grade, 0.05f, "%+.2f")) + { + points[index + 1].elevation = point.elevation + grade / 1000.0 * (points[index + 1].chainage - point.chainage); + changed = true; + } + } + if (false == end) + { + field_row("Vertical curve R (m)"); + auto const box{ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x + ImGui::CalcTextSize(STR_C("auto")).x}; + ImGui::SetNextItemWidth(std::max(40.0f, ImGui::GetContentRegionAvail().x - box - ImGui::GetStyle().ItemSpacing.x)); + if (ImGui::DragScalar("##pointradius", ImGuiDataType_Double, &point.radius, 10.0f, nullptr, nullptr, "%.0f")) + { + point.automatic = false; + changed = true; + } + if (ImGui::IsItemHovered()) + { + auto const change{(profile::grade_after(state.line, index) - profile::grade_after(state.line, index - 1)) * 1000.0}; + auto const tangent{point.radius * std::abs(change) / 2000.0}; + auto text{std::string{STR_C("The radius of the vertical curve which rounds off the break of the grade here")} + "\n" + format(STR_C("Change of grade %+.2f per mille, %s"), change, kind_of(change))}; + if (point.radius > 0.0) + text += "\n" + format(STR_C("Tangent T %.2f m, sagitta f %.3f m"), tangent, tangent * tangent / (2.0 * point.radius)); + ImGui::SetTooltip("%s", text.c_str()); + } + point.radius = std::max(0.0, point.radius); + ImGui::SameLine(); + changed |= ImGui::Checkbox(STR_C("auto"), &point.automatic); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("R follows the limits: the required radius where the change of grade needs a curve; 0: no curve")); + } + ImGui::EndTable(); } - ImGui::PopItemWidth(); if (index + 1 < count && ImGui::Button(STR_C("Add a point after"))) { profile::pvi inserted; @@ -1401,32 +1974,44 @@ struct editor_mode::profile_view void editor_mode::render_profile_toolbar() { auto &state{m_profile}; - auto *draw{ImGui::GetWindowDrawList()}; - auto const swatch = [&](ImU32 const Colour) { - auto const at{ImGui::GetCursorScreenPos()}; - auto const height{ImGui::GetFrameHeight()}; - draw->AddLine(ImVec2(at.x, at.y + height * 0.5f), ImVec2(at.x + 16.0f, at.y + height * 0.5f), Colour, 3.0f); - ImGui::Dummy(ImVec2(16.0f, height)); - ImGui::SameLine(0.0f, 4.0f); - }; - auto const legend = [&](ImU32 const Colour, char const *Label) { - swatch(Colour); - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(STR_C(Label)); - ImGui::SameLine(0.0f, 14.0f); - }; - auto const toggle = [&](ImU32 const Colour, char const *Label, bool &Value) { - swatch(Colour); - ImGui::Checkbox(STR_C(Label), &Value); - ImGui::SameLine(0.0f, 14.0f); - }; - legend(kGradeLine, "Grade line"); - legend(kTangents, "Tangents"); - toggle(kTrack, STR_C("Track"), state.show_track); - toggle(kTerrain, STR_C("Terrain"), state.show_terrain); - toggle(kPlan, STR_C("Plan curvature"), state.show_plan); - legend(kSwitchLabel, "Switches"); - ImGui::SetNextItemWidth(130.0f); + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", STR_C("Show:")); + ImGui::SameLine(); + pill_toggle(STR_C("Track"), kTrack, state.show_track); + ImGui::SameLine(0.0f, 4.0f); + pill_toggle(STR_C("Terrain"), kTerrain, state.show_terrain); + ImGui::SameLine(0.0f, 4.0f); + pill_toggle(STR_C("Plan curvature"), kPlan, state.show_plan); + ImGui::SameLine(0.0f, 6.0f); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(STR_C("Legend")); + ImGui::Separator(); + auto *draw{ImGui::GetWindowDrawList()}; + auto const entry = [&](ImU32 const Colour, char const *Label) { + auto const at{ImGui::GetCursorScreenPos()}; + auto const height{ImGui::GetTextLineHeight()}; + draw->AddLine(ImVec2(at.x, at.y + height * 0.5f), ImVec2(at.x + 22.0f, at.y + height * 0.5f), Colour, 3.0f); + ImGui::Dummy(ImVec2(22.0f, height)); + ImGui::SameLine(0.0f, 8.0f); + ImGui::TextUnformatted(STR_C(Label)); + }; + entry(kGradeLine, "Grade line"); + entry(kTangents, "Tangents"); + entry(kTrack, "Track"); + entry(kTerrain, "Terrain"); + entry(kFormation, "Formation"); + entry(kPlan, "Plan curvature"); + entry(kSwitchLabel, "Switches"); + entry(kSharedEdge, "Shared timbers"); + entry(kError, "Errors"); + entry(kWarning, "Warnings"); + ImGui::EndTooltip(); + } + ImGui::SameLine(0.0f, 14.0f); + ImGui::SetNextItemWidth(120.0f); ImGui::SliderFloat("##exaggeration", &state.exaggeration, 1.0f, 500.0f, STR_C("heights x%.0f"), ImGuiSliderFlags_Logarithmic); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Exaggeration of the heights against the lengths, also Ctrl+wheel over the plot")); @@ -1441,7 +2026,9 @@ void editor_mode::render_profile_canvas(float const Height) { auto &state{m_profile}; auto const available{ImGui::GetContentRegionAvail()}; - ImVec2 const size{std::max(200.0f, available.x), std::max(100.0f, Height)}; + auto const hintheight{std::round(ImGui::GetTextLineHeight() * 1.2f)}; + auto const hintrow{hintheight + 8.0f}; + ImVec2 const size{std::max(200.0f, available.x), std::max(100.0f, Height - hintrow)}; ImVec2 const corner{ImGui::GetCursorScreenPos()}; ImGui::InvisibleButton("##profilecanvas", size); bool const hovered{ImGui::IsItemHovered()}; @@ -1466,24 +2053,38 @@ void editor_mode::render_profile_canvas(float const Height) profile_canvas_tooltip(); } - // what the mouse does over the thing under it - char const *hint{"wheel: zoom Ctrl+wheel: heights RMB/MMB drag: pan double click on the line: add a point"}; - if (state.dragging >= 0) - hint = "Ctrl: only the height Shift: keep the grade before"; - else if (state.curve_grip >= 0 || state.hover_grip >= 0) - hint = "drag up or down: radius of the vertical curve"; - else if (state.hover_point >= 0) - hint = "drag: move the point Ctrl+drag: only the height Shift+drag: keep the grade before Del: remove"; - else if (state.hover_line) - hint = "double click: add a point here"; - if (hovered || state.dragging >= 0 || state.curve_grip >= 0) - { - auto const text{std::string{STR_C(hint)}}; - auto const size{ImGui::CalcTextSize(text.c_str())}; - plot_label(*draw, ImVec2(view.bottom_right().x - size.x - 8.0f, view.plot_bottom() - size.y - 6.0f), IM_COL32(255, 255, 255, 230), text); - } draw->PopClipRect(); + // what the mouse does over the thing under it, along the bottom of the plot + std::vector> hints{{"wheel", "zoom"}, {"Ctrl+wheel", "heights"}, {"RMB move", "pan"}, {"LMB LMB", "on the line: add a point"}}; + if (state.dragging >= 0) + hints = {{"Ctrl", "only the height"}, {"Shift", "keep the grade before"}}; + else if (state.curve_grip >= 0 || state.hover_grip >= 0) + hints = {{"LMB move", "up or down: radius of the vertical curve"}}; + else if (state.hover_point >= 0) + hints = {{"LMB move", "move the point"}, {"Ctrl+LMB", "only the height"}, {"Shift+LMB", "keep the grade before"}, {"Del", "remove"}}; + else if (state.hover_line) + hints = {{"LMB LMB", "add a point here"}}; + { + auto const height{hintheight}; + auto x{corner.x + 6.0f}; + auto const y{corner.y + size.y + 4.0f}; + draw->AddRectFilled(ImVec2(corner.x, corner.y + size.y), ImVec2(corner.x + size.x, corner.y + size.y + hintrow), IM_COL32(14, 15, 19, 255)); + draw->PushClipRect(ImVec2(corner.x, corner.y + size.y), ImVec2(corner.x + size.x, corner.y + size.y + hintrow), true); + for (auto const &hint : hints) + { + auto const words{gesture_words(hint.first)}; + auto const text{STR(hint.second)}; + if (x + gesture_width(height, words) + 6.0f + ImGui::CalcTextSize(text.c_str()).x > corner.x + size.x - 6.0f) + break; + x += gesture_draw(*draw, ImVec2(x, y), height, words, kLabel) + 6.0f; + draw->AddText(ImVec2(x, std::round(y + (height - ImGui::GetTextLineHeight()) * 0.5f)), IM_COL32(235, 235, 235, 255), text.c_str()); + x += ImGui::CalcTextSize(text.c_str()).x + 18.0f; + } + draw->PopClipRect(); + } + ImGui::Dummy(ImVec2(size.x, hintrow)); + auto const &points{state.line.points}; if (state.dragging >= 0 && state.dragging < static_cast(points.size())) { @@ -1619,11 +2220,16 @@ bool editor_mode::profile_canvas_edit(profile_view const &View, bool const Hover { state.dragging = point; state.selected = state.dragging; + state.drag_press = mouse; + state.drag_grab = {points[point].chainage - View.chainage_of(mouse.x), points[point].elevation - View.height_of(mouse.y)}; + state.drag_moved = false; } else if (grip >= 0) { state.curve_grip = grip; state.selected = state.curve_grip; + state.drag_press = mouse; + state.drag_moved = false; } else state.selected = -1; @@ -1646,38 +2252,42 @@ bool editor_mode::profile_canvas_edit(profile_view const &View, bool const Hover } if (state.dragging >= 0 && state.dragging < static_cast(points.size())) { - if (ImGui::IsMouseDown(0)) + // a click selects the point, it moves only when dragged, by as much as the mouse went + state.drag_moved = state.drag_moved || std::abs(mouse.x - state.drag_press.x) > 3.0f || std::abs(mouse.y - state.drag_press.y) > 3.0f; + if (false == ImGui::IsMouseDown(0)) + state.dragging = -1; + else if (state.drag_moved) { auto const index{state.dragging}; auto &point{points[index]}; bool const end{index == 0 || index + 1 == static_cast(points.size())}; auto const incoming{index > 0 ? profile::grade_after(line, index - 1) : 0.0}; if (false == end && false == io.KeyCtrl) - point.chainage = std::clamp(std::round(View.chainage_of(mouse.x) * 100.0) / 100.0, points[index - 1].chainage + 0.5, points[index + 1].chainage - 0.5); + point.chainage = std::clamp(std::round((View.chainage_of(mouse.x) + state.drag_grab.x) * 100.0) / 100.0, points[index - 1].chainage + 0.5, points[index + 1].chainage - 0.5); if (end_locked(line, state.context, index)) ; else if (io.KeyShift && index > 0) point.elevation = points[index - 1].elevation + incoming * (point.chainage - points[index - 1].chainage); else - point.elevation = std::round(View.height_of(mouse.y) * 1000.0) / 1000.0; + point.elevation = std::round((View.height_of(mouse.y) + state.drag_grab.y) * 1000.0) / 1000.0; changed = true; } - else - state.dragging = -1; } else state.dragging = -1; if (state.curve_grip > 0 && state.curve_grip + 1 < static_cast(points.size())) { - if (ImGui::IsMouseDown(0)) + // a click on the grip leaves the radius as it is + state.drag_moved = state.drag_moved || std::abs(mouse.y - state.drag_press.y) > 3.0f; + if (false == ImGui::IsMouseDown(0)) + state.curve_grip = -1; + else if (state.drag_moved) { auto const sagitta{View.height_of(mouse.y) - points[state.curve_grip].elevation}; points[state.curve_grip].radius = std::max(10.0, std::round(profile::radius_for_sagitta(line, state.curve_grip, sagitta) / 10.0) * 10.0); points[state.curve_grip].automatic = false; changed = true; } - else - state.curve_grip = -1; } else state.curve_grip = -1; @@ -1718,9 +2328,40 @@ void editor_mode::draw_profile_canvas(profile_view const &View, ImDrawList &Draw } for (auto const &zone : state.context.switches) { - Draw.AddRectFilled(ImVec2(View.x_of(zone.from), plottop), ImVec2(std::max(View.x_of(zone.to), View.x_of(zone.from) + 2.0f), plotbottom), kSwitch); - if (View.x_of(zone.to) - View.x_of(zone.from) > 20.0f || View.across < 0.5) - Draw.AddText(ImVec2(View.x_of(zone.from) + 2.0f, plotbottom - 16.0f), kSwitchLabel, zone.name.c_str()); + auto const left{View.x_of(zone.from)}; + auto const right{std::max(View.x_of(zone.to), left + 2.0f)}; + if (zone.name == STR("shared timbers")) + { + // the timbers shared with another track, past a switch or at a crossing: drawn with their bounds, nothing + // breaks the grade over them + Draw.PushClipRect(ImVec2(left, plottop), ImVec2(right, plotbottom), true); + auto const height{plotbottom - plottop}; + for (auto x = left - height; x < right; x += 8.0f) + Draw.AddLine(ImVec2(x, plotbottom), ImVec2(x + height, plottop), kShared, 1.5f); + Draw.PopClipRect(); + Draw.AddLine(ImVec2(left, plottop), ImVec2(left, plotbottom), kSharedEdge, 1.0f); + Draw.AddLine(ImVec2(right, plottop), ImVec2(right, plotbottom), kSharedEdge, 1.0f); + if (right - left > 4.0f) + Draw.AddText(ImVec2(left + 2.0f, plotbottom - 32.0f), kSharedEdge, zone.name.c_str()); + continue; + } + Draw.AddRectFilled(ImVec2(left, plottop), ImVec2(right, plotbottom), kSwitch); + if (right - left > 20.0f || View.across < 0.5) + Draw.AddText(ImVec2(left + 2.0f, plotbottom - 16.0f), kSwitchLabel, zone.name.c_str()); + } + // the ends which inherit the grade line from a switch, said where they are + if (false == state.start_inherits.empty()) + { + auto const x{View.x_of(0.0)}; + Draw.AddLine(ImVec2(x, plottop), ImVec2(x, plotbottom), kSwitchLabel, 2.0f); + Draw.AddText(ImVec2(x + 4.0f, plottop + 18.0f), kSwitchLabel, ("<- " + state.start_inherits).c_str()); + } + if (false == state.end_inherits.empty()) + { + auto const x{View.x_of(state.route.length)}; + auto const text{state.end_inherits + " ->"}; + Draw.AddLine(ImVec2(x, plottop), ImVec2(x, plotbottom), kSwitchLabel, 2.0f); + Draw.AddText(ImVec2(x - 4.0f - ImGui::CalcTextSize(text.c_str()).x, plottop + 34.0f), kSwitchLabel, text.c_str()); } auto const polyline = [&](auto const &Height, ImU32 const Colour, float const Thickness) { @@ -1771,6 +2412,8 @@ void editor_mode::draw_profile_canvas(profile_view const &View, ImDrawList &Draw open = true; } polyline([&](std::size_t const I) { return state.terrain[I]; }, kTerrain, 1.5f); + // the formation under the grade line, which is what meets the ground + polyline([&](std::size_t const I) { return profile::elevation(state.line, samples[I].chainage) - (I < state.beds.size() ? state.beds[I] : 0.0); }, kFormation, 1.0f); } if (state.show_track) polyline([&](std::size_t const I) { return samples[I].position.y; }, kTrack, 1.5f); @@ -1882,7 +2525,11 @@ void editor_mode::profile_canvas_tooltip() const auto const next{std::lower_bound(samples.begin(), samples.end(), state.hover, [](editor_track::route_sample const &Sample, double const Value) { return Sample.chainage < Value; })}; auto const index{static_cast(std::distance(samples.begin(), std::min(next, std::prev(samples.end()))))}; if (index < state.terrain.size() && false == std::isnan(state.terrain[index])) - ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(kTerrain), STR_C("Terrain %.3f m, %s %.2f m"), state.terrain[index], designed > state.terrain[index] ? STR_C("fill") : STR_C("cut"), std::abs(designed - state.terrain[index])); + { + auto const formation{designed - (index < state.beds.size() ? state.beds[index] : 0.0)}; + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(kFormation), STR_C("Formation %.3f m, %.2f m under the rail top"), formation, designed - formation); + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(kTerrain), STR_C("Terrain %.3f m, %s %.2f m"), state.terrain[index], formation > state.terrain[index] ? STR_C("fill") : STR_C("cut"), std::abs(formation - state.terrain[index])); + } auto const &span{state.route.spans[samples[index].span]}; ImGui::TextDisabled("%s%s", span.track->name().c_str(), span.track->eType != tt_Normal ? STR_C(" (switch, tilted as a whole with its branch)") : ""); ImGui::EndTooltip(); @@ -1910,16 +2557,73 @@ void editor_mode::draw_profile_overlay() const previous = next; designedprevious = designed; } + // the grade line told in the view: the grades of its straight stretches, the ranges of its vertical curves, its points + glm::dvec3 const eye{Global.pCamera.Pos}; + double constexpr labels_range{1500.0}; + auto const on_line = [&](double const Chainage) { + auto position{editor_track::sampled_position(samples, Chainage)}; + position.y = profile::elevation(state.line, Chainage); + return position; + }; + auto const label = [&](glm::dvec3 const &At, std::string const &Text, ImU32 const Colour) { + ImVec2 screen; + if (glm::distance(At, eye) > labels_range || false == projection.project(At + glm::dvec3{0.0, 1.5, 0.0}, screen)) + return; + auto const size{ImGui::CalcTextSize(Text.c_str())}; + ImVec2 const corner{screen.x - size.x * 0.5f - 4.0f, screen.y - size.y - 4.0f}; + drawlist->AddRectFilled(corner, ImVec2(corner.x + size.x + 8.0f, corner.y + size.y + 4.0f), IM_COL32(20, 20, 24, 190), 3.0f); + drawlist->AddText(ImVec2(corner.x + 4.0f, corner.y + 2.0f), Colour, Text.c_str()); + }; auto const &points{state.line.points}; + auto const curves{profile::curves(state.line)}; + // the straight stretches between the curves + for (std::size_t k = 0; k + 1 < points.size(); ++k) + { + auto from{points[k].chainage}; + auto to{points[k + 1].chainage}; + for (auto const &curve : curves) + { + if (curve.point == k) + from = std::max(from, curve.end); + if (curve.point == k + 1) + to = std::min(to, curve.start); + } + if (to - from < 5.0) + continue; + auto const grade{(points[k + 1].elevation - points[k].elevation) / std::max(1e-6, points[k + 1].chainage - points[k].chainage) * 1000.0}; + label(on_line((from + to) * 0.5), format("%+.2f o/oo L %.0f m", grade, points[k + 1].chainage - points[k].chainage), kGradeLine); + } + // the vertical curves: their range along the track, the ends marked, the radius at the point + for (auto const &curve : curves) + { + glm::dvec3 previouscurve{on_line(curve.start)}; + auto const steps{std::max(2, static_cast((curve.end - curve.start) / 2.0))}; + for (int s = 1; s <= steps; ++s) + { + auto const next{on_line(curve.start + (curve.end - curve.start) * s / steps)}; + projection.line(drawlist, previouscurve, next, kTangents, 5.0f); + previouscurve = next; + } + for (auto const end : {curve.start, curve.end}) + { + auto const at{on_line(end)}; + auto const sample{editor_track::sampled_position(samples, std::min(state.route.length, end + 0.5)) - editor_track::sampled_position(samples, std::max(0.0, end - 0.5))}; + glm::dvec2 across{-sample.z, sample.x}; + across = glm::length(across) > 1e-9 ? glm::normalize(across) * 2.0 : glm::dvec2{2.0, 0.0}; + projection.line(drawlist, at - glm::dvec3{across.x, 0.0, across.y}, at + glm::dvec3{across.x, 0.0, across.y}, kTangents, 2.0f); + } + label(on_line(points[curve.point].chainage) + glm::dvec3{0.0, 1.2, 0.0}, format("R %.0f T %.1f m", points[curve.point].radius, curve.tangent), kTangents); + } for (int i = 0; i < static_cast(points.size()); ++i) { - auto position{editor_track::sampled_position(samples, points[i].chainage)}; - position.y = profile::elevation(state.line, points[i].chainage); + auto const position{on_line(points[i].chainage)}; ImVec2 screen; if (false == projection.project(position, screen)) continue; drawlist->AddCircleFilled(screen, i == state.selected ? 7.0f : 5.0f, overlay_color::marked); drawlist->AddText(ImVec2(screen.x + 8.0f, screen.y - 16.0f), overlay_color::marked, format("%d", i + 1).c_str()); + if (glm::distance(position, eye) <= labels_range * 0.5) + drawlist->AddText(ImVec2(screen.x + 8.0f, screen.y - 2.0f), overlay_color::marked, format("km %s, %.2f m", profile::format_chainage(points[i].chainage + state.origin).c_str(), points[i].elevation).c_str()); } if (state.hover >= 0.0 && state.hover <= state.route.length) { diff --git a/application/editormoderecords.cpp b/application/editormoderecords.cpp index 5a3ac2ce..711dba03 100644 --- a/application/editormoderecords.cpp +++ b/application/editormoderecords.cpp @@ -134,7 +134,9 @@ std::vector drift_notes(record_drift const &Drift) if (Drift.followed > 0) notes.push_back(STR("The kept design of the line went along with its paths")); if (Drift.departed > 0) - notes.push_back(format(STR_C("%d path(s) left the design kept in %s, by up to %.2f m: Ctrl+Z takes it back, the route design lays them by the design again or forgets it"), Drift.departed, Drift.layer.c_str(), Drift.departure)); + notes.push_back(format(STR_P("%d path left the design kept in %s, by up to %.2f m: Ctrl+Z takes it back, the Plan tool (G) lays it by the design again or forgets the design", + "%d paths left the design kept in %s, by up to %.2f m: Ctrl+Z takes it back, the Plan tool (G) lays them by the design again or forgets it", Drift.departed), + Drift.departed, Drift.layer.c_str(), Drift.departure)); if (Drift.cant > 5.0) notes.push_back(format(STR_C("The cant differs from the kept design by up to %.0f mm"), Drift.cant)); return notes; @@ -160,7 +162,8 @@ switch_store::record switch_entry(editor_track::switch_template const &Shape) } else { - entry.type = "-"; + // Rz, Rlj or Rld, as classify finds it by the shape + entry.type = "Rz"; entry.radius = Shape.radius; entry.ratio = Shape.ratio; entry.source = "measured"; @@ -379,24 +382,35 @@ void editor_mode::render_switch_record(TTrack const &Track) const } auto entry{*known}; ImGui::Text(STR_C("Type kept in the scenery: %s"), switch_store::label(entry).c_str()); - if (entry.kind == "switch" && entry.main_radius != 0.0) - ImGui::TextDisabled(STR_C("Diverging track R %.0f m: the R %.0f m of the type bent with the main track"), switch_store::branch_radius(entry), entry.radius); - if (entry.source == "measured") - ImGui::TextDisabled("%s", STR_C("Copied from a switch of the scenery: the size is measured from its shape")); - ImGui::PushItemWidth(100.0f); - bool changed{edit_dashed<32>(STR_C("Number##switchnumber"), entry.number)}; - item_tooltip("Number of the switch in the station, as the plans of the station give it"); - ImGui::SameLine(); - changed |= edit_dashed<16>(STR_C("Rail##switchrail"), entry.rail); - item_tooltip("Rail of the switch: 60E1, 49E1..."); - auto km{entry.km.has_value() ? *entry.km / 1000.0 : 0.0}; - if (ImGui::InputDouble(STR_C("Km of the start##switchkm"), &km, 0.0, 0.0, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue)) + if (ImGui::IsItemHovered()) { - entry.km = km * 1000.0; - changed = true; + std::string text; + if (entry.kind == "switch" && entry.main_radius != 0.0) + text = format(STR_C("Diverging track R %.0f m: the R %.0f m of the type bent with the main track"), switch_store::branch_radius(entry), entry.radius); + if (entry.source == "measured") + text += (text.empty() ? "" : "\n") + std::string{STR_C("The size is measured from the shape of the switch, it's of no template of the catalogue")}; + if (false == text.empty()) + ImGui::SetTooltip("%s", text.c_str()); + } + bool changed{false}; + if (begin_fields("##switchrecord", 0.4f)) + { + field_row("Number##switchnumber"); + changed |= edit_dashed<32>("##switchnumber", entry.number); + item_tooltip("Number of the switch in the station, as the plans of the station give it"); + field_row("Rail##switchrail"); + changed |= edit_dashed<16>("##switchrail", entry.rail); + item_tooltip("Rail of the switch: 60E1, 49E1..."); + auto km{entry.km.has_value() ? *entry.km / 1000.0 : 0.0}; + field_row("Km of the start##switchkm"); + if (ImGui::InputDouble("##switchkm", &km, 0.0, 0.0, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue)) + { + entry.km = km * 1000.0; + changed = true; + } + item_tooltip("Chainage of the start of the switch; taken from the horizontal alignment kept in the scenery when the switch lies on one"); + ImGui::EndTable(); } - item_tooltip("Chainage of the start of the switch; taken from the horizontal alignment kept in the scenery when the switch lies on one"); - ImGui::PopItemWidth(); if (changed) { switch_store::put(entry, entry.layer); diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp index a07da795..8591f41a 100644 --- a/application/editormoderoad.cpp +++ b/application/editormoderoad.cpp @@ -10,8 +10,12 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" #include "application/editoruilayer.h" +#include "application/editormodeshared.h" +#include "application/editorgestures.h" +#include "editor/editorFormat.hpp" #include "utilities/Globals.h" +#include "utilities/translation.h" #include "rendering/renderer.h" #include "world/Track.h" #include "world/Road.h" @@ -170,10 +174,15 @@ roadpoint_node::kind_type point_kind(int const Index) char const *point_label(roadpoint_node::kind_type const Kind) { - return Kind == roadpoint_node::kind_type::spawn ? "spawn point" : - Kind == roadpoint_node::kind_type::despawn ? "removal point" : - Kind == roadpoint_node::kind_type::crosswalk ? "pedestrian crossing" : - "level crossing"; + return Kind == roadpoint_node::kind_type::spawn ? STR_C("spawn point") : + Kind == roadpoint_node::kind_type::despawn ? STR_C("removal point") : + Kind == roadpoint_node::kind_type::crosswalk ? STR_C("pedestrian crossing") : + STR_C("level crossing"); +} + +std::string cannot_change(std::string const &Name, std::string const &Reason) +{ + return format(STR_C("\"%s\" can't be changed: %s"), Name.c_str(), STR(Reason).c_str()); } // gathers new definitions for the pieces which meet or end at provided points. @@ -250,7 +259,7 @@ void editor_mode::update_road_tool() tool.mouse = camera + GfxRenderer->Mouse_Position(); if (glm::distance(tool.mouse, camera) > kReach) { - tool.plan.error = "The cursor isn't over anything near enough"; + tool.plan.error = STR_C("The cursor isn't over anything near enough"); tool.hastarget = false; tool.aimedkind = -1; return; @@ -299,9 +308,9 @@ void editor_mode::road_select(road_node *Road) tool.junction = nullptr; tool.settings = Road->definition(); std::string reason; - tool.status = "Selected: " + (Road->name().empty() ? std::string{"(unnamed road)"} : Road->name()); + tool.status = format(STR_C("Selected: %s"), Road->name().empty() ? STR_C("(unnamed road)") : Road->name().c_str()); if (false == editor_road::can_edit(*Road, &reason)) - tool.status += ". It can't be changed: " + reason; + tool.status += ". " + format(STR_C("It can't be changed: %s"), STR(reason).c_str()); } else { @@ -325,9 +334,9 @@ void editor_mode::junction_select(junction_node *Junction) return; tool.selected = nullptr; std::string reason; - tool.status = "Selected: " + Junction->name(); + tool.status = format(STR_C("Selected: %s"), Junction->name().c_str()); if (false == editor_road::can_edit(*Junction, &reason)) - tool.status += ". It can't be changed: " + reason; + tool.status += ". " + format(STR_C("It can't be changed: %s"), STR(reason).c_str()); // the fields for the names of materials are shared with the roads copy_text(tool.surface, sizeof(tool.surface), Junction->definition().surface); copy_text(tool.sides[0], sizeof(tool.sides[0]), Junction->definition().side.material); @@ -343,9 +352,9 @@ void editor_mode::roadpoint_select(roadpoint_node *Point) if (Point == nullptr) return; std::string reason; - tool.status = "Selected: " + Point->name(); + tool.status = format(STR_C("Selected: %s"), Point->name().c_str()); if (false == editor_road::can_edit(*Point, &reason)) - tool.status += ". It can't be changed: " + reason; + tool.status += ". " + format(STR_C("It can't be changed: %s"), STR(reason).c_str()); } // works out the point a click of the place tool would make with the cursor where it is @@ -370,7 +379,7 @@ void editor_mode::roadpoint_aim(bool const Fresh) auto const *road{editor_road::nearest(tool.mouse, kSideMargin)}; if (road == nullptr) { - tool.targetnote = "Point at a road"; + tool.targetnote = STR_C("Point at a road"); return; } // the stripes go across the road, whichever part of it the cursor is over @@ -385,12 +394,12 @@ void editor_mode::roadpoint_aim(bool const Fresh) auto const *road{editor_road::nearest(tool.mouse, kSideMargin)}; if (road == nullptr) { - tool.targetnote = "Point at the place a road crosses the rails"; + tool.targetnote = STR_C("Point at the place a road crosses the rails"); return; } if (road->definition().changing()) { - tool.targetnote = "The road gains or loses lanes on this piece; a level crossing goes on a piece which keeps its lanes"; + tool.targetnote = STR_C("The road gains or loses lanes on this piece; a level crossing goes on a piece which keeps its lanes"); return; } glm::dvec3 middle{0.0}; @@ -405,14 +414,14 @@ void editor_mode::roadpoint_aim(bool const Fresh) { tool.target.position = road->definition().point(editor_road::nearest_parameter(road->definition(), tool.mouse)); tool.target.clearance = std::max(tool.stopmargin, 2.0f); - tool.targetnote = "No rails cross the road here; a crossing put here won't close until some do"; + tool.targetnote = STR_C("No rails cross the road here; a crossing put here won't close until some do"); } tool.hastarget = true; return; } if (false == editor_road::nearest_lane(tool.mouse, kLaneReach, tool.target.position, tool.targetdirection)) { - tool.targetnote = "Point at a lane of a road"; + tool.targetnote = STR_C("Point at a lane of a road"); return; } tool.hastarget = true; @@ -431,7 +440,7 @@ void editor_mode::roadpoint_place() auto const created{editor_road::create(std::vector{tool.target})}; if (created.empty()) { - tool.status = "No scenery is loaded"; + tool.status = STR_C("No scenery is loaded"); return; } auto const *point{created.front()}; @@ -439,35 +448,33 @@ void editor_mode::roadpoint_place() editor_road::record record; record.points_created = created; push_road_snapshot(std::move(record)); - char text[64]; switch (state.kind) { case roadpoint_node::kind_type::crossing: { - std::snprintf(text, sizeof(text), "%.1f", state.clearance); - tool.status = "Level crossing made: " + std::to_string(point->stops().size()) + " lanes stop " + text + " m from its middle, "; - tool.status += std::to_string(point->rails()) + " tracks are watched for trains."; + tool.status = format(STR_C("Level crossing made, Ctrl+Z takes it back. Lanes stopping %.1f m from its middle: %d, tracks watched for trains: %d"), static_cast(state.clearance), + static_cast(point->stops().size()), static_cast(point->rails())); break; } case roadpoint_node::kind_type::spawn: { - tool.status = (state.vehicles.empty() ? std::string{"Spawn point made, with no vehicles: nothing appears there until it's given some (Select, click the point)."} : - "Spawn point made, with up to " + std::to_string(state.count) + " vehicles on the roads at a time. They appear while the simulation runs."); + tool.status = (state.vehicles.empty() ? std::string{STR_C("Spawn point made, with no vehicles: nothing appears there until it's given some (Select, click the point)")} : + format(STR_C("Spawn point made, Ctrl+Z takes it back. Vehicles on the roads at a time: up to %d. They appear while the simulation runs"), state.count)); break; } case roadpoint_node::kind_type::despawn: { - tool.status = "Removal point made, it takes vehicles from " + std::to_string(point->lanes().size()) + (point->lanes().size() == 1 ? " lane." : " lanes."); + tool.status = format(STR_C("Removal point made, Ctrl+Z takes it back. Lanes it takes vehicles from: %d"), static_cast(point->lanes().size())); break; } case roadpoint_node::kind_type::crosswalk: { - tool.status = "Pedestrian crossing painted."; + tool.status = STR_C("Pedestrian crossing painted, Ctrl+Z takes it back"); break; } } if (false == tool.targetnote.empty()) - tool.status += " " + tool.targetnote + "."; + tool.status += ". " + tool.targetnote; } // replaces the definition of a level crossing or a traffic point, as a step which can be taken back @@ -477,14 +484,14 @@ void editor_mode::roadpoint_apply(roadpoint_node &Point, roadpoint_node::state c std::string reason; if (false == editor_road::can_edit(Point, &reason)) { - tool.status = "\"" + Point.name() + "\" can't be changed: " + reason; + tool.status = cannot_change(Point.name(), reason); return; } editor_road::record record; record.points.emplace_back(&Point, Point.definition()); editor_road::apply(std::vector>{{&Point, State}}); push_road_snapshot(std::move(record)); - tool.status = "Changed"; + tool.status = STR_C("Changed, Ctrl+Z takes it back"); } // draws controls for a level crossing or a traffic point. returns: true if something was changed @@ -505,34 +512,30 @@ bool editor_mode::render_roadpoint_layout(roadpoint_node::state &State, roadpoin { if (Point != nullptr) { - ImGui::Text("%d lanes stop here, %d tracks are watched for trains", static_cast(Point->stops().size()), static_cast(Point->rails())); + ImGui::Text(STR_C("Lanes stopping here: %d, tracks watched for trains: %d"), static_cast(Point->stops().size()), static_cast(Point->rails())); if (Point->rails() == 0) - ImGui::TextColored(warning, "No rails pass through it, so it never closes"); + ImGui::TextColored(warning, "%s", STR_C("No rails pass through it, so it never closes")); // the crossing is tied to what's there when it's made or changed, and when the roads change - if (ImGui::Button("Look for the rails again")) + if (ImGui::Button(STR_C("Look for the rails again"))) changed = true; - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "For when the tracks were laid or changed after the crossing was made."); - if (number("Stop this far from the middle [m]", State.clearance, 0.5f, "%.1f")) + item_tooltip("For when the tracks were laid or changed after the crossing was made"); + if (number(STR_C("Stop distance from the middle [m]"), State.clearance, 0.5f, "%.1f")) changed = true; - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Where the vehicles stop, measured along the road from the middle of the crossing.\n" - "The tracks passing within that distance from the middle are the ones the crossing is closed for."); + item_tooltip("Where the vehicles stop, measured along the road from the middle of the crossing.\n" + "The crossing closes for the tracks passing within that distance from the middle"); } else { - if (number("Stop this far from the outermost track [m]", tool.stopmargin, 0.5f, "%.1f")) + if (number(STR_C("Stop distance from the track [m]"), tool.stopmargin, 0.5f, "%.1f")) tool.stopmargin = std::clamp(tool.stopmargin, 2.0f, 50.0f); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Where the vehicles stop, measured along the road from the axis of the first track they'd get to."); + item_tooltip("Where the vehicles stop, measured along the road from the axis of the first track they'd get to"); } - if (number("Closes with a train this far away [m]", State.warning, 50.0f, "%.0f")) + if (number(STR_C("Closing distance [m]"), State.warning, 50.0f, "%.0f")) changed = true; - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Measured along the tracks, both ways, with both ways of each switch counted.\n" - "A rail vehicle within that distance which gets nearer closes the crossing, and so does one right at it.\n" - "It opens a few seconds after the last one is gone."); - if (ImGui::Checkbox("Paint the stop lines", &State.stoplines)) + item_tooltip("How far a train is when the crossing closes. Measured along the tracks, both ways, with both ways of each switch counted.\n" + "A rail vehicle within that distance which gets nearer closes the crossing, and so does one right at it.\n" + "It opens a few seconds after the last one is gone"); + if (ImGui::Checkbox(STR_C("Paint the stop lines"), &State.stoplines)) changed = true; break; } @@ -541,34 +544,31 @@ bool editor_mode::render_roadpoint_layout(roadpoint_node::state &State, roadpoin if (Point != nullptr) { if (Point->lanes().empty()) - ImGui::TextColored(warning, "It isn't on a lane, so nothing appears here"); + ImGui::TextColored(warning, "%s", STR_C("It isn't on a lane, so nothing appears here")); else if (Point->vehicles() == 0) - ImGui::TextDisabled("The vehicles are made when the simulation runs"); + ImGui::TextDisabled("%s", STR_C("The vehicles are made when the simulation runs")); else - ImGui::Text("%d vehicles made, %d of them wait for their turn", static_cast(Point->vehicles()), static_cast(Point->waiting())); + ImGui::Text(STR_C("Vehicles made: %d, waiting for their turn: %d"), static_cast(Point->vehicles()), static_cast(Point->waiting())); } - if (number("A vehicle every [s]", State.interval, 1.0f, "%.0f")) + if (number(STR_C("A vehicle every [s]"), State.interval, 1.0f, "%.0f")) changed = true; - if (number("Uneven by [0-1]", State.variation, 0.1f, "%.2f")) + if (number(STR_C("Randomness [0-1]"), State.variation, 0.1f, "%.2f")) changed = true; - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Part of the time between vehicles which is drawn at random each time.\n" - "0: they come like clockwork, 1: anything from one right after another to twice the time."); - if (number("Speed [km/h]", State.velocity, 5.0f, "%.0f")) + item_tooltip("Part of the time between vehicles which is drawn at random each time.\n" + "0: they come like clockwork, 1: anything from one right after another to twice the time"); + if (number(STR_C("Speed [km/h]"), State.velocity, 5.0f, "%.0f")) changed = true; - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "The speed the vehicles appear with. From there on the drivers go by the speed limits of the lanes."); + item_tooltip("The speed the vehicles appear with. From there on the drivers go by the speed limits of the lanes"); ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputInt("Vehicles on the roads at a time", &State.count, 1, 5, flags)) + if (ImGui::InputInt(STR_C("Vehicles at a time"), &State.count, 1, 5, flags)) changed = true; - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "How many vehicles of this point can be on the scenery at once. That many are made along with the scenery,\n" - "drawn from the listed ones in turn; one of those which wait is put on the road each time its turn comes,\n" - "until all of them are out. A vehicle taken by a removal point goes back to wait for its next turn,\n" - "and so does one which stood still for 15 seconds, other than at a closed level crossing, or went backwards."); + item_tooltip("How many vehicles of this point can be on the scenery at once. That many are made along with the scenery,\n" + "drawn from the listed ones in turn; one of those which wait is put on the road each time its turn comes,\n" + "until all of them are out. A vehicle taken by a removal point goes back to wait for its next turn,\n" + "and so does one which stood still for 15 seconds, other than at a closed level crossing, or went backwards"); // the set ImGui::Separator(); - ImGui::Text("Vehicles of the set: %d", static_cast(State.vehicles.size())); + ImGui::Text(STR_C("Vehicles of the set: %d"), static_cast(State.vehicles.size())); int removed{-1}; for (std::size_t index = 0; index < State.vehicles.size(); ++index) { @@ -576,6 +576,7 @@ bool editor_mode::render_roadpoint_layout(roadpoint_node::state &State, roadpoin ImGui::PushID(static_cast(index)); if (ImGui::SmallButton("x")) removed = static_cast(index); + item_tooltip("Remove from the set"); ImGui::SameLine(); if (vehicle.load > 0) ImGui::Text("%s / %s, %s, %d %s", vehicle.folder.c_str(), vehicle.type.c_str(), vehicle.skin.c_str(), vehicle.load, vehicle.loadtype.c_str()); @@ -589,14 +590,18 @@ bool editor_mode::render_roadpoint_layout(roadpoint_node::state &State, roadpoin changed = true; } if (State.vehicles.empty()) - ImGui::TextColored(warning, "No vehicles, so nothing appears here"); + ImGui::TextColored(warning, "%s", STR_C("No vehicles, so nothing appears here")); ImGui::InputTextMultiline("##vehicles", tool.vehicles, sizeof(tool.vehicles), ImVec2(-1.0f, ImGui::GetTextLineHeight() * 5.0f)); - if (ImGui::Button("Add to the set")) + item_tooltip("node -1 0 none dynamic \n" + " enddynamic\n" + "or , one vehicle a line.\n" + "The path, the offset, the driver and the velocity don't matter"); + if (ImGui::Button(STR_C("Add to the set"))) { auto const parsed{roadpoint_node::parse_vehicles(tool.vehicles)}; if (parsed.empty()) { - tool.status = "No vehicle found in the text"; + tool.status = STR_C("No vehicle found in the text"); } else { @@ -606,36 +611,35 @@ bool editor_mode::render_roadpoint_layout(roadpoint_node::state &State, roadpoin } } ImGui::SameLine(); - if (ImGui::Button("Empty the set") && false == State.vehicles.empty()) + if (ImGui::Button(STR_C("Empty the set")) && false == State.vehicles.empty()) { State.vehicles.clear(); changed = true; } - ImGui::TextDisabled("Paste the vehicles the way they're put in a scenery:"); - ImGui::TextDisabled("node -1 0 none dynamic "); - ImGui::TextDisabled(" enddynamic"); - ImGui::TextDisabled("or as , one vehicle a line."); - ImGui::TextDisabled("The path, the offset, the driver and the velocity don't matter."); + ImGui::TextDisabled("%s", STR_C("Paste the vehicles above the way a scenery lists them")); + item_tooltip("node -1 0 none dynamic \n" + " enddynamic\n" + "or , one vehicle a line.\n" + "The path, the offset, the driver and the velocity don't matter"); break; } case roadpoint_node::kind_type::despawn: { if (Point != nullptr) - ImGui::Text("Takes vehicles from %d %s", static_cast(Point->lanes().size()), Point->lanes().size() == 1 ? "lane" : "lanes"); - if (number("Takes vehicles this close [m]", State.radius, 0.5f, "%.1f")) + ImGui::Text(STR_C("Lanes it takes vehicles from: %d"), static_cast(Point->lanes().size())); + if (number(STR_C("Reach [m]"), State.radius, 0.5f, "%.1f")) changed = true; - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Road vehicles on the lanes passing within this distance from the point are taken off the road when they get here,\n" - "except for the one the user drives. Vehicles which came from a spawn point go back to wait for their next turn.\n" - "The distance it starts with takes in a single lane."); + item_tooltip("Road vehicles on the lanes passing within this distance from the point are taken off the road when they get here,\n" + "except for the one the user drives. Vehicles which came from a spawn point go back to wait for their next turn.\n" + "The distance it starts with takes in a single lane"); break; } case roadpoint_node::kind_type::crosswalk: { - if (number("Length along the road [m]", State.length, 0.5f, "%.1f")) + if (number(STR_C("Length along the road [m]"), State.length, 0.5f, "%.1f")) changed = true; - ImGui::TextDisabled("Stripes painted from one edge of the road to the other."); - ImGui::TextDisabled("The drivers don't stop for it: there's nobody to cross."); + ImGui::TextDisabled("%s", STR_C("Stripes across the whole road")); + item_tooltip("The drivers don't stop for it: there's nobody to cross"); break; } } @@ -651,17 +655,17 @@ void editor_mode::junction_apply(junction_node &Junction, junction_node::state c std::string reason; if (false == editor_road::can_edit(Junction, &reason)) { - tool.status = "\"" + Junction.name() + "\" can't be changed: " + reason; + tool.status = cannot_change(Junction.name(), reason); // what the window shows goes back to what the junction is like junction_select(&Junction); - tool.status = "\"" + Junction.name() + "\" can't be changed: " + reason; + tool.status = cannot_change(Junction.name(), reason); return; } editor_road::record record; record.junctions.emplace_back(&Junction, Junction.definition()); editor_road::apply(std::vector>{{&Junction, State}}); push_road_snapshot(std::move(record)); - tool.status = "Changed"; + tool.status = STR_C("Changed, Ctrl+Z takes it back"); } // draws controls for what a junction is like, and makes the changes as they're made @@ -677,61 +681,62 @@ void editor_mode::render_junction_layout(junction_node &Junction) }; auto limit{state.velocity > 0.f ? state.velocity : 0.f}; - if (number("Speed limit [km/h], 0: none", limit, 5.0f, "%.0f")) + if (number(STR_C("Speed limit [km/h]"), limit, 5.0f, "%.0f")) { state.velocity = (limit > 0.f ? limit : -1.f); changed = true; } + item_tooltip("0: no limit"); auto centre{static_cast(state.centre.y)}; - if (number("Height of the middle [m]", centre, 0.1f, "%.2f")) + if (number(STR_C("Height of the middle [m]"), centre, 0.1f, "%.2f")) { state.centre.y = centre; changed = true; } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "The surface is spanned between the ends of the roads and this point.\nThe ends of the roads are raised or lowered as points: select the white dot at the end of a road."); + item_tooltip("The surface is spanned between the ends of the roads and this point.\nThe ends of the roads are raised or lowered as points: select the white dot at the end of a road"); // appearance ImGui::Separator(); - char const *colours[]{"none", "white", "orange"}; + char const *colours[]{STR_C("no markings"), STR_C("white"), STR_C("orange")}; int colour{state.markings == road_node::marking_colour::white ? 1 : state.markings == road_node::marking_colour::orange ? 2 : 0}; ImGui::SetNextItemWidth(120.0f); - if (ImGui::Combo("Markings", &colour, colours, 3)) + if (ImGui::Combo(STR_C("Markings"), &colour, colours, 3)) { state.markings = (colour == 1 ? road_node::marking_colour::white : colour == 2 ? road_node::marking_colour::orange : road_node::marking_colour::none); changed = true; } if (render_road_material("Surface", tool.surface, sizeof(tool.surface), state.surface)) changed = true; - if (number("Texture length [m]", state.texturelength, 0.5f, "%.2f")) + if (number(STR_C("Texture length [m]"), state.texturelength, 0.5f, "%.2f")) { state.texturelength = std::max(0.1f, state.texturelength); changed = true; } + item_tooltip("How many metres of the surface one copy of the texture covers"); // what the corners between the roads are lined with - if (ImGui::Checkbox("Kerb around the corners", &state.kerbs)) + if (ImGui::Checkbox(STR_C("Kerb around the corners"), &state.kerbs)) changed = true; if (state.kerbs) { ImGui::Indent(); - if (number("Kerb width [m]", state.kerbwidth, 0.05f, "%.2f")) + if (number(STR_C("Kerb width [m]"), state.kerbwidth, 0.05f, "%.2f")) { state.kerbwidth = std::clamp(state.kerbwidth, 0.05f, 2.0f); changed = true; } if (render_road_material("Kerb material", tool.kerbtext, sizeof(tool.kerbtext), state.kerbmaterial)) changed = true; - if (state.side.type != road_node::side_type::sidewalk && number("Kerb height [m]", state.kerbheight, 0.01f, "%.2f")) + if (state.side.type != road_node::side_type::sidewalk && number(STR_C("Kerb height [m]"), state.kerbheight, 0.01f, "%.2f")) { state.kerbheight = std::clamp(state.kerbheight, 0.0f, 1.0f); changed = true; } ImGui::Unindent(); } - char const *sidetypes[]{"none", "shoulder", "sidewalk"}; + char const *sidetypes[]{STR_C("no side"), STR_C("shoulder"), STR_C("sidewalk")}; int type{state.side.type == road_node::side_type::shoulder ? 1 : state.side.type == road_node::side_type::sidewalk ? 2 : 0}; ImGui::SetNextItemWidth(120.0f); - if (ImGui::Combo("Sides of the corners", &type, sidetypes, 3)) + if (ImGui::Combo(STR_C("Beside the corners"), &type, sidetypes, 3)) { state.side.type = (type == 1 ? road_node::side_type::shoulder : type == 2 ? road_node::side_type::sidewalk : road_node::side_type::none); if (state.side.type != road_node::side_type::none && state.side.width <= 0.0f) @@ -741,7 +746,7 @@ void editor_mode::render_junction_layout(junction_node &Junction) if (state.side.type != road_node::side_type::none) { ImGui::Indent(); - if (number("Width [m]", state.side.width, 0.25f, "%.2f")) + if (number(STR_C("Width [m]"), state.side.width, 0.25f, "%.2f")) { state.side.width = std::clamp(state.side.width, 0.1f, 20.0f); changed = true; @@ -749,10 +754,10 @@ void editor_mode::render_junction_layout(junction_node &Junction) if (render_road_material("Material", tool.sides[0], sizeof(tool.sides[0]), state.side.material)) changed = true; if (state.side.material.empty() || state.side.material == "none") - ImGui::TextDisabled("Pick a material to have the sides drawn"); + ImGui::TextDisabled("%s", STR_C("Pick a material to have the sides drawn")); if (state.side.type == road_node::side_type::sidewalk) { - if (number("Kerb height [m]", state.kerbheight, 0.01f, "%.2f")) + if (number(STR_C("Kerb height [m]"), state.kerbheight, 0.01f, "%.2f")) { state.kerbheight = std::clamp(state.kerbheight, 0.0f, 1.0f); changed = true; @@ -760,12 +765,12 @@ void editor_mode::render_junction_layout(junction_node &Junction) } else { - if (number("Bank width [m]", state.slope.x, 0.25f, "%.2f")) + if (number(STR_C("Bank width [m]"), state.slope.x, 0.25f, "%.2f")) { state.slope.x = std::clamp(state.slope.x, 0.0f, 20.0f); changed = true; } - if (number("Bank drop [m]", state.slope.y, 0.1f, "%.2f")) + if (number(STR_C("Bank height [m]"), state.slope.y, 0.1f, "%.2f")) { state.slope.y = std::clamp(state.slope.y, 0.0f, 20.0f); changed = true; @@ -775,32 +780,31 @@ void editor_mode::render_junction_layout(junction_node &Junction) } // banks leading from the corners to the ground - if (ImGui::CollapsingHeader("Banks to the ground")) + if (ImGui::CollapsingHeader(STR_C("Banks to the ground##banks"))) { if (render_road_material("Bank material", tool.banktext, sizeof(tool.banktext), state.bankmaterial)) changed = true; - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "What the banks are covered with next to a sidewalk, a kerb or the bare edge.\nA bank of a shoulder is covered with the material of the shoulder."); + item_tooltip("What the banks are covered with next to a sidewalk, a kerb or the bare edge.\nA bank of a shoulder is covered with the material of the shoulder"); ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputFloat("Run for a metre of drop [m]", &tool.bankgrade, 0.25f, 0.5f, "%.2f")) + if (ImGui::InputFloat(STR_C("Bank slope 1:n"), &tool.bankgrade, 0.25f, 0.5f, "%.2f")) tool.bankgrade = std::clamp(tool.bankgrade, 0.25f, 10.0f); - if (ImGui::Button("Lead the banks to the ground")) + item_tooltip("n: metres across for a metre of height; 1.5 is a usual embankment"); + if (ImGui::Button(STR_C("Lead the banks to the ground"))) { if (changed) junction_apply(Junction, state); junction_bank_to_ground(Junction); return; } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Looks for the ground past each corner, at both roads the corner runs between,\n" - "and sets the bank of the corner to reach it. Done for the roads too, the banks meet at the ends of the roads."); + item_tooltip("Looks for the ground past each corner, at both roads the corner runs between,\n" + "and sets the bank of the corner to reach it. Done for the roads too, the banks meet at the ends of the roads"); auto banked{false}; for (auto const &arm : state.arms) banked = banked || arm.bank.set; if (banked) { ImGui::SameLine(); - if (ImGui::Button("Remove them")) + if (ImGui::Button(STR_C("Remove the banks"))) { for (auto &arm : state.arms) arm.bank = road_node::bank_data{}; @@ -812,41 +816,40 @@ void editor_mode::render_junction_layout(junction_node &Junction) { // where a road changes its lanes: what's between its two directions, if the roads keep them apart ImGui::Separator(); - char const *mediantypes[]{"nothing, the ground shows", "surface closed to the traffic", "island"}; + char const *mediantypes[]{STR_C("nothing, the ground shows"), STR_C("surface closed to the traffic"), STR_C("island")}; int median{state.median == road_node::median_type::gap ? 0 : state.median == road_node::median_type::island ? 2 : 1}; ImGui::SetNextItemWidth(220.0f); - if (ImGui::Combo("Between the directions", &median, mediantypes, 3)) + if (ImGui::Combo(STR_C("Between the directions"), &median, mediantypes, 3)) { state.median = (median == 0 ? road_node::median_type::gap : median == 2 ? road_node::median_type::island : road_node::median_type::painted); changed = true; } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Shows where a road which keeps its two directions apart meets this stretch:\n" - "what's between the directions is led on to the other road, where it closes or carries on."); + item_tooltip("Shows where a road which keeps its two directions apart meets this stretch:\n" + "what's between the directions is led on to the other road, where it closes or carries on"); if (state.median == road_node::median_type::island && render_road_material("Island material", tool.mediantext, sizeof(tool.mediantext), state.medianmaterial)) changed = true; } // the roads - if (ImGui::CollapsingHeader("Roads of the junction", ImGuiTreeNodeFlags_DefaultOpen)) + if (ImGui::CollapsingHeader(STR_C("Roads of the junction##arms"), ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::TextDisabled("The lanes are taken from the roads: change them on the road, the junction follows."); - ImGui::TextDisabled("The roads are numbered in the view. Lanes are counted from the middle of the road;"); - ImGui::TextDisabled("automatic: the inner lane turns left, the outer one right, all go straight."); + ImGui::TextDisabled("%s", STR_C("Lanes come from the roads: change them on the road")); + item_tooltip("The junction follows the roads. The roads are numbered in the view. Lanes are counted from the middle of the road;\n" + "automatic: the inner lane turns left, the outer one right, all go straight on"); for (std::size_t arm = 0; arm < state.arms.size(); ++arm) { ImGui::PushID(static_cast(arm)); auto &data{state.arms[arm]}; - ImGui::Text("%d: %d in, %d out, %.2f m a lane", static_cast(arm) + 1, data.incoming, data.outgoing, data.width); + ImGui::Text(STR_C("%d: %d in, %d out, lanes %.2f m wide"), static_cast(arm) + 1, data.incoming, data.outgoing, data.width); if (state.arms.size() > 2) { ImGui::SameLine(); - if (ImGui::Checkbox("Stop line", &data.stopline)) + if (ImGui::Checkbox(STR_C("Stop line"), &data.stopline)) changed = true; // stripes for the pedestrians, across the whole road where it meets the junction ImGui::SameLine(); bool zebra{data.crosswalk > 0.f}; - if (ImGui::Checkbox("Pedestrian crossing", &zebra)) + if (ImGui::Checkbox(STR_C("Pedestrian crossing"), &zebra)) { data.crosswalk = (zebra ? 4.f : 0.f); changed = true; @@ -860,13 +863,14 @@ void editor_mode::render_junction_layout(junction_node &Junction) data.crosswalk = std::clamp(data.crosswalk, 1.0f, 20.0f); changed = true; } + item_tooltip("Length of the pedestrian crossing along the road"); } } if (data.incoming > 0 && state.arms.size() > 2) { ImGui::Indent(); // the entries are in the order of the stored values - char const *priorities[]{"no sign: gives way to the right", "has the right of way", "gives way", "stops, then gives way"}; + char const *priorities[]{STR_C("no sign: gives way to the right"), STR_C("has the right of way"), STR_C("gives way"), STR_C("stops, then gives way")}; int priority{static_cast(data.priority)}; ImGui::SetNextItemWidth(220.0f); if (ImGui::Combo("##priority", &priority, priorities, 4)) @@ -874,13 +878,14 @@ void editor_mode::render_junction_layout(junction_node &Junction) data.priority = static_cast(std::clamp(priority, 0, 3)); changed = true; } + item_tooltip("Right of way of the traffic coming in from this road"); // ways each lane can be left by - char const *ways[]{"automatic", "left", "straight", "left, straight", "right", "left, right", "straight, right", "any"}; + char const *ways[]{STR_C("automatic"), STR_C("left"), STR_C("straight on"), STR_C("left, straight on"), STR_C("right"), STR_C("left, right"), STR_C("straight on, right"), STR_C("any way")}; for (int lane = 0; lane < data.incoming; ++lane) { ImGui::PushID(100 + lane); int turns{static_cast(lane) < data.turns.size() ? data.turns[lane] & 7 : 0}; - auto const label{"lane " + std::to_string(lane + 1) + " goes"}; + auto const label{format(STR_C("Lane %d goes"), lane + 1)}; ImGui::SetNextItemWidth(140.0f); if (ImGui::Combo(label.c_str(), &turns, ways, 8)) { @@ -918,7 +923,7 @@ editor_mode::road_plan editor_mode::road_preview(bool const Fresh) auto const leaving{tool.reversed ? layout.forward : layout.backward}; float lanewidth{0.f}; auto const evenlanes{editor_road::lane_width(layout, 0.0, lanewidth)}; - char const *unevenlanes{"Lanes of the road being built differ in width, and a junction takes roads with lanes of the same width"}; + char const *unevenlanes{STR_C("Lanes of the road being built differ in width, and a junction takes roads with lanes of the same width")}; // where the road is led to. a loose end of a road or an arm of a junction near the cursor comes first plan.snap = editor_road::find_end(tool.mouse, kSnapRadius); @@ -949,10 +954,9 @@ editor_mode::road_plan editor_mode::road_preview(bool const Fresh) // the junction has its ways through made for a set of lanes at each arm; a single roadway of a road takes the lanes it has if (false == evenlanes || arriving != plan.snap.backward || leaving != plan.snap.forward || std::abs(lanewidth - plan.snap.width) > 0.01f) { - char width[32]; - std::snprintf(width, sizeof(width), "%.2f", plan.snap.width); - plan.error = std::string{plan.snap.junction != nullptr ? "This arm of the junction" : "This roadway"} + " takes a road with "; - plan.error += std::to_string(plan.snap.backward) + " lane(s) leading to it and " + std::to_string(plan.snap.forward) + " away from it, " + width + " m wide each"; + plan.error = format(plan.snap.junction != nullptr ? STR_C("This arm of the junction takes a road with lanes: %d leading to it and %d away from it, %.2f m wide each") : + STR_C("This roadway takes a road with lanes: %d leading to it and %d away from it, %.2f m wide each"), + plan.snap.backward, plan.snap.forward, static_cast(plan.snap.width)); return plan; } } @@ -1003,7 +1007,7 @@ editor_mode::road_plan editor_mode::road_preview(bool const Fresh) glm::dvec2 const away{target.x - tool.point.x, target.z - tool.point.z}; if (glm::length(away) < 1.0) { - plan.error = "Move the cursor to where the road is to go"; + plan.error = STR_C("Move the cursor to where the road is to go"); return plan; } // the axis of the road goes away from the junction, so the lanes going against it are the ones leading into the junction @@ -1032,13 +1036,13 @@ editor_mode::road_plan editor_mode::road_preview(bool const Fresh) shared = shared || touches(plan.end, road); if (shared) { - plan.error = "The junctions at both ends would take the same piece of road; lead the road somewhere else first"; + plan.error = STR_C("The junctions at both ends would take the same piece of road; lead the road somewhere else first"); return plan; } } if (glm::distance(start, target) > kLongestStretch) { - plan.error = "Too far from the last point"; + plan.error = STR_C("Too far from the last point"); return plan; } plan.pieces = editor_road::plan(start, hasdirection ? &direction : nullptr, target, tied ? &enddirection : nullptr, Global.ctrlState, plan.error); @@ -1113,7 +1117,7 @@ void editor_mode::road_click() glm::dvec3 const ground{camera + GfxRenderer->Mouse_Position()}; if (glm::distance(ground, camera) > kReach) { - tool.status = "The click didn't land on anything near enough"; + tool.status = STR_C("The click didn't land on anything near enough"); return; } tool.mouse = ground; @@ -1147,14 +1151,14 @@ void editor_mode::road_click() tool.points.emplace_back(joint); tool.selected = nullptr; tool.junction = nullptr; - tool.status = std::to_string(tool.points.size()) + (tool.points.size() == 1 ? " point selected" : " points selected"); + tool.status = format(STR_C("Points selected: %d"), static_cast(tool.points.size())); return; } tool.points.clear(); auto *road{editor_road::nearest(ground, 1.5)}; auto *junction{road == nullptr ? editor_road::nearest_junction(ground) : nullptr}; if (road == nullptr && junction == nullptr) - tool.status = "No road here"; + tool.status = STR_C("No road here"); road_select(road); junction_select(junction); return; @@ -1163,7 +1167,7 @@ void editor_mode::road_click() std::string reason; if (false == editor_road::available(&reason)) { - tool.status = reason; + tool.status = STR(reason); return; } if (tool.ring) @@ -1186,7 +1190,7 @@ void editor_mode::road_click() // started from a loose end of a road it carries that road on, started from an arm of a junction it's the road the junction is made for there if (false == editor_road::can_join(end, &reason)) { - tool.status = "Can't build from here: " + reason; + tool.status = format(STR_C("Can't build from here: %s"), STR(reason).c_str()); return; } tool.point = end.position; @@ -1207,7 +1211,7 @@ void editor_mode::road_click() tool.settings.median.width = {gap, gap}; // the banks were made for the ground where they are tool.settings.banks = {}; - tool.status = "Carrying on \"" + end.road->name() + "\" with its layout"; + tool.status = format(STR_C("Carrying on \"%s\" with its layout"), end.road->name().c_str()); } else { @@ -1228,8 +1232,8 @@ void editor_mode::road_click() tool.settings.lanes.clear(); tool.settings.changes.clear(); tool.settings.normalize(); - tool.status = (end.junction != nullptr ? "Leading a road out of \"" + end.junction->name() + "\", with the lanes the junction has there" : - "Carrying on one of the roadways of \"" + end.road->name() + "\" as a one-way road"); + tool.status = (end.junction != nullptr ? format(STR_C("Leading a road out of \"%s\", with the lanes the junction has there"), end.junction->name().c_str()) : + format(STR_C("Carrying on one of the roadways of \"%s\" as a one-way road"), end.road->name().c_str())); } road_select(nullptr); } @@ -1245,24 +1249,24 @@ void editor_mode::road_click() tool.branchroad = road; tool.branchat = editor_road::nearest_parameter(road->definition(), ground); tool.point = road->definition().point(tool.branchat); - tool.status = "Leading a road out of the side of \"" + road->name() + "\": click where it goes"; + tool.status = format(STR_C("Leading a road out of the side of \"%s\": click where it goes"), road->name().c_str()); } else if (auto *junction{editor_road::nearest_junction(ground)}; junction != nullptr) { // started from a junction it's another road of that junction if (junction->definition().arms.size() >= 4) { - tool.status = "\"" + junction->name() + "\" has four roads already"; + tool.status = format(STR_C("\"%s\" has four roads already"), junction->name().c_str()); return; } tool.branchjunction = junction; tool.point = junction->definition().centre; - tool.status = "Leading another road out of \"" + junction->name() + "\": click where it goes"; + tool.status = format(STR_C("Leading another road out of \"%s\": click where it goes"), junction->name().c_str()); } else { tool.point = ground + glm::dvec3{0.0, tool.offset, 0.0}; - tool.status = "Start set"; + tool.status = STR_C("Start set: click the next points, Esc finishes"); } } tool.chain = true; @@ -1272,12 +1276,12 @@ void editor_mode::road_click() auto plan{road_preview(true)}; if (plan.pieces.empty()) { - tool.status = "Can't build this: " + plan.error; + tool.status = format(STR_C("Can't build this: %s"), STR(plan.error).c_str()); return; } if (plan.snap.valid() && false == editor_road::can_join(plan.snap, &reason)) { - tool.status = "Can't join here: " + reason; + tool.status = format(STR_C("Can't join here: %s"), STR(reason).c_str()); return; } auto pieces{plan.pieces}; @@ -1346,15 +1350,15 @@ void editor_mode::road_click() if (created.empty()) { tool.chain = false; - tool.status = "The road wasn't created"; + tool.status = STR_C("The road wasn't created"); return; } if (plan.snap.valid() || plan.hasend) { tool.chain = false; - tool.status = (plan.snap.valid() ? "Joined with \"" + (plan.snap.road != nullptr ? plan.snap.road->name() : plan.snap.junction->name()) + "\"" : - plan.end.junction != nullptr ? "Led into \"" + plan.end.junction->name() + "\" as another of its roads" : - std::string{"Led into the side of the road, with a junction made there"}); + tool.status = (plan.snap.valid() ? format(STR_C("Joined with \"%s\", Ctrl+Z takes it back"), (plan.snap.road != nullptr ? plan.snap.road->name() : plan.snap.junction->name()).c_str()) : + plan.end.junction != nullptr ? format(STR_C("Led into \"%s\" as another of its roads, Ctrl+Z takes it back"), plan.end.junction->name().c_str()) : + std::string{STR_C("Led into the side of the road, with a junction made there, Ctrl+Z takes it back")}); return; } glm::dvec2 const planar{tangent.x, tangent.z}; @@ -1366,9 +1370,10 @@ void editor_mode::road_click() } tool.grade = grade; tool.hasgrade = true; - tool.status = std::to_string(created.size()) + (created.size() == 1 ? " piece added" : " pieces added"); + tool.status = format(STR_C("Pieces added: %d"), static_cast(created.size())); if (plan.hasstart) - tool.status += (plan.start.junction != nullptr ? ", as another road of the junction" : ", with a junction where the road leaves"); + tool.status += (plan.start.junction != nullptr ? STR_C(", as another road of the junction") : STR_C(", with a junction where the road leaves")); + tool.status += STR_C(", Ctrl+Z takes them back"); } void editor_mode::road_apply() @@ -1390,7 +1395,7 @@ void editor_mode::road_apply() { // what the window shows goes back to what the road is like road_select(tool.selected); - tool.status = "\"" + road->name() + "\" can't be changed: " + reason; + tool.status = cannot_change(road->name(), reason); return; } auto state{tool.settings}; @@ -1423,11 +1428,11 @@ void editor_mode::road_apply() auto const fitted{record.roads.size() + record.roads_removed.size() + record.junctions.size() + record.junctions_created.size() - pieces.size()}; push_road_snapshot(std::move(record)); tool.settings = tool.selected->definition(); - tool.status = (pieces.size() == 1 ? std::string{"Changed."} : "Changed " + std::to_string(pieces.size()) + " pieces."); + tool.status = (pieces.size() == 1 ? std::string{STR_C("Changed, Ctrl+Z takes it back")} : format(STR_C("Pieces changed: %d, Ctrl+Z takes it back"), static_cast(pieces.size()))); if (fitted > 0) - tool.status += " What's next to it was fitted to it."; + tool.status += std::string{". "} + STR_C("What's next to it was fitted to it"); if (false == notes.empty()) - tool.status += " " + notes; + tool.status += ". " + notes; } bool editor_mode::road_delete() @@ -1442,14 +1447,14 @@ bool editor_mode::road_delete() auto *point{tool.marker}; if (false == editor_road::can_edit(*point, &reason)) { - tool.status = "Can't delete: " + reason; + tool.status = format(STR_C("Can't delete: %s"), STR(reason).c_str()); return true; } editor_road::remove(std::vector{point}); record.points_removed.emplace_back(point); push_road_snapshot(std::move(record)); tool.marker = nullptr; - tool.status = "\"" + point->name() + "\" deleted, Ctrl+Z brings it back"; + tool.status = format(STR_C("\"%s\" deleted, Ctrl+Z brings it back"), point->name().c_str()); return true; } if (false == tool.points.empty()) @@ -1461,27 +1466,27 @@ bool editor_mode::road_delete() auto *junction{tool.junction}; if (false == editor_road::can_edit(*junction, &reason)) { - tool.status = "Can't delete: " + reason; + tool.status = format(STR_C("Can't delete: %s"), STR(reason).c_str()); return true; } editor_road::remove(std::vector{junction}); record.junctions_removed.emplace_back(junction); push_road_snapshot(std::move(record)); tool.junction = nullptr; - tool.status = "\"" + junction->name() + "\" deleted, Ctrl+Z brings it back"; + tool.status = format(STR_C("\"%s\" deleted, Ctrl+Z brings it back"), junction->name().c_str()); return true; } auto *road{tool.selected}; if (false == editor_road::can_edit(*road, &reason)) { - tool.status = "Can't delete: " + reason; + tool.status = format(STR_C("Can't delete: %s"), STR(reason).c_str()); return true; } editor_road::remove(std::vector{road}); record.roads_removed.emplace_back(road); push_road_snapshot(std::move(record)); tool.selected = nullptr; - tool.status = "\"" + road->name() + "\" deleted, Ctrl+Z brings it back"; + tool.status = format(STR_C("\"%s\" deleted, Ctrl+Z brings it back"), road->name().c_str()); return true; } @@ -1492,13 +1497,13 @@ void editor_mode::road_roundabout(glm::dvec3 const &Ground) std::string reason; if (false == editor_road::available(&reason)) { - tool.status = reason; + tool.status = STR(reason); return; } auto const states{editor_road::roundabout(Ground + glm::dvec3{0.0, tool.offset, 0.0}, tool.ringradius, tool.settings, tool.island)}; if (states.empty()) { - tool.status = "The roundabout is too small for its lanes: give it a larger radius"; + tool.status = STR_C("The roundabout is too small for its lanes: give it a larger radius"); return; } for (auto const &state : states) @@ -1507,7 +1512,7 @@ void editor_mode::road_roundabout(glm::dvec3 const &Ground) { if (editor_road::nearest(state.point(t), 0.0) != nullptr || editor_road::nearest_junction(state.point(t)) != nullptr) { - tool.status = "There's a road in the way. Make the roundabout next to it, then lead the road into its side"; + tool.status = STR_C("There's a road in the way. Make the roundabout next to it, then lead the road into its side"); return; } } @@ -1515,13 +1520,13 @@ void editor_mode::road_roundabout(glm::dvec3 const &Ground) auto const created{editor_road::create(states)}; if (created.empty()) { - tool.status = "The roundabout wasn't created"; + tool.status = STR_C("The roundabout wasn't created"); return; } editor_road::record record; record.roads_created = created; push_road_snapshot(std::move(record)); - tool.status = "Roundabout made. Untick Roundabout and lead roads out of its side, or into it: the traffic joining it gives way to the traffic going around."; + tool.status = STR_C("Roundabout made, Ctrl+Z takes it back. Untick Roundabout and lead roads out of its side, or into it: the traffic joining it gives way to the traffic going around"); } // how far from specified point, and how far down, a slope of the grade set for the banks gets to the ground @@ -1579,11 +1584,11 @@ void editor_mode::junction_bank_to_ground(junction_node &Junction) } junction_apply(Junction, state); auto &tool{m_roadtool}; - if (tool.status == "Changed") + if (tool.status == STR_C("Changed, Ctrl+Z takes it back")) { - tool.status = "Banks of the junction led to the ground."; + tool.status = STR_C("Banks of the junction led to the ground, Ctrl+Z takes it back"); if ((false == sided || state.side.type != road_node::side_type::shoulder) && (state.bankmaterial.empty() || state.bankmaterial == "none")) - tool.status += " Pick a bank material to have them drawn."; + tool.status += std::string{". "} + STR_C("Pick a bank material to have them drawn"); } } @@ -1599,7 +1604,7 @@ void editor_mode::road_bank_to_ground() { if (false == editor_road::can_edit(*road, &reason)) { - tool.status = "\"" + road->name() + "\" can't be changed: " + reason; + tool.status = cannot_change(road->name(), reason); return; } } @@ -1637,10 +1642,10 @@ void editor_mode::road_bank_to_ground() editor_road::apply(changes); push_road_snapshot(std::move(record)); road_select(tool.selected); - tool.status = "Banks of " + std::to_string(changes.size()) + (changes.size() == 1 ? " piece" : " pieces") + " led to the ground."; + tool.status = format(STR_C("Banks led to the ground, pieces: %d, Ctrl+Z takes it back"), static_cast(changes.size())); auto const &layout{tool.selected->definition()}; if ((layout.sides[0].type != road_node::side_type::shoulder || layout.sides[1].type != road_node::side_type::shoulder) && (layout.bankmaterial.empty() || layout.bankmaterial == "none")) - tool.status += " Pick a bank material to have them drawn where the road has no shoulder."; + tool.status += std::string{". "} + STR_C("Pick a bank material to have them drawn where the road has no shoulder"); } // takes the selected points out of their roads @@ -1655,14 +1660,14 @@ bool editor_mode::road_points_delete() std::string error; if (editor_road::dissolve(point, record, error)) ++taken; - else if (notes.find(error) == std::string::npos) - notes += " A point was left: " + error + "."; + else if (auto const note{". " + format(STR_C("A point was left: %s"), STR(error).c_str())}; notes.find(note) == std::string::npos) + notes += note; } push_road_snapshot(std::move(record)); // the points which stayed are still selected, so it's clear which ones these are tool.points.erase(std::remove_if(tool.points.begin(), tool.points.end(), [](glm::dvec3 const &Point) { return editor_road::ends_at(Point).empty(); }), tool.points.end()); - tool.status = (taken == 0 ? std::string{"Nothing deleted."} : taken == 1 ? std::string{"Point deleted, Ctrl+Z brings it back."} : - std::to_string(taken) + " points deleted, Ctrl+Z brings them back."); + tool.status = (taken == 0 ? std::string{STR_C("Nothing deleted")} : taken == 1 ? std::string{STR_C("Point deleted, Ctrl+Z brings it back")} : + format(STR_C("Points deleted: %d, Ctrl+Z brings them back"), taken)); tool.status += notes; return true; } @@ -1676,12 +1681,12 @@ bool editor_mode::road_split() std::string reason; if (false == editor_road::can_edit(*road, &reason)) { - tool.status = "Can't split: " + reason; + tool.status = format(STR_C("Can't split: %s"), STR(reason).c_str()); return true; } if (road->definition().changing()) { - tool.status = "Can't split a piece which gains or loses lanes; give it the same lanes at both ends first"; + tool.status = STR_C("Can't split a piece which gains or loses lanes; give it the same lanes at both ends first"); return true; } editor_road::record record; @@ -1689,13 +1694,14 @@ bool editor_mode::road_split() auto *created{editor_road::split(*road, editor_road::nearest_parameter(road->definition(), tool.mouse))}; if (created == nullptr) { - tool.status = "Can't split this close to the end"; + tool.status = STR_C("Can't split this close to the end"); return true; } record.roads_created.emplace_back(created); push_road_snapshot(std::move(record)); road_select(road); - tool.status = "Split in two: \"" + road->name() + "\" and \"" + created->name() + "\". Click the point between them to set the width or the height of the road there"; + tool.status = format(STR_C("Split in two: \"%s\" and \"%s\", Ctrl+Z takes it back. Click the point between them to set the width or the height of the road there"), road->name().c_str(), + created->name().c_str()); return true; } @@ -1705,7 +1711,7 @@ bool editor_mode::road_points_apply(std::vectorname() + "\" can't be changed: " + reason; + tool.status = cannot_change(change.first->name(), reason); return false; } record.roads.emplace_back(change.first, change.first->definition()); @@ -1723,7 +1729,7 @@ bool editor_mode::road_points_apply(std::vectorname() + "\" can't be changed: " + reason; + tool.status = cannot_change(change.first->name(), reason); return false; } record.junctions.emplace_back(change.first, change.first->definition()); @@ -1735,9 +1741,9 @@ bool editor_mode::road_points_apply(std::vector(Changes.size())); if (false == notes.empty()) - tool.status += " " + notes; + tool.status += ". " + notes; return true; } @@ -1895,13 +1901,14 @@ bool editor_mode::render_road_material(char const *Label, char *Buffer, std::siz ImGui::SameLine(); if (ImGui::Button("...")) ImGui::OpenPopup("pick"); + item_tooltip("Pick from the materials in the texture folder"); ImGui::SameLine(); - ImGui::TextUnformatted(Label); + ImGui::TextUnformatted(STR_C(Label)); if (ImGui::BeginPopup("pick")) { - ImGui::TextDisabled("Materials and images found in the texture folder"); + ImGui::TextDisabled("%s", STR_C("Materials and images found in the texture folder")); ImGui::SetNextItemWidth(200.0f); - ImGui::InputText("Filter", tool.filter, sizeof(tool.filter)); + ImGui::InputText(STR_C("Filter"), tool.filter, sizeof(tool.filter)); auto const filter{lower_case(tool.filter)}; std::vector listed; for (auto const &name : editor_road::materials()) @@ -1948,35 +1955,36 @@ bool editor_mode::render_road_layout(road_node::state &State) int counts[2][2]{{State.lanes_at(0, true), State.lanes_at(0, false)}, {State.lanes_at(1, true), State.lanes_at(1, false)}}; bool relaned{false}; ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputInt(selecting && tool.varied ? "Lanes along the axis, at the start" : "Lanes along the axis", &counts[0][0])) + if (ImGui::InputInt(selecting && tool.varied ? STR_C("Lanes along the axis, at the start") : STR_C("Lanes along the axis"), &counts[0][0])) relaned = true; + item_tooltip("Lanes going the way the road was built, from its start to its end; drawn green with Show lanes"); ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputInt(selecting && tool.varied ? "Lanes against the axis, at the start" : "Lanes against the axis", &counts[0][1])) + if (ImGui::InputInt(selecting && tool.varied ? STR_C("Lanes against the axis, at the start") : STR_C("Lanes against the axis"), &counts[0][1])) relaned = true; + item_tooltip("Lanes going back, from the end of the road to its start; drawn orange with Show lanes"); if (selecting) { // ticked it only shows the lanes of the end to be set; unticked it gives the end the lanes of the start - if (ImGui::Checkbox("Other lanes at the end of the piece", &tool.varied) && false == tool.varied && State.changing()) + if (ImGui::Checkbox(STR_C("Other lanes at the end of the piece"), &tool.varied) && false == tool.varied && State.changing()) relaned = true; - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "The road gains or loses lanes along this piece: it gets wider or narrower, a lane it gains branches off the outermost lane\n" - "of its direction (two at most), a lane it loses joins the outermost one left.\n" - "What's next to the piece is fitted to the lanes the piece has at that end.\n" - "The axis goes the way the lanes drawn in green do: the start of the piece is where these come from.\n" - "Split the piece (K) first to have the lanes change over a part of it."); + item_tooltip("The road gains or loses lanes along this piece: it gets wider or narrower, a lane it gains branches off the outermost lane\n" + "of its direction (two at most), a lane it loses joins the outermost one left.\n" + "What's next to the piece is fitted to the lanes the piece has at that end.\n" + "The axis goes the way the lanes drawn in green do: the start of the piece is where these come from.\n" + "Split the piece (K) first to have the lanes change over a part of it"); if (tool.varied) { ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputInt("Lanes along the axis, at the end", &counts[1][0])) + if (ImGui::InputInt(STR_C("Lanes along the axis, at the end"), &counts[1][0])) relaned = true; ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputInt("Lanes against the axis, at the end", &counts[1][1])) + if (ImGui::InputInt(STR_C("Lanes against the axis, at the end"), &counts[1][1])) relaned = true; } } // the traffic going along the axis gets on the piece at its start, the one going against it at its end if (State.lanes_at(1, true) - State.lanes_at(0, true) > 2 || State.lanes_at(0, false) - State.lanes_at(1, false) > 2) - ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "A lane forks into three at most: lanes past the second one gained can't be got on"); + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR_C("A lane forks into three at most: lanes past the second one gained can't be got on")); if (relaned) { if (false == selecting || false == tool.varied) @@ -1989,7 +1997,7 @@ bool editor_mode::render_road_layout(road_node::state &State) changed = true; } } - if (number("Lane width [m]", State.lanewidth, 0.25f, "%.2f")) + if (number(STR_C("Lane width [m]"), State.lanewidth, 0.25f, "%.2f")) { State.lanewidth = std::clamp(State.lanewidth, 1.0f, 10.0f); for (auto &lane : State.lanes) @@ -1997,33 +2005,29 @@ bool editor_mode::render_road_layout(road_node::state &State) changed = true; } float limit{State.velocity > 0.0f ? State.velocity : 0.0f}; - if (number("Speed limit [km/h], 0: none", limit, 10.0f, "%.0f")) + if (number(STR_C("Speed limit [km/h]"), limit, 10.0f, "%.0f")) { State.velocity = (limit > 0.0f ? limit : -1.0f); for (auto &lane : State.lanes) lane.velocity = State.velocity; changed = true; } + item_tooltip("0: no limit"); // appearance ImGui::Separator(); - char const *colours[]{"none", "white", "orange"}; + char const *colours[]{STR_C("no markings"), STR_C("white"), STR_C("orange")}; int colour{State.markings == road_node::marking_colour::white ? 1 : State.markings == road_node::marking_colour::orange ? 2 : 0}; ImGui::SetNextItemWidth(120.0f); - if (ImGui::Combo("Markings", &colour, colours, 3)) + if (ImGui::Combo(STR_C("Markings"), &colour, colours, 3)) { State.markings = (colour == 1 ? road_node::marking_colour::white : colour == 2 ? road_node::marking_colour::orange : road_node::marking_colour::none); changed = true; } if (material("Surface", tool.surface, sizeof(tool.surface), State.surface)) changed = true; - if (number("Texture length [m]", State.texturelength, 0.5f, "%.2f")) - { - State.texturelength = std::max(0.1f, State.texturelength); - changed = true; - } - char const *sidenames[]{"Left side", "Right side"}; - char const *sidetypes[]{"none", "shoulder", "sidewalk"}; + char const *sidenames[]{STR_C("Left side"), STR_C("Right side")}; + char const *sidetypes[]{STR_C("no side"), STR_C("shoulder"), STR_C("sidewalk")}; for (int side = 0; side < 2; ++side) { ImGui::PushID(side); @@ -2040,7 +2044,7 @@ bool editor_mode::render_road_layout(road_node::state &State) if (data.type != road_node::side_type::none) { ImGui::Indent(); - if (number("Width [m]", data.width, 0.25f, "%.2f")) + if (number(STR_C("Width [m]"), data.width, 0.25f, "%.2f")) { data.width = std::clamp(data.width, 0.1f, 20.0f); changed = true; @@ -2048,50 +2052,36 @@ bool editor_mode::render_road_layout(road_node::state &State) if (material("Material", tool.sides[side], sizeof(tool.sides[side]), data.material)) changed = true; if (data.material.empty() || data.material == "none") - ImGui::TextDisabled("Pick a material to have the side drawn"); + ImGui::TextDisabled("%s", STR_C("Pick a material to have the side drawn")); ImGui::Unindent(); } ImGui::PopID(); } // kerbs - if (ImGui::Checkbox("Kerb on the left", &State.kerbs[0])) + if (ImGui::Checkbox(STR_C("Kerb on the left"), &State.kerbs[0])) changed = true; ImGui::SameLine(); - if (ImGui::Checkbox("on the right", &State.kerbs[1])) + if (ImGui::Checkbox(STR_C("on the right##kerb"), &State.kerbs[1])) changed = true; auto const island{State.divided() && State.median.type == road_node::median_type::island}; if (State.kerbs[0] || State.kerbs[1] || island) { ImGui::Indent(); - if (number("Kerb width [m]", State.kerbwidth, 0.05f, "%.2f")) - { - State.kerbwidth = std::clamp(State.kerbwidth, 0.05f, 2.0f); - changed = true; - } if (material("Kerb material", tool.kerbtext, sizeof(tool.kerbtext), State.kerbmaterial)) changed = true; if ((State.kerbs[0] || State.kerbs[1]) && (State.kerbmaterial.empty() || State.kerbmaterial == "none")) - ImGui::TextDisabled("Pick a material to have the kerbs drawn"); + ImGui::TextDisabled("%s", STR_C("Pick a material to have the kerbs drawn")); ImGui::Unindent(); } - if (State.kerbs[0] || State.kerbs[1] || island || State.sides[0].type == road_node::side_type::sidewalk || State.sides[1].type == road_node::side_type::sidewalk) - { - if (number("Kerb height [m]", State.kerbheight, 0.01f, "%.2f")) - { - State.kerbheight = std::clamp(State.kerbheight, 0.0f, 1.0f); - changed = true; - } - } if (State.sides[0].type == road_node::side_type::shoulder || State.sides[1].type == road_node::side_type::shoulder) { - if (number("Bank width [m]", State.slope.x, 0.25f, "%.2f")) + if (number(STR_C("Bank width [m]"), State.slope.x, 0.25f, "%.2f")) { State.slope.x = std::clamp(State.slope.x, 0.0f, 20.0f); changed = true; } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "The bank every shoulder of the road is closed with, unless the side has a bank of its own set below."); - if (number("Bank drop [m]", State.slope.y, 0.1f, "%.2f")) + item_tooltip("The bank every shoulder of the road is closed with, unless the side has a bank of its own set below"); + if (number(STR_C("Bank height [m]"), State.slope.y, 0.1f, "%.2f")) { State.slope.y = std::clamp(State.slope.y, 0.0f, 20.0f); changed = true; @@ -2099,12 +2089,11 @@ bool editor_mode::render_road_layout(road_node::state &State) } // banks leading to the ground - if (ImGui::CollapsingHeader("Banks to the ground")) + if (ImGui::CollapsingHeader(STR_C("Banks to the ground##banks"))) { - ImGui::TextDisabled("A slope from the edge of the road down, or up, to the ground."); - ImGui::TextDisabled("Set here it goes to every piece the changes go to; the button below"); - ImGui::TextDisabled("works out each piece on its own."); - char const *banknames[]{"Bank on the left", "Bank on the right"}; + ImGui::TextDisabled("%s", STR_C("A slope from the edge of the road to the ground")); + item_tooltip("It goes down, or up, to the ground. Set here it goes to every piece the changes go to;\nthe button below works out each piece on its own"); + char const *banknames[]{STR_C("Bank on the left"), STR_C("Bank on the right")}; for (int side = 0; side < 2; ++side) { ImGui::PushID(100 + side); @@ -2121,13 +2110,13 @@ bool editor_mode::render_road_layout(road_node::state &State) if (bank.set) { ImGui::Indent(); - if (number("Width at the start [m]", bank.width[0], 0.5f, "%.2f")) + if (number(STR_C("Width at the start [m]"), bank.width[0], 0.5f, "%.2f")) changed = true; - if (number("Drop at the start [m]", bank.drop[0], 0.25f, "%.2f")) + if (number(STR_C("Height at the start [m]"), bank.drop[0], 0.25f, "%.2f")) changed = true; - if (number("Width at the end [m]", bank.width[1], 0.5f, "%.2f")) + if (number(STR_C("Width at the end [m]"), bank.width[1], 0.5f, "%.2f")) changed = true; - if (number("Drop at the end [m]", bank.drop[1], 0.25f, "%.2f")) + if (number(STR_C("Height at the end [m]"), bank.drop[1], 0.25f, "%.2f")) changed = true; ImGui::Unindent(); } @@ -2135,30 +2124,29 @@ bool editor_mode::render_road_layout(road_node::state &State) } if (material("Bank material", tool.banktext, sizeof(tool.banktext), State.bankmaterial)) changed = true; - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "What a bank is covered with next to a sidewalk, a kerb or the bare edge of the road.\nA bank of a shoulder is covered with the material of the shoulder."); + item_tooltip("What a bank is covered with next to a sidewalk, a kerb or the bare edge of the road.\nA bank of a shoulder is covered with the material of the shoulder"); if (tool.selected != nullptr) { ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputFloat("Run for a metre of drop [m]", &tool.bankgrade, 0.25f, 0.5f, "%.2f")) + if (ImGui::InputFloat(STR_C("Bank slope 1:n"), &tool.bankgrade, 0.25f, 0.5f, "%.2f")) tool.bankgrade = std::clamp(tool.bankgrade, 0.25f, 10.0f); - if (ImGui::Button("Lead the banks to the ground")) + item_tooltip("n: metres across for a metre of height; 1.5 is a usual embankment"); + if (ImGui::Button(STR_C("Lead the banks to the ground"))) { road_bank_to_ground(); // the layout shown was replaced with what the road is like now return false; } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Looks for the ground on both sides of the road, at the start and at the end of each piece,\n" - "and sets the banks to reach it with the slope given above: down where the road is over the ground, up where it's cut into it.\n" - "Done for the selected piece, or for every piece of the road if the changes go to the whole road."); + item_tooltip("Looks for the ground on both sides of the road, at the start and at the end of each piece,\n" + "and sets the banks to reach it with the slope given above: down where the road is over the ground, up where it's cut into it.\n" + "Done for the selected piece, or for every piece of the road if the changes go to the whole road"); } } // the two directions kept apart - if (State.forward > 0 && State.backward > 0 && ImGui::CollapsingHeader("Between the directions")) + if (State.forward > 0 && State.backward > 0 && ImGui::CollapsingHeader(STR_C("Between the directions##median"))) { - char const *mediantypes[]{"nothing, the ground shows", "surface closed to the traffic", "island"}; + char const *mediantypes[]{STR_C("nothing, the ground shows"), STR_C("surface closed to the traffic"), STR_C("island")}; int type{State.median.type == road_node::median_type::gap ? 0 : State.median.type == road_node::median_type::island ? 2 : 1}; ImGui::SetNextItemWidth(220.0f); if (ImGui::Combo("##median", &type, mediantypes, 3)) @@ -2166,49 +2154,78 @@ bool editor_mode::render_road_layout(road_node::state &State) State.median.type = (type == 0 ? road_node::median_type::gap : type == 2 ? road_node::median_type::island : road_node::median_type::painted); changed = true; } - if (number("Apart at the start [m]", State.median.width[0], 0.5f, "%.2f")) + char const *apart{"How far apart the two directions are led at each end of the piece; 0 at one end and more at the other makes them part along it.\n" + "Set for each piece on its own. The pieces next to it follow it where they meet, and so do the junctions.\n" + "A road with its directions apart can be carried on as two one-way roads: start the Build tool\n" + "on the middle of one of its roadways, at an end of the road with nothing attached"}; + if (number(STR_C("Apart at the start [m]"), State.median.width[0], 0.5f, "%.2f")) changed = true; - if (number("Apart at the end [m]", State.median.width[1], 0.5f, "%.2f")) + item_tooltip(apart); + if (number(STR_C("Apart at the end [m]"), State.median.width[1], 0.5f, "%.2f")) changed = true; - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "How far apart the two directions are led at each end of the piece; 0 at one end and more at the other makes them part along it.\n" - "Set for each piece on its own. The pieces next to it follow it where they meet, and so do the junctions.\n" - "A road with its directions apart can be carried on as two one-way roads: start the Build tool\n" - "on the middle of one of its roadways, at an end of the road with nothing attached."); + item_tooltip(apart); if (State.median.type == road_node::median_type::island) { if (material("Island material", tool.mediantext, sizeof(tool.mediantext), State.median.material)) changed = true; - ImGui::TextDisabled("The island is as high as the kerbs; with a kerb material set it gets kerbs of its own."); - if (number("Rounded at the start [m]", State.median.round[0], 0.5f, "%.2f")) + item_tooltip("The island is as high as the kerbs; with a kerb material set it gets kerbs of its own"); + char const *rounded{"Length of the rounded end of the island at that end of the piece; half of the width of the island makes it a half circle.\n" + "0: the island is cut straight, with a face. Use it where the next piece has no island"}; + if (number(STR_C("Rounded at the start [m]"), State.median.round[0], 0.5f, "%.2f")) changed = true; - if (number("Rounded at the end [m]", State.median.round[1], 0.5f, "%.2f")) + item_tooltip(rounded); + if (number(STR_C("Rounded at the end [m]"), State.median.round[1], 0.5f, "%.2f")) changed = true; - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Length of the rounded end of the island at that end of the piece; half of the width of the island makes it a half circle.\n" - "0: the island is cut straight, with a face. Use it where the next piece has no island."); + item_tooltip(rounded); } } - // traffic - if (number("Traffic share, 100: like any other", State.weight, 10.0f, "%.0f")) + ImGui::PushID("layout"); + if (ImGui::TreeNode(STR_C("More settings"))) { - State.weight = std::clamp(State.weight, 0.0f, 10000.0f); - changed = true; + if (number(STR_C("Texture length [m]"), State.texturelength, 0.5f, "%.2f")) + { + State.texturelength = std::max(0.1f, State.texturelength); + changed = true; + } + item_tooltip("How many metres of the surface one copy of the texture covers"); + if (State.kerbs[0] || State.kerbs[1] || island) + { + if (number(STR_C("Kerb width [m]"), State.kerbwidth, 0.05f, "%.2f")) + { + State.kerbwidth = std::clamp(State.kerbwidth, 0.05f, 2.0f); + changed = true; + } + } + if (State.kerbs[0] || State.kerbs[1] || island || State.sides[0].type == road_node::side_type::sidewalk || State.sides[1].type == road_node::side_type::sidewalk) + { + if (number(STR_C("Kerb height [m]"), State.kerbheight, 0.01f, "%.2f")) + { + State.kerbheight = std::clamp(State.kerbheight, 0.0f, 1.0f); + changed = true; + } + } + // traffic + if (number(STR_C("Traffic share"), State.weight, 10.0f, "%.0f")) + { + State.weight = std::clamp(State.weight, 0.0f, 10000.0f); + changed = true; + } + item_tooltip("How willing the drivers are to take this road when they leave a junction, next to the other roads they can take there.\n" + "100: like any other. With 100 for the main road and 20 for a dirt road leaving it, one vehicle in six turns into the dirt road.\n" + "0: nobody takes it, unless there's no other way"); + if (ImGui::Checkbox(STR_C("Part of a roundabout"), &State.roundabout)) + changed = true; + item_tooltip("A junction made on this road gives the right of way to the traffic going along it,\nand tells the traffic of the road joining it to give way"); + ImGui::TreePop(); } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "How willing the drivers are to take this road when they leave a junction, next to the other roads they can take there.\n" - "With 100 for the main road and 20 for a dirt road leaving it, one vehicle in six turns into the dirt road.\n" - "0: nobody takes it, unless there's no other way."); - if (ImGui::Checkbox("Part of a roundabout", &State.roundabout)) - changed = true; - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "A junction made on this road gives the right of way to the traffic going along it,\nand tells the traffic of the road joining it to give way."); + ImGui::PopID(); // single lanes, and the rules for changing between them - if (ImGui::CollapsingHeader("Lanes and lane changes", ImGuiTreeNodeFlags_DefaultOpen)) + if (ImGui::CollapsingHeader(STR_C("Lanes and lane changes##lanes"))) { - ImGui::TextDisabled("Left to right when facing along the axis. f: along the axis, b: against it"); + ImGui::TextDisabled("%s", STR_C("Left to right, facing along the axis")); + item_tooltip("f: a lane along the axis, b: against it"); for (std::size_t lane = 0; lane < State.lanes.size(); ++lane) { ImGui::PushID(static_cast(lane)); @@ -2221,6 +2238,7 @@ bool editor_mode::render_road_layout(road_node::state &State) data.width = std::clamp(data.width, 1.0f, 10.0f); changed = true; } + item_tooltip("Width of the lane"); ImGui::SameLine(); float lanelimit{data.velocity > 0.0f ? data.velocity : 0.0f}; if (number("km/h##velocity", lanelimit, 10.0f, "%.0f")) @@ -2228,17 +2246,19 @@ bool editor_mode::render_road_layout(road_node::state &State) data.velocity = (lanelimit > 0.0f ? lanelimit : -1.0f); changed = true; } + item_tooltip("Speed limit of the lane, 0: no limit"); if (lane + 1 < State.lanes.size() && lane < State.changes.size()) { // the entries are in the order of the stored values: none, to the right, to the left, both auto const next{State.lane_id(lane + 1)}; - auto const toright{"only " + id + " to " + next}; - auto const toleft{"only " + next + " to " + id}; - char const *modes[]{"no lane change", toright.c_str(), toleft.c_str(), "lane change both ways"}; + auto const toright{format(STR_C("only %s to %s"), id.c_str(), next.c_str())}; + auto const toleft{format(STR_C("only %s to %s"), next.c_str(), id.c_str())}; + char const *modes[]{STR_C("no lane change"), toright.c_str(), toleft.c_str(), STR_C("lane change both ways")}; ImGui::Indent(); ImGui::SetNextItemWidth(200.0f); if (ImGui::Combo("##change", &State.changes[lane], modes, 4)) changed = true; + item_tooltip("Which way a vehicle may change between these two lanes"); ImGui::Unindent(); } ImGui::PopID(); @@ -2270,50 +2290,54 @@ void editor_mode::render_road_tool_options() auto &tool{m_roadtool}; std::string reason; if (false == editor_road::available(&reason)) - ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", reason.c_str()); + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", STR(reason).c_str()); // the tools and what the place tool puts are chosen in the toolbar if (tool.tool == 1) { - if (ImGui::Checkbox("Roundabout", &tool.ring)) + if (ImGui::Checkbox(STR_C("Roundabout"), &tool.ring)) road_cancel(); + item_tooltip("A one-way road going around, with the lanes set below as the lanes along the axis.\nThen untick this and lead the roads out of its side, or into it"); if (tool.ring) { - ImGui::TextDisabled("LMB: put a roundabout around the cursor. It's a one-way road going around,"); - ImGui::TextDisabled("with the lanes set below as the lanes along the axis."); - ImGui::TextDisabled("Then untick this and lead the roads out of its side, or into it."); + gesture_rows({{"LMB", "a roundabout around the cursor"}}); ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputFloat("Radius of the middle of its road [m]", &tool.ringradius, 1.0f, 5.0f, "%.1f")) + if (ImGui::InputFloat(STR_C("Radius [m]##ring"), &tool.ringradius, 1.0f, 5.0f, "%.1f")) tool.ringradius = std::clamp(tool.ringradius, 8.0f, 200.0f); + item_tooltip("Radius of the middle of the road going around"); render_road_material("Island in the middle", tool.islandtext, sizeof(tool.islandtext), tool.island); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "What the middle is covered with; it's raised on a kerb as high as the kerbs of the road.\nnone: the middle is left as it is."); + item_tooltip("What the middle is covered with; it's raised on a kerb as high as the kerbs of the road.\nnone: the middle is left as it is"); } else { - ImGui::TextDisabled("LMB: start, then each next point. Ctrl: straight ahead. Esc: finish"); - ImGui::TextDisabled("Start or end on a loose end of a road to join it."); - ImGui::TextDisabled("Start or end on the side of a road to make a junction there,"); - ImGui::TextDisabled("or on a junction to give it another road."); + gesture_rows({{"LMB", "the start, then each next point"}, + {"Ctrl", "straight ahead"}, + {"Esc", "finish the road"}, + {"LMB on a road or a junction", "the new road is joined to it there"}}); } if (tool.chain) { - if (ImGui::Button("Finish (Esc)")) + if (ImGui::Button(STR_C("Finish (Esc)"))) road_cancel(); } // how the road is put on the ground - ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputFloat("Height above the ground [m]", &tool.offset, 0.05f, 0.25f, "%.2f")) - tool.offset = std::clamp(tool.offset, -10.0f, 100.0f); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "How far over the ground under the cursor the points are put.\nA few centimetres keep the surface from flickering where it lies on the ground."); - ImGui::Checkbox("Follow the ground", &tool.follow); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "On: the road is led over the ground between the clicked points, in pieces as short as the ground calls for,\nwith the profile rounded so the slope doesn't change abruptly.\nOff: the road goes straight from a point to the next one, like a ramp or a viaduct."); - ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputFloat("Speed limit on new junctions [km/h]", &tool.crossingspeed, 5.0f, 10.0f, "%.0f")) - tool.crossingspeed = std::clamp(tool.crossingspeed, 5.0f, 200.0f); + ImGui::Checkbox(STR_C("Follow the ground"), &tool.follow); + item_tooltip("On: the road is led over the ground between the clicked points, in pieces as short as the ground calls for,\nwith the profile rounded so the slope doesn't change abruptly.\nOff: the road goes straight from a point to the next one, like a ramp or a viaduct"); + ImGui::PushID("build"); + if (ImGui::TreeNode(STR_C("More settings"))) + { + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputFloat(STR_C("Height above the ground [m]"), &tool.offset, 0.05f, 0.25f, "%.2f")) + tool.offset = std::clamp(tool.offset, -10.0f, 100.0f); + item_tooltip("How far over the ground under the cursor the points are put.\nA few centimetres keep the surface from flickering where it lies on the ground"); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputFloat(STR_C("Junction speed limit [km/h]"), &tool.crossingspeed, 5.0f, 10.0f, "%.0f")) + tool.crossingspeed = std::clamp(tool.crossingspeed, 5.0f, 200.0f); + item_tooltip("The speed limit the junctions made by the new roads get"); + ImGui::TreePop(); + } + ImGui::PopID(); ImGui::Separator(); // what's set here is what the next pieces get render_road_layout(tool.settings); @@ -2323,21 +2347,10 @@ void editor_mode::render_road_tool_options() tool.placekind = std::clamp(tool.placekind, 0, 3); switch (tool.placekind) { - case 0: - ImGui::TextDisabled("LMB on a road where the rails cross it. The vehicles stop ahead"); - ImGui::TextDisabled("of the rails for as long as a train is coming."); - break; - case 1: - ImGui::TextDisabled("LMB on a lane. The vehicles of the set appear there one by one"); - ImGui::TextDisabled("and drive on the way the lane goes."); - break; - case 3: - ImGui::TextDisabled("LMB on a road: stripes are painted across it there."); - break; - default: - ImGui::TextDisabled("LMB on a lane. The vehicles which get there are taken off the road;"); - ImGui::TextDisabled("put one where the road leaves the scenery."); - break; + case 0: gesture_rows({{"LMB on a road", "where the rails cross it; the vehicles stop ahead of the rails while a train is coming"}}); break; + case 1: gesture_rows({{"LMB on a lane", "the vehicles of the set appear there one by one and drive the way the lane goes"}}); break; + case 3: gesture_rows({{"LMB on a road", "stripes are painted across it there"}}); break; + default: gesture_rows({{"LMB on a lane", "the vehicles getting there are taken off the road; put one where the road leaves the scenery"}}); break; } ImGui::Separator(); // what's set here is what the next points get @@ -2346,7 +2359,7 @@ void editor_mode::render_road_tool_options() render_roadpoint_layout(placing, nullptr); } else - ImGui::TextDisabled("The selected road is set up in the inspector"); + ImGui::TextDisabled("%s", STR_C("The selected road is set up in the inspector")); if (false == tool.status.empty()) { ImGui::Separator(); @@ -2364,7 +2377,7 @@ void editor_mode::render_road_selection() return ImGui::InputFloat(Label, &Value, Step, Step * 4.0f, Format, ImGuiInputTextFlags_EnterReturnsTrue); }; if (tool.tool != 0) - ImGui::TextDisabled("The select tool shows the road picked in the view here"); + ImGui::TextDisabled("%s", STR_C("The Select tool shows the road picked in the view here")); else if (tool.marker != nullptr) { auto &point{*tool.marker}; @@ -2372,38 +2385,36 @@ void editor_mode::render_road_selection() ImGui::Text("%s, %s", point.name().c_str(), point_label(state.kind)); if (render_roadpoint_layout(state, &point)) roadpoint_apply(point, state); - if (ImGui::Button("Delete (Del)")) + if (ImGui::Button(STR_C("Delete (Del)"))) road_delete(); } else if (false == tool.points.empty()) { // the road at the selected points - ImGui::Text("%d %s selected", static_cast(tool.points.size()), tool.points.size() == 1 ? "point" : "points"); - ImGui::TextDisabled("Shift+LMB: add a point, or take it out. K on a selected piece: make a point at the cursor"); + ImGui::Text(STR_C("Points selected: %d"), static_cast(tool.points.size())); + gesture_rows({{"Shift+LMB", "adds a point, or takes it out"}, {"K", "on a selected piece: a point at the cursor"}}); auto const ends{editor_road::ends_at(tool.points.front())}; if (false == ends.empty()) { auto const &layout{ends.front().road->definition()}; auto width{static_cast(layout.width(ends.front().atend ? 1 : 0) * layout.taper[ends.front().atend ? 1 : 0])}; - if (number("Road width here [m]", width, 0.25f, "%.2f")) + if (number(STR_C("Road width here [m]"), width, 0.25f, "%.2f")) road_point_width(std::clamp(width, 1.0f, 100.0f)); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Combined width of the lanes at this point; each lane takes its share of it.\nThe pieces on both sides get wider or narrower along their length to meet it.\nSelect several points to keep the width over the pieces between them."); + item_tooltip("Combined width of the lanes at this point; each lane takes its share of it.\nThe pieces on both sides get wider or narrower along their length to meet it.\nSelect several points to keep the width over the pieces between them"); // heights: set outright, moved by a step, or taken from the ground auto height{static_cast(tool.points.front().y)}; - if (number("Height [m]", height, 0.1f, "%.2f")) + if (number(STR_C("Height [m]"), height, 0.1f, "%.2f")) road_point_heights(std::vector(tool.points.size(), height)); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Height of the axis of the road at the point; with several points selected all are put at this height.\nThe slopes of the pieces at the point are kept, so the road stays smooth."); + item_tooltip("Height of the axis of the road at the point; with several points selected all are put at this height.\nThe slopes of the pieces at the point are kept, so the road stays smooth"); std::vector heights; for (auto const &point : tool.points) heights.emplace_back(point.y); - auto const moved{ImGui::Button("Lower") ? -1.0 : 0.0}; + auto const moved{ImGui::Button(STR_C("Lower")) ? -1.0 : 0.0}; ImGui::SameLine(); - auto const raised{ImGui::Button("Raise") ? 1.0 : 0.0}; + auto const raised{ImGui::Button(STR_C("Raise")) ? 1.0 : 0.0}; ImGui::SameLine(); ImGui::SetNextItemWidth(80.0f); - if (ImGui::InputFloat("by [m]", &tool.heightstep, 0.0f, 0.0f, "%.2f")) + if (ImGui::InputFloat(STR_C("by [m]##heightstep"), &tool.heightstep, 0.0f, 0.0f, "%.2f")) tool.heightstep = std::clamp(tool.heightstep, 0.01f, 50.0f); if (moved + raised != 0.0) { @@ -2412,7 +2423,7 @@ void editor_mode::render_road_selection() entry += (moved + raised) * tool.heightstep; road_point_heights(heights); } - if (ImGui::Button("Put on the ground")) + if (ImGui::Button(STR_C("Put on the ground"))) { // a point with nothing under it comes back with the height it has, and is left where it is auto const ground{ground_heights(tool.points, true)}; @@ -2422,31 +2433,30 @@ void editor_mode::render_road_selection() road_point_heights(heights); } if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Puts each point %.2f m over the ground under it; the height above the ground is set in the Build tool.", tool.offset); + ImGui::SetTooltip(STR_C("Puts each point %.2f m over the ground under it; the height above the ground is set in the Build tool"), tool.offset); } - if (ImGui::Button(tool.points.size() == 1 ? "Delete the point (Del)" : "Delete the points (Del)")) + if (ImGui::Button(tool.points.size() == 1 ? STR_C("Delete the point (Del)") : STR_C("Delete the points (Del)"))) road_points_delete(); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "Takes the point out of the road: the pieces on its two sides become one piece,\n" - "led from the far end of one to the far end of the other the way the road goes there.\n" - "A point the road ends at takes the last piece of the road with it."); + item_tooltip("Takes the point out of the road: the pieces on its two sides become one piece,\n" + "led from the far end of one to the far end of the other the way the road goes there.\n" + "A point the road ends at takes the last piece of the road with it"); } else if (tool.junction != nullptr) { auto &junction{*tool.junction}; if (junction.definition().arms.size() == 2) { - ImGui::Text("%s, a stretch where the road changes its lanes", junction.name().c_str()); - ImGui::TextDisabled("It's drawn as a part of the road, and leads the lanes of one side to the lanes of the other."); + ImGui::Text(STR_C("%s, a stretch where the road changes its lanes"), junction.name().c_str()); + ImGui::TextDisabled("%s", STR_C("A part of the road, leading the lanes of one side to the other")); } else { - ImGui::Text("%s, %d roads, %d ways through", junction.name().c_str(), static_cast(junction.definition().arms.size()), static_cast(junction.movements().size())); - ImGui::TextDisabled("The ways through are made for the roads attached at the moment."); - ImGui::TextDisabled("Build tool, LMB on the junction: lead another road out of it"); + ImGui::Text(STR_C("%s, roads: %d, ways through: %d"), junction.name().c_str(), static_cast(junction.definition().arms.size()), static_cast(junction.movements().size())); + ImGui::TextDisabled("%s", STR_C("The ways through follow the roads attached now")); + item_tooltip("Build tool, LMB on the junction: lead another road out of it"); } render_junction_layout(junction); - if (ImGui::Button("Delete (Del)")) + if (ImGui::Button(STR_C("Delete (Del)"))) road_delete(); } else if (tool.selected != nullptr) @@ -2454,28 +2464,26 @@ void editor_mode::render_road_selection() auto const &road{*tool.selected}; auto const &layout{road.definition()}; if (layout.changing()) - ImGui::Text("%s, %.1f m, %d + %d to %d + %d lanes", road.name().c_str(), layout.length(), layout.lanes_at(0, true), layout.lanes_at(0, false), layout.lanes_at(1, true), + ImGui::Text(STR_C("%s, %.1f m, lanes %d + %d to %d + %d"), road.name().c_str(), layout.length(), layout.lanes_at(0, true), layout.lanes_at(0, false), layout.lanes_at(1, true), layout.lanes_at(1, false)); else - ImGui::Text("%s, %.1f m, %d + %d lanes", road.name().c_str(), layout.length(), layout.forward, layout.backward); - if (ImGui::Button("Delete (Del)")) + ImGui::Text(STR_C("%s, %.1f m, lanes %d + %d"), road.name().c_str(), layout.length(), layout.forward, layout.backward); + if (ImGui::Button(STR_C("Delete (Del)"))) road_delete(); ImGui::SameLine(); - ImGui::TextDisabled("K: split at the cursor"); + ImGui::TextDisabled("%s", STR_C("K: split at the cursor")); if (tool.settings.changing()) - ImGui::TextDisabled("This piece has lanes of its own at its end, so it's changed alone"); + ImGui::TextDisabled("%s", STR_C("This piece has lanes of its own at its end, so it's changed alone")); else - ImGui::Checkbox("Changes go to the whole road", &tool.whole); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", "A road is a string of pieces. With this on a change of the layout is made to all of them,\nexcept for the lane change rules, which are always set for the selected piece alone.\nWith this off the selected piece alone is changed, and the road next to it is fitted to it:\nmade wider or narrower towards it, or given a stretch where the lanes change if their number differs.\nSplit a piece (K) to get a piece of the length you need."); + ImGui::Checkbox(STR_C("Changes go to the whole road"), &tool.whole); + item_tooltip("A road is a string of pieces. With this on a change of the layout is made to all of them,\nexcept for the lane change rules, which are always set for the selected piece alone.\nWith this off the selected piece alone is changed, and the road next to it is fitted to it:\nmade wider or narrower towards it, or given a stretch where the lanes change if their number differs.\nSplit a piece (K) to get a piece of the length you need"); ImGui::Separator(); if (tool.selected != nullptr && render_road_layout(tool.settings)) road_apply(); } else { - ImGui::TextDisabled("LMB: select a road piece, a junction, a point where pieces meet,"); - ImGui::TextDisabled("a level crossing or a traffic point"); + gesture_rows({{"LMB", "a road piece, a junction, a point where pieces meet, a level crossing or a traffic point"}}); } } @@ -2647,28 +2655,28 @@ void editor_mode::draw_road_overlay() const } } if (labelled) - drawlabel(State.position, Color, closed ? "crossing, closed" : "crossing"); + drawlabel(State.position, Color, closed ? STR_C("crossing, closed") : STR_C("crossing")); break; } case roadpoint_node::kind_type::spawn: { drawpoint(State.position, Color, 11.0f, false); if (labelled) - drawlabel(State.position, Color, "spawn"); + drawlabel(State.position, Color, STR_C("spawn")); break; } case roadpoint_node::kind_type::despawn: { drawring(State.position, State.radius, Color); if (labelled) - drawlabel(State.position, Color, "removal"); + drawlabel(State.position, Color, STR_C("removal")); break; } case roadpoint_node::kind_type::crosswalk: { drawpoint(State.position, Color, 11.0f, false); if (labelled) - drawlabel(State.position, Color, "pedestrians"); + drawlabel(State.position, Color, STR_C("pedestrians")); break; } } @@ -2716,7 +2724,7 @@ void editor_mode::draw_road_overlay() const auto const centre{tool.mouse + glm::dvec3{0.0, tool.offset, 0.0}}; drawring(centre, tool.ringradius + 0.5 * layout.width(), kPreviewColor); drawring(centre, std::max(0.0, tool.ringradius - 0.5 * layout.width()), kPreviewColor); - hint = "LMB: put a roundabout here"; + hint = STR_C("LMB: put a roundabout here"); } else if (tool.tool == 1) { @@ -2727,7 +2735,7 @@ void editor_mode::draw_road_overlay() const if (plan.pieces.empty()) { projection.line(drawlist, tool.point, tool.mouse, kRefusedColor, 2.0f); - hint = plan.error + " "; + hint = STR(plan.error) + " "; } else { @@ -2745,9 +2753,9 @@ void editor_mode::draw_road_overlay() const } if (plan.snap.valid()) drawpoint(plan.snap.position, kSnapColor, 14.0f, false); - hint += (tool.branchroad != nullptr ? "LMB: lead the road out to the cursor, with a junction where it leaves Esc: give up" : - tool.branchjunction != nullptr ? "LMB: lead the road out to the cursor, as another road of the junction Esc: give up" : - "LMB: build up to the cursor Ctrl: straight ahead Esc: finish Ctrl+Z: undo the last piece"); + hint += (tool.branchroad != nullptr ? STR_C("LMB: lead the road out to the cursor, with a junction where it leaves Esc: give up") : + tool.branchjunction != nullptr ? STR_C("LMB: lead the road out to the cursor, as another road of the junction Esc: give up") : + STR_C("LMB: build up to the cursor Ctrl: straight ahead Esc: finish Ctrl+Z: undo the last piece")); } else { @@ -2755,17 +2763,17 @@ void editor_mode::draw_road_overlay() const if (end.valid()) { drawpoint(end.position, kSnapColor, 14.0f, false); - hint = "LMB: carry on from this end"; + hint = STR_C("LMB: carry on from this end"); } else if (tool.hashover) { drawpoint(tool.hover, kSnapColor, 5.0f, true); drawpoint(tool.hover, kSnapColor, 14.0f, false); - hint = "LMB: lead a road out of here, with a junction"; + hint = STR_C("LMB: lead a road out of here, with a junction"); } else { - hint = "LMB: set where the road starts; on a loose end of a road to carry it on, on the side of a road or on a junction to lead a road out of it"; + hint = STR_C("LMB: set where the road starts; on a loose end of a road to carry it on, on the side of a road or on a junction to lead a road out of it"); } } } @@ -2784,13 +2792,13 @@ void editor_mode::draw_road_overlay() const projection.line(drawlist, tip, tip - direction * 1.2 + across * 0.8, kPreviewColor, 2.0f); projection.line(drawlist, tip, tip - direction * 1.2 - across * 0.8, kPreviewColor, 2.0f); } - hint = std::string{"LMB: put a "} + point_label(tool.target.kind) + " here"; + hint = format(STR_C("LMB: put a %s here"), point_label(tool.target.kind)); if (false == tool.targetnote.empty()) hint += " " + tool.targetnote; } else { - hint = (tool.targetnote.empty() ? std::string{"LMB: put a "} + point_label(point_kind(tool.placekind)) + " on a road" : tool.targetnote); + hint = (tool.targetnote.empty() ? format(STR_C("LMB: put a %s on a road"), point_label(point_kind(tool.placekind))) : tool.targetnote); } } else @@ -2808,11 +2816,11 @@ void editor_mode::draw_road_overlay() const drawpoint(point, kSelectedColor, 6.0f, true); drawpoint(point, kSelectedColor, 11.0f, false); } - hint = tool.marker != nullptr ? "LMB: select a piece, a junction, a point or a marker Del: delete Ctrl+Z: undo" : - false == tool.points.empty() ? "LMB: select a point Shift+LMB: add a point or take it out Del: delete the points Ctrl+Z: undo" : - tool.selected != nullptr ? "LMB: select a piece, a junction or a point (white dot) K: split at the cursor Del: delete Ctrl+Z: undo" : - tool.junction != nullptr ? "LMB: select a piece, a junction or a point (white dot) Del: delete Ctrl+Z: undo" : - "LMB: select a piece, a junction, a point (white dot) or a marker"; + hint = tool.marker != nullptr ? STR_C("LMB: select a piece, a junction, a point or a marker Del: delete Ctrl+Z: undo") : + false == tool.points.empty() ? STR_C("LMB: select a point Shift+LMB: add a point or take it out Del: delete the points Ctrl+Z: undo") : + tool.selected != nullptr ? STR_C("LMB: select a piece, a junction or a point (white dot) K: split at the cursor Del: delete Ctrl+Z: undo") : + tool.junction != nullptr ? STR_C("LMB: select a piece, a junction or a point (white dot) Del: delete Ctrl+Z: undo") : + STR_C("LMB: select a piece, a junction, a point (white dot) or a marker"); } // shown above the place the track tools put their hints at ImVec2 const position{editor_ui::view_min().x + 12.0f, editor_ui::view_max().y - 56.0f}; diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index 33bbc00e..a176a4b8 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -13,6 +13,7 @@ http: #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "application/editormodeshared.h" +#include "application/editorgestures.h" #include "editor/editorFormat.hpp" #include "editor/editorAlignmentStore.hpp" #include "editor/editorSwitchStore.hpp" @@ -155,7 +156,7 @@ bool editor_mode::route_from_curve(TTrack &Track) design.vertices = {vertex, vertex}; } route_update(); - m_route.status = STR_C("Curve of ") + std::to_string(m_route.chain.tracks.size()) + STR_C(" paths, turning ") + std::to_string(static_cast(std::round(glm::degrees(curve.turn)))) + STR_C(" deg"); + m_route.status = format(STR_P("Curve of %zu path, turning %d deg", "Curve of %zu paths, turning %d deg", m_route.chain.tracks.size()), m_route.chain.tracks.size(), static_cast(std::round(glm::degrees(curve.turn)))); return true; } @@ -268,23 +269,22 @@ void editor_mode::route_take_stored(int const Index) 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"); - if (ImGui::SmallButton(STR_C("Digitize all the tracks"))) + if (begin_fields("##routeorigin")) { - digitize_all(); - return; + field_row("Km of the start"); + if (ImGui::InputDouble("##routekm", &km, 0.0, 0.0, "%.3f")) + route.origin = km * 1000.0; + 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"); + ImGui::EndTable(); } - item_tooltip("Every line of the scenery not digitized yet is kept in it with its straights, arcs and transition curves, the way the drawn tracks are kept; the paths stay as they are"); 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::PushTextWrapPos(0.0f); 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); + ImGui::PopTextWrapPos(); if (ImGui::SmallButton(STR_C("Take the kept design"))) { route_take_stored(route.departed); @@ -297,7 +297,7 @@ void editor_mode::render_route_stored() alignment_store::forget(route.departed); alignment_store::settle(); route.departed = -1; - route.status = STR_C("The kept design will be removed from ") + layer + STR_C(" on save"); + route.status = format(STR_C("The kept design will be removed from %s on save"), layer.c_str()); } item_tooltip("The paths stay as they are, the scenery no longer keeps a design for them"); } @@ -330,7 +330,7 @@ bool editor_mode::digitize_drawn(std::vector const &Created, bool cons return false; } -void editor_mode::digitize_all() +void editor_mode::digitize_all(int *Kept, int *Failed) { std::set visited; int kept{0}; @@ -356,6 +356,10 @@ void editor_mode::digitize_all() first = reason; } alignment_store::settle(); + if (Kept != nullptr) + *Kept = kept; + if (Failed != nullptr) + *Failed = failed; auto text{format(STR_C("Lines digitized: %d, paths left out: %d"), kept, failed)}; if (false == first.empty()) text += STR_C(", the first: ") + first; @@ -377,7 +381,7 @@ int editor_mode::digitize_parts(editor_track::chain const &Chain, std::string *R if (count == 1) { ++Skipped; - reason = (Chain.tracks.front()->name().empty() ? std::string{"(noname)"} : Chain.tracks.front()->name()) + ": " + reason; + reason = (Chain.tracks.front()->name().empty() ? std::string{STR_C("(no name)")} : Chain.tracks.front()->name()) + ": " + reason; WriteLog("Editor: digitizing - " + reason, logtype::generic); if (Reason != nullptr && Reason->empty()) *Reason = reason; @@ -402,7 +406,7 @@ bool editor_mode::digitize_chain(editor_track::chain const &Chain, std::string * { if (Chain.tracks.empty()) return false; - auto const name = [](TTrack const *Track) { return Track == nullptr || Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; + auto const name = [](TTrack const *Track) { return Track == nullptr || Track->name().empty() ? std::string{STR_C("(no name)")} : Track->name(); }; auto const nearest = [&](glm::dvec2 const &Point) { TTrack *found{nullptr}; double distance{std::numeric_limits::max()}; @@ -564,7 +568,7 @@ bool editor_mode::route_from_line(TTrack &Track) } route_update(); } - route.status = format(STR_C("Line of %zu paths, %.0f m, %zu vertices"), route.chain.tracks.size(), route.chain.length, design.vertices.size()); + route.status = format(STR_P("Line of %zu path, %.0f m, %zu vertices", "Line of %zu paths, %.0f m, %zu vertices", route.chain.tracks.size()), route.chain.tracks.size(), route.chain.length, design.vertices.size()); return true; } @@ -587,6 +591,111 @@ void editor_mode::route_apply() involved.push_back(passage.track); if (false == tracks_editable(involved, route.error)) return; + // the crossovers of the line can't be moved as their switches are: they're taken out, the line laid again, and they go back + // where they were, onto the new line + if (false == route.relaying) + { + struct again + { + int shape; + glm::dvec3 press, release; + std::string name; + std::vector names; // of the switches at the press and at the release + }; + std::vector crossovers; + std::vector seen; + // a line which ends on a switch of a crossover would be laid again only up to it, the crossover left on a kink + for (auto const &end : {chain.start, chain.end}) + for (auto *track : chain.tracks) + for (auto const &[other, point] : editor_track::connected_points(*track, end)) + if (other != nullptr && other->eType == tt_Switch && crossover_of(*other).has_value()) + { + route.error = format(STR_C("The line ends on the switch %s of a crossover: move the crossover off the curve or take it out, change both lines, then lay it again"), other->name().c_str()); + return; + } + std::vector switches; + for (auto const &passage : chain.switches) + switches.push_back(passage.track); + for (auto *switchtrack : switches) + { + auto const crossover{crossover_of(*switchtrack)}; + if (false == crossover.has_value() || std::find(seen.begin(), seen.end(), crossover->first) != seen.end()) + continue; + seen.push_back(crossover->first); + // the press on this line, the release on the other one + auto *here{crossover->first == switchtrack ? crossover->first : crossover->second}; + auto *there{here == crossover->first ? crossover->second : crossover->first}; + crossovers.push_back({crossover_template(crossover->record), here->m_paths.front().points[segment_data::point::start], there->m_paths.front().points[segment_data::point::start], crossover->first->name(), {here->name(), there->name()}}); + } + if (false == crossovers.empty()) + { + auto const tab{m_track_tab}; + std::vector> states; + std::vector created; + std::vector removed; + for (auto *switchtrack : switches) + if (auto const crossover{crossover_of(*switchtrack)}) + { + TTrack *first{nullptr}; + TTrack *second{nullptr}; + take_out_crossover(*crossover, states, created, removed, first, second); + } + switch_store::follow(); + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + records_to_history(); + show_track_tab(tab); + // the line between the same points again: taking the lift back may have cut its paths + auto const end_at = [&](glm::dvec3 const &Point) -> TTrack * { + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && track->eType == tt_Normal && false == track->m_paths.empty()) + for (auto const which : {segment_data::point::start, segment_data::point::end}) + if (plan_distance(track->m_paths.front().points[which], Point) < 0.01) + return track; + return nullptr; + }; + auto *start{end_at(route.chain.start)}; + auto *finish{end_at(route.chain.end)}; + editor_track::chain refreshed; + std::string error; + if (start != nullptr && finish != nullptr && editor_track::find_chain(start, finish, refreshed, error, true)) + { + route.from = start; + route.to = finish; + route.chain = refreshed; + } + route.relaying = true; + auto const steps{m_history.size()}; + route_apply(); + route.relaying = false; + if (m_history.size() == steps) + { + undo_last(); + return; + } + join_last_steps(); + std::string back, left; + for (auto const &crossover : crossovers) + { + // the new line moved sideways: the press onto it, the release onto the other line + if (lay_crossover(crossover.shape, crossover.press, crossover.release, 30.0, crossover.names)) + { + join_last_steps(); + back += (back.empty() ? "" : ", ") + crossover.name; + } + else + { + left += (left.empty() ? "" : ", ") + crossover.name; + route.crossovers_out.push_back({crossover.shape, crossover.press, crossover.release, crossover.name, crossover.names}); + } + } + show_track_tab(tab); + if (false == back.empty()) + route.status += "\n" + format(STR_C("Crossovers laid again on the new line: %s"), back.c_str()); + if (false == left.empty()) + route.status += "\n" + format(STR_C("Crossovers taken out which don't fit the new line, lay them again: %s"), left.c_str()); + return; + } + } std::vector> states; std::vector created; @@ -667,9 +776,27 @@ void editor_mode::route_apply() } route_store(); records_to_history(); - 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{}); + route.status = added > 0 ? format(STR_P("Re-laid %zu path, added %zu", "Re-laid %zu paths, added %zu", chain.tracks.size()), chain.tracks.size(), added) : format(STR_P("Re-laid %zu path", "Re-laid %zu paths", chain.tracks.size()), chain.tracks.size()); if (startextension > 0.0 || endextension > 0.0) - route.status += format(", the curve took over %.2f / %.2f m of the adjoining straights", startextension, endextension); + route.status += format(STR_C(", the curve took over %.2f / %.2f m of the adjoining straights"), startextension, endextension); + // the crossovers taken out before which fit the lines now + if (false == route.relaying && false == route.crossovers_out.empty()) + { + auto const tab{m_track_tab}; + std::string back; + for (auto out{route.crossovers_out.begin()}; out != route.crossovers_out.end();) + if (lay_crossover(out->shape, out->press, out->release, 30.0, out->names)) + { + join_last_steps(); + back += (back.empty() ? "" : ", ") + out->name; + out = route.crossovers_out.erase(out); + } + else + ++out; + show_track_tab(tab); + if (false == back.empty()) + route.status += "\n" + format(STR_C("Crossovers laid again on the new line: %s"), back.c_str()); + } WriteLog("Editor: route design - " + route.status, logtype::generic); } @@ -678,7 +805,7 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, { auto &route{m_route}; auto const &result{route.result}; - auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; + auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{STR_C("(no name)")} : Track->name(); }; auto const plan = [](glm::dvec3 const &Vector) { return glm::normalize(plan_of(Vector)); }; struct placement { @@ -712,7 +839,7 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, auto const from{starts[k] + length * 0.5 < oldlength * 0.5 ? starts[k] : result.length - (oldlength - starts[k])}; if (from < 0.0 || from + length > result.length || (false == breaks.empty() && from < breaks.back() - 0.01)) { - route.error = STR_C("The switch ") + name(passage.track) + STR_C(" doesn't fit on the designed line"); + route.error = format(STR_C("The switch %s doesn't fit on the designed line"), name(passage.track).c_str()); return false; } auto const entry{main.points[passage.forward ? segment_data::point::start : segment_data::point::end]}; @@ -895,6 +1022,25 @@ void editor_mode::draw_route_overlay() const if (projection.project(alignment::evaluate(result, e.chainage).position, screen)) drawlist->AddCircleFilled(screen, 3.5f, overlay_color::highlight); } + // what the mouse points at in the band of the curvature in the panel + if (route.band.hot >= 0 && route.band.hot < static_cast(result.elements.size())) + { + auto const &e{result.elements[route.band.hot]}; + int const steps = std::clamp(static_cast(e.length / 2.0), 1, 400); + auto previous{alignment::evaluate(result, e.chainage).position}; + for (int j = 1; j <= steps; ++j) + { + auto const next{alignment::evaluate(result, e.chainage + e.length * j / steps).position}; + projection.line(drawlist, previous, next, IM_COL32(255, 255, 255, 110), 8.0f); + previous = next; + } + } + ImVec2 pointed; + if (route.band.at >= 0.0 && projection.project(alignment::evaluate(result, std::min(route.band.at, result.length)).position, pointed)) + { + drawlist->AddCircleFilled(pointed, 5.0f, overlay_color::highlight); + drawlist->AddCircle(pointed, 10.0f, overlay_color::highlight, 20, 2.0f); + } } for (auto const atend : {false, true}) @@ -997,17 +1143,21 @@ void editor_mode::render_route_ui() if (route.chain.tracks.empty()) return; - ImGui::TextDisabled(STR_C("%zu paths, %.2f m"), route.chain.tracks.size(), route.chain.length); - ImGui::SameLine(); - if (ImGui::SmallButton(STR_C("Reset design"))) + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled(STR_P("%zu path, %.2f m", "%zu paths, %.2f m", route.chain.tracks.size()), route.chain.tracks.size(), route.chain.length); + auto const reset{STR_C("Reset design")}; + auto const reset_width{ImGui::CalcTextSize(reset).x + ImGui::GetStyle().FramePadding.x * 2.0f}; + ImGui::SameLine(std::max(ImGui::GetCursorPosX(), ImGui::GetWindowContentRegionMax().x - reset_width)); + if (ImGui::Button(reset)) { auto *track{route.chain.tracks[route.chain.tracks.size() / 2]}; route_load(*track); return; } item_tooltip("Reads the curves of the line from the scenery again, dropping the changes not applied"); + auto changed{render_route_band()}; render_route_stored(); - auto changed{render_route_vertices()}; + changed |= render_route_vertices(); if (ImGui::CollapsingHeader(STR_C("Design speed and limits"))) changed |= render_route_parameters(); if (changed) @@ -1015,38 +1165,329 @@ void editor_mode::render_route_ui() ImGui::Separator(); render_route_result(); + + if (ImGui::CollapsingHeader(STR_C("Digitize the whole scenery"))) + { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", STR_C("Keeps the plan of the lines in the scenery as straights, arcs and transitions, so the tools can edit them; the paths stay as they are")); + ImGui::PopTextWrapPos(); + if (ImGui::Button(STR_C("Digitize all the tracks"))) + { + digitize_all(); + return; + } + item_tooltip("Every line of the scenery not digitized yet is kept in it with its straights, arcs and transition curves, the way the drawn tracks are kept; the paths stay as they are"); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Digitize the scenery"))) + { + digitize_scenery(true); + return; + } + item_tooltip("All at once: the lines, the switches with their types out of the catalogue (or the size measured from their shape), the crossovers as connections of switches and the signals, signs, poles and catenary bound to the tracks. The paths and the objects stay as they are, the scenery keeps it on save"); + } +} + +// the curvature along the fragment, the way the designers draw it: straights on the axis, the transitions as ramps, +// the arcs as plateaus at 1/R, in the colours of the overlay in the view. dragging a plateau changes its radius, +// dragging a ramp sideways the length of the transition, the wheel does both in steps +bool editor_mode::render_route_band() +{ + auto &route{m_route}; + auto &design{route.design}; + auto const &result{route.result}; + auto &band{route.band}; + band.hot = -1; + band.at = -1.0; + if (result.elements.empty() || result.length <= 1e-3 || design.vertices.empty()) + { + band.vertex = -1; + return false; + } + auto const font{ImGui::GetFontSize()}; + auto const pad{std::round(font * 0.5f)}; + auto const width{ImGui::GetContentRegionAvail().x}; + auto const height{std::round(font * 6.0f)}; + auto const top{ImGui::GetCursorScreenPos()}; + ImGui::InvisibleButton("##curvature", ImVec2(width, height)); + ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY); + auto const hovered{ImGui::IsItemHovered()}; + auto const active{ImGui::IsItemActive()}; + auto *list{ImGui::GetWindowDrawList()}; + ImVec2 const bottom{top.x + width, top.y + height}; + list->AddRectFilled(top, bottom, ImGui::GetColorU32(ImGuiCol_FrameBg), std::round(font * 0.35f)); + + auto const ends = [](alignment::element const &Element) -> std::pair { + auto const sign{static_cast(Element.turn)}; + auto const k{Element.radius > 0.0 ? sign / Element.radius : 0.0}; + switch (Element.kind) + { + case alignment::element_kind::transition_in: return {0.0, k}; + case alignment::element_kind::arc: return {k, k}; + case alignment::element_kind::transition_out: return {k, 0.0}; + case alignment::element_kind::spiral: return {sign * Element.curvature_start, sign * Element.curvature_end}; + default: return {0.0, 0.0}; + } + }; + auto const colour = [](alignment::element const &Element) { + return Element.kind == alignment::element_kind::straight ? IM_COL32(255, 255, 255, 230) : Element.kind == alignment::element_kind::arc ? overlay_color::grip : IM_COL32(255, 140, 30, 255); + }; + double peak{0.0}; + bool left{false}, right{false}; + for (auto const &element : result.elements) + { + auto const [from, to]{ends(element)}; + peak = std::max({peak, std::abs(from), std::abs(to)}); + left |= from > 1e-9 || to > 1e-9; + right |= from < -1e-9 || to < -1e-9; + } + auto const line{ImGui::GetTextLineHeight()}; + auto const inner_left{top.x + pad}; + auto const inner_width{width - 2.0f * pad}; + auto const plot_top{top.y + pad * 0.5f + line}; + auto const plot_bottom{bottom.y - pad * 0.5f - line}; + auto const both{left && right}; + auto const axis{both ? std::round((plot_top + plot_bottom) * 0.5f) : plot_bottom}; + // the curves to one side only grow upwards + auto const flip{(false == both && right) ? -1.0f : 1.0f}; + auto const dragging{band.vertex >= 0}; + auto const metres{dragging ? band.metres : inner_width / static_cast(result.length)}; + auto const curvature{dragging ? band.curvature : (peak > 0.0 ? (axis - plot_top) * 0.85f / static_cast(peak) : 0.0f)}; + auto const x_of = [&](double const Chainage) { return inner_left + static_cast(Chainage) * metres; }; + auto const y_of = [&](double const K) { return axis - flip * static_cast(K) * curvature; }; + auto const mouse{ImGui::GetIO().MousePos}; + auto const count{static_cast(result.elements.size())}; + + // the element under the mouse and what of it a drag changes: near the ends of a plateau its transitions, also the ones of no length yet + int under{-1}; + int part{0}; + if (hovered && false == dragging) + { + for (int i = 0; i < count; ++i) + { + auto const &element{result.elements[i]}; + if (mouse.x < x_of(element.chainage) || mouse.x >= x_of(element.chainage + element.length)) + continue; + under = i; + auto const from{x_of(element.chainage)}; + auto const to{x_of(element.chainage + element.length)}; + auto const edge{std::min(std::round(font * 0.5f), (to - from) * 0.25f)}; + switch (element.kind) + { + case alignment::element_kind::arc: part = mouse.x < from + edge ? 2 : mouse.x >= to - edge ? 3 : 1; break; + case alignment::element_kind::transition_in: part = 2; break; + case alignment::element_kind::transition_out: part = 3; break; + default: break; + } + if (element.vertex < 0 || element.vertex >= static_cast(design.vertices.size())) + part = 0; + break; + } + } + auto const value_of = [&](int const Vertex, int const Part) -> double & { + auto &vertex{design.vertices[Vertex]}; + return Part == 1 ? vertex.radius : Part == 2 ? vertex.transition_in : vertex.transition_out; + }; + auto const rounded_radius = [](double const Radius) { + auto const step{Radius < 1000.0 ? 1.0 : 10.0}; + return std::clamp(std::round(Radius / step) * step, 10.0, 100000.0); + }; + bool changed{false}; + if (part != 0 && ImGui::IsItemActivated()) + { + band.vertex = result.elements[under].vertex; + band.part = part; + band.value = value_of(band.vertex, part); + band.press = mouse; + band.metres = metres; + band.curvature = curvature; + } + if (band.vertex >= 0 && band.vertex < static_cast(design.vertices.size()) && active) + { + auto &value{value_of(band.vertex, band.part)}; + double wanted{value}; + if (band.part == 1) + { + int turn{1}; + for (auto const &element : result.elements) + if (element.vertex == band.vertex && element.turn != 0) + turn = element.turn; + auto const start{y_of(turn / std::max(1.0, band.value)) + (mouse.y - band.press.y)}; + auto const k{(axis - start) * flip * turn / std::max(1e-6f, curvature)}; + wanted = rounded_radius(1.0 / std::max(1e-5, static_cast(k))); + } + else + { + auto const shift{(mouse.x - band.press.x) / std::max(1e-6f, metres)}; + wanted = std::max(0.0, std::round(band.value + (band.part == 2 ? -shift : shift))); + } + if (wanted != value) + { + value = wanted; + changed = true; + } + ImGui::SetMouseCursor(band.part == 1 ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW); + } + else if (dragging && false == active) + { + band.vertex = -1; + } + if (part != 0) + { + ImGui::SetMouseCursor(part == 1 ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW); + if (auto const wheel{ImGui::GetIO().MouseWheel}; wheel != 0.0f) + { + auto &value{value_of(result.elements[under].vertex, part)}; + value = part == 1 ? rounded_radius(value * (wheel > 0.0f ? 0.95 : 1.0 / 0.95)) : std::max(0.0, std::round(value / 5.0) * 5.0 + (wheel > 0.0f ? 5.0 : -5.0)); + changed = true; + } + } + + ImGui::PushClipRect(top, bottom, true); + auto const disabled{ImGui::GetColorU32(ImGuiCol_TextDisabled)}; + list->AddLine(ImVec2(inner_left, axis), ImVec2(inner_left + inner_width, axis), ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.5f)); + auto const hot_vertex{dragging ? band.vertex : under >= 0 ? result.elements[under].vertex : -1}; + for (int i = 0; i < count; ++i) + { + auto const &element{result.elements[i]}; + auto const [from, to]{ends(element)}; + ImVec2 const a{x_of(element.chainage), y_of(from)}; + ImVec2 const b{x_of(element.chainage + element.length), y_of(to)}; + auto const lit{element.vertex >= 0 && element.vertex == hot_vertex}; + auto const tint{colour(element)}; + if (element.kind != alignment::element_kind::straight) + list->AddQuadFilled(ImVec2(a.x, axis), a, b, ImVec2(b.x, axis), (tint & ~IM_COL32_A_MASK) | (lit ? IM_COL32(0, 0, 0, 90) : IM_COL32(0, 0, 0, 40))); + list->AddLine(a, b, tint, lit ? 3.0f : 2.0f); + if (i > 0) + list->AddLine(ImVec2(a.x, axis - 3.0f), ImVec2(a.x, axis + 3.0f), disabled); + } + // the labels: the radius over its plateau, the transition lengths along the axis, the km of the ends at the bottom + std::vector> taken; + auto const place = [&](float const Left, float const Right) { + for (auto const &range : taken) + if (Left < range.second + 4.0f && Right > range.first - 4.0f) + return false; + taken.emplace_back(Left, Right); + return true; + }; + auto const start_km{format("km %s", profile::format_chainage(route.origin).c_str())}; + auto const end_km{format("km %s", profile::format_chainage(route.origin + result.length).c_str())}; + auto const label_y{bottom.y - pad * 0.25f - line}; + auto const start_size{ImGui::CalcTextSize(start_km.c_str())}; + auto const end_size{ImGui::CalcTextSize(end_km.c_str())}; + place(inner_left, inner_left + start_size.x); + list->AddText(ImVec2(inner_left, label_y), disabled, start_km.c_str()); + if (place(inner_left + inner_width - end_size.x, inner_left + inner_width)) + list->AddText(ImVec2(inner_left + inner_width - end_size.x, label_y), disabled, end_km.c_str()); + for (auto const &element : result.elements) + { + auto const from{x_of(element.chainage)}; + auto const to{x_of(element.chainage + element.length)}; + auto const middle{(from + to) * 0.5f}; + if (dragging && element.vertex == band.vertex) + continue; + if (element.kind == alignment::element_kind::arc || (element.kind == alignment::element_kind::spiral && element.curvature_start == element.curvature_end && element.radius > 0.0)) + { + auto const &vertex{design.vertices[std::clamp(element.vertex, 0, static_cast(design.vertices.size()) - 1)]}; + auto text{format("R %.0f", element.radius)}; + if (vertex.cant > 0.0) + { + auto const longer{text + format(" \xc2\xb7 %.0f mm", vertex.cant)}; + if (ImGui::CalcTextSize(longer.c_str()).x <= to - from) + text = longer; + } + auto const size{ImGui::CalcTextSize(text.c_str())}; + auto const y{ends(element).first * flip >= 0.0 ? y_of(ends(element).first) - size.y - 1.0f : y_of(ends(element).first) + 1.0f}; + if (size.x <= std::max(to - from, size.x * 0.8f)) + list->AddText(ImVec2(std::round(middle - size.x * 0.5f), std::round(std::max(top.y + 1.0f, y))), colour(element), text.c_str()); + } + else if (element.kind == alignment::element_kind::transition_in || element.kind == alignment::element_kind::transition_out) + { + auto const text{format("%.0f m", element.length)}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + if (both) + { + auto const y{ends(element).first + ends(element).second > 0.0 ? axis + 2.0f : axis - size.y - 2.0f}; + if (size.x <= to - from + 8.0f) + list->AddText(ImVec2(std::round(middle - size.x * 0.5f), std::round(y)), colour(element), text.c_str()); + } + else if (place(middle - size.x * 0.5f, middle + size.x * 0.5f)) + list->AddText(ImVec2(std::round(middle - size.x * 0.5f), label_y), colour(element), text.c_str()); + } + } + // the point under the mouse, shown in the view too + if (hovered || dragging) + { + auto const at{std::clamp(static_cast((mouse.x - inner_left) / std::max(1e-6f, metres)), 0.0, result.length)}; + band.at = at; + band.hot = dragging ? -1 : under; + if (false == dragging) + list->AddLine(ImVec2(mouse.x, plot_top), ImVec2(mouse.x, plot_bottom), IM_COL32(255, 255, 255, 60)); + } + if (dragging && band.vertex < static_cast(design.vertices.size())) + { + auto const &vertex{design.vertices[band.vertex]}; + auto const text{band.part == 1 ? format("R %.0f m", vertex.radius) : format("%.0f m", band.part == 2 ? vertex.transition_in : vertex.transition_out)}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + ImVec2 const at{std::clamp(mouse.x + pad, top.x, bottom.x - size.x - pad), std::clamp(mouse.y - size.y - pad, top.y, bottom.y - size.y)}; + list->AddRectFilled(ImVec2(at.x - 3.0f, at.y - 1.0f), ImVec2(at.x + size.x + 3.0f, at.y + size.y + 1.0f), IM_COL32(0, 0, 0, 170), 3.0f); + list->AddText(at, IM_COL32(255, 255, 255, 255), text.c_str()); + } + ImGui::PopClipRect(); + if (changed) + first_step_done(first_step::plan_band); + + if (part != 0 && false == dragging && ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + { + auto const &vertex{design.vertices[result.elements[under].vertex]}; + ImGui::BeginTooltip(); + if (part == 1) + ImGui::Text(STR_C("Curve %d: R %.0f m, cant %.0f mm"), result.elements[under].vertex + 1, vertex.radius, vertex.cant); + else + ImGui::Text(part == 2 ? STR_C("Transition into curve %d: %.0f m") : STR_C("Transition out of curve %d: %.0f m"), result.elements[under].vertex + 1, part == 2 ? vertex.transition_in : vertex.transition_out); + gesture_rows({{"Drag", part == 1 ? "up or down: the radius" : "sideways: the length"}, {"Wheel", part == 1 ? "the radius by 5%" : "the length by 5 m"}}); + ImGui::EndTooltip(); + } + return changed; } bool editor_mode::render_route_parameters() { auto &design{m_route.design}; bool changed{false}; - ImGui::PushItemWidth(120.0f); - changed |= ImGui::InputDouble(STR_C("Design speed (km/h)"), &design.speed, 0.0, 0.0, "%.0f"); + ImGui::TextDisabled("%s", STR_C("Design speed (km/h)")); + ImGui::SetNextItemWidth(-FLT_MIN); + changed |= ImGui::InputDouble("##designspeed", &design.speed, 10.0, 20.0, "%.0f"); item_tooltip("Speed the cant, the transitions and the recommended radius are designed for"); + changed |= value_chips("designspeeds", design.speed, {60.0, 80.0, 100.0, 120.0, 160.0, 200.0}, "%.0f"); design.speed = std::max(1.0, design.speed); + ImGui::TextDisabled("%s", STR_C("Transition curve")); int shape = static_cast(design.shape); - char const *shapes[] = {STR_C("Cubic parabola"), STR_C("Clothoid")}; - if (ImGui::Combo(STR_C("Transition curve"), &shape, shapes, IM_ARRAYSIZE(shapes))) + if (segmented("transitionshape", shape, + {{static_cast(alignment::transition_shape::cubic_parabola), STR_C("Cubic parabola"), "The usual one on the Polish lines"}, + {static_cast(alignment::transition_shape::clothoid), STR_C("Clothoid"), "Curvature growing linearly with the length"}})) { design.shape = static_cast(shape); changed = true; } - item_tooltip("Cubic parabola: the usual one on the Polish lines; clothoid: curvature growing linearly with the length"); - ImGui::PopItemWidth(); - if (ImGui::TreeNode(STR_C("Limits"))) + if (ImGui::TreeNode(STR_C("Norms"))) { auto &norms{design.norms}; - ImGui::PushItemWidth(100.0f); - changed |= ImGui::InputDouble(STR_C("Rail axes distance (mm)"), &norms.gauge, 0.0, 0.0, "%.0f"); - changed |= ImGui::InputDouble(STR_C("Unbalanced acceleration (m/s2)"), &norms.unbalanced, 0.0, 0.0, "%.2f"); - changed |= ImGui::InputDouble(STR_C("Maximum cant (mm)"), &norms.cant_max, 0.0, 0.0, "%.0f"); - changed |= ImGui::InputDouble(STR_C("Cant change rate (mm/s)"), &norms.cant_rate, 0.0, 0.0, "%.0f"); - changed |= ImGui::InputDouble(STR_C("Cant ramp 1:(k*V), k"), &norms.ramp_factor, 0.0, 0.0, "%.1f"); - changed |= ImGui::InputDouble(STR_C("Unbalanced acc. change rate (m/s3)"), &norms.jerk, 0.0, 0.0, "%.2f"); - changed |= ImGui::InputDouble(STR_C("Straight between curves (s of travel)"), &norms.tangent_min_time, 0.0, 0.0, "%.1f"); - ImGui::PopItemWidth(); + if (begin_fields("##routelimits", 0.6f)) + { + auto const limit = [&](char const *Label, char const *Id, double &Value, char const *Format) { + field_row(Label); + changed |= ImGui::InputDouble(Id, &Value, 0.0, 0.0, Format); + }; + limit("Rail axes distance (mm)", "##gauge", norms.gauge, "%.0f"); + limit("Unbalanced acceleration (m/s2)", "##unbalanced", norms.unbalanced, "%.2f"); + limit("Maximum cant (mm)", "##cantmax", norms.cant_max, "%.0f"); + limit("Cant change rate (mm/s)", "##cantrate", norms.cant_rate, "%.0f"); + limit("Cant ramp 1:(k*V), k", "##ramp", norms.ramp_factor, "%.1f"); + limit("Unbalanced acc. change rate (m/s3)", "##jerk", norms.jerk, "%.2f"); + limit("Straight between curves (s of travel)", "##tangent", norms.tangent_min_time, "%.1f"); + ImGui::EndTable(); + } norms.gauge = std::max(100.0, norms.gauge); norms.cant_rate = std::max(1.0, norms.cant_rate); norms.jerk = std::max(0.01, norms.jerk); @@ -1076,9 +1517,26 @@ bool editor_mode::render_route_vertex(int const Index) auto const count{static_cast(design.vertices.size())}; bool changed{false}; if (count > 1) - ImGui::TextDisabled(STR_C("Curve %d of the S-curve"), Index + 1); + ImGui::SeparatorText(format(STR_C("Curve %d of the S-curve"), Index + 1).c_str()); + // two fields in a row, the name and the unit inside them, a bar in the colour of the element in the band and in the view + auto const half{std::floor((ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f)}; + auto const field = [&](char const *Id, double &Value, float const Speed, char const *Format, ImU32 const Colour, char const *Tooltip) { + ImGui::SetNextItemWidth(half); + auto const edited{ImGui::DragScalar(Id, ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format)}; + auto const low{ImGui::GetItemRectMin()}; + auto const high{ImGui::GetItemRectMax()}; + ImGui::GetWindowDrawList()->AddRectFilled(low, ImVec2(low.x + 3.0f, high.y), Colour, 2.0f, ImDrawFlags_RoundCornersLeft); + item_tooltip(Tooltip); + return edited; + }; + ImU32 const transition{IM_COL32(255, 140, 30, 255)}; + changed |= field("##radius", vertex.radius, 5.0f, vertex.compound ? STR_C("R1 %.0f m") : STR_C("R %.0f m"), overlay_color::grip, "Radius of the arc: drag sideways, or a double click to type it"); + ImGui::SameLine(); + changed |= field("##cant", vertex.cant, 1.0f, STR_C("cant %.0f mm"), IM_COL32(240, 170, 60, 255), "Height of the outer rail above the inner one"); + changed |= field("##entry", vertex.transition_in, 1.0f, STR_C("entry %.0f m"), transition, "Length of the transition curve leading into the arc"); + ImGui::SameLine(); + changed |= field("##exit", vertex.transition_out, 1.0f, STR_C("exit %.0f m"), transition, "Length of the transition curve leading out of the arc"); ImGui::PushItemWidth(110.0f); - changed |= ImGui::DragScalar(vertex.compound ? STR_C("Radius R1 (m)") : STR_C("Radius R (m)"), ImGuiDataType_Double, &vertex.radius, 5.0f, nullptr, nullptr, "%.0f"); if (ImGui::Checkbox(STR_C("Compound curve (several radii)"), &vertex.compound)) { changed = true; @@ -1132,10 +1590,6 @@ bool editor_mode::render_route_vertex(int const Index) item_tooltip("Another arc after the last one, the angle of the curve is shared out by the shares"); ImGui::Separator(); } - changed |= ImGui::DragScalar(STR_C("Transition in (m)"), ImGuiDataType_Double, &vertex.transition_in, 1.0f, nullptr, nullptr, "%.0f"); - changed |= ImGui::DragScalar(STR_C("Transition out (m)"), ImGuiDataType_Double, &vertex.transition_out, 1.0f, nullptr, nullptr, "%.0f"); - changed |= ImGui::DragScalar(STR_C("Cant (mm)"), ImGuiDataType_Double, &vertex.cant, 1.0f, nullptr, nullptr, "%.0f"); - item_tooltip("Height of the outer rail above the inner one"); ImGui::PopItemWidth(); vertex.radius = std::max(1.0, vertex.radius); vertex.transition_in = std::max(0.0, vertex.transition_in); @@ -1148,9 +1602,12 @@ bool editor_mode::render_route_vertex(int const Index) route_recommend(vertex); changed = true; } - item_tooltip("Cant and transitions recommended for the design speed and the radius"); - auto const recommended{alignment::recommend(design.speed, vertex.radius, design.norms)}; - ImGui::TextDisabled(STR_C("For %.0f km/h: R min %.0f m, cant %.0f mm (equilibrium %.0f, min %.0f), transition %.0f m"), design.speed, recommended.radius_min, recommended.cant, recommended.cant_equilibrium, recommended.cant_min, recommended.transition); + if (ImGui::IsItemHovered()) + { + auto const recommended{alignment::recommend(design.speed, vertex.radius, design.norms)}; + ImGui::SetTooltip("%s\n%s", STR_C("Cant and transitions recommended for the design speed and the radius"), + format(STR_C("For %.0f km/h: R min %.0f m, cant %.0f mm (equilibrium %.0f, min %.0f), transition %.0f m"), design.speed, recommended.radius_min, recommended.cant, recommended.cant_equilibrium, recommended.cant_min, recommended.transition).c_str()); + } } if (ImGui::TreeNode(STR_C("Details"))) { @@ -1173,25 +1630,28 @@ void editor_mode::render_route_result() auto const &route{m_route}; auto const &design{route.design}; auto const &result{route.result}; - if (result.length > 0.0) - { - auto const pieces{alignment::minimum_pieces(result, design)}; - ImGui::Text(STR_C("Length %.2f m (now %.2f m), %zu elements"), result.length, route.chain.length, result.elements.size()); - ImGui::TextDisabled(STR_C("Paths: %zu in the fragment, %zu needed%s"), route.chain.tracks.size(), pieces, pieces > route.chain.tracks.size() ? STR_C(" (copies of the neighbours will be added)") : ""); - if (result.start_extension > 0.0 || result.end_extension > 0.0) - ImGui::Text(STR_C("Takes over %.2f m of the straight at the start, %.2f m at the end"), result.start_extension, result.end_extension); - } + if (result.length > 0.0 && (result.start_extension > 0.0 || result.end_extension > 0.0)) + ImGui::Text(STR_C("Takes over %.2f m of the straight at the start, %.2f m at the end"), result.start_extension, result.end_extension); for (auto const &error : result.errors) ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", error.c_str()); for (auto const &warning : result.warnings) ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", warning.c_str()); + ImGui::PushID("routeresult"); + if (result.length > 0.0 && ImGui::TreeNode(STR_C("Details"))) + { + auto const pieces{alignment::minimum_pieces(result, design)}; + ImGui::Text(STR_P("Length %.2f m (now %.2f m), %zu element", "Length %.2f m (now %.2f m), %zu elements", result.elements.size()), result.length, route.chain.length, result.elements.size()); + ImGui::TextDisabled(STR_C("Paths: %zu in the fragment, %zu needed%s"), route.chain.tracks.size(), pieces, pieces > route.chain.tracks.size() ? STR_C(" (copies of the neighbours will be added)") : ""); + ImGui::TreePop(); + } + ImGui::PopID(); 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", "spiral"}; + static char const *const kinds[] = {"straight", "transition", "arc", "transition", "transition between arcs"}; for (auto const &e : result.elements) - 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::Text("km %s %-10s L %8.2f m%s", profile::format_chainage(m_route.origin + e.chainage).c_str(), STR_C(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(); } @@ -1200,7 +1660,10 @@ void editor_mode::render_route_result() ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); if (ImGui::Button(STR_C("Apply to the scenery"), ImVec2(-1.0f, 28.0f))) + { route_apply(); + first_step_done(first_step::plan_apply); + } ImGui::PopStyleColor(2); } else diff --git a/application/editormodeselection.cpp b/application/editormodeselection.cpp index a8c84533..732bd721 100644 --- a/application/editormodeselection.cpp +++ b/application/editormodeselection.cpp @@ -269,7 +269,9 @@ void editor_mode::group_selection() std::string reason; if (node->from_template() || false == scene::Layers.editable(node, &reason)) { - ui()->set_status("\"" + (node->name().empty() ? std::string{"(unnamed node)"} : node->name()) + "\" can't be grouped: " + reason, true); + if (reason.empty()) + reason = STR("it's a part of a scenery template"); + ui()->set_status(format(STR_C("\"%s\" can't be grouped: %s"), node->name().empty() ? STR_C("(no name)") : node->name().c_str(), reason.c_str()), true); return; } } @@ -332,7 +334,7 @@ void editor_mode::render_context_menu() if (count > 1) ImGui::TextDisabled(STR_C("%zu models selected"), count); else if (selected) - ImGui::TextDisabled("%s", m_node->name().empty() ? "(unnamed node)" : m_node->name().c_str()); + ImGui::TextDisabled("%s", m_node->name().empty() ? STR_C("(no name)") : m_node->name().c_str()); else ImGui::TextDisabled("%s", STR_C("Nothing selected")); ImGui::Separator(); @@ -347,11 +349,11 @@ void editor_mode::render_context_menu() if (item(STR_C("Paste here"), "Ctrl+V", false == m_clipboard.empty(), STR_C("Puts the copied models here, around this point as they were around the selected one"))) paste_clipboard(m_contextpoint); ImGui::Separator(); - if (item(STR_C("Group"), "Ctrl+G", model && count > 1, STR_C("The selected models move together from now on; a click on one selects them all"))) + if (item(STR_C("Group together"), "Ctrl+G", model && count > 1, STR_C("The selected models move together from now on; a click on one selects them all"))) group_selection(); if (item(STR_C("Ungroup"), "Ctrl+Shift+G", selected && m_node->group() > 1, STR_C("The models of the group move each by itself again"))) ungroup_selection(); - if (item(STR_C("Drop to the ground"), STR_C("End"), selected, STR_C("Puts each selected model, also each model of their groups, down onto the terrain or the object under it"))) + if (item(STR_C("Drop to the ground"), "End", selected, STR_C("Puts each selected model, also each model of their groups, down onto the terrain or the object under it"))) drop_selection_to_ground(); ImGui::Separator(); if (item(STR_C("Fly to the selected"), "F", selected, STR_C("The camera flies to the selected model and looks at it"))) diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index c8e8124b..2a7909bd 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -51,7 +51,7 @@ http://mozilla.org/MPL/2.0/. editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &command, std::istringstream &words) { - for (auto const group : {&editor_mode::selftest_scene, &editor_mode::selftest_sleepers, &editor_mode::selftest_sweep, &editor_mode::selftest_track, &editor_mode::selftest_switch, &editor_mode::selftest_rails, &editor_mode::selftest_join, &editor_mode::selftest_signal, &editor_mode::selftest_terrain, &editor_mode::selftest_props}) + for (auto const group : {&editor_mode::selftest_scene, &editor_mode::selftest_sleepers, &editor_mode::selftest_sweep, &editor_mode::selftest_track, &editor_mode::selftest_switch, &editor_mode::selftest_rails, &editor_mode::selftest_join, &editor_mode::selftest_signal, &editor_mode::selftest_terrain, &editor_mode::selftest_props, &editor_mode::selftest_monkey}) if (auto const result{(this->*group)(command, words)}; result != selftest_result::unhandled) return result; return selftest_result::unhandled; diff --git a/application/editormodeselftestjoin.cpp b/application/editormodeselftestjoin.cpp index 71564abf..0a10834a 100644 --- a/application/editormodeselftestjoin.cpp +++ b/application/editormodeselftestjoin.cpp @@ -250,8 +250,9 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma if (false == points.empty() && std::none_of(points.begin(), points.end(), [&](glm::dvec3 const &Point) { return geometry::plan_distance(Point, issue.position) < 5.0; })) continue; ++around; - WriteLog(format("SELFTEST joints: %s %.3f at %.1f %.1f, %s / %s", kinds[static_cast(issue.type)], issue.value, issue.position.x, issue.position.z, issue.track != nullptr ? issue.track->name().c_str() : "-", - issue.other != nullptr ? issue.other->name().c_str() : "-")); + auto const name = [](TTrack const *Track) { return Track == nullptr ? std::string{"-"} : Track->name() + (Track->eType == tt_Switch ? "(switch)" : ""); }; + auto const rise{issue.other != nullptr ? editor_track::point_position(*issue.other, issue.theirs).y - issue.position.y : 0.0}; + WriteLog(format("SELFTEST joints: %s %.3f at %.1f %.1f, %s / %s, rise %.3f", kinds[static_cast(issue.type)], issue.value, issue.position.x, issue.position.z, name(issue.track).c_str(), name(issue.other).c_str(), rise)); } WriteLog(format("SELFTEST joints: %d issues around the points, %zu in all", around, m_joints.issues.size())); return selftest_result::handled; diff --git a/application/editormodeselftestmonkey.cpp b/application/editormodeselftestmonkey.cpp new file mode 100644 index 00000000..c949f68e --- /dev/null +++ b/application/editormodeselftestmonkey.cpp @@ -0,0 +1,352 @@ +/* +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/. +*/ + +// monkey tests: random gestures over the track tools, and the checks of the tracks they're run against + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "imgui/imgui_internal.h" + +#include +#include +#include +#include +#include +#include + +// monkey random gestures of the mouse and the keys over the 3d view, the tools, their options, +// the inspector and the profile, written into the script after this line; the tracks +// are checked every tenth step +// monkeyview the camera over a track picked at random, looking at it from 30..120 m +// monkeyat the mouse on a track seen in the view, at an end, the middle or a point between +// invariants logs the tracks whose points, rolls or length aren't finite, or that have no path +// undoall undoes every change in the history +// trackhash logs the number of the tracks and a hash of their names and points to the millimetre +// tutorial opens the tutorial at the lesson, from 0; -1 closes it +// shortcuts [filter] opens the list of the shortcuts +editor_mode::selftest_result editor_mode::selftest_monkey(std::string const &command, std::istringstream &words) +{ + if (command == "monkey") + { + unsigned int seed{1}; + int steps{100}; + words >> seed >> steps; + std::mt19937 random{seed}; + auto const pick = [&](int const Count) { return std::uniform_int_distribution(0, Count - 1)(random); }; + auto const within = [&](float const Low, float const High) { return std::uniform_real_distribution(Low, std::max(Low + 1.0f, High))(random); }; + struct area + { + ImVec2 low, high; + }; + std::vector panels; + for (auto const *name : {"##editortoolbar", "###tooloptions", "###inspector", "###profilestrip"}) + if (auto const *window{ImGui::FindWindowByName(name)}; window != nullptr && (window->Active || window->WasActive)) + panels.push_back({window->Pos, ImVec2(window->Pos.x + window->Size.x, window->Pos.y + window->Size.y)}); + auto const view_low{editor_ui::view_min()}; + auto const view_high{editor_ui::view_max()}; + auto const in_view = [&]() { return ImVec2(within(view_low.x + 8.0f, view_high.x - 8.0f), within(view_low.y + 8.0f, view_high.y - 8.0f)); }; + auto const somewhere = [&]() { + if (panels.empty() || pick(3) == 0) + return in_view(); + auto const &panel{panels[pick(static_cast(panels.size()))]}; + return ImVec2(within(panel.low.x + 4.0f, panel.high.x - 4.0f), within(panel.low.y + 4.0f, panel.high.y - 4.0f)); + }; + std::vector tracks; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && false == track->name().empty()) + tracks.push_back(track); + std::vector lines; + auto const mouse = [&](ImVec2 const At) { + if (At.x >= view_low.x && At.x <= view_high.x && At.y >= view_low.y && At.y <= view_high.y && pick(2) == 0) + lines.push_back(format("monkeyat %u", static_cast(random()))); + else + lines.push_back(format("mouse %.0f %.0f", At.x, At.y)); + }; + auto const click = [&](char const *Button) { + lines.push_back(std::string{"button "} + Button + " press"); + lines.emplace_back("wait 1"); + lines.push_back(std::string{"button "} + Button + " release"); + lines.emplace_back("wait 2"); + }; + char const *const tools[]{"esc", "l", "g", "c", "t", "n", "u", "h", "b", "p", "v", "j", "k", "o", "q", "w", "e"}; + char const *const modifiers[]{"", "ctrl", "shift", "alt"}; + for (int step = 0; step < steps; ++step) + { + lines.push_back(format("log monkey %u step %d", seed, step)); + switch (pick(12)) + { + case 0: + case 1: + { + // a click, now and then with a modifier held + auto const modifier{modifiers[pick(6) < 4 ? 0 : 1 + pick(3)]}; + mouse(somewhere()); + lines.emplace_back("wait 1"); + if (*modifier != '\0') + lines.push_back(std::string{"press "} + modifier); + click("left"); + if (*modifier != '\0') + lines.push_back(std::string{"release "} + modifier); + break; + } + case 2: + mouse(somewhere()); + lines.emplace_back("wait 1"); + click("left"); + click("left"); + break; + case 3: + { + // a drag, in the view or on a panel; now and then with Shift held, a box of tracks + auto const from{pick(3) == 0 ? somewhere() : in_view()}; + auto const boxed{pick(4) == 0}; + mouse(from); + lines.emplace_back("wait 1"); + if (boxed) + lines.emplace_back("press shift"); + lines.emplace_back("button left press"); + lines.emplace_back("wait 2"); + auto const to{ImVec2(from.x + within(-200.0f, 200.0f), from.y + within(-150.0f, 150.0f))}; + for (int part = 1; part <= 4; ++part) + { + mouse(ImVec2(from.x + (to.x - from.x) * part / 4.0f, from.y + (to.y - from.y) * part / 4.0f)); + lines.emplace_back("wait 1"); + } + lines.emplace_back("button left release"); + if (boxed) + lines.emplace_back("release shift"); + lines.emplace_back("wait 2"); + break; + } + case 4: + mouse(in_view()); + lines.emplace_back("wait 1"); + click("right"); + if (pick(2) == 0) + lines.emplace_back("tap esc"); + break; + case 5: + { + auto const modifier{modifiers[pick(4)]}; + mouse(pick(4) == 0 ? somewhere() : in_view()); + lines.emplace_back("wait 1"); + if (*modifier != '\0') + lines.push_back(std::string{"press "} + modifier); + lines.push_back(format("wheel %d", pick(2) == 0 ? 1 : -1)); + lines.emplace_back("wait 1"); + if (*modifier != '\0') + lines.push_back(std::string{"release "} + modifier); + break; + } + case 6: + lines.push_back(std::string{"tap "} + tools[pick(static_cast(std::size(tools)))]); + lines.emplace_back("wait 2"); + break; + case 7: + { + char const *const keys[]{"enter", "backspace", "del", "esc", "tab", "space"}; + lines.push_back(std::string{"tap "} + keys[pick(static_cast(std::size(keys)))]); + lines.emplace_back("wait 2"); + break; + } + case 8: + lines.emplace_back(pick(3) == 0 ? "redo" : "undo"); + lines.emplace_back("wait 2"); + break; + case 9: + if (false == tracks.empty()) + lines.push_back("select " + tracks[pick(static_cast(tracks.size()))]->name()); + lines.emplace_back("wait 1"); + // now and then the plan of its line, with the band of the curvature in the panel, or the grade line of the + // whole line + if (auto const tool{pick(4)}; tool < 2) + { + lines.emplace_back(tool == 0 ? "tracktab G" : "tracktab P"); + lines.emplace_back("wait 2"); + } + break; + case 10: + lines.push_back(format("type %d", pick(500) - 50)); + lines.emplace_back("wait 1"); + break; + default: + // a gesture held a while: the mouse moved with the button down over a few frames, keys tapped meanwhile + mouse(in_view()); + lines.emplace_back("wait 1"); + lines.emplace_back("button left press"); + lines.emplace_back("wait 3"); + mouse(in_view()); + lines.emplace_back("wait 1"); + lines.push_back(std::string{"press "} + (pick(2) == 0 ? "ctrl" : "shift")); + lines.push_back(format("wheel %d", pick(2) == 0 ? 1 : -1)); + lines.emplace_back("wait 1"); + lines.emplace_back("release ctrl"); + lines.emplace_back("release shift"); + mouse(in_view()); + lines.emplace_back("wait 1"); + lines.emplace_back("button left release"); + lines.emplace_back("wait 2"); + break; + } + if (step % 10 == 9) + lines.emplace_back("invariants"); + if (step % 25 == 0) + lines.push_back(format("monkeyview %u", static_cast(random()))); + } + lines.emplace_back("tap esc"); + lines.emplace_back("wait 2"); + lines.emplace_back("invariants"); + WriteLog(format("SELFTEST monkey %u: %d steps, %zu lines, %zu panels", seed, steps, lines.size(), panels.size())); + m_selftest.lines.insert(m_selftest.lines.begin() + static_cast(m_selftest.next), lines.begin(), lines.end()); + return selftest_result::handled; + } + if (command == "monkeyview" || command == "monkeyat") + { + unsigned int seed{1}; + words >> seed; + std::mt19937 random{seed}; + std::vector tracks; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && false == track->m_paths.empty()) + tracks.push_back(track); + if (tracks.empty()) + return selftest_result::handled; + auto const share = [&]() { + double const shares[]{0.0, 0.5, 1.0, 0.25, 0.75, 0.1, 0.9}; + return shares[std::uniform_int_distribution(0, 6)(random)]; + }; + auto const along = [&](TTrack const &Track, double const Share) { + auto const &path{Track.m_paths.front()}; + auto const &a{path.points[segment_data::point::start]}; + auto const &b{path.points[segment_data::point::end]}; + auto const c1{a + path.points[segment_data::point::control1]}; + auto const c2{b + path.points[segment_data::point::control2]}; + auto const u{1.0 - Share}; + return u * u * u * a + 3.0 * u * u * Share * c1 + 3.0 * u * Share * Share * c2 + Share * Share * Share * b; + }; + if (command == "monkeyview") + { + auto const &track{*tracks[std::uniform_int_distribution(0, tracks.size() - 1)(random)]}; + auto const target{along(track, 0.5)}; + auto const distance{std::uniform_real_distribution(30.0, 120.0)(random)}; + auto const angle{std::uniform_real_distribution(0.0, 6.2831853)(random)}; + glm::dvec3 const from{target.x + std::cos(angle) * distance * 0.7, target.y + distance * 0.6, target.z + std::sin(angle) * distance * 0.7}; + auto const look{glm::normalize(target - from)}; + Camera.Pos = from; + Camera.Angle = glm::vec3(static_cast(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast(std::atan2(-look.x, -look.z)), 0.0f); + Global.pCamera = Camera; + m_focus_active = false; + WriteLog("SELFTEST monkeyview " + (track.name().empty() ? std::string{"(noname)"} : track.name())); + return selftest_result::handled; + } + screen_projection const projection; + auto const low{editor_ui::view_min()}; + auto const high{editor_ui::view_max()}; + std::shuffle(tracks.begin(), tracks.end(), random); + for (auto const *track : tracks) + { + ImVec2 at; + if (false == projection.project(along(*track, share()), at) || at.x < low.x + 4.0f || at.y < low.y + 4.0f || at.x > high.x - 4.0f || at.y > high.y - 4.0f) + continue; + m_selftest.input.mouse_to(glm::dvec2{at.x, at.y}); + return selftest_result::handled; + } + m_selftest.input.mouse_to(glm::dvec2{(low.x + high.x) * 0.5f, (low.y + high.y) * 0.5f}); + return selftest_result::handled; + } + if (command == "undoall") + { + std::size_t count{0}; + while (false == m_history.empty() && count < 10000) + { + auto const before{m_history.size()}; + undo_last(); + ++count; + if (m_history.size() >= before) + break; + } + WriteLog(format("SELFTEST undoall: %zu undone, %zu left", count, m_history.size())); + return selftest_result::handled; + } + if (command == "trackhash") + { + std::vector entries; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved) + continue; + std::string entry{track->name()}; + for (auto const &path : track->m_paths) + for (auto const &point : path.points) + entry += format(" %.3f %.3f %.3f", point.x, point.y, point.z); + entries.push_back(std::move(entry)); + } + std::sort(entries.begin(), entries.end()); + std::uint64_t hash{1469598103934665603ull}; + for (auto const &entry : entries) + for (auto const character : entry) + hash = (hash ^ static_cast(character)) * 1099511628211ull; + WriteLog(format("SELFTEST trackhash %zu tracks %016llx", entries.size(), static_cast(hash))); + return selftest_result::handled; + } + if (command == "tutorial") + { + int page{0}; + words >> page; + m_first_steps_open = page >= 0; + m_tutorial_page = std::max(0, page); + return selftest_result::handled; + } + if (command == "shortcuts") + { + m_shortcuts_open = true; + return selftest_result::handled; + } + if (command == "invariants") + { + std::size_t count{0}; + std::vector bad; + auto const finite = [](glm::dvec3 const &Point) { return std::isfinite(Point.x) && std::isfinite(Point.y) && std::isfinite(Point.z); }; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved) + continue; + ++count; + std::string fault; + if (track->m_paths.empty()) + fault = "no path"; + for (auto const &path : track->m_paths) + { + for (auto const &point : path.points) + if (false == finite(point)) + fault = "point not finite"; + if (false == std::isfinite(path.rolls[0]) || false == std::isfinite(path.rolls[1])) + fault = "roll not finite"; + } + if (false == std::isfinite(track->Length()) || track->Length() < 0.0) + fault = format("length %f", track->Length()); + if (false == fault.empty()) + bad.push_back((track->name().empty() ? std::string{"(noname)"} : track->name()) + ": " + fault); + } + auto text{format("SELFTEST invariants: %zu tracks, %zu bad, history %zu, %.0f fps", count, bad.size(), m_history.size(), ImGui::GetIO().Framerate)}; + for (std::size_t i = 0; i < bad.size() && i < 10; ++i) + text += "\n " + bad[i]; + WriteLog(text); + return selftest_result::handled; + } + return selftest_result::unhandled; +} diff --git a/application/editormodeselftestswitch.cpp b/application/editormodeselftestswitch.cpp index 74b7785d..25894069 100644 --- a/application/editormodeselftestswitch.cpp +++ b/application/editormodeselftestswitch.cpp @@ -198,12 +198,29 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com tool.connection_second = find(second); tool.connection = kindword == "single" ? -1 : static_cast(kind); auto const line{straight_near(at, {}, 3.0)}; - if (line.tracks.empty()) + TTrack *start{line.tracks.empty() ? nullptr : line.tracks.front()}; + if (start == nullptr) { - WriteLog("SELFTEST connection: no straight at the start"); + // along a curve: the track nearest to the press + auto best{3.0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const distance{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, at)), at)}; + if (distance < best) + { + best = distance; + start = track; + } + } + } + if (start == nullptr) + { + WriteLog("SELFTEST connection: no track at the start"); return selftest_result::handled; } - m_node = line.tracks.front(); + m_node = start; ui()->set_node(m_node); m_cursor_override = at; auto const started{start_switch_placement()}; @@ -214,6 +231,12 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com return selftest_result::handled; } tool.mouse = glm::dvec3{toward.x, tool.point.y, toward.z}; + if (tool.curved) + { + finish_switch_placement(); + WriteLog("SELFTEST connection along the curve: " + tool.status); + return selftest_result::handled; + } if (command == "connectionhold") { m_cursor_override = toward; diff --git a/application/editormodeselftestterrain.cpp b/application/editormodeselftestterrain.cpp index 1337757b..8ee06f02 100644 --- a/application/editormodeselftestterrain.cpp +++ b/application/editormodeselftestterrain.cpp @@ -82,6 +82,123 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co WriteLog("SELFTEST nmtmake " + where + ": " + m_nmt_state.status); return selftest_result::handled; } + if (command == "nmtunder") + { + // the terrain under the tracks, the tracks left as they are, as the button of the terrain panel does + nmt_under_start(); + WriteLog("SELFTEST nmtunder: " + m_nmt_state.status); + return selftest_result::handled; + } + if (command == "nmtpending") + { + // the chunks still waiting for the model: their place and the squares of the model they wait for + WriteLog(format("SELFTEST nmtpending: %zu chunks, %zu made, model waiting %zu, fitting %d, stage %d, under %d, status %s", m_nmt_state.pending.size(), m_nmt_state.made, m_nmt.waiting(), m_nmt_state.fitting ? 1 : 0, static_cast(m_nmt_state.stage), m_nmt_state.under ? 1 : 0, m_nmt_state.status.c_str())); + for (auto const &key : m_nmt_state.pending) + { + auto const low{heightmap::chunk_corner(key)}; + auto const high{low + glm::dvec2{heightmap::chunk_size}}; + double height{0.0}; + auto const known{m_nmt.height(0.5 * (low.x + high.x), 0.5 * (low.y + high.y), height)}; + WriteLog(format("SELFTEST nmtpending chunk %d,%d at %.0f..%.0f %.0f..%.0f: exists %d, model in %d, height in the middle %d %.2f", key.first, key.second, low.x, high.x, low.y, high.y, m_streamer.exists(key) ? 1 : 0, m_nmt.request(low - glm::dvec2{2.0}, high + glm::dvec2{2.0}) ? 1 : 0, known ? 1 : 0, height)); + } + return selftest_result::handled; + } + if (command == "nmtcheck") + { + // the ground under the axis of the tracks against their formation (the foot of the ballast), every Step m: over the top of the + // ballast it sticks out of the track, far below it the track floats; and the places with no terrain at all + double step{10.0}; + int top{20}; + words >> step >> top; + struct finding + { + double off; + glm::dvec3 at; + std::string track; + }; + std::vector over, under; + std::size_t places{0}, none{0}, bridges{0}; + std::vector offs; + for (auto const &place : nmt_track_places(step)) + { + if (false == place.shapes) + { + ++bridges; + continue; + } + ++places; + double height{0.0}; + if (false == m_streamer.height_at(place.position.x, place.position.z, height)) + { + ++none; + continue; + } + auto const off{height - place.formation}; + offs.push_back(off); + auto const name{place.track->name().empty() ? std::string{"(noname)"} : place.track->name()}; + if (height > place.crown + 0.02) + over.push_back({height - place.crown, place.position, name}); + else if (off < -0.3) + under.push_back({off, place.position, name}); + } + std::sort(offs.begin(), offs.end()); + auto const quantile = [&](double const Q) { return offs.empty() ? 0.0 : offs[static_cast(Q * (offs.size() - 1))]; }; + WriteLog(format("SELFTEST nmtcheck: %zu places, %zu without terrain, %zu on bridges or in tunnels; ground less formation: 5%% %.2f, median %.2f, 95%% %.2f m; over the top of the ballast %zu, more than 0.3 m under the formation %zu; modifiers %zu", + places, none, bridges, quantile(0.05), quantile(0.5), quantile(0.95), over.size(), under.size(), m_streamer.manifest().modifiers.size())); + for (auto *list : {&over, &under}) + { + std::sort(list->begin(), list->end(), [](finding const &A, finding const &B) { return std::abs(A.off) > std::abs(B.off); }); + for (std::size_t i = 0; i < std::min(list->size(), static_cast(top)); ++i) + WriteLog(format("SELFTEST nmtcheck %s %+.2f m at %.1f %.1f %.1f %s", list == &over ? "over the crown" : "under the formation", (*list)[i].off, (*list)[i].at.x, (*list)[i].at.y, (*list)[i].at.z, (*list)[i].track.c_str())); + } + return selftest_result::handled; + } + if (command == "nmtauto") + { + // the tracks put onto the terrain model, as the button of the terrain panel does; nmtwait waits for the end + nmt_auto_start(); + WriteLog("SELFTEST nmtauto: " + m_nmt_state.status); + return selftest_result::handled; + } + if (command == "nmtprobe") + { + // heights across the tracks: the rail top, the top of the ballast under the rails, the model, the ground under the axis + // and beside the track + std::string name; + double step{250.0}; + words >> name >> step; + auto const places{nmt_track_places(step)}; + for (std::size_t i = 0; i + 1 < places.size(); ++i) + { + auto const &place{places[i]}; + auto const &next{places[i + 1]}; + if (place.track != next.track || place.path != next.path || (false == name.empty() && place.track->name() != name)) + continue; + glm::dvec2 along{next.position.x - place.position.x, next.position.z - place.position.z}; + if (glm::length(along) < 1e-6) + continue; + along = glm::normalize(along); + glm::dvec2 const across{-along.y, along.x}; + std::vector points; + for (double const offset : {0.0, -2.0, 2.0, -3.5, 3.5, -6.0, 6.0}) + points.emplace_back(place.position.x + across.x * offset, place.position.y, place.position.z + across.y * offset); + std::vector found; + auto const ground{ground_heights(points, true, &found)}; + double model{std::numeric_limits::quiet_NaN()}; + if (double height; m_nmt.height(place.position.x, place.position.z, height)) + model = height + m_nmt_state.offset; + std::string text{format("SELFTEST nmtprobe %s at %.0f %.0f: rail top %.2f, ballast top %.2f, foot %.2f, model %.2f; ground/model at axis, -2, 2, -3.5, 3.5, -6, 6 m:", place.track->name().c_str(), place.position.x, place.position.z, place.position.y, place.crown, place.formation, model)}; + for (std::size_t k = 0; k < points.size(); ++k) + { + double side{std::numeric_limits::quiet_NaN()}; + if (double height; m_nmt.height(points[k].x, points[k].z, height)) + side = height + m_nmt_state.offset; + text += (found[k] != 0 ? format(" %.2f", ground[k]) : std::string{" -"}) + format("/%.2f", side); + } + WriteLog(text); + } + return selftest_result::handled; + } if (command == "nmtoffer") { // the offer of the terrain to a scenery begun from nothing: logged, and taken with the place given, if any @@ -100,7 +217,7 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co if (command == "nmtwait") { // till the model is in and the chunks waiting for it are made - if (m_nmt_state.fitting || false == m_nmt_state.pending.empty()) + if (m_nmt_state.fitting || m_nmt_state.stage != nmt_state::automatic::idle || false == m_nmt_state.pending.empty()) { --m_selftest.next; return selftest_result::pause; diff --git a/application/editormodeselftesttrack.cpp b/application/editormodeselftesttrack.cpp index 087b88f4..2ecf86ea 100644 --- a/application/editormodeselftesttrack.cpp +++ b/application/editormodeselftesttrack.cpp @@ -279,6 +279,18 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm WriteLog("SELFTEST " + command + ": " + m_route.status + " " + m_route.error); return selftest_result::handled; } + if (command == "digitizescenery") + { + // "digitizescenery write" digitizes, without the word only counts what it would do + std::string mode; + words >> mode; + auto const text{digitize_scenery(mode == "write")}; + std::istringstream lines{text}; + std::string line; + while (std::getline(lines, line)) + WriteLog("SELFTEST digitizescenery: " + line); + return selftest_result::handled; + } if (command == "digitizeall") { digitize_all(); @@ -318,6 +330,35 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm } return selftest_result::handled; } + if (command == "vprofilezoom") + { + // the plot of the open profile from one chainage to another, the heights exaggerated as given (0: fitted) + double from{0.0}, to{100.0}, exaggeration{0.0}; + words >> from >> to >> exaggeration; + m_profile.view_from = from; + m_profile.view_to = to; + m_profile.exaggeration = static_cast(exaggeration); + std::string zones; + for (auto const &zone : m_profile.context.switches) + zones += format(" [%.1f..%.1f %s]", zone.from, zone.to, zone.name.c_str()); + std::string points; + for (auto const &point : m_profile.line.points) + points += format(" (%.1f %.3f R%.0f%s)", point.chainage, point.elevation, point.radius, point.joint ? " joint" : ""); + std::size_t kept{0}; + std::string nearby; + for (auto const &entry : m_profiles) + { + ++kept; + if (false == m_profile.samples.empty() && glm::length(glm::dvec2{entry.start.x - m_profile.samples.front().position.x, entry.start.z - m_profile.samples.front().position.z}) < 50.0) + nearby += format(" [%.1f m, start %.1f %.1f end %.1f %.1f]", entry.length, entry.start.x, entry.start.z, entry.end.x, entry.end.z); + } + WriteLog(format("SELFTEST vprofilezoom library %zu (loaded %d), route %.1f m from %.1f %.1f to %.1f %.1f; near:%s", kept, m_profiles_loaded ? 1 : 0, m_profile.route.length, m_profile.samples.front().position.x, m_profile.samples.front().position.z, m_profile.samples.back().position.x, m_profile.samples.back().position.z, nearby.c_str())); + std::string issues; + for (auto const &issue : m_profile.issues) + issues += std::string{issue.error ? " | error: " : " | "} + issue.text; + WriteLog(format("SELFTEST vprofilezoom %.1f..%.1f; departure of the track %.3f m; %s; zones%s; points%s%s", from, to, m_profile.departure, m_profile.status.c_str(), zones.c_str(), points.c_str(), issues.c_str())); + return selftest_result::handled; + } if (command == "vprofile") { std::string from; @@ -443,6 +484,183 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm WriteLog("SELFTEST btf kept: " + line); return selftest_result::handled; } + if (command == "infrascan") + { + // what the binding of the objects along the tracks finds over the whole scenery: how many of each kind and group, and + // the files the rules don't know, the most used first + double corridor{0.0}; + int top{30}; + std::string usage; + words >> corridor >> top >> usage; + m_infra.scope = 3; + if (corridor > 0.0) + m_infra.corridor = static_cast(corridor); + infra_recognize(); + std::map kinds, groups; + std::map> unknown; + for (auto const &candidate : m_infra.candidates) + { + auto const &binding{candidate.binding}; + ++kinds[infra::name(binding.type)]; + ++groups[infra::name(binding.group)]; + if (binding.group != infra::category::other) + continue; + std::string file; + if (binding.type == infra::kind::include) + file = *scene::Layers.instance(binding.include).file; + else if (binding.type == infra::kind::model) + { + auto const *model{static_cast(binding.node)}; + file = model->Model() != nullptr ? model->Model()->NameGet() : std::string{"?"}; + } + else + file = infra::name(binding.type); + auto &entry{unknown[std::string{infra::name(binding.type)} + " " + file]}; + ++entry.first; + entry.second = format("%.1f m", candidate.offset); + } + std::string text; + for (auto const &[name, count] : kinds) + text += format(" %s %d", name.c_str(), count); + text += " |"; + for (auto const &[name, count] : groups) + text += format(" %s %d", name.c_str(), count); + WriteLog(format("SELFTEST infrascan: %zu objects along %zu paths, corridor %.1f m:", m_infra.candidates.size(), m_infra.tracks.size(), m_infra.corridor) + text + " | " + m_infra.status); + std::vector> sorted; + for (auto const &[name, entry] : unknown) + sorted.emplace_back(entry.first, name + " (e.g. " + entry.second + " off the axis)"); + std::sort(sorted.begin(), sorted.end(), [](auto const &A, auto const &B) { return A.first > B.first; }); + for (std::size_t i = 0; i < sorted.size() && static_cast(i) < top; ++i) + WriteLog(format("SELFTEST infrascan unknown %d x %s", sorted[i].first, sorted[i].second.c_str())); + WriteLog(format("SELFTEST infrascan: %zu files the rules don't know", sorted.size())); + // how each template and model stands by the tracks: the distance from the axis, the height over the rail top, the turn + // against the path, as tab separated values for the descriptions of the templates + if (false == usage.empty()) + { + struct use + { + std::string kind; + std::string group; + std::vector offsets, heights, turns; + }; + std::map uses; + for (auto const &candidate : m_infra.candidates) + { + auto const &binding{candidate.binding}; + if (binding.anchors.empty() || (binding.type != infra::kind::include && binding.type != infra::kind::model)) + continue; + std::string file; + if (binding.type == infra::kind::include) + file = *scene::Layers.instance(binding.include).file; + else + { + auto const *model{static_cast(binding.node)}; + file = model->Model() != nullptr ? model->Model()->NameGet() : std::string{}; + } + if (file.empty()) + continue; + auto &entry{uses[file]}; + entry.kind = infra::name(binding.type); + entry.group = infra::name(binding.group); + entry.offsets.push_back(binding.anchors.front().offset); + entry.heights.push_back(binding.anchors.front().height); + if (binding.turns) + { + auto turn{std::fmod(glm::degrees(binding.yaw), 360.0)}; + entry.turns.push_back(turn < 0.0 ? turn + 360.0 : turn); + } + } + std::ofstream out{usage}; + out << "file\tkind\tgroup\tcount\toffsets\theights\tturns\n"; + auto const list = [](std::vector const &Values) { + std::string text; + for (auto const value : Values) + text += format("%s%.2f", text.empty() ? "" : ",", value); + return text; + }; + for (auto const &[file, entry] : uses) + out << file << '\t' << entry.kind << '\t' << entry.group << '\t' << entry.offsets.size() << '\t' << list(entry.offsets) << '\t' << list(entry.heights) << '\t' << list(entry.turns) << '\n'; + WriteLog(format("SELFTEST infrascan: the use of %zu templates and models written to %s", uses.size(), usage.c_str())); + } + return selftest_result::handled; + } + if (command == "rollcheck") + { + // at each joint near the point the cant of the paths meeting, told along a common direction, and their heights + glm::dvec3 point{0.0}; + double radius{100.0}; + words >> point.x >> point.z >> radius; + struct end + { + TTrack *track; + std::size_t path; + bool last; + glm::dvec3 position; + double roll; // along the direction leaving the joint + }; + std::vector ends; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved) + continue; + for (std::size_t i = 0; i < track->m_paths.size(); ++i) + { + auto const &path{track->m_paths[i]}; + for (auto const last : {false, true}) + { + auto const &at{path.points[last ? segment_data::point::end : segment_data::point::start]}; + if (geometry::plan_distance(at, point) > radius) + continue; + // the roll of the path told along it; leaving the joint from its end the direction is the other way + auto const roll{static_cast(path.rolls[last ? 1 : 0])}; + ends.push_back({track, i, last, at, last ? -roll : roll}); + } + } + } + std::size_t joints{0}, wrong{0}; + for (std::size_t a = 0; a < ends.size(); ++a) + for (std::size_t b = a + 1; b < ends.size(); ++b) + { + if (ends[a].track == ends[b].track || glm::distance(ends[a].position, ends[b].position) > 0.02) + continue; + ++joints; + // leaving the joint both ways, one path's cant is the other's turned round + auto const mismatch{std::abs(ends[a].roll + ends[b].roll)}; + if (mismatch > 0.05) + { + ++wrong; + WriteLog(format("SELFTEST rollcheck %s/%zu %s and %s/%zu %s at %.2f %.2f %.2f: cant %.3f vs %.3f deg", ends[a].track->name().c_str(), ends[a].path, ends[a].last ? "end" : "start", ends[b].track->name().c_str(), ends[b].path, ends[b].last ? "end" : "start", ends[a].position.x, ends[a].position.y, ends[a].position.z, ends[a].roll, -ends[b].roll)); + } + } + WriteLog(format("SELFTEST rollcheck: %zu joints, %zu with the cant not matching", joints, wrong)); + return selftest_result::handled; + } + if (command == "freeends") + { + // the ends of the rail paths joined to nothing near the point, and how far the nearest other end is from each + glm::dvec3 point{0.0}; + double radius{100.0}; + words >> point.x >> point.z >> radius; + for (auto const &end : editor_track::free_ends(nullptr, 1, point, radius, {})) + { + double nearest{std::numeric_limits::max()}; + double rise{0.0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track == end.track || track->m_editorremoved) + continue; + for (auto const &path : track->m_paths) + for (auto const which : {segment_data::point::start, segment_data::point::end}) + if (auto const distance{glm::distance(path.points[which], end.position)}; distance < nearest) + { + nearest = distance; + rise = path.points[which].y - end.position.y; + } + } + WriteLog(format("SELFTEST freeend %s at %.3f %.3f %.3f, nearest other end %.3f m away, %+.3f m higher", end.track->name().c_str(), end.position.x, end.position.y, end.position.z, nearest, rise)); + } + return selftest_result::handled; + } if (command == "pathsnear") { glm::dvec3 point{0.0}; @@ -461,7 +679,7 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm continue; auto const &c1{path.points[segment_data::point::control1]}; auto const &c2{path.points[segment_data::point::control2]}; - WriteLog(format("SELFTEST path %s/%zu %s: %.3f %.3f -> %.3f %.3f, c1 %.3f %.3f, c2 %.3f %.3f, %.3f m", track->name().c_str(), i, track->eType == tt_Switch ? "switch" : "normal", a.x, a.z, b.x, b.z, c1.x, c1.z, c2.x, c2.z, geometry::bezier{path}.plan_length())); + WriteLog(format("SELFTEST path %s/%zu %s: %.3f %.3f -> %.3f %.3f, c1 %.3f %.3f, c2 %.3f %.3f, %.3f m, y %.3f -> %.3f, cant %.2f -> %.2f deg", track->name().c_str(), i, track->eType == tt_Switch ? "switch" : "normal", a.x, a.z, b.x, b.z, c1.x, c1.z, c2.x, c2.z, geometry::bezier{path}.plan_length(), a.y, b.y, path.rolls[0], path.rolls[1])); } } return selftest_result::handled; diff --git a/application/editormodeshared.h b/application/editormodeshared.h index 494a7f39..149a4a09 100644 --- a/application/editormodeshared.h +++ b/application/editormodeshared.h @@ -9,7 +9,14 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include +#include +#include +#include #include +#include +#include +#include #include #include "imgui/imgui.h" @@ -52,3 +59,348 @@ template inline void fill_from_nearest(std::vector &He visit(index + Side); } } + +// the families of the track tools, each with its colour: building the track, equipping it, checking it +inline ImU32 family_colour(int const Family, float const Alpha = 1.0f) +{ + static ImVec4 const colours[]{{0.30f, 0.60f, 0.95f, 1.0f}, {0.95f, 0.66f, 0.22f, 1.0f}, {0.25f, 0.80f, 0.66f, 1.0f}}; + auto colour{colours[std::clamp(Family, 0, 2)]}; + colour.w = Alpha; + return ImGui::ColorConvertFloat4ToU32(colour); +} + +inline char const *family_name(int const Family) +{ + static char const *const names[]{"Building", "Equipment", "Checks"}; + return STR_C(names[std::clamp(Family, 0, 2)]); +} + +// a numbered step of the work in a panel: the number in a circle, its name, a line to the edge of the panel +inline void step_heading(int const Number, char const *Text) +{ + ImGui::Spacing(); + auto *draw{ImGui::GetWindowDrawList()}; + auto const at{ImGui::GetCursorScreenPos()}; + auto const height{ImGui::GetTextLineHeight()}; + auto const radius{std::round(height * 0.6f)}; + ImVec2 const centre{at.x + radius, at.y + radius}; + draw->AddCircleFilled(centre, radius, ImGui::GetColorU32(ImGuiCol_CheckMark), 20); + char digit[8]; + std::snprintf(digit, sizeof(digit), "%d", Number); + auto const size{ImGui::CalcTextSize(digit)}; + draw->AddText(ImVec2(std::round(centre.x - size.x * 0.5f), std::round(centre.y - size.y * 0.5f)), IM_COL32(16, 18, 22, 255), digit); + ImGui::Dummy(ImVec2(radius * 2.0f, radius * 2.0f)); + ImGui::SameLine(0.0f, radius * 0.6f); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + radius - height * 0.5f); + ImGui::TextUnformatted(Text); + auto const end{ImGui::GetItemRectMax().x + radius * 0.6f}; + auto const edge{ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x}; + if (edge > end) + draw->AddLine(ImVec2(end, centre.y), ImVec2(edge, centre.y), ImGui::GetColorU32(ImGuiCol_Separator)); + ImGui::Spacing(); +} + +// a toggle drawn as a pill: a swatch of the colour of what it shows and its name, lit while on +inline bool pill_toggle(char const *Label, ImU32 const Swatch, bool &Value) +{ + auto const text{ImGui::CalcTextSize(Label)}; + auto const height{ImGui::GetFrameHeight()}; + auto const swatch{std::round(height * 0.7f)}; + auto const pad{std::round(height * 0.35f)}; + ImVec2 const size{pad + swatch + pad * 0.6f + text.x + pad, height}; + auto const at{ImGui::GetCursorScreenPos()}; + ImGui::PushID(Label); + auto const clicked{ImGui::InvisibleButton("##pill", size)}; + ImGui::PopID(); + if (clicked) + Value = !Value; + auto const hovered{ImGui::IsItemHovered()}; + auto *draw{ImGui::GetWindowDrawList()}; + ImVec2 const end{at.x + size.x, at.y + size.y}; + draw->AddRectFilled(at, end, ImGui::GetColorU32(Value ? (hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Header) : (hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg)), height * 0.5f); + auto const middle{at.y + height * 0.5f}; + auto const colour{Value ? Swatch : (Swatch & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 70)}; + draw->AddLine(ImVec2(at.x + pad, middle), ImVec2(at.x + pad + swatch, middle), colour, std::max(2.0f, height * 0.16f)); + draw->AddText(ImVec2(at.x + pad + swatch + pad * 0.6f, std::round(middle - text.y * 0.5f)), ImGui::GetColorU32(Value ? ImGuiCol_Text : ImGuiCol_TextDisabled), Label); + return clicked; +} + +// a choice of a few as one bar of segments across the panel, the chosen one lit; true when the choice changed +struct segment_option +{ + int value; + char const *label; + char const *tooltip{nullptr}; +}; +inline bool segmented(char const *Id, int &Value, std::initializer_list Options) +{ + auto const count{static_cast(Options.size())}; + auto const width{ImGui::GetContentRegionAvail().x}; + auto const height{ImGui::GetFrameHeight()}; + auto const at{ImGui::GetCursorScreenPos()}; + auto *draw{ImGui::GetWindowDrawList()}; + auto const rounding{height * 0.5f}; + draw->AddRectFilled(at, ImVec2(at.x + width, at.y + height), ImGui::GetColorU32(ImGuiCol_FrameBg), rounding); + ImGui::PushID(Id); + bool changed{false}; + float index{0.0f}; + for (auto const &option : Options) + { + auto const left{std::round(at.x + width * index / count)}; + auto const right{std::round(at.x + width * (index + 1.0f) / count)}; + ImGui::SetCursorScreenPos(ImVec2(left, at.y)); + ImGui::PushID(option.value); + if (ImGui::InvisibleButton("##segment", ImVec2(std::max(1.0f, right - left), height)) && Value != option.value) + { + Value = option.value; + changed = true; + } + auto const hovered{ImGui::IsItemHovered()}; + if (option.tooltip != nullptr && hovered) + ImGui::SetTooltip("%s", STR_C(option.tooltip)); + ImGui::PopID(); + auto const chosen{Value == option.value}; + if (chosen || hovered) + draw->AddRectFilled(ImVec2(left + 2.0f, at.y + 2.0f), ImVec2(right - 2.0f, at.y + height - 2.0f), chosen ? family_colour(0, 0.8f) : ImGui::GetColorU32(ImGuiCol_FrameBgHovered), rounding - 2.0f); + auto const text{ImGui::CalcTextSize(option.label)}; + draw->PushClipRect(ImVec2(left + 4.0f, at.y), ImVec2(right - 4.0f, at.y + height), true); + draw->AddText(ImVec2(std::round(std::max(left + 6.0f, (left + right - text.x) * 0.5f)), std::round(at.y + (height - text.y) * 0.5f)), ImGui::GetColorU32(chosen ? ImGuiCol_Text : ImGuiCol_TextDisabled), option.label); + draw->PopClipRect(); + index += 1.0f; + } + ImGui::PopID(); + ImGui::SetCursorScreenPos(at); + ImGui::Dummy(ImVec2(width, height)); + return changed; +} + +// values picked with a click, as round chips which go on to the next line at the edge of the panel; true when one was picked +inline bool value_chips(char const *Id, double &Value, std::initializer_list Values, char const *Format) +{ + auto const edge{ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x}; + auto const gap{std::round(ImGui::GetStyle().ItemSpacing.x * 0.6f)}; + ImGui::PushID(Id); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ImGui::GetFrameHeight() * 0.5f); + bool changed{false}; + bool first{true}; + for (auto const value : Values) + { + char text[32]; + std::snprintf(text, sizeof(text), Format, value); + auto const width{ImGui::CalcTextSize(text).x + ImGui::GetStyle().FramePadding.x * 2.0f}; + if (false == first) + { + ImGui::SameLine(0.0f, gap); + if (ImGui::GetCursorScreenPos().x + width > edge) + ImGui::NewLine(); + } + first = false; + auto const chosen{std::abs(Value - value) < 1e-6}; + ImGui::PushStyleColor(ImGuiCol_Button, chosen ? family_colour(0, 0.8f) : ImGui::GetColorU32(ImGuiCol_FrameBg)); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(chosen ? ImGuiCol_Text : ImGuiCol_TextDisabled)); + if (ImGui::Button(text)) + { + Value = value; + changed = true; + } + ImGui::PopStyleColor(2); + } + ImGui::PopStyleVar(); + ImGui::PopID(); + return changed; +} + +// one of a few named choices as round chips which go on to the next line at the edge of the panel, where a tab bar +// would cut the names; true when one was picked +inline bool choice_chips(char const *Id, int &Value, std::vector const &Options) +{ + auto const edge{ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x}; + auto const gap{std::round(ImGui::GetStyle().ItemSpacing.x * 0.6f)}; + ImGui::PushID(Id); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ImGui::GetFrameHeight() * 0.5f); + bool changed{false}; + bool first{true}; + for (auto const &option : Options) + { + auto const width{ImGui::CalcTextSize(option.label).x + ImGui::GetStyle().FramePadding.x * 2.0f}; + if (false == first) + { + ImGui::SameLine(0.0f, gap); + if (ImGui::GetCursorScreenPos().x + width > edge) + ImGui::NewLine(); + } + first = false; + auto const chosen{Value == option.value}; + ImGui::PushID(option.value); + ImGui::PushStyleColor(ImGuiCol_Button, chosen ? family_colour(0, 0.8f) : ImGui::GetColorU32(ImGuiCol_FrameBg)); + if (ImGui::Button(option.label) && false == chosen) + { + Value = option.value; + changed = true; + } + if (option.tooltip != nullptr) + item_tooltip(option.tooltip); + ImGui::PopStyleColor(); + ImGui::PopID(); + } + ImGui::PopStyleVar(); + ImGui::PopID(); + return changed; +} + +// values at a glance as tiles: the value in its colour over its name, a bar of the colour at the left, on to the next row +// at the edge of the panel +struct metric_tile +{ + std::string value; + char const *label; + ImU32 colour; +}; +inline void metric_tiles(std::initializer_list Tiles) +{ + auto const font{ImGui::GetFontSize()}; + auto const height{std::round(font * 2.5f)}; + auto const edge{ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x}; + auto const gap{std::round(font * 0.3f)}; + auto *list{ImGui::GetWindowDrawList()}; + bool first{true}; + for (auto const &entry : Tiles) + { + auto const value{ImGui::CalcTextSize(entry.value.c_str())}; + auto const label{ImGui::CalcTextSize(entry.label)}; + auto const width{std::max(std::round(font * 5.0f), std::max(value.x, label.x) + font)}; + if (false == first) + { + ImGui::SameLine(0.0f, gap); + if (ImGui::GetCursorScreenPos().x + width > edge) + ImGui::NewLine(); + } + first = false; + auto const at{ImGui::GetCursorScreenPos()}; + ImGui::Dummy(ImVec2(width, height)); + list->AddRectFilled(at, ImVec2(at.x + width, at.y + height), ImGui::GetColorU32(ImGuiCol_FrameBg), std::round(font * 0.3f)); + list->AddRectFilled(at, ImVec2(at.x + 3.0f, at.y + height), entry.colour, 2.0f, ImDrawFlags_RoundCornersLeft); + list->AddText(ImVec2(at.x + font * 0.5f, at.y + height * 0.12f), entry.colour, entry.value.c_str()); + list->AddText(ImVec2(at.x + font * 0.5f, at.y + height * 0.12f + value.y), ImGui::GetColorU32(ImGuiCol_TextDisabled), entry.label); + } +} + +// a choice as a tile: a sketch drawn by Sketch into the box on the left (in the colours of the main line and of what it makes), +// the name beside it; lit while chosen, true when clicked +inline bool sketch_tile(char const *Id, char const *Label, bool const Chosen, float const Width, std::function const &Sketch) +{ + auto const font{ImGui::GetFontSize()}; + auto const height{std::round(font * 2.4f)}; + auto const top{ImGui::GetCursorScreenPos()}; + auto const clicked{ImGui::InvisibleButton(Id, ImVec2(Width, height))}; + auto const hovered{ImGui::IsItemHovered()}; + auto *list{ImGui::GetWindowDrawList()}; + auto const accent{family_colour(0)}; + ImVec2 const bottom{top.x + Width, top.y + height}; + auto const rounding{std::round(font * 0.35f)}; + list->AddRectFilled(top, bottom, Chosen ? (accent & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 46) : ImGui::GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), rounding); + list->AddRect(top, bottom, Chosen ? accent : ImGui::GetColorU32(ImGuiCol_Border), rounding, 0, Chosen ? 2.0f : 1.0f); + auto const pad{std::round(font * 0.4f)}; + auto const side{std::round(font * 2.2f)}; + ImVec2 const low{top.x + pad, top.y + pad}; + ImVec2 const high{top.x + pad + side, bottom.y - pad}; + Sketch(*list, low, high, Chosen ? accent : ImGui::GetColorU32(ImGuiCol_Text, 0.7f), Chosen ? IM_COL32(255, 210, 60, 255) : ImGui::GetColorU32(ImGuiCol_Text, 0.45f)); + auto const text_left{high.x + pad}; + auto const wrap{bottom.x - pad - text_left}; + auto const size{ImGui::CalcTextSize(Label, nullptr, false, wrap)}; + list->AddText(nullptr, 0.0f, ImVec2(text_left, std::round(top.y + (height - size.y) * 0.5f)), ImGui::GetColorU32(Chosen ? ImGuiCol_Text : ImGuiCol_TextDisabled), Label, nullptr, wrap); + return clicked; +} + +// the state of something as a rounded chip in its colour; true when clicked +inline bool state_chip(char const *Label, ImVec4 const &Colour) +{ + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ImGui::GetFrameHeight() * 0.5f); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(Colour.x * 0.3f, Colour.y * 0.3f, Colour.z * 0.3f, 0.9f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(Colour.x * 0.45f, Colour.y * 0.45f, Colour.z * 0.45f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(Colour.x * 0.55f, Colour.y * 0.55f, Colour.z * 0.55f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, Colour); + auto const clicked{ImGui::Button(Label)}; + ImGui::PopStyleColor(4); + ImGui::PopStyleVar(); + return clicked; +} + +// fields as rows of a table: the name wrapped in a column on the left, the field as wide as the rest of the panel; +// field_row takes the untranslated "Name##id" (a literal: the translations are cached by the pointer, a name made at run +// time goes with Translate false) and leaves the cursor where the field goes +inline bool begin_fields(char const *Id, float const Names = 0.42f) +{ + if (false == ImGui::BeginTable(Id, 2, ImGuiTableFlags_SizingStretchProp)) + return false; + ImGui::TableSetupColumn("##name", ImGuiTableColumnFlags_WidthStretch, Names); + ImGui::TableSetupColumn("##field", ImGuiTableColumnFlags_WidthStretch, 1.0f - Names); + return true; +} +inline void field_row(char const *Label, bool const Translate = true) +{ + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::AlignTextToFramePadding(); + auto const *text{Translate ? STR_C(Label) : Label}; + ImGui::PushTextWrapPos(0.0f); + ImGui::TextUnformatted(text, std::strstr(text, "##")); + ImGui::PopTextWrapPos(); + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(-FLT_MIN); +} + +// switches of a few things as round chips which go on to the next line at the edge of the panel, lit while on; +// the index of the one clicked, or -1 +struct toggle_option +{ + char const *label; + bool *value; + char const *tooltip{nullptr}; +}; +inline int toggle_chips(char const *Id, std::initializer_list Options) +{ + auto const edge{ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x}; + auto const gap{std::round(ImGui::GetStyle().ItemSpacing.x * 0.6f)}; + auto const mark{std::round(ImGui::GetFontSize() * 0.45f)}; + ImGui::PushID(Id); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ImGui::GetFrameHeight() * 0.5f); + int clicked{-1}; + int index{0}; + for (auto const &option : Options) + { + auto const on{*option.value}; + auto const width{ImGui::CalcTextSize(option.label).x + ImGui::GetStyle().FramePadding.x * 2.0f + mark * 1.6f}; + if (index > 0) + { + ImGui::SameLine(0.0f, gap); + if (ImGui::GetCursorScreenPos().x + width > edge) + ImGui::NewLine(); + } + auto const at{ImGui::GetCursorScreenPos()}; + ImGui::PushID(index); + ImGui::PushStyleColor(ImGuiCol_Button, on ? family_colour(0, 0.8f) : ImGui::GetColorU32(ImGuiCol_FrameBg)); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(on ? ImGuiCol_Text : ImGuiCol_TextDisabled)); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(1.0f, 0.5f)); + if (ImGui::Button(option.label, ImVec2(width, 0.0f))) + { + *option.value = !on; + clicked = index; + } + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + if (option.tooltip != nullptr && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C(option.tooltip)); + ImGui::PopID(); + auto const middle{ImVec2(at.x + ImGui::GetStyle().FramePadding.x + mark * 0.5f, at.y + ImGui::GetFrameHeight() * 0.5f)}; + auto *draw{ImGui::GetWindowDrawList()}; + if (*option.value) + draw->AddCircleFilled(middle, mark * 0.5f, IM_COL32(255, 210, 60, 255)); + else + draw->AddCircle(middle, mark * 0.5f, ImGui::GetColorU32(ImGuiCol_TextDisabled), 0, 1.5f); + ++index; + } + ImGui::PopStyleVar(); + ImGui::PopID(); + return clicked; +} diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index ab7b211a..91d1097f 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormode.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" +#include "application/editormodeshared.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIncludeInfo.hpp" @@ -338,7 +339,7 @@ std::string editor_mode::signal_intent_label(signal_template const &Template, si if (Template.read.empty()) return format(STR_C("Click: %s %s (%s)%s, the train doesn't read it"), STR(kKindLabels[kind_index(Template.kind)]).c_str(), name.c_str(), variant.c_str(), where.c_str()) + "\n" + STR("Drag along: the trains it faces run the way of the drag"); auto const event{editor_includes::substitute(Template.read, {name})}; - auto const path{Spot.track->name().empty() ? std::string{"(noname)"} : Spot.track->name()}; + auto const path{Spot.track->name().empty() ? std::string{STR("(no name)")} : Spot.track->name()}; return format(STR_C("Click: %s %s (%s)%s, read by %s %s of %s"), STR(kKindLabels[kind_index(Template.kind)]).c_str(), name.c_str(), variant.c_str(), where.c_str(), editor_track::event_keyword(Spot.list), event.c_str(), path.c_str()) + "\n" + STR("Drag along: the trains it faces run the way of the drag"); } @@ -572,7 +573,8 @@ void editor_mode::signal_release() push_track_snapshot(std::move(states)); m_history.back().directives.emplace_back(found->include.instance, std::move(before)); } - tool.status = found->read.empty() ? format(STR_C("Signal %s moved"), found->name.c_str()) : format(STR_C("Signal %s moved, read by %s %s of %s"), found->name.c_str(), editor_track::event_keyword(spot.list), found->read.c_str(), spot.track->name().empty() ? "(noname)" : spot.track->name().c_str()); + tool.status = found->read.empty() ? format(STR_C("Signal %s moved, Ctrl+Z takes it back"), found->name.c_str()) + : format(STR_C("Signal %s moved, read by %s %s of %s, Ctrl+Z takes it back"), found->name.c_str(), editor_track::event_keyword(spot.list), found->read.c_str(), spot.track->name().empty() ? STR_C("(no name)") : spot.track->name().c_str()); signal_refresh(true); signal_select(found->include.instance); } @@ -649,14 +651,14 @@ bool editor_mode::signal_place(signal_spot const &Spot, signal_template const &T auto const instance{m_history.back().instances.front()}; auto const *placed{signal_template_of(file)}; auto const read{placed != nullptr && false == placed->read.empty() ? editor_includes::substitute(placed->read, {name}) : std::string{}}; - auto const path{Spot.track->name().empty() ? std::string{"(noname)"} : Spot.track->name()}; + auto const path{Spot.track->name().empty() ? std::string{STR("(no name)")} : Spot.track->name()}; std::string reason; if (read.empty()) - tool.status = format(STR_C("Signal %s placed (%s), the train doesn't read it"), name.c_str(), file.c_str()); + tool.status = format(STR_C("Signal %s placed (%s), the train doesn't read it, Ctrl+Z takes it back"), name.c_str(), file.c_str()); else if (false == Spot.track->Dynamics.empty()) - tool.status = format(STR_C("Signal %s placed (%s), but vehicles stand on the path: add %s to its %s by hand"), name.c_str(), file.c_str(), read.c_str(), editor_track::event_keyword(Spot.list)); + tool.status = format(STR_C("Signal %s placed (%s), but vehicles stand on the path: add %s to its %s by hand, Ctrl+Z takes it back"), name.c_str(), file.c_str(), read.c_str(), editor_track::event_keyword(Spot.list)); else if (false == scene::Layers.editable(Spot.track, &reason)) - tool.status = format(STR_C("Signal %s placed (%s), but the path can't take its event: %s"), name.c_str(), file.c_str(), reason.c_str()); + tool.status = format(STR_C("Signal %s placed (%s), but the path can't take its event: %s, Ctrl+Z takes it back"), name.c_str(), file.c_str(), reason.c_str()); else { auto snap{std::move(m_history.back())}; @@ -668,11 +670,12 @@ bool editor_mode::signal_place(signal_spot const &Spot, signal_template const &T Spot.track->mark_dirty(); push_track_snapshot({{Spot.track, before}}); m_history.back().instances = std::move(snap.instances); - tool.status = format(STR_C("Signal %s placed (%s), read by %s %s of %s. Its logic works after the scenery is saved and opened again"), name.c_str(), file.c_str(), editor_track::event_keyword(Spot.list), read.c_str(), path.c_str()); + tool.status = format(STR_C("Signal %s placed (%s), read by %s %s of %s, Ctrl+Z takes it back. Its logic works after the scenery is saved and opened again"), name.c_str(), file.c_str(), editor_track::event_keyword(Spot.list), read.c_str(), path.c_str()); } std::snprintf(tool.name, sizeof(tool.name), "%s", signal_next_name(name).c_str()); signal_refresh(true); signal_select(instance); + first_step_done(first_step::signal); return true; } @@ -861,10 +864,13 @@ void editor_mode::render_signal_palette() ImGui::EndCombo(); } if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Variant of the chosen type. A head leaning to one side is swapped for its counterpart leaning towards the track on the other side")); + { + auto const read{armed->read.empty() ? std::string{armed->kind == "warning" || armed->kind == "repeater" ? STR_C("The train doesn't read it: it repeats what the signal ahead of it shows") : STR_C("The train doesn't read it")} + : format(STR_C("The train reads it by %s on the path it stands by"), editor_includes::substitute(armed->read, {tool.name[0] != '\0' ? std::string{tool.name} : std::string{"?"}}).c_str())}; + ImGui::SetTooltip("%s\n%s", STR_C("Variant of the chosen type. A head leaning to one side is swapped for its counterpart leaning towards the track on the other side"), read.c_str()); + } if (false == armed->description.empty()) ImGui::TextWrapped("%s", armed->description.c_str()); - ImGui::TextDisabled("%s", armed->read.empty() ? (armed->kind == "warning" || armed->kind == "repeater" ? STR_C("The train doesn't read it: it repeats what the signal ahead of it shows") : STR_C("The train doesn't read it")) : format(STR_C("The train reads it by %s on the path it stands by"), editor_includes::substitute(armed->read, {tool.name[0] != '\0' ? std::string{tool.name} : std::string{"?"}}).c_str()).c_str()); } void editor_mode::render_signal_selected() @@ -875,13 +881,13 @@ void editor_mode::render_signal_selected() return; ImGui::Separator(); ImGui::TextUnformatted(format(STR_C("Selected: %s (%s)"), found->name.c_str(), found->include.file.c_str()).c_str()); + item_tooltip("Name, plate and the linked signal: in the node properties. Del removes it with its event"); if (found->read.empty()) ImGui::TextDisabled("%s", STR_C("The train doesn't read it")); else if (found->track != nullptr) - ImGui::TextDisabled(STR_C("Read by %s %s of %s"), editor_track::event_keyword(found->list), found->read.c_str(), found->track->name().empty() ? "(noname)" : found->track->name().c_str()); + ImGui::TextDisabled(STR_C("Read by %s %s of %s"), editor_track::event_keyword(found->list), found->read.c_str(), found->track->name().empty() ? STR_C("(no name)") : found->track->name().c_str()); else ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), STR_C("No path has %s among its events: the train doesn't see it"), found->read.c_str()); - ImGui::TextDisabled("%s", STR_C("Name, plate and the linked signal: in the node properties. Del removes it with its event")); } void editor_mode::render_signal_ui() @@ -893,43 +899,78 @@ void editor_mode::render_signal_ui() ImGui::TextWrapped("%s", STR_C("No signal templates: no template of the scenery directory is described with category: signal. tools/editor/signal_headers.py writes the descriptions into the signal templates.")); return; } - if (ImGui::BeginTabBar("##signalkinds")) - { - for (int k = 0; k < kKindCount; ++k) + std::vector labels; + labels.reserve(kKindCount); + std::vector kinds; + for (int k = 0; k < kKindCount; ++k) + if (std::any_of(tool.templates.begin(), tool.templates.end(), [&](signal_template const &Item) { return Item.kind == kKinds[k]; })) { - if (std::none_of(tool.templates.begin(), tool.templates.end(), [&](signal_template const &Item) { return Item.kind == kKinds[k]; })) - continue; - if (ImGui::BeginTabItem(STR(kKindLabels[k]).c_str())) - { - tool.kind = k; - ImGui::EndTabItem(); - } + labels.push_back(STR(kKindLabels[k])); + kinds.push_back({k, labels.back().c_str()}); } - ImGui::EndTabBar(); - } + if (false == kinds.empty() && std::none_of(kinds.begin(), kinds.end(), [&](segment_option const &Option) { return Option.value == tool.kind; })) + tool.kind = kinds.front().value; + choice_chips("signalkinds", tool.kind, kinds); render_signal_palette(); auto const *armed{signal_armed()}; ImGui::Separator(); - ImGui::PushItemWidth(140.0f); - ImGui::InputText(STR_C("Name"), tool.name, sizeof(tool.name)); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Name of the next signal; it counts up after each one placed. Its events and memory cell are named after it")); - if (armed != nullptr && armed->plate) + if (begin_fields("##signalnames", 0.3f)) { - auto const automatic{format(STR_C("auto: %s"), plate_for({}, tool.name).c_str())}; - ImGui::InputTextWithHint(STR_C("Plate"), automatic.c_str(), tool.plate, sizeof(tool.plate)); + field_row("Name"); + ImGui::InputText("##signalname", tool.name, sizeof(tool.name)); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Texture of the plate from textures/tabl, e.g. a-12 or tob. Left empty: the one named like the signal, if there is one")); - auto const &plates{plate_textures()}; - auto const typed{lower(tool.plate)}; - auto const exists{typed.empty() || std::binary_search(plates.begin(), plates.end(), typed)}; - if (false == exists) + ImGui::SetTooltip("%s", STR_C("Name of the next signal; it counts up after each one placed. Its events and memory cell are named after it")); + ImGui::EndTable(); + } + ImGui::PushID("signalmore"); + if (ImGui::TreeNode(STR_C("More settings"))) + { + bool missing{false}; + if (begin_fields("##signalplace", 0.45f)) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR_C("no such texture")); + if (armed != nullptr && armed->plate) + { + auto const automatic{format(STR_C("auto: %s"), plate_for({}, tool.name).c_str())}; + field_row("Plate"); + ImGui::InputTextWithHint("##signalplate", automatic.c_str(), tool.plate, sizeof(tool.plate)); + auto const typing{ImGui::IsItemActive()}; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Texture of the plate from textures/tabl, e.g. a-12 or tob. Left empty: the one named like the signal, if there is one")); + auto const &plates{plate_textures()}; + auto const typed{lower(tool.plate)}; + auto const exists{typed.empty() || std::binary_search(plates.begin(), plates.end(), typed)}; + if (false == exists) + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR_C("no such texture")); + missing = typing || (false == exists && false == typed.empty()); + } + if (armed != nullptr && armed->linked) + { + field_row("Linked signal"); + ImGui::InputText("##linked", tool.linked, sizeof(tool.linked)); + item_tooltip("(p7) of the template: the warning signal a semaphore drives, the previous block signal, \"none\" for no link"); + } + auto offset{static_cast(EditorSettings.signal_offset())}; + field_row("From the axis (m)"); + if (ImGui::InputFloat("##signaloffset", &offset, 0.05f, 0.25f, "%.2f")) + { + EditorSettings.signal_offset(std::clamp(static_cast(offset), 0.0, 10.0)); + EditorSettings.save(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Distance of the mast from the axis of the track. 2.25 m is what the signals of the sceneries stand at")); + if (armed != nullptr && armed->mount == "gantry") + { + field_row("Over the rails (m)"); + ImGui::InputFloat("##signalheight", &tool.height, 0.1f, 0.5f, "%.2f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("A head for a gantry hangs over the axis of the track this high over the rail heads")); + } + ImGui::EndTable(); } - if (ImGui::IsItemActive() || (false == exists && false == typed.empty())) + if (missing) { + auto const &plates{plate_textures()}; + auto const typed{lower(tool.plate)}; int shown{0}; for (auto const &plate : plates) { @@ -943,28 +984,9 @@ void editor_mode::render_signal_ui() break; } } - } - if (armed != nullptr && armed->linked && ImGui::TreeNode(STR_C("Linked signal"))) - { - ImGui::InputText("##linked", tool.linked, sizeof(tool.linked)); - ImGui::TextDisabled("%s", STR_C("(p7) of the template: the warning signal a semaphore drives, the previous block signal, \"none\" for no link")); ImGui::TreePop(); } - auto offset{static_cast(EditorSettings.signal_offset())}; - if (ImGui::InputFloat(STR_C("From the axis (m)"), &offset, 0.05f, 0.25f, "%.2f")) - { - EditorSettings.signal_offset(std::clamp(static_cast(offset), 0.0, 10.0)); - EditorSettings.save(); - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Distance of the mast from the axis of the track. 2.25 m is what the signals of the sceneries stand at")); - if (armed != nullptr && armed->mount == "gantry") - { - ImGui::InputFloat(STR_C("Over the rails (m)"), &tool.height, 0.1f, 0.5f, "%.2f"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("A head for a gantry hangs over the axis of the track this high over the rail heads")); - } - ImGui::PopItemWidth(); + ImGui::PopID(); if (false == tool.status.empty()) ImGui::TextWrapped("%s", tool.status.c_str()); render_signal_selected(); diff --git a/application/editormodespeed.cpp b/application/editormodespeed.cpp index 9843c7b0..5eb8ea44 100644 --- a/application/editormodespeed.cpp +++ b/application/editormodespeed.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormode.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" +#include "application/editormodeshared.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" @@ -38,7 +39,7 @@ bool rail(TTrack const &Track) std::string speed_text(double const Speed) { - return Speed > 0.0 ? format("%.0f", Speed) : std::string{"none"}; + return Speed > 0.0 ? format("%.0f", Speed) : std::string{STR_C("(none)")}; } } // namespace @@ -183,7 +184,7 @@ void editor_mode::speed_apply(std::vector const &Indices, bool const Branch auto const speed{speed_check::suggested(safe)}; limit = limit > 0.0 ? std::min(limit, speed) : speed; } - auto const skipped{locked > 0 ? format(", %zu in the files which can't be changed are left as they are", locked) : std::string{}}; + auto const skipped{locked > 0 ? format(STR_C(", %zu in the files which can't be changed are left as they are"), locked) : std::string{}}; if (changes.empty()) { if (locked > 0) @@ -207,10 +208,10 @@ void editor_mode::speed_apply(std::vector const &Indices, bool const Branch void editor_mode::render_speed_body() { auto &state{m_speed}; - ImGui::TextDisabled(STR_C("Speed limits of the paths against the speed their geometry allows:\nradius and cant, cant ramps, curves without the transitions, switch branches")); - ImGui::RadioButton(STR_C("Line through the selected path"), &state.scope, 0); - ImGui::SameLine(); - ImGui::RadioButton(STR_C("Whole scenery"), &state.scope, 1); + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", STR_C("Checks that the speed on the tracks isn't above what their geometry allows")); + ImGui::PopTextWrapPos(); + segmented("speedscope", state.scope, {{0, STR_C("Line through the selected path")}, {1, STR_C("Whole scenery")}}); bool const running{state.next < state.queue.size()}; if (running) { @@ -220,32 +221,43 @@ void editor_mode::render_speed_body() if (ImGui::SmallButton(STR_C("Cancel"))) state.queue.resize(state.next); } - else if (ImGui::Button(STR_C("Check"))) + else { - speed_start(); + ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(family_colour(2, 0.55f))); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(family_colour(2, 0.75f))); + if (ImGui::Button(STR_C("Check"), ImVec2(-1.0f, std::round(ImGui::GetFrameHeight() * 1.3f)))) + speed_start(); + ImGui::PopStyleColor(2); + item_tooltip("Looks at the radius and the cant, the cant ramps, the curves without the transitions and the switch branches"); } - if (ImGui::TreeNode(STR_C("Limits"))) + if (ImGui::TreeNode(STR_C("Thresholds"))) { auto &options{state.options}; - ImGui::PushItemWidth(100.0f); - ImGui::InputDouble(STR_C("Unbalanced acceleration (m/s2)"), &options.norms.unbalanced, 0.0, 0.0, "%.2f"); - ImGui::InputDouble(STR_C("Switch branches (m/s2)"), &options.branch_unbalanced, 0.0, 0.0, "%.2f"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip(STR_C("Unbalanced acceleration allowed on the diverging tracks of the switches, which have no cant.\n0.65 m/s2 gives 40 km/h for R 190 m and 100 km/h for R 1200 m")); - ImGui::InputDouble(STR_C("Cant change rate (mm/s)"), &options.norms.cant_rate, 0.0, 0.0, "%.0f"); - ImGui::InputDouble(STR_C("Cant ramp 1:(k*V), k"), &options.norms.ramp_factor, 0.0, 0.0, "%.1f"); - ImGui::InputDouble(STR_C("Unbalanced acc. change rate (m/s3)"), &options.norms.jerk, 0.0, 0.0, "%.2f"); - ImGui::InputDouble(STR_C("Reference length (m)"), &options.reference_length, 0.0, 0.0, "%.0f"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip(STR_C("Length over which an abrupt change of the curvature or the cant is felt,\nabout the length of a car. Shorter: stricter for the curves without the transitions")); - ImGui::PopItemWidth(); + if (begin_fields("##speedlimits", 0.6f)) + { + auto const limit = [](char const *Label, char const *Id, double &Value, char const *Format) { + field_row(Label); + ImGui::InputDouble(Id, &Value, 0.0, 0.0, Format); + }; + limit("Unbalanced acceleration (m/s2)", "##speedunbalanced", options.norms.unbalanced, "%.2f"); + limit("Switch branches (m/s2)", "##speedbranch", options.branch_unbalanced, "%.2f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Unbalanced acceleration allowed on the diverging tracks of the switches, which have no cant.\n0.65 m/s2 gives 40 km/h for R 190 m and 100 km/h for R 1200 m")); + limit("Cant change rate (mm/s)", "##speedcantrate", options.norms.cant_rate, "%.0f"); + limit("Cant ramp 1:(k*V), k", "##speedramp", options.norms.ramp_factor, "%.1f"); + limit("Unbalanced acc. change rate (m/s3)", "##speedjerk", options.norms.jerk, "%.2f"); + limit("Reference length (m)", "##speedreference", options.reference_length, "%.0f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Length over which an abrupt change of the curvature or the cant is felt,\nabout the length of a car. Shorter: stricter for the curves without the transitions")); + ImGui::EndTable(); + } options.norms.unbalanced = std::max(0.05, options.norms.unbalanced); options.branch_unbalanced = std::max(0.05, options.branch_unbalanced); options.norms.cant_rate = std::max(1.0, options.norms.cant_rate); options.norms.ramp_factor = std::max(1.0, options.norms.ramp_factor); options.norms.jerk = std::max(0.01, options.norms.jerk); options.reference_length = std::clamp(options.reference_length, 2.0, 200.0); - ImGui::TextDisabled(STR_C("Change the limits, then Check again")); + ImGui::TextDisabled("%s", STR_C("Change the thresholds, then Check again")); ImGui::TreePop(); } if (false == state.status.empty()) @@ -254,12 +266,15 @@ void editor_mode::render_speed_body() return; if (state.history != m_history.size()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("The scenery changed since the check")); - ImGui::SetNextItemWidth(190.0f); - ImGui::Combo("##speedlimits", &state.limits, "All the paths\0With a speed limit set\0Without a speed limit\0"); + auto const *const within{STR_C("Within their limits too")}; + auto const checkbox{ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x + ImGui::CalcTextSize(within).x}; + ImGui::SetNextItemWidth(std::max(120.0f, ImGui::GetContentRegionAvail().x - checkbox - ImGui::GetStyle().ItemSpacing.x)); + char const *const limits[]{STR_C("All the paths"), STR_C("With a speed limit set"), STR_C("Without a speed limit")}; + ImGui::Combo("##speedlimits", &state.limits, limits, IM_ARRAYSIZE(limits)); if (ImGui::IsItemHovered()) - ImGui::SetTooltip(STR_C("Paths without a speed limit of their own run at the line speed,\nthose are listed whenever the geometry limits the speed at all")); + ImGui::SetTooltip("%s", STR_C("Paths without a speed limit of their own run at the line speed,\nthose are listed whenever the geometry limits the speed at all")); ImGui::SameLine(); - ImGui::Checkbox(STR_C("Within their limits too"), &state.show_all); + ImGui::Checkbox(within, &state.show_all); auto const listed{speed_listed()}; std::vector violated; for (auto const index : listed) @@ -267,8 +282,7 @@ void editor_mode::render_speed_body() violated.push_back(index); if (false == violated.empty()) { - ImGui::SameLine(); - if (ImGui::Button(format(STR_C("Set the safe speed on %zu paths"), violated.size()).c_str())) + if (ImGui::Button(format(STR_P("Set the safe speed on %zu track", "Set the safe speed on %zu tracks", static_cast(violated.size())), violated.size()).c_str(), ImVec2(-FLT_MIN, 0.0f))) speed_apply(violated, false); if (ImGui::IsItemHovered()) ImGui::SetTooltip(STR_C("Lowers the speed limit of each listed path to its safe speed, rounded down to 10 km/h (5 km/h below 40).\n" @@ -277,18 +291,18 @@ void editor_mode::render_speed_body() "When the main track needs a limit of its own, the lower of the two applies to both")); } auto const namewidth{std::max(120.0f, ImGui::GetContentRegionAvail().x * 0.3f)}; - ImGui::Columns(6, STR_C("speedcolumns"), false); + ImGui::Columns(6, "speedcolumns", false); ImGui::SetColumnWidth(0, namewidth + 8.0f); - for (auto const *header : {"Path", "Set", "Safe", "Suggested", "Cause", "R / cant"}) + for (auto const *header : {"Path", "Limit", "Safe", "Suggested", "Cause", "R / cant"}) { - ImGui::TextDisabled("%s", header); + ImGui::TextDisabled("%s", STR_C(header)); ImGui::NextColumn(); } ImGui::Columns(1); bool const chosen{state.selected >= 0 && state.selected < static_cast(state.results.size())}; auto const details{chosen ? ImGui::GetFrameHeightWithSpacing() + ImGui::GetTextLineHeightWithSpacing() * 5.0f : 0.0f}; ImGui::BeginChild("speedresults", ImVec2(0.0f, -details), true); - ImGui::Columns(6, STR_C("speedrows"), false); + ImGui::Columns(6, "speedrows", false); ImGui::SetColumnWidth(0, namewidth); ImGuiListClipper clipper; clipper.Begin(static_cast(listed.size())); @@ -300,9 +314,9 @@ void editor_mode::render_speed_body() bool const violated{verdict.violated(state.options)}; bool const bad{violated || verdict.warned(state.options)}; auto const colour{violated ? ImVec4(1.0f, 0.45f, 0.4f, 1.0f) : ImVec4(1.0f, 0.75f, 0.3f, 1.0f)}; - auto name{verdict.track->name().empty() ? std::string{"(noname)"} : verdict.track->name()}; + auto name{verdict.track->name().empty() ? STR("(no name)") : verdict.track->name()}; if (verdict.track->eType == tt_Switch) - name += verdict.path == 0 ? " (main)" : " (branch)"; + name += verdict.path == 0 ? STR_C(" (main)") : STR_C(" (branch)"); if (bad) ImGui::PushStyleColor(ImGuiCol_Text, colour); if (ImGui::Selectable((name + "##speed" + std::to_string(index)).c_str(), state.selected == index, ImGuiSelectableFlags_SpanAllColumns)) @@ -322,7 +336,7 @@ void editor_mode::render_speed_body() ImGui::NextColumn(); ImGui::TextUnformatted(verdict.safe >= state.options.top ? "-" : format("%.0f", speed_check::suggested(verdict.safe)).c_str()); ImGui::NextColumn(); - ImGui::TextUnformatted(speed_check::describe(verdict.why)); + ImGui::TextUnformatted(STR_C(speed_check::describe(verdict.why))); ImGui::NextColumn(); if (verdict.radius > 0.0) ImGui::Text(STR_C("%.0f m / %.0f mm"), verdict.radius, verdict.cant); diff --git a/application/editormodestraights.cpp b/application/editormodestraights.cpp index f68114fc..992bc0a4 100644 --- a/application/editormodestraights.cpp +++ b/application/editormodestraights.cpp @@ -302,33 +302,47 @@ glm::dvec3 editor_mode::snap_straight_direction(glm::dvec3 const &Pivot, glm::dv void editor_mode::render_straights_ui() { auto &state{m_straights}; - ImGui::TextDisabled(STR_C("Straights recognized from chains of collinear straight paths.\nLMB: select a path to see the straight it belongs to")); - - ImGui::PushItemWidth(100.0f); - double angle{glm::degrees(state.tolerance.angle)}; - if (ImGui::InputDouble(STR_C("Angle tolerance (deg)"), &angle, 0.0, 0.0, "%.4f")) - state.tolerance.angle = glm::radians(std::max(0.0, angle)); - item_tooltip("Largest change of the direction between the paths of one straight"); - ImGui::InputDouble(STR_C("Offset tolerance (m)"), &state.tolerance.offset, 0.0, 0.0, "%.3f"); - item_tooltip("Largest distance of the paths from the common axis of the straight"); - ImGui::InputDouble(STR_C("Treat as straight above R (m)"), &state.tolerance.radius, 0.0, 0.0, "%.0f"); - item_tooltip("Curves of a larger radius count as straight"); - ImGui::InputDouble(STR_C("Minimum length (m)"), &state.minimum_length, 0.0, 0.0, "%.0f"); - item_tooltip("Shorter straights aren't listed"); - ImGui::PopItemWidth(); - if (ImGui::Button(STR_C("Recognize straights in the scenery"))) + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", STR_C("Straights recognized from chains of collinear straight paths.\nLMB: select a path to see the straight it belongs to")); + ImGui::PopTextWrapPos(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.42f, 0.24f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.22f, 0.52f, 0.30f, 1.0f)); + if (ImGui::Button(STR_C("Recognize straights in the scenery"), ImVec2(-FLT_MIN, ImGui::GetFrameHeight() * 1.4f))) { state.found = editor_track::find_straights(state.minimum_length, state.tolerance); state.listed = -1; state.current_for = nullptr; } + ImGui::PopStyleColor(2); + if (ImGui::TreeNode(STR_C("What counts as one straight"))) + { + if (begin_fields("##straighttolerance", 0.55f)) + { + double angle{glm::degrees(state.tolerance.angle)}; + field_row("Angle tolerance (deg)"); + if (ImGui::InputDouble("##straightangle", &angle, 0.0, 0.0, "%.4f")) + state.tolerance.angle = glm::radians(std::max(0.0, angle)); + item_tooltip("Largest change of the direction between the paths of one straight"); + field_row("Offset tolerance (m)"); + ImGui::InputDouble("##straightoffset", &state.tolerance.offset, 0.0, 0.0, "%.3f"); + item_tooltip("Largest distance of the paths from the common axis of the straight"); + field_row("Treat as straight above R (m)"); + ImGui::InputDouble("##straightradius", &state.tolerance.radius, 0.0, 0.0, "%.0f"); + item_tooltip("Curves of a larger radius count as straight"); + field_row("Minimum length (m)"); + ImGui::InputDouble("##straightminimum", &state.minimum_length, 0.0, 0.0, "%.0f"); + item_tooltip("Shorter straights aren't listed"); + ImGui::EndTable(); + } + ImGui::TreePop(); + } if (false == state.found.empty()) { + ImGui::AlignTextToFramePadding(); ImGui::Text(STR_C("%zu straights"), state.found.size()); ImGui::SameLine(); - ImGui::PushItemWidth(150.0f); + ImGui::SetNextItemWidth(-FLT_MIN); ImGui::InputTextWithHint("##straightfilter", STR_C("path name"), state.filter, sizeof(state.filter)); - ImGui::PopItemWidth(); ImGui::BeginChild("##straights", ImVec2(0.0f, 160.0f), true); for (int i = 0; i < static_cast(state.found.size()); ++i) { @@ -359,30 +373,55 @@ void editor_mode::render_straight_ui() ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", state.status.c_str()); return; } - ImGui::TextUnformatted(describe(line).c_str()); + metric_tiles({{format("%.2f m", line.length), STR_C("length"), ImGui::GetColorU32(ImGuiCol_Text)}, + {format("%.2f\xc2\xb0", line.azimuth), STR_C("azimuth"), ImGui::GetColorU32(ImGuiCol_Text)}, + {format("%+.2f \xe2\x80\xb0", line.grade * 1000.0), STR_C("grade"), ImGui::GetColorU32(ImGuiCol_Text)}, + {format("%zu", line.tracks.size()), STR_C("paths"), ImGui::GetColorU32(ImGuiCol_TextDisabled)}}); if (false == m_straights.neighbours.empty()) ImGui::TextDisabled("%s", STR_C("Nearby straight: the diamond snaps parallel at 4.00 / 4.50 / 4.75 m")); - auto const toolbutton = [&](char const *Label, int const Tool, char const *Tooltip) { + // what else can be done with the straight, as tiles with a sketch of it + auto const gap{std::round(ImGui::GetFontSize() * 0.3f)}; + auto const width{std::floor((ImGui::GetContentRegionAvail().x - 2.0f * gap) / 3.0f)}; + auto const toolbutton = [&](char const *Label, int const Tool, char const *Tooltip, std::function const &Sketch) { bool const active{state.tool == Tool}; - if (active) - ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); - if (ImGui::Button(Label)) + if (sketch_tile(Label, Label, active, width, Sketch)) { state.tool = active ? 0 : Tool; state.tool_placed = false; state.handle = -1; } - if (active) - ImGui::PopStyleColor(); item_tooltip(Tooltip); }; - toolbutton(STR_C("Break"), 1, STR_C("Turns the rest of the straight from a point, a curve goes at the break (Ctrl+drag in the view does the same)")); - ImGui::SameLine(); - toolbutton(STR_C("S-curve"), 2, STR_C("Shifts a piece of the straight sideways around an obstacle and back: four clicks on the straight (Shift+click in the view does the same)")); - ImGui::SameLine(); + auto const at = [](ImVec2 const Low, ImVec2 const High, float const X, float const Y) { return ImVec2(Low.x + (High.x - Low.x) * X, Low.y + (High.y - Low.y) * Y); }; + toolbutton(STR_C("Break"), 1, STR_C("Turns the rest of the straight from a point, a curve goes at the break (Ctrl+drag in the view does the same)"), [&](ImDrawList &List, ImVec2 Low, ImVec2 High, ImU32 Line, ImU32 Made) { + List.AddLine(at(Low, High, 0.0f, 0.8f), at(Low, High, 1.0f, 0.8f), (Line & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 60), 1.0f); + List.AddLine(at(Low, High, 0.0f, 0.8f), at(Low, High, 0.4f, 0.8f), Line, 2.0f); + List.AddBezierQuadratic(at(Low, High, 0.4f, 0.8f), at(Low, High, 0.62f, 0.8f), at(Low, High, 1.0f, 0.1f), Made, 2.0f); + }); + ImGui::SameLine(0.0f, gap); + toolbutton(STR_C("S-curve"), 2, STR_C("Shifts a piece of the straight sideways around an obstacle and back: four clicks on the straight (Shift+click in the view does the same)"), [&](ImDrawList &List, ImVec2 Low, ImVec2 High, ImU32 Line, ImU32 Made) { + List.AddLine(at(Low, High, 0.0f, 0.8f), at(Low, High, 1.0f, 0.8f), (Line & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 60), 1.0f); + List.AddLine(at(Low, High, 0.0f, 0.8f), at(Low, High, 0.15f, 0.8f), Line, 2.0f); + List.AddBezierCubic(at(Low, High, 0.15f, 0.8f), at(Low, High, 0.3f, 0.8f), at(Low, High, 0.25f, 0.2f), at(Low, High, 0.42f, 0.2f), Made, 2.0f); + List.AddLine(at(Low, High, 0.42f, 0.2f), at(Low, High, 0.58f, 0.2f), Made, 2.0f); + List.AddBezierCubic(at(Low, High, 0.58f, 0.2f), at(Low, High, 0.75f, 0.2f), at(Low, High, 0.7f, 0.8f), at(Low, High, 0.85f, 0.8f), Made, 2.0f); + List.AddLine(at(Low, High, 0.85f, 0.8f), at(Low, High, 1.0f, 0.8f), Line, 2.0f); + List.AddCircleFilled(at(Low, High, 0.5f, 0.8f), 2.5f, IM_COL32(230, 90, 70, 255)); + }); + ImGui::SameLine(0.0f, gap); bool const member{in_straight_set(line)}; - if (ImGui::Button(member ? STR_C("Leave the group") : STR_C("Move with others"))) + auto const grouped{sketch_tile("##group", member ? STR_C("Leave the group") : STR_C("Move with others"), member, width, [&](ImDrawList &List, ImVec2 Low, ImVec2 High, ImU32 Line, ImU32 Made) { + List.AddLine(at(Low, High, 0.0f, 0.2f), at(Low, High, 1.0f, 0.2f), Line, 2.0f); + List.AddLine(at(Low, High, 0.0f, 0.8f), at(Low, High, 1.0f, 0.8f), Line, 2.0f); + for (auto const x : {0.3f, 0.7f}) + { + List.AddLine(at(Low, High, x, 0.32f), at(Low, High, x, 0.68f), Made, 1.5f); + List.AddTriangleFilled(at(Low, High, x - 0.08f, 0.38f), at(Low, High, x + 0.08f, 0.38f), at(Low, High, x, 0.26f), Made); + List.AddTriangleFilled(at(Low, High, x - 0.08f, 0.62f), at(Low, High, x + 0.08f, 0.62f), at(Low, High, x, 0.74f), Made); + } + })}; + if (grouped) { if (member) state.set.erase(std::remove_if(state.set.begin(), state.set.end(), [&](editor_track::straight const &Member) { return std::any_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return std::find(Member.tracks.begin(), Member.tracks.end(), Track) != Member.tracks.end(); }); }), state.set.end()); @@ -392,17 +431,24 @@ void editor_mode::render_straight_ui() item_tooltip("Straights of a group move together and stay parallel; Alt+click in the view adds another one"); if (state.tool != 0) { - ImGui::PushItemWidth(100.0f); - ImGui::InputDouble(STR_C("Curve R (m, 0: automatic)"), &state.curve_radius, 0.0, 0.0, "%.0f"); + ImGui::TextDisabled("%s", STR_C("Curve R (m, 0: automatic)")); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::InputDouble("##straightcurve", &state.curve_radius, 10.0, 100.0, "%.0f"); + state.curve_radius = std::max(0.0, state.curve_radius); item_tooltip("Radius of the curves made by the break and the S-curve; automatic: 1.5 times the minimum radius for the speed of the line"); - ImGui::Checkbox(STR_C("Transitions from the line speed"), &state.auto_transitions); - item_tooltip("Lengths of the transition curves from the speed of the paths and the limits of the curve design"); + value_chips("straightcurves", state.curve_radius, {0.0, 300.0, 600.0, 1000.0, 2000.0}, "%.0f"); + ImGui::TextDisabled("%s", STR_C("Transition curves")); + int transitions{state.auto_transitions ? 0 : 1}; + if (segmented("straighttransitions", transitions, + {{0, STR_C("From the line speed"), "Lengths of the transition curves from the speed of the paths and the limits of the curve design"}, + {1, STR_C("Given length"), "The same length of the transition curves at every curve"}})) + state.auto_transitions = transitions == 0; if (false == state.auto_transitions) { - ImGui::SameLine(); - ImGui::InputDouble(STR_C("Transition (m)"), &state.transition, 0.0, 0.0, "%.1f"); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::InputDouble("##straighttransition", &state.transition, 1.0, 10.0, "%.1f m"); + state.transition = std::max(0.0, state.transition); } - ImGui::PopItemWidth(); } if (false == state.status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", state.status.c_str()); @@ -423,29 +469,39 @@ void editor_mode::render_straight_ui() } if (ImGui::CollapsingHeader(STR_C("Exact values"))) { - ImGui::PushItemWidth(-110.0f); - ImGui::InputScalarN(STR_C("Start (x, y, z)"), ImGuiDataType_Double, glm::value_ptr(state.edit_start), 3, nullptr, nullptr, "%.3f"); - ImGui::InputScalarN(STR_C("End (x, y, z)"), ImGuiDataType_Double, glm::value_ptr(state.edit_end), 3, nullptr, nullptr, "%.3f"); - ImGui::PopItemWidth(); - if (ImGui::Button(STR_C("Apply ends"))) + if (begin_fields("##straightexact", 0.3f)) + { + field_row("Start (x, y, z)"); + ImGui::InputScalarN("##straightstart", ImGuiDataType_Double, glm::value_ptr(state.edit_start), 3, nullptr, nullptr, "%.3f"); + field_row("End (x, y, z)"); + ImGui::InputScalarN("##straightend", ImGuiDataType_Double, glm::value_ptr(state.edit_end), 3, nullptr, nullptr, "%.3f"); + ImGui::EndTable(); + } + if (ImGui::Button(STR_C("Apply ends"), ImVec2(-FLT_MIN, 0.0f))) straight_apply(line, state.edit_start, state.edit_end); - ImGui::PushItemWidth(120.0f); - ImGui::InputDouble(STR_C("Length (m)"), &state.edit_length, 0.0, 0.0, "%.3f"); - ImGui::SameLine(); - if (ImGui::Button(STR_C("Set from the start##length"))) + if (begin_fields("##straightmeasures", 0.3f)) { - glm::dvec3 const direction{line.direction.x, line.grade, line.direction.y}; - straight_apply(line, line.start, line.start + direction * std::max(0.1, state.edit_length)); + field_row("Length (m)"); + ImGui::InputDouble("##straightlength", &state.edit_length, 0.0, 0.0, "%.3f"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + if (ImGui::Button(STR_C("Set from the start##length"), ImVec2(-FLT_MIN, 0.0f))) + { + glm::dvec3 const direction{line.direction.x, line.grade, line.direction.y}; + straight_apply(line, line.start, line.start + direction * std::max(0.1, state.edit_length)); + } + field_row("Azimuth (deg)"); + ImGui::InputDouble("##straightazimuth", &state.edit_azimuth, 0.0, 0.0, "%.5f"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + if (ImGui::Button(STR_C("Turn around the start"), ImVec2(-FLT_MIN, 0.0f))) + { + auto const azimuth{glm::radians(state.edit_azimuth)}; + glm::dvec3 const direction{std::sin(azimuth), line.grade, std::cos(azimuth)}; + straight_apply(line, line.start, line.start + direction * line.length); + } + ImGui::EndTable(); } - ImGui::InputDouble(STR_C("Azimuth (deg)"), &state.edit_azimuth, 0.0, 0.0, "%.5f"); - ImGui::SameLine(); - if (ImGui::Button(STR_C("Turn around the start"))) - { - auto const azimuth{glm::radians(state.edit_azimuth)}; - glm::dvec3 const direction{std::sin(azimuth), line.grade, std::cos(azimuth)}; - straight_apply(line, line.start, line.start + direction * line.length); - } - ImGui::PopItemWidth(); } } @@ -914,7 +970,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co std::string error; editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error, true); straight_refresh(); - state.status = STR_C("Done, select a curve to adjust it in the Curve mode"); + state.status = STR_C("Done: click a curve to adjust it; Ctrl+Z takes it back"); } double editor_mode::straight_tool_radius(editor_track::straight const &Line) const diff --git a/application/editormodesweep.cpp b/application/editormodesweep.cpp index c77da640..8b817798 100644 --- a/application/editormodesweep.cpp +++ b/application/editormodesweep.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormode.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" +#include "application/editormodeshared.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIncludeInfo.hpp" @@ -101,14 +102,16 @@ void platform_edge(sweep_node::state &State, std::string const &Model, double co bool platform_distance() { double const distances[]{1.725, 1.675, 1.650}; - char const *labels[]{STR_C("1.725 m: lines of out-of-gauge loads, older platforms"), STR_C("1.675 m: nominal by the 0.55 and 0.76 m platforms"), STR_C("1.650 m: the least allowed in service")}; auto const current{EditorSettings.platform_edge()}; int chosen{0}; for (int i = 0; i < IM_ARRAYSIZE(distances); ++i) if (std::abs(current - distances[i]) < std::abs(current - distances[chosen])) chosen = i; - ImGui::SetNextItemWidth(300.0f); - if (false == ImGui::Combo(STR_C("edge of the platform from the axis"), &chosen, labels, IM_ARRAYSIZE(labels))) + ImGui::TextDisabled("%s", STR_C("The edge of the platform from the axis of the track")); + if (false == segmented("platformdistance", chosen, + {{0, "1.725 m", "1.725 m: lines of out-of-gauge loads, older platforms"}, + {1, "1.675 m", "1.675 m: nominal by the 0.55 and 0.76 m platforms"}, + {2, "1.650 m", "1.650 m: the least allowed in service"}})) return false; EditorSettings.platform_edge(distances[chosen]); EditorSettings.save(); @@ -718,7 +721,9 @@ void editor_mode::render_sweep_ui() auto &tool{m_sweep}; sweep_curve(); sweep_scan_models(); + ImGui::PushTextWrapPos(0.0f); ImGui::TextDisabled("%s", STR_C("A model bent to follow the track, or repeated along it: platforms, walls, fences, barriers, lamps")); + ImGui::PopTextWrapPos(); bool changed{false}; auto const edited = [&](bool const Changed) { changed |= Changed; }; @@ -814,13 +819,14 @@ void editor_mode::render_sweep_ui() } if (false == tool.parameters.empty() && ImGui::TreeNodeEx(STR_C("Parameters of the template"), ImGuiTreeNodeFlags_DefaultOpen)) { - for (std::size_t i = 0; i < tool.parameters.size(); ++i) + auto const table{begin_fields("##sweepparameters", 0.45f)}; + for (std::size_t i = 0; table && i < tool.parameters.size(); ++i) { ImGui::PushID(static_cast(i)); - ImGui::SetNextItemWidth(160.0f); auto const label{i < tool.parameter_labels.size() ? tool.parameter_labels[i] : "p" + std::to_string(i + 1)}; + field_row(label.c_str(), false); auto const *choices{i < tool.parameter_choices.size() && false == tool.parameter_choices[i].empty() ? &tool.parameter_choices[i] : nullptr}; - if (choices != nullptr && ImGui::BeginCombo(label.c_str(), tool.parameters[i].data())) + if (choices != nullptr && ImGui::BeginCombo("##parameter", tool.parameters[i].data())) { for (auto const &choice : *choices) if (ImGui::Selectable(choice.c_str(), choice == tool.parameters[i].data())) @@ -833,7 +839,7 @@ void editor_mode::render_sweep_ui() } else if (choices == nullptr) { - ImGui::InputText(label.c_str(), tool.parameters[i].data(), tool.parameters[i].size()); + ImGui::InputText("##parameter", tool.parameters[i].data(), tool.parameters[i].size()); if (ImGui::IsItemDeactivatedAfterEdit()) { tool.measured.clear(); @@ -842,7 +848,11 @@ void editor_mode::render_sweep_ui() } ImGui::PopID(); } + if (table) + ImGui::EndTable(); + ImGui::PushTextWrapPos(0.0f); ImGui::TextDisabled("%s", STR_C("The location and the rotation are zeros, the template is laid out along the track")); + ImGui::PopTextWrapPos(); ImGui::TreePop(); } sweep_measure(); @@ -853,109 +863,123 @@ void editor_mode::render_sweep_ui() auto &settings{tool.settings}; int mode{settings.bend ? 0 : 1}; - edited(ImGui::RadioButton(STR_C("Bent along the track"), &mode, 0)); - ImGui::SameLine(); - edited(ImGui::RadioButton(STR_C("Repeated"), &mode, 1)); + edited(segmented("sweepmode", mode, {{0, STR_C("Bent along the track"), "One model stretched and bent along the curve of the track"}, {1, STR_C("Repeated"), "Copies of the model one after another, each one straight"}})); settings.bend = mode == 0; - if (false == settings.bend) + if (false == settings.bend && begin_fields("##sweepstep", 0.45f)) { - ImGui::SameLine(); - ImGui::SetNextItemWidth(80.0f); - edited(ImGui::InputDouble(STR_C("every (m, 0: its length)"), &settings.step, 0.0, 0.0, "%.2f")); + field_row("every (m, 0: its length)"); + edited(ImGui::InputDouble("##sweepevery", &settings.step, 0.1, 1.0, "%.2f")); settings.step = std::max(0.0, settings.step); + ImGui::EndTable(); } - ImGui::TextDisabled("%s", STR_C("Platform edge:")); - for (auto const height : {0.55, 0.76, 0.96}) + if (platform_model(tool.model) || settings.platform) { - ImGui::SameLine(); - if (ImGui::SmallButton(format("%.2f m##platform", height).c_str())) + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", STR_C("Platform height:")); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, ImGui::GetFrameHeight() * 0.5f); + for (auto const height : {0.55, 0.76, 0.96}) { - platform_edge(settings, tool.model, 0.0); - settings.height = height; - settings.face = true; + ImGui::SameLine(); + if (ImGui::Button(format("%.2f m##platform", height).c_str())) + { + platform_edge(settings, tool.model, 0.0); + settings.height = height; + settings.face = true; + changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("The edge of the platform %.3f m from the axis of the track, its top this high over the rail top"), EditorSettings.platform_edge()); + } + ImGui::PopStyleVar(); + if (platform_distance() && settings.platform) + { + settings.lateral = std::copysign(EditorSettings.platform_edge(), settings.lateral == 0.0 ? 1.0 : settings.lateral); changed = true; } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip(STR_C("The edge of the platform %.3f m from the axis of the track, its top this high over the rail top"), EditorSettings.platform_edge()); } - if (platform_distance() && settings.platform) + if (false == tool.curve.empty() && tool.model_length > 0.0) { - settings.lateral = std::copysign(EditorSettings.platform_edge(), settings.lateral == 0.0 ? 1.0 : settings.lateral); - changed = true; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.42f, 0.24f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.22f, 0.52f, 0.30f, 1.0f)); + if (ImGui::Button(STR_C("Lay it along the selected path"), ImVec2(-FLT_MIN, ImGui::GetFrameHeight() * 1.4f))) + sweep_create(); + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("%.2f m %s of the axis, %.2f m over the rail top"), std::abs(settings.lateral), settings.lateral >= 0.0 ? STR_C("on the right") : STR_C("on the left"), settings.height); } - ImGui::TextDisabled(STR_C("%.2f m %s of the axis, %.2f m over the rail top"), std::abs(settings.lateral), settings.lateral >= 0.0 ? STR_C("right") : STR_C("left"), settings.height); - if (false == tool.curve.empty() && tool.model_length > 0.0 && ImGui::Button(STR_C("Lay it along the selected path"))) - sweep_create(); changed = false; if (ImGui::CollapsingHeader(STR_C("More settings"))) { - ImGui::SetNextItemWidth(200.0f); - edited(ImGui::InputText(STR_C("Skin"), tool.skin, sizeof(tool.skin), ImGuiInputTextFlags_EnterReturnsTrue)); - edited(ImGui::IsItemDeactivatedAfterEdit()); - ImGui::PushItemWidth(90.0f); - edited(ImGui::InputDouble(STR_C("Sideways (m, + right)"), &settings.lateral, 0.0, 0.0, "%.3f")); - ImGui::SameLine(); - ImGui::SetNextItemWidth(150.0f); - char const *sides[] = {STR_C("model origin"), STR_C("side facing the track"), STR_C("far side"), STR_C("middle")}; - edited(ImGui::Combo("##sweepsideanchor", &settings.side_anchor, sides, IM_ARRAYSIZE(sides))); - edited(ImGui::InputDouble(STR_C("Up (m, from the rail top)"), &settings.height, 0.0, 0.0, "%.3f")); - ImGui::SameLine(); - ImGui::SetNextItemWidth(150.0f); - char const *heights[] = {STR_C("model origin"), STR_C("bottom"), STR_C("top"), STR_C("top of the edge by the track")}; - edited(ImGui::Combo("##sweepheightanchor", &settings.height_anchor, heights, IM_ARRAYSIZE(heights))); - if (settings.bend) + if (begin_fields("##sweepplace", 0.4f)) { - edited(ImGui::InputDouble(STR_C("Copy length (m, 0: the model's)"), &settings.step, 0.0, 0.0, "%.2f")); - settings.step = std::max(0.0, settings.step); + field_row("Texture (skin)"); + edited(ImGui::InputText("##sweepskin", tool.skin, sizeof(tool.skin), ImGuiInputTextFlags_EnterReturnsTrue)); + edited(ImGui::IsItemDeactivatedAfterEdit()); + auto const split = [] { return std::floor((ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.4f); }; + field_row("Sideways (m, + right)"); + ImGui::SetNextItemWidth(split()); + edited(ImGui::InputDouble("##sweeplateral", &settings.lateral, 0.0, 0.0, "%.3f")); + ImGui::SameLine(); + ImGui::SetNextItemWidth(-FLT_MIN); + char const *sides[] = {STR_C("model origin"), STR_C("side facing the track"), STR_C("far side"), STR_C("middle")}; + edited(ImGui::Combo("##sweepsideanchor", &settings.side_anchor, sides, IM_ARRAYSIZE(sides))); + field_row("Up (m, from the rail top)"); + ImGui::SetNextItemWidth(split()); + edited(ImGui::InputDouble("##sweepheight", &settings.height, 0.0, 0.0, "%.3f")); + ImGui::SameLine(); + ImGui::SetNextItemWidth(-FLT_MIN); + char const *heights[] = {STR_C("model origin"), STR_C("bottom"), STR_C("top"), STR_C("top of the edge by the track")}; + edited(ImGui::Combo("##sweepheightanchor", &settings.height_anchor, heights, IM_ARRAYSIZE(heights))); + if (settings.bend) + { + field_row("Copy length (m, 0: the model's)"); + edited(ImGui::InputDouble("##sweepcopy", &settings.step, 0.0, 0.0, "%.2f")); + settings.step = std::max(0.0, settings.step); + } + field_row("Axis of the model along the track"); + if (segmented("sweepaxis", tool.axis, {{0, "auto"}, {1, "z"}, {2, "x"}})) + { + tool.measured.clear(); + changed = true; + } + ImGui::EndTable(); } - ImGui::PopItemWidth(); - char const *axes[] = {"auto", "z", "x"}; - ImGui::SetNextItemWidth(70.0f); - if (ImGui::Combo(STR_C("Axis of the model along the track"), &tool.axis, axes, IM_ARRAYSIZE(axes))) - { - tool.measured.clear(); + auto const platform{settings.platform}; + auto const toggled{toggle_chips("sweepflags", {{STR_C("Edge to the track"), &settings.face, "The model is turned so its -x side faces the track, the way the platforms and the walls of the scenery are made"}, + {STR_C("Turned around"), &settings.flip}, + {STR_C("Mirrored"), &settings.mirror}, + {STR_C("Leans with the cant"), &settings.tilt}, + {STR_C("Gauge widening in the curves"), &settings.widen, + "The offset grows by the widening of the gauge below the platforms: 3.75/R in the curves of R >= 250 m,\n" + "more in the sharper ones, and on the inner side of a canted curve by the cant times the height of the edge / 1.5;\n" + "it begins 20 m (inner side) and 26 m (outer side) ahead of the curve, as in the gauge check"}, + {STR_C("Platform"), &settings.platform, + "Puts the edge facing the track at the distance chosen from its axis, set back by the widening of the gauge in the curves.\n" + "Stands on the plane of the rail heads carried on to its edge: lower on the inner side of a canted curve,\n" + "higher on the outer one; on the inner side also set back by the cant times the height of the edge / 1.5"}})}; + if (toggled >= 0) changed = true; - } - edited(ImGui::Checkbox(STR_C("Edge to the track"), &settings.face)); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("The model is turned so its -x side faces the track, the way the platforms and the walls of the scenery are made")); - ImGui::SameLine(); - edited(ImGui::Checkbox(STR_C("Turned around"), &settings.flip)); - ImGui::SameLine(); - edited(ImGui::Checkbox(STR_C("Mirrored"), &settings.mirror)); - ImGui::SameLine(); - edited(ImGui::Checkbox(STR_C("Leans with the cant"), &settings.tilt)); - edited(ImGui::Checkbox(STR_C("Set back by the widening of the structure gauge in the curves"), &settings.widen)); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("The offset grows by the widening of the gauge below the platforms: 3.75/R in the curves of R >= 250 m,\n" - "more in the sharper ones, and on the inner side of a canted curve by the cant times the height of the edge / 1.5;\n" - "it begins 20 m (inner side) and 26 m (outer side) ahead of the curve, as in the gauge check")); - if (ImGui::Checkbox(STR_C("Platform"), &settings.platform)) - { - if (settings.platform) - platform_edge(settings, tool.model, 0.0); - changed = true; - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Puts the edge facing the track at the distance chosen from its axis, set back by the widening of the gauge in the curves.\n" - "Stands on the plane of the rail heads carried on to its edge: lower on the inner side of a canted curve,\n" - "higher on the outer one; on the inner side also set back by the cant times the height of the edge / 1.5")); + if (settings.platform && false == platform) + platform_edge(settings, tool.model, 0.0); ImGui::TextDisabled("%s", STR_C("Length")); - ImGui::SameLine(); - edited(ImGui::RadioButton(STR_C("as the original"), &tool.length_mode, 0)); - ImGui::SameLine(); - edited(ImGui::RadioButton(STR_C("given"), &tool.length_mode, 1)); - ImGui::SameLine(); - edited(ImGui::RadioButton(STR_C("the whole stretch"), &tool.length_mode, 2)); - ImGui::PushItemWidth(90.0f); - edited(ImGui::InputDouble(STR_C("From (m)"), &settings.from, 0.0, 0.0, "%.1f")); - ImGui::SameLine(); - if (tool.length_mode == 1) - edited(ImGui::InputDouble(STR_C("Length (m)"), &tool.length, 0.0, 0.0, "%.1f")); - else if (tool.length_mode == 2) - edited(ImGui::InputDouble(STR_C("To (m, -1: the end)"), &settings.to, 0.0, 0.0, "%.1f")); - ImGui::PopItemWidth(); + edited(segmented("sweeplength", tool.length_mode, {{0, STR_C("as the original")}, {1, STR_C("given")}, {2, STR_C("the whole stretch")}})); + if (begin_fields("##sweepstretch", 0.4f)) + { + field_row("From (m)"); + edited(ImGui::InputDouble("##sweepfrom", &settings.from, 0.0, 0.0, "%.1f")); + if (tool.length_mode == 1) + { + field_row("Length (m)"); + edited(ImGui::InputDouble("##sweeplength", &tool.length, 0.0, 0.0, "%.1f")); + } + else if (tool.length_mode == 2) + { + field_row("To (m, -1: the end)"); + edited(ImGui::InputDouble("##sweepto", &settings.to, 0.0, 0.0, "%.1f")); + } + ImGui::EndTable(); + } settings.from = std::max(0.0, settings.from); if (tool.model_length > 0.0) { @@ -1326,7 +1350,7 @@ bool editor_mode::bend_definition(bend_source const &Source, TTrack &Track, swee for (auto const &record : records) Tracks.push_back(record.track); auto const residual{glm::degrees(std::acos(std::clamp(std::abs(glm::dot(direction, forward)), 0.0, 1.0)))}; - tool.status = residual > 1.0 ? format(STR_C("The model stands %.1f deg off the track, bent it'll follow the track"), residual) : std::string{}; + tool.status = residual > 1.0 ? format(STR_C("The model stands %.1f° off the track, bent it'll follow the track"), residual) : std::string{}; return true; } @@ -1612,25 +1636,30 @@ void editor_mode::render_bend() char const *sides[] = {STR_C("model origin"), STR_C("side facing the track"), STR_C("far side"), STR_C("middle"), STR_C("picked point")}; char const *heights[] = {STR_C("model origin"), STR_C("bottom"), STR_C("top"), STR_C("top of the edge by the track"), STR_C("picked point")}; auto anchors = [&](bool &Changed) { - ImGui::SetNextItemWidth(170.0f); - Changed |= ImGui::Combo(STR_C("along the track"), &tool.along_anchor, alongs, IM_ARRAYSIZE(alongs)); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Reference point: the model bends about it, the line through it keeps its length, the offsets are its place")); - ImGui::SetNextItemWidth(170.0f); - Changed |= ImGui::Combo(STR_C("across"), &tool.side_anchor, sides, IM_ARRAYSIZE(sides)); - ImGui::SetNextItemWidth(170.0f); - Changed |= ImGui::Combo(STR_C("up"), &tool.height_anchor, heights, IM_ARRAYSIZE(heights)); - if (tool.along_anchor == 4 || tool.side_anchor == 4 || tool.height_anchor == 4) + ImGui::TextDisabled("%s", STR_C("Reference point")); + if (begin_fields("##bendanchors", 0.35f)) { - ImGui::SetNextItemWidth(220.0f); - glm::vec3 point{tool.point}; - if (ImGui::DragFloat3(STR_C("point (x, y, z of the model)"), &point.x, 0.01f, 0.0f, 0.0f, "%.3f")) + field_row("Along the track"); + Changed |= ImGui::Combo("##bendalong", &tool.along_anchor, alongs, IM_ARRAYSIZE(alongs)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Reference point: the model bends about it, the line through it keeps its length, the offsets are its place")); + field_row("Across"); + Changed |= ImGui::Combo("##bendacross", &tool.side_anchor, sides, IM_ARRAYSIZE(sides)); + field_row("Up"); + Changed |= ImGui::Combo("##bendup", &tool.height_anchor, heights, IM_ARRAYSIZE(heights)); + if (tool.along_anchor == 4 || tool.side_anchor == 4 || tool.height_anchor == 4) { - tool.point = glm::dvec3(point); - Changed = true; + glm::vec3 point{tool.point}; + field_row("Point (x, y, z of the model)"); + if (ImGui::DragFloat3("##bendpoint", &point.x, 0.01f, 0.0f, 0.0f, "%.3f")) + { + tool.point = glm::dvec3(point); + Changed = true; + } } + ImGui::EndTable(); } - if (ImGui::Button(tool.picking ? STR_C("Click the point on the model...") : STR_C("Pick the point on the model"))) + if (ImGui::Button(tool.picking ? STR_C("Click the point on the model...") : STR_C("Pick the point on the model"), ImVec2(-FLT_MIN, 0.0f))) tool.picking = false == tool.picking; if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("The next click in the view sets the reference point where it lands on the model")); @@ -1653,88 +1682,117 @@ void editor_mode::render_bend() tool.side_anchor = state.side_anchor; tool.height_anchor = state.height_anchor; tool.point = state.point; - auto const side{tool.side_anchor}, height{tool.height_anchor}; - auto const point{tool.point}; - bool anchored{false}; - anchors(anchored); - if (anchored && (side != tool.side_anchor || height != tool.height_anchor || point != tool.point)) + if (begin_fields("##bendplace", 0.5f)) { - bend_reanchor(state, tool.side_anchor, tool.height_anchor, tool.point); - changed = true; + field_row("From the track axis (m, + right)"); + changed |= ImGui::InputDouble("##bendlateral", &state.lateral, 0.05, 0.5, "%.3f"); + double overhead{state.height - kRailTop}; + field_row("Over the rail head (m)"); + if (ImGui::InputDouble("##bendheight", &overhead, 0.01, 0.1, "%.3f")) + { + state.height = overhead + kRailTop; + changed = true; + } + double along{sweep->distance_at(bend_fixed(*sweep, state.point))}; + field_row("Along the track (m)"); + if (ImGui::InputDouble("##bendalongat", &along, 0.1, 1.0, "%.2f")) + { + bend_stretch(*sweep, along, stretch, state.point, state); + changed = true; + } + double length{stretch}; + field_row("Length (m)"); + if (ImGui::InputDouble("##bendlength", &length, 0.5, 5.0, "%.2f") && length > 0.1) + { + bend_stretch(*sweep, bend_fixed(*sweep, state.point), length, state.point, state); + changed = true; + } + ImGui::EndTable(); } - ImGui::PushItemWidth(110.0f); - changed |= ImGui::InputDouble(STR_C("from the track axis (m, + right)"), &state.lateral, 0.05, 0.5, "%.3f"); - double overhead{state.height - kRailTop}; - if (ImGui::InputDouble(STR_C("over the rail head (m)"), &overhead, 0.01, 0.1, "%.3f")) - { - state.height = overhead + kRailTop; - changed = true; - } - double along{sweep->distance_at(bend_fixed(*sweep, state.point))}; - if (ImGui::InputDouble(STR_C("along the track (m)"), &along, 0.1, 1.0, "%.2f")) - { - bend_stretch(*sweep, along, stretch, state.point, state); - changed = true; - } - double length{stretch}; - if (ImGui::InputDouble(STR_C("length (m)"), &length, 0.5, 5.0, "%.2f") && length > 0.1) - { - bend_stretch(*sweep, bend_fixed(*sweep, state.point), length, state.point, state); - changed = true; - } - ImGui::PopItemWidth(); - if (own > 0.0 && ImGui::SmallButton(STR_C("As long as the model"))) + auto const half{std::floor((ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f)}; + ImGui::BeginDisabled(own <= 0.0); + if (ImGui::Button(STR_C("As long as the model"), ImVec2(half, 0.0f))) { bend_stretch(*sweep, bend_fixed(*sweep, state.point), own, state.point, state); changed = true; } + ImGui::EndDisabled(); ImGui::SameLine(); - if (ImGui::SmallButton(STR_C("Along the whole track"))) + if (ImGui::Button(STR_C("Along the whole track"), ImVec2(-FLT_MIN, 0.0f))) { state.from = 0.0; state.to = -1.0; changed = true; } int mode{state.bend ? 0 : 1}; - changed |= ImGui::RadioButton(STR_C("Bent"), &mode, 0); - ImGui::SameLine(); - changed |= ImGui::RadioButton(STR_C("Repeated"), &mode, 1); + changed |= segmented("bendmode", mode, {{0, STR_C("Bent"), "One model stretched and bent along the curve of the track"}, {1, STR_C("Repeated"), "Copies of the model one after another, each one straight"}}); state.bend = mode == 0; - ImGui::SameLine(); - ImGui::SetNextItemWidth(80.0f); - changed |= ImGui::InputDouble(STR_C("copy every (m, 0: its length)"), &state.step, 0.0, 0.0, "%.2f"); - state.step = std::max(0.0, state.step); - changed |= ImGui::Checkbox(STR_C("Turned around"), &state.flip); - ImGui::SameLine(); - changed |= ImGui::Checkbox(STR_C("Mirrored"), &state.mirror); - ImGui::SameLine(); - changed |= ImGui::Checkbox(STR_C("Leans with the cant"), &state.tilt); - changed |= ImGui::Checkbox(STR_C("Set back by the widening of the structure gauge in the curves"), &state.widen); - if (ImGui::Checkbox(STR_C("Platform"), &state.platform)) - { - if (state.platform) + auto const every = [&] { + if (begin_fields("##bendstep", 0.5f)) { - bend_reanchor(state, 1, 3, state.point); - platform_edge(state, state.model, kRailTop); + field_row("Copy every (m, 0: its length)"); + changed |= ImGui::InputDouble("##bendevery", &state.step, 0.0, 0.0, "%.2f"); + ImGui::EndTable(); } - changed = true; - } + state.step = std::max(0.0, state.step); + }; + if (false == state.bend) + every(); if (state.platform && platform_distance()) { state.lateral = std::copysign(EditorSettings.platform_edge(), state.lateral); changed = true; } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Puts the edge facing the track at the distance chosen from its axis, set back by the widening of the gauge in the curves.\n" - "Stands on the plane of the rail heads carried on to its edge: lower on the inner side of a canted curve,\n" - "higher on the outer one; on the inner side also set back by the cant times the height of the edge / 1.5")); + ImGui::PushID("bendmore"); + if (ImGui::TreeNode(STR_C("More settings"))) + { + auto const side{tool.side_anchor}, height{tool.height_anchor}; + auto const point{tool.point}; + bool anchored{false}; + anchors(anchored); + if (anchored && (side != tool.side_anchor || height != tool.height_anchor || point != tool.point)) + { + bend_reanchor(state, tool.side_anchor, tool.height_anchor, tool.point); + changed = true; + } + if (state.bend) + every(); + auto const platform{state.platform}; + changed |= toggle_chips("bendflags", {{STR_C("Turned around"), &state.flip}, + {STR_C("Mirrored"), &state.mirror}, + {STR_C("Leans with the cant"), &state.tilt}, + {STR_C("Gauge widening in the curves"), &state.widen, + "The offset grows by the widening of the gauge below the platforms: 3.75/R in the curves of R >= 250 m,\n" + "more in the sharper ones, and on the inner side of a canted curve by the cant times the height of the edge / 1.5;\n" + "it begins 20 m (inner side) and 26 m (outer side) ahead of the curve, as in the gauge check"}, + {STR_C("Platform"), &state.platform, + "Puts the edge facing the track at the distance chosen from its axis, set back by the widening of the gauge in the curves.\n" + "Stands on the plane of the rail heads carried on to its edge: lower on the inner side of a canted curve,\n" + "higher on the outer one; on the inner side also set back by the cant times the height of the edge / 1.5"}}) >= 0; + if (state.platform && false == platform) + { + bend_reanchor(state, 1, 3, state.point); + platform_edge(state, state.model, kRailTop); + } + ImGui::TreePop(); + } + ImGui::PopID(); + ImGui::PushTextWrapPos(0.0f); ImGui::TextDisabled("%s", STR_C("In the view: drag the blue cross along the track and aside; Shift: height; Ctrl: only along")); + ImGui::PopTextWrapPos(); if (changed) bend_edit(state); - if (ImGui::Button(STR_C("Done"))) + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.42f, 0.24f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.22f, 0.52f, 0.30f, 1.0f)); + auto const done{ImGui::Button(STR_C("Done"), ImVec2(half, ImGui::GetFrameHeight() * 1.3f))}; + ImGui::PopStyleColor(2); + if (done) tool.edited = nullptr; ImGui::SameLine(); - if (ImGui::Button(STR_C("Delete"))) + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.55f, 0.18f, 0.15f, 1.0f)); + auto const removed{ImGui::Button(STR_C("Delete"), ImVec2(-FLT_MIN, ImGui::GetFrameHeight() * 1.3f))}; + ImGui::PopStyleColor(); + if (removed) { sweep_delete(*sweep); tool.edited = nullptr; @@ -1753,23 +1811,39 @@ void editor_mode::render_bend() else { tool.track = std::clamp(tool.track, 0, static_cast(tool.tracks.size()) - 1); - auto const label = [](std::pair const &Entry) { return format("%s (%.1f m)", Entry.first->name().empty() ? "(unnamed)" : Entry.first->name().c_str(), Entry.second); }; - ImGui::SetNextItemWidth(220.0f); - if (ImGui::BeginCombo(STR_C("track"), label(tool.tracks[tool.track]).c_str())) + auto const label = [](std::pair const &Entry) { return format("%s (%.1f m)", Entry.first->name().empty() ? STR_C("(no name)") : Entry.first->name().c_str(), Entry.second); }; + if (begin_fields("##bendsource", 0.35f)) { - for (int i = 0; i < static_cast(tool.tracks.size()); ++i) - if (ImGui::Selectable(label(tool.tracks[i]).c_str(), i == tool.track)) - tool.track = i; - ImGui::EndCombo(); + field_row("Track"); + if (ImGui::BeginCombo("##bendtrack", label(tool.tracks[tool.track]).c_str())) + { + for (int i = 0; i < static_cast(tool.tracks.size()); ++i) + if (ImGui::Selectable(label(tool.tracks[i]).c_str(), i == tool.track)) + tool.track = i; + ImGui::EndCombo(); + } + ImGui::EndTable(); } - char const *axes[] = {STR_C("the one nearest the track"), "z", "x"}; - ImGui::SetNextItemWidth(170.0f); - ImGui::Combo(STR_C("axis of the model along the track"), &tool.axis, axes, IM_ARRAYSIZE(axes)); - ImGui::Checkbox(STR_C("Turned around"), &tool.turned); - if (platform_model(source.file)) - platform_distance(); - bool anchored{false}; - anchors(anchored); + auto const bend{ImGui::Button(STR_C("Bend it along the track (B)"))}; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The model stays where it stands and bends with the track; Ctrl+Z gives it back")); + ImGui::PushID("bendmore"); + if (ImGui::TreeNode(STR_C("More settings"))) + { + if (begin_fields("##bendaxis", 0.35f)) + { + field_row("Axis of the model along the track"); + segmented("bendaxis", tool.axis, {{0, STR_C("the one nearest the track")}, {1, "z"}, {2, "x"}}); + ImGui::EndTable(); + } + toggle_chips("bendturned", {{STR_C("Turned around"), &tool.turned}}); + if (platform_model(source.file)) + platform_distance(); + bool anchored{false}; + anchors(anchored); + ImGui::TreePop(); + } + ImGui::PopID(); auto const signature{format("%p %d %d %d %d %d %d %.4f %.4f %.4f %.3f", key, tool.track, tool.axis, tool.turned ? 1 : 0, tool.along_anchor, tool.side_anchor, tool.height_anchor, tool.point.x, tool.point.y, tool.point.z, EditorSettings.platform_edge())}; if (signature != tool.signature) { @@ -1779,10 +1853,8 @@ void editor_mode::render_bend() std::vector tracks; bend_definition(source, *tool.tracks[tool.track].first, state, tracks); } - if (ImGui::Button(STR_C("Bend it along the track (B)"))) + if (bend) bend_selection(); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("The model stays where it stands and bends with the track; Ctrl+Z gives it back")); } } else diff --git a/application/editormodeswitchdesign.cpp b/application/editormodeswitchdesign.cpp index 6a0a64fb..9d22a388 100644 --- a/application/editormodeswitchdesign.cpp +++ b/application/editormodeswitchdesign.cpp @@ -21,6 +21,8 @@ http://mozilla.org/MPL/2.0/. #include #include +std::string switch_name_translated(std::string Title); + namespace { @@ -60,7 +62,7 @@ void set_kind(switch_design::part &Part, part_kind const Kind) bool edit_parts(std::vector &Parts, char const *Id, bool const Editable) { bool changed{false}; - char const *kinds[] = {STR_C("Straight"), STR_C("Arc"), STR_C("Transition curve")}; + char const *kinds[] = {STR_C("Straight"), STR_C("Arc"), STR_C("Transition")}; ImGui::PushID(Id); if (false == Editable) ImGui::BeginDisabled(); @@ -98,7 +100,7 @@ bool edit_parts(std::vector &Parts, char const *Id, bool co if (part.radius1 != 0.0) { ImGui::SameLine(); - ImGui::TextDisabled("%.3f deg", glm::degrees(part.length / part.radius1)); + ImGui::TextDisabled("%.3f°", glm::degrees(part.length / part.radius1)); } break; case part_kind::transition: @@ -119,9 +121,11 @@ bool edit_parts(std::vector &Parts, char const *Id, bool co ImGui::SameLine(); if (ImGui::SmallButton("^") && i > 0) up = i; + item_tooltip("Move the part up"); ImGui::SameLine(); if (ImGui::SmallButton("x")) erase = i; + item_tooltip("Remove the part"); } ImGui::PopID(); } @@ -276,7 +280,7 @@ void editor_mode::render_switch_designer() ImGui::TextDisabled("%s", STR_C("Catalogue (read only)")); item_tooltip("The catalogue switches start tangentially at the joint before the blades: without the pre-blade stretch and the angle of the blade, which a copy can be given"); for (int i = 0; i < static_cast(designer.catalogue.size()); ++i) - if (ImGui::Selectable((designer.catalogue[i].name + "##catalogue" + std::to_string(i)).c_str(), designer.from_catalogue && designer.selected == i)) + if (ImGui::Selectable((switch_name_translated(designer.catalogue[i].name) + "##catalogue" + std::to_string(i)).c_str(), designer.from_catalogue && designer.selected == i)) { designer.from_catalogue = true; designer.selected = i; @@ -310,7 +314,8 @@ void editor_mode::render_switch_designer() auto const &list{designer.from_catalogue ? designer.catalogue : designer.own}; if (designer.selected < 0 || designer.selected >= static_cast(list.size())) { - ImGui::TextWrapped("%s", STR_C("Choose a template on the left. The catalogue ones can be copied to own templates and changed there. A template is a straight main track and the diverging track from the start of the switch as a chain of straights, arcs and transition curves; a double switch is two switches, the second one starting on the main track of the first.")); + ImGui::TextWrapped("%s", STR_C("Choose a template on the left; copy a catalogue one to own templates to change it")); + item_tooltip("A template is a straight main track and the diverging track from the start of the switch as a chain of straights, arcs and transition curves; a double switch is two switches, the second one starting on the main track of the first"); ImGui::EndChild(); ImGui::End(); return; @@ -325,7 +330,7 @@ void editor_mode::render_switch_designer() auto copy{entry}; copy.own = true; copy.note.clear(); - copy.name += STR(" (copy)"); + copy.name = switch_name_translated(copy.name) + STR(" (copy)"); designer.own.push_back(copy); designer.from_catalogue = false; designer.selected = static_cast(designer.own.size()) - 1; @@ -339,83 +344,47 @@ void editor_mode::render_switch_designer() } if (false == editable) ImGui::BeginDisabled(); - char name[96]; - std::snprintf(name, sizeof(name), "%s", entry.name.c_str()); - ImGui::SetNextItemWidth(240.0f); - if (ImGui::InputText(STR_C("Name##designname"), name, sizeof(name))) + auto const input = [&](char const *Label, char const *Id, double &Value, char const *Format) { + field_row(Label); + return ImGui::InputDouble(Id, &Value, 0.0, 0.0, Format); + }; + if (begin_fields("##designfields", 0.5f)) { - entry.name = name; - changed = true; - } - char const *types[] = {STR_C("Rz ordinary"), STR_C("Rpd double, to both sides"), STR_C("Rpj double, to one side"), STR_C("Rps double, symmetric")}; - char const *codes[] = {"Rz", "Rpd", "Rpj", "Rps"}; - int type{entry.type == "Rpd" ? 1 : entry.type == "Rpj" ? 2 : entry.type == "Rps" ? 3 : 0}; - ImGui::SetNextItemWidth(240.0f); - if (ImGui::Combo(STR_C("Type##designtype"), &type, types, 4)) - { - entry.type = codes[type]; - if (type > 0 && entry.stagger <= 0.0) - entry.stagger = 5.0; - changed = true; - } - item_tooltip("A double switch is laid as two switches: the first one with its diverging track and the main track up to the second one, which starts there with the other diverging track"); - char rail[32]; - std::snprintf(rail, sizeof(rail), "%s", entry.rail == "-" ? "" : entry.rail.c_str()); - ImGui::SetNextItemWidth(120.0f); - if (ImGui::InputText(STR_C("Rail##designrail"), rail, sizeof(rail))) - { - entry.rail = rail[0] != '\0' ? std::string{rail} : std::string{"-"}; - changed = true; - } - item_tooltip("Rail of the switch kept in its record: 60E1, 49E1..."); - ImGui::PushItemWidth(90.0f); - changed |= ImGui::InputDouble(STR_C("Pre-blade stretch, m##designlead"), &entry.lead, 0.0, 0.0, "%.3f"); - item_tooltip("From the start of the switch (the joint before the blades) to the tip of the blade; the diverging track runs along the main one there"); - ImGui::SameLine(); - changed |= ImGui::InputDouble(STR_C("Blade angle, deg##designentry"), &entry.entry, 0.0, 0.0, "%.4f"); - item_tooltip("The angle at which the blade meets the stock rail at its tip: the diverging track starts there at this angle instead of tangentially. Curved blades a fraction of a degree, straight blades 1 to 2 degrees; 0: tangential"); - ImGui::SameLine(); - changed |= ImGui::InputDouble(STR_C("Tip of a tangent blade, m##designtip"), &entry.tip, 0.0, 0.0, "%.3f"); - item_tooltip("With the blade angle 0: the tip of the blade from the start of the switch as its scheme gives it, the diverging track is broken there into pieces; 0: where the blade gets 5 mm thick, less the 125 mm its nose is planed over"); - changed |= ImGui::InputDouble(STR_C("Blade heel, m##designheel"), &entry.heel, 0.0, 0.0, "%.3f"); - item_tooltip("Where the moving blades end, from the start of the switch, as its scheme gives it; the switch keeps it in the scenery. 0: as long as the flangeway of 58 mm behind them needs"); - ImGui::SameLine(); - changed |= ImGui::InputDouble(STR_C("Blade throw, m##designthrow"), &entry.bladethrow, 0.0, 0.0, "%.3f"); - item_tooltip("How far the tips of the blades move over between the two paths; 0: the usual 0.160 m"); - ImGui::PopItemWidth(); - if (editable) - { - ImGui::PushItemWidth(70.0f); - ImGui::TextDisabled("%s", STR_C("Straight blade:")); - ImGui::SameLine(); - ImGui::InputDouble(STR_C("l, m##bladelength"), &designer.blade_length, 0.0, 0.0, "%.2f"); - ImGui::SameLine(); - ImGui::InputDouble(STR_C("z, m##bladegap"), &designer.blade_gap, 0.0, 0.0, "%.3f"); - ImGui::PopItemWidth(); - ImGui::SameLine(); - if (ImGui::SmallButton(STR_C("Angle from l and z")) && designer.blade_length > 0.1 && designer.blade_gap > 0.0 && designer.blade_gap < designer.blade_length) + char name[96]; + std::snprintf(name, sizeof(name), "%s", entry.name.c_str()); + field_row("Name##designname"); + if (ImGui::InputText("##designname", name, sizeof(name))) { - entry.entry = glm::degrees(std::asin(designer.blade_gap / designer.blade_length)); + entry.name = name; changed = true; } - item_tooltip("sin angle = z / l: l the length of a straight blade, z the gap between the running edges of the stock rail and the blade at its heel"); + char const *types[] = {STR_C("Rz ordinary"), STR_C("Rpd double, to both sides"), STR_C("Rpj double, to one side"), STR_C("Rps double, symmetric")}; + char const *codes[] = {"Rz", "Rpd", "Rpj", "Rps"}; + int type{entry.type == "Rpd" ? 1 : entry.type == "Rpj" ? 2 : entry.type == "Rps" ? 3 : 0}; + field_row("Type##designtype"); + if (ImGui::Combo("##designtype", &type, types, 4)) + { + entry.type = codes[type]; + if (type > 0 && entry.stagger <= 0.0) + entry.stagger = 5.0; + changed = true; + } + item_tooltip("A double switch is laid as two switches: the first one with its diverging track and the main track up to the second one, which starts there with the other diverging track"); + ImGui::EndTable(); } if (false == editable) ImGui::EndDisabled(); ImGui::Separator(); ImGui::TextDisabled("%s", STR_C("Diverging track from the start of the switch")); - if (editable) + if (editable && begin_fields("##quickfields", 0.5f)) { - ImGui::PushItemWidth(80.0f); - ImGui::InputDouble(STR_C("R##quickradius"), &designer.quick_radius, 0.0, 0.0, "%.0f"); - ImGui::SameLine(); - ImGui::InputDouble(STR_C("1:n##quickratio"), &designer.quick_ratio, 0.0, 0.0, "%.2f"); - ImGui::SameLine(); - ImGui::InputDouble(STR_C("straight after, m##quickstraight"), &designer.quick_straight, 0.0, 0.0, "%.3f"); - ImGui::PopItemWidth(); - ImGui::SameLine(); - if (ImGui::SmallButton(STR_C("Fill in")) && designer.quick_radius >= 10.0 && designer.quick_ratio > 1.0) + input("R##quickradius", "##quickradius", designer.quick_radius, "%.0f"); + input("1:n##quickratio", "##quickratio", designer.quick_ratio, "%.2f"); + input("straight after, m##quickstraight", "##quickstraight", designer.quick_straight, "%.3f"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + if (ImGui::Button(STR_C("Fill in"), ImVec2(-FLT_MIN, 0.0f)) && designer.quick_radius >= 10.0 && designer.quick_ratio > 1.0) { auto const angle{std::atan(1.0 / designer.quick_ratio)}; entry.parts = {{designer.quick_radius * angle, designer.quick_radius, designer.quick_radius}}; @@ -424,6 +393,7 @@ void editor_mode::render_switch_designer() changed = true; } item_tooltip("Replaces the parts with an arc of R up to the angle 1:n and the straight after it, the shape of an ordinary switch"); + ImGui::EndTable(); } changed |= edit_parts(entry.parts, "##parts", editable); bool const twofold{switch_design::double_switch(entry.type)}; @@ -432,9 +402,12 @@ void editor_mode::render_switch_designer() if (twofold) { ImGui::Separator(); - ImGui::SetNextItemWidth(90.0f); - changed |= ImGui::InputDouble(STR_C("Second switch starts at, m##stagger"), &entry.stagger, 0.0, 0.0, "%.3f"); - item_tooltip("Distance from the start of the first switch to the start of the second one, along the main track"); + if (begin_fields("##staggerfields", 0.5f)) + { + changed |= input("Second switch starts at, m##stagger", "##stagger", entry.stagger, "%.3f"); + item_tooltip("Distance from the start of the first switch to the start of the second one, along the main track"); + ImGui::EndTable(); + } bool own{false == entry.parts2.empty() && entry.type != "Rps"}; if (entry.type == "Rps") ImGui::TextDisabled("%s", STR_C("The second switch is the mirror image of the first one")); @@ -447,28 +420,28 @@ void editor_mode::render_switch_designer() item_tooltip("Otherwise the second switch diverges the same way as the first, to the other side for Rpd, to the same side for Rpj"); if (own) { - ImGui::PushItemWidth(90.0f); - auto lead2{entry.lead2 >= 0.0 ? entry.lead2 : entry.lead}; - if (ImGui::InputDouble(STR_C("Pre-blade stretch, m##designlead2"), &lead2, 0.0, 0.0, "%.3f")) + if (begin_fields("##design2fields", 0.5f)) { - entry.lead2 = std::max(0.0, lead2); - changed = true; + auto lead2{entry.lead2 >= 0.0 ? entry.lead2 : entry.lead}; + if (input("Pre-blade stretch, m##designlead2", "##designlead2", lead2, "%.3f")) + { + entry.lead2 = std::max(0.0, lead2); + changed = true; + } + auto entry2{entry.entry2 >= 0.0 ? entry.entry2 : entry.entry}; + if (input("Blade angle, deg##designentry2", "##designentry2", entry2, "%.4f")) + { + entry.entry2 = std::max(0.0, entry2); + changed = true; + } + auto tip2{entry.tip2 >= 0.0 ? entry.tip2 : entry.tip}; + if (input("Tip of a tangent blade, m##designtip2", "##designtip2", tip2, "%.3f")) + { + entry.tip2 = std::max(0.0, tip2); + changed = true; + } + ImGui::EndTable(); } - ImGui::SameLine(); - auto entry2{entry.entry2 >= 0.0 ? entry.entry2 : entry.entry}; - if (ImGui::InputDouble(STR_C("Blade angle, deg##designentry2"), &entry2, 0.0, 0.0, "%.4f")) - { - entry.entry2 = std::max(0.0, entry2); - changed = true; - } - ImGui::SameLine(); - auto tip2{entry.tip2 >= 0.0 ? entry.tip2 : entry.tip}; - if (ImGui::InputDouble(STR_C("Tip of a tangent blade, m##designtip2"), &tip2, 0.0, 0.0, "%.3f")) - { - entry.tip2 = std::max(0.0, tip2); - changed = true; - } - ImGui::PopItemWidth(); changed |= edit_parts(entry.parts2, "##parts2", editable); } else if (entry.lead2 >= 0.0 || entry.entry2 >= 0.0 || entry.tip2 >= 0.0) @@ -477,33 +450,83 @@ void editor_mode::render_switch_designer() changed = true; } } - ImGui::Separator(); - ImGui::PushItemWidth(90.0f); - changed |= ImGui::InputDouble(STR_C("Main track, m##designmain"), &entry.main, 0.0, 0.0, "%.3f"); - item_tooltip("Length of the main track, 0: as far as the diverging tracks reach"); - ImGui::SameLine(); - changed |= ImGui::InputDouble(STR_C("a##designa"), &entry.a, 0.0, 0.0, "%.3f"); - item_tooltip("From the start to the point where the tangents of both tracks meet, as the catalogue gives it; 0: from the geometry"); - ImGui::SameLine(); - changed |= ImGui::InputDouble(STR_C("b##designb"), &entry.b, 0.0, 0.0, "%.3f"); - item_tooltip("From the point where the tangents meet to the end, as the catalogue gives it; 0: the main track less a"); - ImGui::PopItemWidth(); if (false == editable) ImGui::EndDisabled(); + if (ImGui::TreeNode(STR_C("More settings"))) + { + if (false == editable) + ImGui::BeginDisabled(); + if (begin_fields("##designmorefields", 0.5f)) + { + char rail[32]; + std::snprintf(rail, sizeof(rail), "%s", entry.rail == "-" ? "" : entry.rail.c_str()); + field_row("Rail##designrail"); + if (ImGui::InputText("##designrail", rail, sizeof(rail))) + { + entry.rail = rail[0] != '\0' ? std::string{rail} : std::string{"-"}; + changed = true; + } + item_tooltip("Rail of the switch kept in its record: 60E1, 49E1..."); + changed |= input("Pre-blade stretch, m##designlead", "##designlead", entry.lead, "%.3f"); + item_tooltip("From the start of the switch (the joint before the blades) to the tip of the blade; the diverging track runs along the main one there"); + changed |= input("Blade angle, deg##designentry", "##designentry", entry.entry, "%.4f"); + item_tooltip("The angle at which the blade meets the stock rail at its tip: the diverging track starts there at this angle instead of tangentially. Curved blades a fraction of a degree, straight blades 1 to 2 degrees; 0: tangential"); + changed |= input("Tip of a tangent blade, m##designtip", "##designtip", entry.tip, "%.3f"); + item_tooltip("With the blade angle 0: the tip of the blade from the start of the switch as its scheme gives it, the diverging track is broken there into pieces; 0: where the blade gets 5 mm thick, less the 125 mm its nose is planed over"); + changed |= input("Blade heel, m##designheel", "##designheel", entry.heel, "%.3f"); + item_tooltip("Where the moving blades end, from the start of the switch, as its scheme gives it; the switch keeps it in the scenery. 0: as long as the flangeway of 58 mm behind them needs"); + changed |= input("Blade throw, m##designthrow", "##designthrow", entry.bladethrow, "%.3f"); + item_tooltip("How far the tips of the blades move over between the two paths; 0: the usual 0.160 m"); + ImGui::EndTable(); + } + if (editable) + { + ImGui::TextDisabled("%s", STR_C("Straight blade:")); + if (begin_fields("##bladefields", 0.5f)) + { + input("l, m##bladelength", "##bladelength", designer.blade_length, "%.2f"); + input("z, m##bladegap", "##bladegap", designer.blade_gap, "%.3f"); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + if (ImGui::Button(STR_C("Angle from l and z"), ImVec2(-FLT_MIN, 0.0f)) && designer.blade_length > 0.1 && designer.blade_gap > 0.0 && designer.blade_gap < designer.blade_length) + { + entry.entry = glm::degrees(std::asin(designer.blade_gap / designer.blade_length)); + changed = true; + } + item_tooltip("sin angle = z / l: l the length of a straight blade, z the gap between the running edges of the stock rail and the blade at its heel"); + ImGui::EndTable(); + } + } + if (begin_fields("##designmainfields", 0.5f)) + { + changed |= input("Main track, m##designmain", "##designmain", entry.main, "%.3f"); + item_tooltip("Length of the main track, 0: as far as the diverging tracks reach"); + changed |= input("a##designa", "##designa", entry.a, "%.3f"); + item_tooltip("From the start to the point where the tangents of both tracks meet, as the catalogue gives it; 0: from the geometry"); + changed |= input("b##designb", "##designb", entry.b, "%.3f"); + item_tooltip("From the point where the tangents meet to the end, as the catalogue gives it; 0: the main track less a"); + ImGui::EndTable(); + } + if (false == editable) + ImGui::EndDisabled(); + ImGui::TreePop(); + } std::string error; auto const shape{switch_design::build(entry, error)}; ImGui::Separator(); if (false == error.empty()) - ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "%s", STR_C(error.c_str())); + ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "%s", STR(error).c_str()); else { ImGui::Text(STR_C("Angle 1:%.3f (%.4f deg), sharpest R %.1f m, a %.3f m, b %.3f m"), shape.ratio, glm::degrees(std::atan(1.0 / shape.ratio)), shape.radius, shape.a, shape.b); - ImGui::Text(STR_C("Length %.3f m, the diverging track ends %.3f m beside the main one, in %zu pieces"), shape.length, shape.offset, std::max(1, shape.paths[1].pieces.size())); - if (entry.entry > 0.0) - ImGui::Text(STR_C("Tip of the blade %.3f m from the start, the diverging track leaves there at %.4f deg"), shape.tip, entry.entry); - else - ImGui::Text(STR_C("Tip of the blade %.3f m from the start, the diverging track leaves tangentially"), shape.tip); + if (ImGui::IsItemHovered()) + { + auto const length{format(STR_C("Length %.3f m, the diverging track ends %.3f m beside the main one, in %zu pieces"), shape.length, shape.offset, std::max(1, shape.paths[1].pieces.size()))}; + auto const tip{entry.entry > 0.0 ? format(STR_C("Tip of the blade %.3f m from the start, the diverging track leaves there at %.4f deg"), shape.tip, entry.entry) + : format(STR_C("Tip of the blade %.3f m from the start, the diverging track leaves tangentially"), shape.tip)}; + ImGui::SetTooltip("%s\n%s", length.c_str(), tip.c_str()); + } } ImGui::Checkbox(STR_C("Stretch across"), &designer.stretched); item_tooltip("Draws the offsets from the main track larger, so the shape of the diverging track can be seen"); diff --git a/application/editormodeswitchdrive.cpp b/application/editormodeswitchdrive.cpp index af2a5c3e..af3eb6ec 100644 --- a/application/editormodeswitchdrive.cpp +++ b/application/editormodeswitchdrive.cpp @@ -165,7 +165,7 @@ void editor_mode::switch_drives_committed(std::vector const &Tracks) std::string editor_mode::missing_drive_variant(int const Family, bool const Rightswitch, bool const Driveright) const { - return format(STR_C("The drive %s has no variant for a %s switch with the drive on the %s"), m_switch.drives[Family].name.c_str(), Rightswitch ? STR_C("right") : STR_C("left"), Driveright ? STR_C("right") : STR_C("left")); + return format(Rightswitch ? STR_C("The drive %s has no variant for a right-hand switch with the drive %s") : STR_C("The drive %s has no variant for a left-hand switch with the drive %s"), m_switch.drives[Family].name.c_str(), Driveright ? STR_C("on the right") : STR_C("on the left")); } bool editor_mode::place_switch_drive(TTrack &Switch) @@ -293,9 +293,11 @@ bool editor_mode::render_switch_drive_choice() return false; } auto const label = [&](int const Index) { return Index < 0 ? std::string{STR_C("no drive")} : tool.drives[Index].name; }; - ImGui::PushItemWidth(200.0f); bool changed{false}; - if (ImGui::BeginCombo(STR_C("Drive"), label(tool.drive).c_str())) + if (false == begin_fields("##drivefields", 0.3f)) + return false; + field_row("Drive"); + if (ImGui::BeginCombo("##drive", label(tool.drive).c_str())) { for (int i = -1; i < static_cast(tool.drives.size()); ++i) if (ImGui::Selectable(label(i).c_str(), tool.drive == i)) @@ -305,15 +307,10 @@ bool editor_mode::render_switch_drive_choice() } ImGui::EndCombo(); } - ImGui::PopItemWidth(); + ImGui::EndTable(); item_tooltip("Templates described with category: switch drive, the side of the switch and of the drive, and a parameter with the role track for the name of the switch.\nIts events + and - switch it with the keys by the switch and in the map during the game"); if (tool.drive >= 0) - { - ImGui::SameLine(); - changed |= ImGui::RadioButton(STR_C("diverging side"), &tool.drive_side, 0); - ImGui::SameLine(); - changed |= ImGui::RadioButton(STR_C("other side"), &tool.drive_side, 1); - } + changed |= segmented("driveside", tool.drive_side, {{0, STR_C("diverging side")}, {1, STR_C("other side")}}); return changed; } diff --git a/application/editormodeswitchplace.cpp b/application/editormodeswitchplace.cpp index a0b72800..217eadc5 100644 --- a/application/editormodeswitchplace.cpp +++ b/application/editormodeswitchplace.cpp @@ -151,9 +151,7 @@ void editor_mode::finish_switch_placement() insert_connection(); return; } - tool.placing = false; - tool.status = STR_C("A connection is laid between straights"); - ui()->set_status(tool.status, true); + insert_curved_connection(); return; } switch_link plan; @@ -250,6 +248,11 @@ void editor_mode::switch_placed(std::vector const &Created) void editor_mode::advance_switch_name() { + if (false == m_switch.keep_names.empty()) + { + m_switch.keep_names.erase(m_switch.keep_names.begin()); + return; + } std::string name{m_switch.name}; auto digits{name.size()}; while (digits > 0 && std::isdigit(static_cast(name[digits - 1]))) @@ -263,6 +266,8 @@ void editor_mode::advance_switch_name() std::string editor_mode::switch_name_for_new() const { + if (false == m_switch.keep_names.empty() && simulation::Paths.find(m_switch.keep_names.front()) == nullptr) + return m_switch.keep_names.front(); if (m_switch.name[0] != '\0') return m_switch.name; for (int i = 1;; ++i) @@ -277,10 +282,19 @@ void editor_mode::join_last_steps() { if (m_history.size() < 2) return; - auto last{std::move(m_history.back())}; + auto &last{m_history.back()}; + // a step of more than the includes placed, as the paths laid after others were taken out, is undone together with the one before + auto const more{false == last.tracks.empty() || false == last.created.empty() || false == last.removed.empty() || false == last.directives.empty() || false == last.sweeps.empty() || false == last.sweeps_toggled.empty() || + false == last.infra.empty() || last.alignments.has_value() || last.switch_records.has_value() || last.profiles || false == last.copies.empty() || last.terrain != nullptr}; + if (more) + { + last.joined = true; + return; + } + auto instances{std::move(last.instances)}; m_history.pop_back(); auto &previous{m_history.back()}; - previous.instances.insert(previous.instances.end(), last.instances.begin(), last.instances.end()); + previous.instances.insert(previous.instances.end(), instances.begin(), instances.end()); } bool editor_mode::cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack **Style) @@ -427,7 +441,7 @@ std::vector editor_mode::create_switch_set(editor_track::switch_templa } editor_track::commit({track}); record_switch(*track, Shape); - m_switch.status = STR_C("Switch ") + track->name() + STR_C(" inserted"); + m_switch.status = format(STR_C("Switch %s inserted, Ctrl+Z takes it back"), track->name().c_str()); return {track}; } @@ -517,7 +531,7 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const std::string editor_mode::link_problem(editor_track::snap_target const &Target, editor_track::branch_link const &Link) const { - auto const name{Target.track != nullptr && false == Target.track->name().empty() ? Target.track->name() : std::string{"(noname)"}}; + auto const name{Target.track != nullptr && false == Target.track->name().empty() ? Target.track->name() : std::string{STR_C("(no name)")}}; if (false == Link.valid) return Link.shortfall > 0.0 ? format(STR_C("The branch can't lead into %s: the R %.0f m curve needs %.2f m more room"), name.c_str(), Link.radius, Link.shortfall) : format(STR_C("The branch can't lead into %s"), name.c_str()); @@ -895,7 +909,7 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv state.status = STR_C("Not enough straight track around the crossing"); return false; } - state.status = STR_C("R reduced to ") + std::to_string(static_cast(curve.radius)) + STR_C(" m, the straights end ") + std::to_string(static_cast(available)) + STR_C(" m from the crossing"); + state.status = format(STR_C("R reduced to %d m, the straights end %d m from the crossing"), static_cast(curve.radius), static_cast(available)); } auto const heightfirst = [&](double const Along) { return Line.start.y + Line.grade * Along; }; @@ -926,7 +940,7 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv } for (auto *track : removed) { - track->m_editorremoved = false; + editor_track::revive(*track); editor_track::commit({track}); } return false; @@ -1031,7 +1045,7 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat { half = oldhalf; radius = curve.radius * half / full; - state.status = STR_C("R limited to ") + std::to_string(static_cast(radius)) + STR_C(" m, a neighbouring path isn't straight"); + state.status = format(STR_C("R limited to %d m, a neighbouring track isn't straight"), static_cast(radius)); break; } auto const line{editor_track::find_straight(*e->outer, m_straights.tolerance)}; @@ -1041,7 +1055,7 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat { half = std::min(half, std::max(reach, reachstart) - 0.5); radius = curve.radius * half / full; - state.status = STR_C("R reduced to ") + std::to_string(static_cast(radius)) + STR_C(" m to fit the straights"); + state.status = format(STR_C("R reduced to %d m to fit the straights"), static_cast(radius)); } } } @@ -1309,7 +1323,7 @@ void editor_mode::insert_curved_switch(int const Direction, int const Side) auto const made{create_switch_set(shape, paths, *tool.frame_tracks.front(), switch_name_for_new())}; created.insert(created.end(), made.begin(), made.end()); if (made.size() == 1) - tool.status = STR_C("Switch ") + made.front()->name() + STR_C(" inserted into the curve"); + tool.status = format(STR_C("Switch %s inserted into the curve, Ctrl+Z takes it back"), made.front()->name().c_str()); auto const placed{created}; push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); diff --git a/application/editormodeswitchslide.cpp b/application/editormodeswitchslide.cpp index f955885b..63040584 100644 --- a/application/editormodeswitchslide.cpp +++ b/application/editormodeswitchslide.cpp @@ -45,7 +45,7 @@ struct switch_end_move std::string track_label(TTrack const &Track) { - return Track.name().empty() ? std::string{"(noname)"} : Track.name(); + return Track.name().empty() ? std::string{STR_C("(no name)")} : Track.name(); } std::optional> switch_end_moves(TTrack const &Track, segment_data const &Main, bool const Online, std::pair const &Room, double const Offset, std::string &Status) @@ -709,6 +709,9 @@ bool editor_mode::slide_switch(TTrack &Track, double const Distance) { if (std::abs(Distance) < 0.005) return false; + // a switch of a crossover along a curve takes the crossover with it + if (crossover_of(Track).has_value()) + return slide_curved_connection(Track, Distance); auto const set{double_of(Track)}; auto &first{set.has_value() ? *set->first : Track}; if (first.m_paths.size() < 2) diff --git a/application/editormodeswitchtool.cpp b/application/editormodeswitchtool.cpp index de4ed558..169dde34 100644 --- a/application/editormodeswitchtool.cpp +++ b/application/editormodeswitchtool.cpp @@ -11,9 +11,12 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" +#include "application/editorgestures.h" #include "application/editormodeshared.h" #include "editor/editorConnections.hpp" #include "editor/editorFormat.hpp" +#include "editor/editorTrack.hpp" +#include "simulation/simulation.h" #include "utilities/translation.h" #include "world/Track.h" @@ -21,17 +24,96 @@ http://mozilla.org/MPL/2.0/. #include #include +// the name of the template in the language of the editor: the words of the catalogue translated, the kind of a slip or +// a crossing left out where its type says it +std::string switch_name_translated(std::string Title) +{ + auto const replace = [&](std::string const &From, std::string const &To) { + for (auto at{Title.find(From)}; at != std::string::npos; at = Title.find(From, at + To.size())) + Title.replace(at, From.size(), To); + }; + for (auto const *redundant : {" single slip", " double slip", " crossing"}) + replace(redundant, ""); + for (auto const *word : {"the blades outside of the diamond", "movable nose", "long", "industrial"}) + replace(word, STR(word)); + if (auto const at{Title.find("gauge ")}; at != std::string::npos) + if (auto const gauge{std::atoi(Title.c_str() + at + 6)}; gauge > 0) + { + auto const end{Title.find_first_not_of("0123456789", at + 6)}; + Title.replace(at, (end == std::string::npos ? Title.size() : end) - at, format(STR_C("gauge %d"), gauge)); + } + return Title; +} + namespace { -// the next radio button on the same line as the last item, if it fits there -void same_line_if_fits(char const *Label) +// a kind to place as a tile: a sketch of its tracks on the left, its name beside it; true when clicked +bool kind_tile(char const *Id, char const *Label, int const Picture, bool const Chosen, float const Width) { - auto const &style{ImGui::GetStyle()}; - auto const width{ImGui::GetFrameHeight() + style.ItemInnerSpacing.x + ImGui::CalcTextSize(Label, nullptr, true).x}; - auto const end{ImGui::GetItemRectMax().x + style.ItemSpacing.x + width}; - if (end <= ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x) - ImGui::SameLine(); + auto const font{ImGui::GetFontSize()}; + auto const height{std::round(font * 2.4f)}; + auto const top{ImGui::GetCursorScreenPos()}; + auto const clicked{ImGui::InvisibleButton(Id, ImVec2(Width, height))}; + auto const hovered{ImGui::IsItemHovered()}; + auto *list{ImGui::GetWindowDrawList()}; + auto const accent{family_colour(0)}; + ImVec2 const bottom{top.x + Width, top.y + height}; + auto const rounding{std::round(font * 0.35f)}; + list->AddRectFilled(top, bottom, Chosen ? (accent & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 46) : ImGui::GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), rounding); + list->AddRect(top, bottom, Chosen ? accent : ImGui::GetColorU32(ImGuiCol_Border), rounding, 0, Chosen ? 2.0f : 1.0f); + auto const pad{std::round(font * 0.4f)}; + auto const w{std::round(font * 2.2f)}; + auto const h{height - 2.0f * pad}; + ImVec2 const o{top.x + pad, top.y + pad}; + auto const at = [&](float const X, float const Y) { return ImVec2(o.x + X * w, o.y + Y * h); }; + auto const line{Chosen ? accent : ImGui::GetColorU32(ImGuiCol_Text, 0.7f)}; + auto const branch{Chosen ? IM_COL32(255, 210, 60, 255) : ImGui::GetColorU32(ImGuiCol_Text, 0.45f)}; + switch (Picture) + { + case 0: // a switch: the straight track and the diverging one + list->AddLine(at(0.0f, 0.75f), at(1.0f, 0.75f), line, 2.0f); + list->AddBezierQuadratic(at(0.15f, 0.75f), at(0.6f, 0.75f), at(1.0f, 0.1f), branch, 2.0f); + break; + case 1: // a slip switch: a crossing with the slip curves inside + list->AddLine(at(0.0f, 0.85f), at(1.0f, 0.15f), line, 2.0f); + list->AddLine(at(0.0f, 0.15f), at(1.0f, 0.85f), line, 2.0f); + list->AddBezierQuadratic(at(0.08f, 0.8f), at(0.5f, 0.45f), at(0.92f, 0.8f), branch, 1.5f); + list->AddBezierQuadratic(at(0.08f, 0.2f), at(0.5f, 0.55f), at(0.92f, 0.2f), branch, 1.5f); + break; + case 2: // a crossing + list->AddLine(at(0.0f, 0.85f), at(1.0f, 0.15f), line, 2.0f); + list->AddLine(at(0.0f, 0.15f), at(1.0f, 0.85f), line, 2.0f); + break; + case 5: // a double crossover: two crossovers facing each other + list->AddLine(at(0.0f, 0.2f), at(1.0f, 0.2f), line, 2.0f); + list->AddLine(at(0.0f, 0.8f), at(1.0f, 0.8f), line, 2.0f); + list->AddLine(at(0.05f, 0.8f), at(0.45f, 0.2f), branch, 2.0f); + list->AddLine(at(0.55f, 0.2f), at(0.95f, 0.8f), branch, 2.0f); + break; + case 6: // scissors: two crossovers through each other + list->AddLine(at(0.0f, 0.2f), at(1.0f, 0.2f), line, 2.0f); + list->AddLine(at(0.0f, 0.8f), at(1.0f, 0.8f), line, 2.0f); + list->AddLine(at(0.15f, 0.8f), at(0.85f, 0.2f), branch, 2.0f); + list->AddLine(at(0.15f, 0.2f), at(0.85f, 0.8f), branch, 2.0f); + break; + case 7: // a ladder: the tracks of a fan going off one by one + list->AddLine(at(0.0f, 0.9f), at(1.0f, 0.9f), line, 2.0f); + for (float const y : {0.6f, 0.35f, 0.1f}) + list->AddLine(at(0.95f - y, y), at(1.0f, y), line, 2.0f); + list->AddLine(at(0.05f, 0.9f), at(0.85f, 0.1f), branch, 2.0f); + break; + default: // a connection of two tracks + list->AddLine(at(0.0f, 0.2f), at(1.0f, 0.2f), line, 2.0f); + list->AddLine(at(0.0f, 0.8f), at(1.0f, 0.8f), line, 2.0f); + list->AddLine(at(0.2f, 0.8f), at(0.8f, 0.2f), branch, 2.0f); + break; + } + auto const text_left{o.x + w + pad}; + auto const wrap{bottom.x - pad - text_left}; + auto const size{ImGui::CalcTextSize(Label, nullptr, false, wrap)}; + list->AddText(nullptr, 0.0f, ImVec2(text_left, std::round(top.y + (height - size.y) * 0.5f)), ImGui::GetColorU32(Chosen ? ImGuiCol_Text : ImGuiCol_TextDisabled), Label, nullptr, wrap); + return clicked; } // whether the template is one of those the kind places @@ -46,34 +128,46 @@ bool of_kind(editor_track::switch_template const &Template, int const Kind) } } -// the rail of the switch templates fitting the track, by the rail profile it's laid with; empty where the profile doesn't tell -std::string rail_of(TTrack const *Track) +std::string template_tooltip(editor_track::switch_template const &Template) { - if (Track == nullptr || (Track->iCategoryFlag & 15) != 1) - return {}; - auto const &profile{Track->m_profile1.first}; - if (profile == "s60" || profile == "uic60") - return "60E1"; - if (profile == "s49") - return "49E1"; - if (profile == "s42") - return "S42"; - return {}; + if (Template.source != nullptr) + return Template.label; + auto text{switch_name_translated(Template.title())}; + if (Template.double_slip) + { + text += "\n"; + text += Template.type == "Rkp" ? STR_C("Single slip (Rkp)") : Template.type == "Rkpd" ? STR_C("Double slip (Rkpd)") : STR_C("Crossing (Skrz)"); + text += "\n" + format(STR_C("a %.3f m from the middle of the crossing to the joint"), Template.a) + "\n"; + text += Template.type == "Rkp" ? STR_C("Click at a crossing of two straights, on the side of the tracks to join") : STR_C("Click at a crossing of two straights"); + return text; + } + text += "\n" + format(STR_C("a %.3f m to the centre of the switch, b %.3f m on to its end"), Template.a, Template.b); + if (auto const note{Template.label.rfind(" (")}; false == Template.own && note != std::string::npos && Template.label.back() == ')') + text += " " + STR(Template.label.substr(note + 1)); + return text; } -// the name of the template in the language of the editor: the words of the catalogue translated, the kind of a slip or -// a crossing left out where its type says it -std::string translated(std::string Title) +// the angle of the template at a glance, steeper than it is so 1:9 and 1:18.5 differ: the main track and the diverging +// one, or the two crossing tracks of a slip or a crossing +void template_glyph(ImDrawList &Draw, ImVec2 const Low, ImVec2 const High, editor_track::switch_template const &Template, bool const Chosen) { - auto const replace = [&](std::string const &From, std::string const &To) { - for (auto at{Title.find(From)}; at != std::string::npos; at = Title.find(From, at + To.size())) - Title.replace(at, From.size(), To); - }; - for (auto const *redundant : {" single slip", " double slip", " crossing"}) - replace(redundant, ""); - for (auto const *word : {"the blades outside of the diamond", "movable nose", "gauge", "long"}) - replace(word, STR(word)); - return Title; + auto const line{ImGui::GetColorU32(ImGuiCol_TextDisabled)}; + auto const made{Chosen ? IM_COL32(255, 255, 255, 255) : IM_COL32(120, 200, 255, 255)}; + auto const ratio{Template.ratio > 0.0 ? Template.ratio : Template.radius > 0.0 && Template.length > 0.0 ? 2.0 * Template.radius / Template.length : 9.0}; + auto const slope{static_cast(std::min(0.9, 3.0 / ratio))}; + auto const width{High.x - Low.x}; + auto const middle{std::round((Low.y + High.y) * 0.5f)}; + if (Template.double_slip) + { + auto const rise{std::min(width * slope * 0.5f, (High.y - Low.y) * 0.5f)}; + Draw.AddLine(ImVec2(Low.x, middle), ImVec2(High.x, middle), line, 2.0f); + Draw.AddLine(ImVec2(Low.x, middle + rise), ImVec2(High.x, middle - rise), made, 2.0f); + return; + } + auto const bottom{std::round(High.y - 2.0f)}; + auto const rise{std::min(width * 0.75f * slope, High.y - Low.y - 4.0f)}; + Draw.AddLine(ImVec2(Low.x, bottom), ImVec2(High.x, bottom), line, 2.0f); + Draw.AddBezierQuadratic(ImVec2(Low.x + width * 0.15f, bottom), ImVec2(Low.x + width * 0.5f, bottom), ImVec2(High.x, bottom - rise), made, 2.0f); } } // namespace @@ -81,8 +175,8 @@ std::string translated(std::string Title) std::string editor_mode::switch_template_label(editor_track::switch_template const &Template) { if (Template.source != nullptr) - return STR("Scenery: ") + Template.title() + " (" + std::to_string(Template.count) + "x)"; - return (Template.own ? STR("Own: ") : std::string{"PLK: "}) + translated(Template.title()); + return format("%s: %s (%dx)", STR_C("From the scenery"), Template.title().c_str(), Template.count); + return (Template.own ? STR("Own: ") : std::string{"PLK: "}) + switch_name_translated(Template.title()); } editor_mode::switch_kind editor_mode::current_switch_kind() const @@ -136,27 +230,37 @@ void editor_mode::render_switch_ui() {switch_kind::slip, "Slip switch", "Rkp or Rkpd at a crossing of two straights"}, {switch_kind::crossing, "Crossing", "A crossing of two straights"}, {switch_kind::connection, "Connection", "Crossover, double, scissors, ladder"}}; - ImGui::TextDisabled("%s", STR_C("What to place")); - auto first{true}; - for (auto const &entry : choices) { - if (entry.kind != switch_kind::single && false == switch_kind_offered(entry.kind)) - continue; - if (false == first) - same_line_if_fits(STR_C(entry.label)); - first = false; - if (ImGui::RadioButton(STR_C(entry.label), kind == entry.kind) && kind != entry.kind) - choose_switch_kind(entry.kind); - item_tooltip(entry.tooltip); + auto const gap{std::round(ImGui::GetFontSize() * 0.4f)}; + auto const width{std::floor((ImGui::GetContentRegionAvail().x - gap) * 0.5f)}; + int shown{0}; + for (auto const &entry : choices) + { + if (entry.kind != switch_kind::single && false == switch_kind_offered(entry.kind)) + continue; + if (shown % 2 == 1) + ImGui::SameLine(0.0f, gap); + else if (shown > 0) + ImGui::Dummy(ImVec2(0.0f, gap * 0.25f)); + if (kind_tile(entry.label, STR_C(entry.label), static_cast(&entry - choices), kind == entry.kind, width) && kind != entry.kind) + choose_switch_kind(entry.kind); + item_tooltip(entry.tooltip); + ++shown; + } } if (kind == switch_kind::connection) { + ImGui::SeparatorText(STR_C("Kind of the connection")); + auto const gap{std::round(ImGui::GetFontSize() * 0.4f)}; + auto const width{std::floor((ImGui::GetContentRegionAvail().x - gap) * 0.5f)}; for (int i = 0; i < connection::kind_count; ++i) { - if (i > 0) - same_line_if_fits(connection_name(i)); + if (i % 2 == 1) + ImGui::SameLine(0.0f, gap); + else if (i > 0) + ImGui::Dummy(ImVec2(0.0f, gap * 0.25f)); ImGui::PushID(i); - if (ImGui::RadioButton(connection_name(i), tool.connection == i)) + if (kind_tile("##connectionkind", connection_name(i), 4 + i, tool.connection == i, width)) { tool.connection = i; tool.connection_kind = i; @@ -166,78 +270,171 @@ void editor_mode::render_switch_ui() } // how to place it with the mouse - char const *how{nullptr}; + std::vector> how; switch (kind) { - case switch_kind::slip: how = "Click a crossing of two straights"; break; - case switch_kind::crossing: how = "Click a crossing of two straights"; break; + case switch_kind::slip: + case switch_kind::crossing: how = {{"LMB on a crossing", "of two straights: it goes there"}}; break; case switch_kind::connection: - how = tool.connection == static_cast(connection::kind::double_crossover) ? "Drag from a straight onto the other track. Shift+wheel: kind, Ctrl+wheel: R, Alt+wheel: where they meet" : - "Drag from a straight onto the other track. Shift+wheel: kind, Ctrl+wheel: R"; + how = {{"Drag", "from a straight onto the other track"}, {"Shift+wheel", "the kind"}, {"Ctrl+wheel", "R of the aligning curve"}}; + if (tool.connection == static_cast(connection::kind::double_crossover)) + how.push_back({"Alt+wheel", "where they meet"}); break; - default: how = "Click a track and drag: along for the direction, sideways for the side"; break; + default: how = {{"Drag on a track", "along: the direction, sideways: the side"}}; break; } - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.3f, 1.0f)); - ImGui::TextWrapped("%s", STR_C(how)); - ImGui::PopStyleColor(); + gesture_rows(how); // the templates of the kind, by their names, for the rail of the selected track unless all are asked for; the lengths // and the rest of the label in the tooltip ImGui::TextDisabled("%s", STR_C(kind == switch_kind::connection ? "The first switch" : "Template")); - auto const rail{rail_of(selected_track())}; + auto const rail{editor_track::rail_of(selected_track())}; if (false == rail.empty()) { ImGui::SameLine(); - ImGui::Checkbox(format(STR_C("Rail of the track: %s##railonly"), rail.c_str()).c_str(), &tool.rail_only); - item_tooltip("Only the templates of the rail the selected track is laid with"); + auto const label{format(STR_C("Rail of the track: %s"), rail.c_str())}; + toggle_chips("railonly", {{label.c_str(), &tool.rail_only, "Only the templates of the rail the selected track is laid with"}}); } + else if (auto *track{selected_track()}; track != nullptr && (track->iCategoryFlag & 15) == 1 && track->m_profile1.first.empty()) + render_rail_profile_hint(*track); auto const shown = [&](editor_track::switch_template const &Template) { return of_kind(Template, static_cast(kind)) && (rail.empty() || false == tool.rail_only || Template.source != nullptr || Template.rail == rail || Template.rail == "-"); }; - auto const listed{static_cast(std::count_if(tool.templates.begin(), tool.templates.end(), shown))}; - ImGui::BeginChild("##switchtemplates", ImVec2(0.0f, std::min(240.0f, (listed + 0.5f) * ImGui::GetTextLineHeightWithSpacing() + 8.0f)), true); - for (int i = 0; i < static_cast(tool.templates.size()); ++i) - { - auto const &entry{tool.templates[i]}; - if (false == shown(entry)) - continue; - if (ImGui::Selectable((switch_template_label(entry) + "##switch" + std::to_string(i)).c_str(), tool.armed == i)) + // in groups: the switches of the scenery, the own templates, then the catalogue rail by rail + auto const group_of = [](editor_track::switch_template const &Template) { + return Template.source != nullptr ? std::string{STR("From the scenery")} : Template.own ? std::string{STR("Own templates")} : "PLK " + (Template.rail == "-" ? std::string{} : Template.rail); + }; + std::vector groups; + int listed{0}; + for (auto const &entry : tool.templates) + if (shown(entry)) { - tool.last = i; - tool.armed = i; + ++listed; + if (auto const group{group_of(entry)}; std::find(groups.begin(), groups.end(), group) == groups.end()) + groups.push_back(group); + } + auto const row{std::round(ImGui::GetTextLineHeight() * 1.3f)}; + auto const header{groups.size() > 1 ? ImGui::GetTextLineHeightWithSpacing() * static_cast(groups.size()) : 0.0f}; + ImGui::BeginChild("##switchtemplates", ImVec2(0.0f, std::min(std::round(ImGui::GetFontSize() * 18.0f), (listed + 0.5f) * (row + ImGui::GetStyle().ItemSpacing.y) + header + 8.0f)), ImGuiChildFlags_Borders); + auto const glyph{std::round(row * 1.8f)}; + for (auto const &group : groups) + { + if (groups.size() > 1) + ImGui::SeparatorText(group.c_str()); + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + { + auto const &entry{tool.templates[i]}; + if (false == shown(entry) || group_of(entry) != group) + continue; + ImGui::PushID(i); + auto const low{ImGui::GetCursorScreenPos()}; + if (ImGui::Selectable("##switch", tool.armed == i, ImGuiSelectableFlags_None, ImVec2(0.0f, row))) + { + tool.last = i; + tool.armed = i; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", template_tooltip(entry).c_str()); + if (ImGui::IsItemVisible()) + { + auto &draw{*ImGui::GetWindowDrawList()}; + template_glyph(draw, ImVec2(low.x + 2.0f, low.y + 2.0f), ImVec2(low.x + glyph, low.y + row - 2.0f), entry, tool.armed == i); + auto const label{entry.source != nullptr ? entry.title() + " (" + std::to_string(entry.count) + "x)" : switch_name_translated(entry.title())}; + draw.AddText(ImVec2(low.x + glyph + ImGui::GetStyle().ItemSpacing.x, std::round(low.y + (row - ImGui::GetTextLineHeight()) * 0.5f)), ImGui::GetColorU32(ImGuiCol_Text), label.c_str()); + } + ImGui::PopID(); } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", entry.label.c_str()); } ImGui::EndChild(); - if (kind == switch_kind::connection) - render_connection_options(); - - // the name and the drive of the next switch; a crossing has no moving parts - if (kind != switch_kind::crossing) + if (kind == switch_kind::connection && tool.connection == static_cast(connection::kind::ladder) && begin_fields("##connectionladder")) { - ImGui::PushItemWidth(120.0f); - ImGui::InputTextWithHint(STR_C("Name##switchname"), switch_name_for_new().c_str(), tool.name, sizeof(tool.name)); - ImGui::PopItemWidth(); - item_tooltip("Name of the next switch, its drive events are named after it; empty: the next free z1, z2...; a number at the end goes up with each switch"); - if (render_switch_drive_choice()) - if (auto *track{selected_track()}; track != nullptr && track->eType == tt_Switch && false == track->m_editorremoved) - apply_switch_drive_choice(*track); + field_row("Spacings, m##connectionspacings"); + ImGui::InputText("##connectionspacings", tool.connection_spacings, sizeof(tool.connection_spacings)); + item_tooltip("Distances between the tracks of the fan from this track on, such as 4.75;4.75;5.0; used when the drag doesn't end on a parallel track, otherwise the parallel tracks up to that one make the fan"); + ImGui::EndTable(); } + if (kind == switch_kind::connection && tool.connection == static_cast(connection::kind::double_crossover)) + { + ImGui::TextDisabled("%s", STR_C("Where the two crossovers meet")); + int together{tool.connection_together ? 0 : 1}; + if (segmented("connectiontogether", together, + {{0, STR_C("On this track"), "The two crossovers meet on the track the drag starts on; Alt+wheel while dragging switches it"}, + {1, STR_C("On the other track"), "The two crossovers meet on the other track; Alt+wheel while dragging switches it"}})) + tool.connection_together = together == 0; + } + + ImGui::PushID("switchmore"); + if (ImGui::TreeNode(STR_C("More settings"))) + { + if (kind == switch_kind::connection) + render_connection_options(); + // the name and the drive of the next switch; a crossing has no moving parts + if (kind != switch_kind::crossing) + { + if (begin_fields("##switchnamefields", 0.3f)) + { + field_row("Name##switchname"); + ImGui::InputTextWithHint("##switchname", switch_name_for_new().c_str(), tool.name, sizeof(tool.name)); + item_tooltip("Name of the next switch, its drive events are named after it; empty: the next free z1, z2...; a number at the end goes up with each switch"); + ImGui::EndTable(); + } + if (render_switch_drive_choice()) + if (auto *track{selected_track()}; track != nullptr && track->eType == tt_Switch && false == track->m_editorremoved) + apply_switch_drive_choice(*track); + } + if (ImGui::Button(STR_C("Collect from the scenery"))) + { + tool.templates = editor_track::standard_switch_templates(); + auto const found{editor_track::find_switch_templates()}; + tool.templates.insert(tool.templates.end(), found.begin(), found.end()); + tool.collected = true; + arm_switch(); + } + item_tooltip("Looks for the switches of the scenery, to use their shapes as the templates"); + ImGui::TreePop(); + } + ImGui::PopID(); if (false == tool.status.empty()) - ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", tool.status.c_str()); - - if (ImGui::SmallButton(STR_C("Collect from the scenery"))) { - tool.templates = editor_track::standard_switch_templates(); - auto const found{editor_track::find_switch_templates()}; - tool.templates.insert(tool.templates.end(), found.begin(), found.end()); - tool.collected = true; - arm_switch(); + ImGui::PushTextWrapPos(0.0f); + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", tool.status.c_str()); + ImGui::PopTextWrapPos(); } - item_tooltip("Looks for the switches of the scenery, to use their shapes as the templates"); - ImGui::SameLine(); - if (ImGui::SmallButton(STR_C("Template editor..."))) + + if (ImGui::Button(STR_C("Template editor..."))) m_switch_designer.open = true; item_tooltip("Own switch templates: diverging tracks of straights, arcs and transition curves, double switches"); } + +// a track with no rail profile, as the sceneries of the older editors have them: the templates of all the rails are offered. +// the profile set at once on all the tracks laid with the same rails and no profile, so a scenery is put right in a few clicks +void editor_mode::render_rail_profile_hint(TTrack &Track) +{ + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", STR_C("Track with no rail profile")); + item_tooltip("The track doesn't say its rail, so the templates of all the rails are offered.\nSet the profile and the list keeps to the templates of the rail; the rails of the track take its shape"); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Set...##railprofile"))) + ImGui::OpenPopup("##railprofileset"); + if (false == ImGui::BeginPopup("##railprofileset")) + return; + auto const material{Track.m_material1}; + auto const same = [&](TTrack const &Other) { return false == Other.m_editorremoved && (Other.iCategoryFlag & 15) == 1 && Other.m_material1 == material && Other.m_profile1.first.empty(); }; + std::vector tracks; + for (auto *other : simulation::Paths.sequence()) + if (other != nullptr && same(*other)) + tracks.push_back(other); + ImGui::TextUnformatted(format(STR_C("Tracks with the rails %s and no profile: %d"), editor_track::material_name(material).c_str(), static_cast(tracks.size())).c_str()); + static std::pair const profiles[] = {{"S60 (60E1)", "s60"}, {"S49 (49E1)", "s49"}, {"S42", "s42"}}; + for (auto const &[label, name] : profiles) + { + if (&label != &profiles[0].first) + ImGui::SameLine(); + if (false == ImGui::Button(label)) + continue; + auto const changed{tracks_apply(tracks, [&](TTrack &Other) { Other.rail_profile(name); return true; }, true, "Rail profile")}; + m_switch.status = format(STR_C("Rail profile %s set on %d tracks"), label, static_cast(changed)); + ImGui::CloseCurrentPopup(); + } + ImGui::TextDisabled("%s", STR_C("Other profiles: the properties of the track")); + ImGui::EndPopup(); +} diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index ab708a5d..509b1b1e 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -13,6 +13,7 @@ http: #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "application/editormodeshared.h" +#include "application/editorgestures.h" #include "editor/editorFormat.hpp" #include "editor/editorAlignmentStore.hpp" #include "editor/editorSwitchStore.hpp" @@ -230,6 +231,7 @@ void editor_mode::push_track_snapshot(std::vector const &Tracks, std::string &Reason) @@ -239,7 +241,7 @@ bool editor_mode::tracks_editable(std::vector const &Tracks, std::stri std::string reason; if (track != nullptr && false == editor_track::can_edit_geometry(*track, reason)) { - Reason = (track->name().empty() ? std::string{"(noname)"} : track->name()) + ": " + reason; + Reason = (track->name().empty() ? std::string{STR_C("(no name)")} : track->name()) + ": " + reason; return false; } } @@ -252,6 +254,7 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve if (Snapshot.tracks.empty() && Snapshot.created.empty() && Snapshot.removed.empty()) return; + ++m_track_edits; std::vector tracks; for (auto const &entry : Snapshot.tracks) tracks.emplace_back(entry.first); @@ -297,16 +300,16 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve editor_track::retire(*track); for (auto *track : Snapshot.removed) { - track->m_editorremoved = false; + editor_track::revive(*track); tracks.push_back(track); } } else { - for (auto *track : Snapshot.created) - track->m_editorremoved = false; for (auto *track : Snapshot.removed) editor_track::retire(*track); + for (auto *track : Snapshot.created) + editor_track::revive(*track); } for (auto const &entry : Snapshot.tracks) editor_track::apply(*entry.first, entry.second); @@ -348,17 +351,17 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve std::array const &editor_mode::track_modes() { static std::array const modes{{ - {"Select", "Esc", track_tab::path, "LMB on a path selects it: its points, control vectors and parameters"}, - {"Lay track", "L", track_tab::lay, "Lays new track through the clicked points, on the ground or from a free end of a track"}, - {"Plan", "G", track_tab::straights, "The plan of the line laid: LMB on a straight drags its ends or the middle, on a curve its vertex and radius. C works too"}, - {"Switch", "T", track_tab::turnout, "Puts switches into the selected straight or curve, edits the geometry of the selected switch"}, - {"Parallel", "N", track_tab::parallel, "A second track beside a straight: LMB on the straight, then the side and the spacing"}, - {"Turntable", "U", track_tab::turntable, "Places a turntable, leads tracks out of it and fits the tracks around to it"}, - {"Signals", "H", track_tab::signals, "Signals by the track, from the templates of the scenery, with the event the train reads them by"}, - {"Objects", "B", track_tab::lineside, "Along the track: hectometre posts, fouling point markers, trainsets"}, - {"Profile", "P", track_tab::profile, "Vertical profile (grade line) along the line"}, - {"Speed", "V", track_tab::speed, "Speed limits of the paths against the speed their geometry allows"}, - {"Joints", "J", track_tab::joints, "Ends of the paths which almost meet, steps, kinks, jumps of the cant and of the grade at the joints"}, + {"Select", "Esc", track_tab::path, "LMB on a path selects it: its points, control vectors and parameters", editor_icons::icon::select, 0}, + {"Lay track", "L", track_tab::lay, "Lays new track through the clicked points, on the ground or from a free end of a track", editor_icons::icon::lay_track, 0}, + {"Plan", "G", track_tab::straights, "The plan of the line laid: LMB on a straight drags its ends or the middle, on a curve its vertex and radius. C works too", editor_icons::icon::curve, 0}, + {"Switch", "T", track_tab::turnout, "Puts switches into the selected straight or curve, edits the geometry of the selected switch", editor_icons::icon::turnout, 0}, + {"Parallel", "N", track_tab::parallel, "A second track beside a track: LMB on it, then the side and the spacing", editor_icons::icon::parallel, 0}, + {"Turntable", "U", track_tab::turntable, "Places a turntable, leads tracks out of it and fits the tracks around to it", editor_icons::icon::turntable, 1}, + {"Signals", "H", track_tab::signals, "Signals by the track, from the templates of the scenery, with the event the train reads them by", editor_icons::icon::signal, 1}, + {"Objects", "B", track_tab::lineside, "Along the track: hectometre posts, fouling point markers, trainsets", editor_icons::icon::objects, 1}, + {"Profile", "P", track_tab::profile, "Vertical profile (grade line) along the line", editor_icons::icon::profile, 2}, + {"Speed", "V", track_tab::speed, "Speed limits of the paths against the speed their geometry allows", editor_icons::icon::speed, 2}, + {"Joints", "J", track_tab::joints, "Ends of the paths which almost meet, steps, kinks, jumps of the cant and of the grade at the joints", editor_icons::icon::joints, 2}, // the infrastructure tool (track_tab::infra) is left out: no button nor key }}; return modes; @@ -538,7 +541,8 @@ void editor_mode::render_track_inspector() ImGui::End(); } - if (m_track_tab == track_tab::profile && m_track_window_open && false == m_profile.route.spans.empty()) + m_profile.strip_top = -1.0f; + if (m_track_tab == track_tab::profile && m_track_window_open) render_profile_strip(); } @@ -549,6 +553,8 @@ void editor_mode::render_track_tool_options() render_track_search(); ImGui::Spacing(); render_track_guide(); + // the texts of the tools go on to the next line at the edge of the panel, none of them makes it scroll sideways + ImGui::PushTextWrapPos(0.0f); switch (m_track_tab) { case track_tab::lay: @@ -585,6 +591,7 @@ void editor_mode::render_track_tool_options() render_track_spread_ui(); break; } + ImGui::PopTextWrapPos(); } void editor_mode::render_track_selection() @@ -594,12 +601,20 @@ void editor_mode::render_track_selection() if (tracks.empty()) { ImGui::TextDisabled("%s", STR_C("Nothing selected")); - if (m_track_tab == track_tab::path) - { - ImGui::PushTextWrapPos(0.0f); - ImGui::TextDisabled("%s", STR_C("Click a track to see its properties. Shift+click adds more tracks to the selection, a drag on the ground selects those in a box")); - ImGui::PopTextWrapPos(); - } + if (m_track_tab != track_tab::path) + return; + ImGui::Spacing(); + gesture_rows({{"LMB on a track", "its properties show here"}, {"Ctrl+F", "finds a track by its name"}}); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + ImGui::TextDisabled("%s", STR_C("Or start with:")); + auto const width{std::floor((ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f)}; + if (ImGui::Button(STR_C("Lay a track (L)"), ImVec2(width, ImGui::GetFrameHeight() * 1.4f))) + show_track_tab(track_tab::lay); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Tutorial"), ImVec2(width, ImGui::GetFrameHeight() * 1.4f))) + m_first_steps_open = true; return; } // the properties belong to the select tool and to the switch tool, the other tools show the track they work with @@ -623,7 +638,7 @@ void editor_mode::render_track_selection() auto *track{tracks.front()}; if (false == sheet) { - ImGui::TextUnformatted((track->name().empty() ? std::string{"(noname)"} : track->name()).c_str()); + ImGui::TextUnformatted(track->name().empty() ? STR_C("(no name)") : track->name().c_str()); ImGui::TextDisabled("%s, %.2f m", STR_C(type_name(*track)), track->Length()); return; } @@ -632,22 +647,109 @@ void editor_mode::render_track_selection() void editor_mode::render_track_guide() { - ImGui::TextColored(ImVec4(0.55f, 0.8f, 1.0f, 1.0f), "%s", track_mode_name().c_str()); - ImGui::SameLine(); - ImGui::TextDisabled("(?)"); - if (ImGui::IsItemHovered()) + // a card of the tool in use: what it is for, and how the mouse and the keys drive it, drawn + auto const &modes{track_modes()}; + auto const tab{false == m_track_window_open ? track_tab::path : m_track_tab == track_tab::route ? track_tab::straights : m_track_tab}; + auto const found{std::find_if(modes.begin(), modes.end(), [&](track_mode const &Mode) { return Mode.tab == tab; })}; + auto const family{found != modes.end() ? found->family : 0}; + auto const accent{family_colour(family)}; + auto const font{ImGui::GetFontSize()}; + auto const pad{std::round(font * 0.6f)}; + auto *list{ImGui::GetWindowDrawList()}; + auto const top{ImGui::GetCursorScreenPos()}; + auto const width{ImGui::GetContentRegionAvail().x}; + auto const right{top.x + width}; + list->ChannelsSplit(2); + list->ChannelsSetCurrent(1); + ImGui::BeginGroup(); + ImGui::Dummy(ImVec2(width, pad * 0.6f)); + ImGui::Indent(pad + 3.0f); + auto const row{ImGui::GetCursorScreenPos()}; + auto const iconside{std::round(font * 1.7f)}; + if (found != modes.end()) + editor_icons::draw(list, found->picture, ImVec2(row.x + iconside * 0.5f, row.y + iconside * 0.5f), accent); + ImGui::Dummy(ImVec2(iconside, iconside)); + ImGui::SameLine(0.0f, pad); + ImGui::BeginGroup(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::SetWindowFontScale(1.15f); + ImGui::TextUnformatted(track_mode_name().c_str()); + ImGui::SetWindowFontScale(1.0f); + ImGui::PopStyleColor(); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(accent)); + ImGui::TextUnformatted(family_name(family)); + ImGui::PopStyleColor(); + ImGui::EndGroup(); + if (found != modes.end() && found->key != nullptr) { - std::string text; - for (auto const &hint : track_key_hints(true)) - text += std::string{hint.key} + " " + hint.action + "\n"; - ImGui::SetTooltip("%s", text.c_str()); + auto const height{std::round(font * 1.35f)}; + auto const cap{std::max(std::round(ImGui::CalcTextSize(found->key).x + height * 0.6f), height)}; + gesture_key(*list, ImVec2(right - pad - cap, row.y + std::round((iconside - height) * 0.5f)), height, found->key); } + ImGui::PushTextWrapPos(right - pad - ImGui::GetWindowPos().x); + if (found != modes.end()) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextWrapped("%s", STR_C(found->tooltip)); + ImGui::PopStyleColor(); + } + // the gestures, each drawn: the mouse with the button lit, the keys as caps + auto const hints{track_key_hints(false)}; + auto const gesture{std::round(font * 1.25f)}; + std::vector> drawn; + float column{0.0f}; + for (auto const &hint : hints) + { + drawn.push_back(gesture_picture(hint.raw != nullptr ? hint.raw : hint.key)); + column = std::max(column, gesture_width(gesture, drawn.back())); + } + column = std::min(column, font * 6.0f); + if (false == hints.empty()) + ImGui::Dummy(ImVec2(0.0f, pad * 0.3f)); + for (std::size_t i = 0; i < hints.size() && i < 7; ++i) + { + auto const at{ImGui::GetCursorScreenPos()}; + auto const line{ImGui::GetTextLineHeight()}; + auto const height{std::max(gesture, line)}; + list->PushClipRect(at, ImVec2(at.x + column, at.y + height), true); + gesture_draw(*list, ImVec2(at.x, at.y + std::round((height - gesture) * 0.5f)), gesture, drawn[i], ImGui::GetColorU32(ImGuiCol_Text)); + list->PopClipRect(); + ImGui::Dummy(ImVec2(column, height)); + ImGui::SameLine(0.0f, pad); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + std::round((height - line) * 0.5f)); + if (hints[i].raw == nullptr || false == gesture_keys_only(hints[i].raw)) + { + ImGui::TextColored(ImVec4(1.0f, 0.83f, 0.3f, 1.0f), "%s", hints[i].key); + ImGui::SameLine(); + } + ImGui::TextWrapped("%s", hints[i].action.c_str()); + } + ImGui::PopTextWrapPos(); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextUnformatted(STR_C("F1: all the shortcuts")); + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if (ImGui::IsItemClicked()) + m_shortcuts_open = true; + ImGui::Unindent(pad + 3.0f); + ImGui::Dummy(ImVec2(width, pad * 0.5f)); + ImGui::EndGroup(); + auto const bottom{ImGui::GetItemRectMax().y}; + list->ChannelsSetCurrent(0); + auto const rounding{std::round(font * 0.35f)}; + list->AddRectFilled(top, ImVec2(right, bottom), (accent & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 28), rounding); + list->AddRect(top, ImVec2(right, bottom), (accent & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 70), rounding); + list->AddRectFilled(top, ImVec2(top.x + 4.0f, bottom), accent, rounding, ImDrawFlags_RoundCornersLeft); + list->ChannelsMerge(); if (false == m_track_mode_notice.empty()) { + ImGui::Spacing(); ImGui::PushTextWrapPos(ImGui::GetWindowContentRegionMax().x - 8.0f); ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", m_track_mode_notice.c_str()); ImGui::PopTextWrapPos(); } + ImGui::Spacing(); } void editor_mode::render_track_name(TTrack &Track) @@ -687,11 +789,15 @@ void editor_mode::render_track_name(TTrack &Track) edit.status = format(STR_C("Renamed from %s"), current.empty() ? STR_C("(no name)") : current.c_str()); sync(); }; - ImGui::SetNextItemWidth(220.f); - if (ImGui::InputTextWithHint(STR_C("Name##trackname"), STR_C("(no name)"), edit.text, sizeof(edit.text), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CharsNoBlank)) - apply(); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Enter renames the path, Esc while typing takes the text back.\nThe events, memory cells and the map of the game find the path by its name")); + if (begin_fields("##tracknamefields", 0.3f)) + { + field_row("Name##trackname"); + if (ImGui::InputTextWithHint("##trackname", STR_C("(no name)"), edit.text, sizeof(edit.text), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CharsNoBlank)) + apply(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Enter renames the path, Esc while typing takes the text back.\nThe events, memory cells and the map of the game find the path by its name")); + ImGui::EndTable(); + } std::string typed{edit.text}; if (typed == edit.synced) { @@ -711,6 +817,15 @@ void editor_mode::render_track_name(TTrack &Track) auto const warned{false == edit.uses.events.empty() || false == edit.uses.loose.empty()}; if (ImGui::SmallButton(warned ? STR_C("Rename anyway") : STR_C("Rename"))) apply(); + std::vector notes; + if (typed != edit.text) + notes.push_back(format(STR_C("Saved as %s: the scenery keeps the names in lower case"), typed.c_str())); + if (edit.uses.cells > 0 || edit.uses.includes > 0) + notes.push_back(format(STR_C("The new name goes to %d memory cell(s) and %d include(s), the drives of a switch among them"), edit.uses.cells, edit.uses.includes)); + if (warned) + notes.push_back(STR_C("The events listed below work until the scenery is loaded again; then they need the new name in their files")); + if (false == notes.empty() && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", lines_of(notes).c_str()); } ImGui::SameLine(); if (ImGui::SmallButton(STR_C("Keep the old"))) @@ -729,10 +844,6 @@ void editor_mode::render_track_name(TTrack &Track) } return; } - if (typed != edit.text) - ImGui::TextDisabled(STR_C("Saved as %s: the scenery keeps the names in lower case"), typed.c_str()); - if (edit.uses.cells > 0 || edit.uses.includes > 0) - ImGui::TextDisabled(STR_C("The new name goes to %d memory cell(s) and %d include(s), the drives of a switch among them"), edit.uses.cells, edit.uses.includes); auto const listed = [](std::vector const &Names) { std::string result; for (std::size_t i = 0; i < Names.size() && i < 4; ++i) @@ -745,8 +856,6 @@ void editor_mode::render_track_name(TTrack &Track) ImGui::TextColored(ImVec4(1.f, 0.75f, 0.3f, 1.f), STR_C("These events aim at the path by the old name and the editor doesn't write them: %s"), listed(edit.uses.events).c_str()); if (false == edit.uses.loose.empty()) ImGui::TextColored(ImVec4(1.f, 0.75f, 0.3f, 1.f), STR_C("These events belong to the path by the old name: %s"), listed(edit.uses.loose).c_str()); - if (false == edit.uses.events.empty() || false == edit.uses.loose.empty()) - ImGui::TextDisabled("%s", STR_C("They work until the scenery is loaded again; then they need the new name in their files")); } void editor_mode::render_turnout_ui() @@ -775,14 +884,90 @@ void editor_mode::render_turnout_ui() auto const offset{glm::dot(end, normal)}; glm::dvec2 const tangent{glm::normalize(glm::dvec2{-control.x, -control.z})}; auto const angle{std::atan2(std::abs(tangent.x * axis.y - tangent.y * axis.x), glm::dot(tangent, axis))}; + std::string about; if (set.has_value()) { auto const &second{set->second->m_paths.front()}; - ImGui::Text(STR_C("Double switch %s + %s: length %.3f m, the second switch starts %.3f m on"), set->first->name().c_str(), set->second->name().c_str(), glm::dot(plan_of(second.points[segment_data::point::end] - origin), axis), glm::dot(plan_of(second.points[segment_data::point::start] - origin), axis)); - ImGui::TextDisabled(STR_C("First switch: diverging %s, end offset %.3f m"), offset > 0.0 ? STR_C("right") : STR_C("left"), std::abs(offset)); + about = format(STR_C("Double switch %s + %s: length %.3f m, the second switch starts %.3f m on"), set->first->name().c_str(), set->second->name().c_str(), glm::dot(plan_of(second.points[segment_data::point::end] - origin), axis), glm::dot(plan_of(second.points[segment_data::point::start] - origin), axis)) + "\n" + + format(STR_C("First switch: diverging %s, end offset %.3f m"), offset > 0.0 ? STR_C("right") : STR_C("left"), std::abs(offset)); } else ImGui::Text(STR_C("Length %.3f m, diverging %s, end offset %.3f m"), length, offset > 0.0 ? STR_C("right") : STR_C("left"), std::abs(offset)); + { + // the switch in plan at one scale: the main track white, the diverging one yellow, the angle at its end, the length under + auto const font{ImGui::GetFontSize()}; + auto const width{ImGui::GetContentRegionAvail().x}; + auto const height{std::round(font * 4.2f)}; + auto const top{ImGui::GetCursorScreenPos()}; + ImGui::Dummy(ImVec2(width, height)); + if (false == about.empty() && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", about.c_str()); + auto *list{ImGui::GetWindowDrawList()}; + list->AddRectFilled(top, ImVec2(top.x + width, top.y + height), ImGui::GetColorU32(ImGuiCol_FrameBg), std::round(font * 0.35f)); + auto const local = [&](glm::dvec3 const &Point) { + glm::dvec2 const relative{Point.x - origin.x, Point.z - origin.z}; + return glm::dvec2{glm::dot(relative, axis), glm::dot(relative, normal)}; + }; + std::vector> lines; + std::vector switches{track}; + if (set.has_value()) + switches.push_back(set->second); + double far_along{0.0}, low_across{0.0}, high_across{0.0}; + for (auto *each : switches) + for (auto const &path : each->m_paths) + { + geometry::bezier const curve{path}; + std::vector points; + for (int i = 0; i <= 24; ++i) + { + auto const point{local(curve.point(i / 24.0))}; + far_along = std::max(far_along, point.x); + low_across = std::min(low_across, point.y); + high_across = std::max(high_across, point.y); + points.push_back(point); + } + lines.push_back(std::move(points)); + } + auto const pad{std::round(font * 0.7f)}; + auto const label{ImGui::GetTextLineHeight()}; + auto const room{height - 2.0f * pad - label}; + auto const scale{static_cast(std::min((width - 2.0 * pad) / std::max(1.0, far_along), room / std::max(0.5, high_across - low_across)))}; + auto const base{top.y + pad + (room - static_cast(high_across - low_across) * scale) * 0.5f - static_cast(low_across) * scale}; + auto const screen = [&](glm::dvec2 const &Point) { return ImVec2(top.x + pad + static_cast(Point.x) * scale, base + static_cast(Point.y) * scale); }; + // the diverging tracks first, the main ones over them + for (auto const main : {false, true}) + for (auto const &line : lines) + { + if ((std::abs(line.back().y - line.front().y) < 0.05) != main) + continue; + std::vector drawn; + for (auto const &point : line) + drawn.push_back(screen(point)); + list->AddPolyline(drawn.data(), static_cast(drawn.size()), main ? IM_COL32(235, 235, 235, 255) : IM_COL32(255, 210, 60, 255), 0, 2.0f); + } + // the point of the blades, and the angle at the end of the diverging track + list->AddCircleFilled(screen(glm::dvec2{0.0, 0.0}), 3.0f, IM_COL32(230, 80, 230, 255)); + if (angle > 1e-6) + { + auto const text{format("1:%.1f", 1.0 / std::tan(angle))}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + auto const at{screen(glm::dvec2{far_along, offset})}; + list->AddText(ImVec2(std::round(at.x - size.x - 2.0f), std::round(offset > 0.0 ? at.y - size.y - 1.0f : at.y + 1.0f)), IM_COL32(255, 210, 60, 255), text.c_str()); + } + if (diverging.radius > 0.0f) + { + auto const text{format("R %.0f", static_cast(diverging.radius))}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + auto const at{screen(glm::dvec2{far_along * 0.45, offset * 0.25})}; + list->AddText(ImVec2(std::round(at.x - size.x * 0.5f), std::round(offset > 0.0 ? at.y + 3.0f : at.y - size.y - 3.0f)), IM_COL32(255, 210, 60, 200), text.c_str()); + } + auto const text{format("%.2f m", far_along)}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + auto const y{top.y + height - pad * 0.4f - label}; + list->AddLine(ImVec2(top.x + pad, y + label * 0.5f), ImVec2(top.x + width - pad, y + label * 0.5f), ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.5f)); + list->AddRectFilled(ImVec2(top.x + (width - size.x) * 0.5f - 4.0f, y), ImVec2(top.x + (width + size.x) * 0.5f + 4.0f, y + label), ImGui::GetColorU32(ImGuiCol_FrameBg)); + list->AddText(ImVec2(std::round(top.x + (width - size.x) * 0.5f), y), ImGui::GetColorU32(ImGuiCol_TextDisabled), text.c_str()); + } if (auto const known{switch_store::of(*track)}; known.has_value()) ImGui::TextUnformatted(switch_store::label(*known).c_str()); // the name, the number, the rail and the kilometre of the switch, with what the scenery keeps of it @@ -801,26 +986,24 @@ void editor_mode::render_turnout_ui() { auto const family{drive->family}; auto const variant{drive->index}; - ImGui::TextDisabled(STR_C("Drive: events %s+ and %s- switch it, also in the map"), Switch.name().c_str(), Switch.name().c_str()); - ImGui::PushItemWidth(200.0f); - if (ImGui::BeginCombo(STR_C("Drive##standing"), m_switch.drives[family].name.c_str())) + if (begin_fields("##standingdrive", 0.3f)) { - for (int i = 0; i < static_cast(m_switch.drives.size()); ++i) - if (ImGui::Selectable(m_switch.drives[i].name.c_str(), family == i) && i != family) - replace_switch_drive(Switch, i, false); - ImGui::EndCombo(); + field_row("Drive##standing"); + if (ImGui::BeginCombo("##standing", m_switch.drives[family].name.c_str())) + { + for (int i = 0; i < static_cast(m_switch.drives.size()); ++i) + if (ImGui::Selectable(m_switch.drives[i].name.c_str(), family == i) && i != family) + replace_switch_drive(Switch, i, false); + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s\n%s", STR_C("Puts another drive in place of the drive of this switch, on the same side"), format(STR_C("Drive: events %s+ and %s- switch it, also in the map"), Switch.name().c_str(), Switch.name().c_str()).c_str()); + ImGui::EndTable(); } - ImGui::PopItemWidth(); - item_tooltip("Puts another drive in place of the drive of this switch, on the same side"); int side{((variant >> 1) == (variant & 1)) ? 0 : 1}; - ImGui::SameLine(); - auto const before{side}; - ImGui::RadioButton(STR_C("diverging side##standing"), &side, 0); - ImGui::SameLine(); - ImGui::RadioButton(STR_C("other side##standing"), &side, 1); - if (side != before) + if (segmented("standingside", side, {{0, STR_C("diverging side"), "Puts the drive of the switch on the other side of the track: the same drive, its variant for the other side"}, + {1, STR_C("other side"), "Puts the drive of the switch on the other side of the track: the same drive, its variant for the other side"}})) flip_switch_drive(Switch); - item_tooltip("Puts the drive of the switch on the other side of the track: the same drive, its variant for the other side"); } else if ((simulation::Events.FindEvent(Switch.name() + "+") != nullptr && simulation::Events.FindEvent(Switch.name() + "-") != nullptr) || (m_track_name.track == &Switch && m_track_name.uses.includes > 0)) ImGui::TextDisabled(STR_C("Drive: events %s+ and %s- switch it, also in the map"), Switch.name().c_str(), Switch.name().c_str()); @@ -836,28 +1019,25 @@ void editor_mode::render_turnout_ui() drive_ui(*track); if (set.has_value()) drive_ui(*set->second); - bool frogs{track->SwitchExtension->m_frogs}; - if (track_features::crossings && ImGui::Checkbox(STR_C("Frog and check rails made from the rails"), &frogs)) - { - auto const before{editor_track::capture(*track)}; - track->SwitchExtension->m_frogs = frogs; - push_track_snapshot({{track, before}}); - editor_track::commit_parameters(*track); - } if (track_features::crossings) - item_tooltip("Cuts the rails of the switch at its frog and lays wing rails and check rails, for rail profiles S60, UIC60 and S39.\n" - "Off for a switch whose include has its own model of them"); - if (track_features::crossings && frogs) { + bool frogs{track->SwitchExtension->m_frogs}; bool movable{track->SwitchExtension->m_movablenose}; - if (ImGui::Checkbox(STR_C("Movable nose"), &movable)) + char const *const frogtip{"Cuts the rails of the switch at its frog and lays wing rails and check rails.\n" + "Off for a switch whose include has its own model of them"}; + char const *const nosetip{"The nose of the frog moves with the blades over to the wing rail of the track set, leaving no gap; the frog has no check rails"}; + if (frogs) + toggle_chips("switchfrogs", {{STR_C("Frog and check rails made from the rails"), &frogs, frogtip}, {STR_C("Movable nose"), &movable, nosetip}}); + else + toggle_chips("switchfrogs", {{STR_C("Frog and check rails made from the rails"), &frogs, frogtip}}); + if (frogs != track->SwitchExtension->m_frogs || movable != track->SwitchExtension->m_movablenose) { auto const before{editor_track::capture(*track)}; + track->SwitchExtension->m_frogs = frogs; track->SwitchExtension->m_movablenose = movable; push_track_snapshot({{track, before}}); editor_track::commit_parameters(*track); } - item_tooltip("The nose of the frog moves with the blades over to the wing rail of the track set, leaving no gap; the frog has no check rails"); } if (false == m_switch.status.empty()) ImGui::TextWrapped("%s", m_switch.status.c_str()); @@ -880,20 +1060,6 @@ void editor_mode::render_turnout_ui() m_turnout_template = static_cast(ordinary - tool.templates.begin()); } ImGui::Separator(); - ImGui::PushItemWidth(-1.0f); - if (ImGui::BeginCombo("##turnouttemplate", switch_template_label(tool.templates[m_turnout_template]).c_str())) - { - for (int i = 0; i < static_cast(tool.templates.size()); ++i) - { - if (false == fits(tool.templates[i])) - continue; - auto const label{switch_template_label(tool.templates[i])}; - if (ImGui::Selectable(label.c_str(), i == m_turnout_template)) - m_turnout_template = i; - } - ImGui::EndCombo(); - } - ImGui::PopItemWidth(); auto const finish_set = [&](std::string const &Status) { switch_store::follow(); records_to_history(); @@ -940,10 +1106,6 @@ void editor_mode::render_turnout_ui() ui()->set_status(status, true); }; int const side{offset > 0.0 ? 1 : -1}; - if (ImGui::Button(STR_C("Replace the geometry with the template"))) - replace(tool.templates[m_turnout_template], side); - item_tooltip("Keeps the start and the direction of the switch, takes the shape of the chosen template"); - ImGui::SameLine(); if (set.has_value() && ImGui::Button(STR_C("Flip the side"))) { auto const mirror = [&](glm::dvec3 const &Vector) { @@ -987,17 +1149,43 @@ void editor_mode::render_turnout_ui() replace(own, -side); } item_tooltip("Mirrors the switch: the diverging track goes to the other side"); - if (double low, high, reach{length}; switch_slides(*track) && (set.has_value() ? switch_slides(*set->second) && double_room(*set, low, high, reach, m_switch.status) : switch_room(*track, low, high, m_switch.status))) + double low{0.0}, high{0.0}, reach{length}; + auto const slides{switch_slides(*track) && (set.has_value() ? switch_slides(*set->second) && double_room(*set, low, high, reach, m_switch.status) : switch_room(*track, low, high, m_switch.status))}; + ImGui::PushID("turnout"); + if (ImGui::TreeNode(STR_C("More settings"))) { - ImGui::PushItemWidth(100.0f); - ImGui::InputDouble("##switchslide", &tool.slide_typed, 0.0, 0.0, "%.2f m"); + ImGui::PushItemWidth(-1.0f); + if (ImGui::BeginCombo("##turnouttemplate", switch_template_label(tool.templates[m_turnout_template]).c_str())) + { + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + { + if (false == fits(tool.templates[i])) + continue; + auto const label{switch_template_label(tool.templates[i])}; + if (ImGui::Selectable(label.c_str(), i == m_turnout_template)) + m_turnout_template = i; + } + ImGui::EndCombo(); + } ImGui::PopItemWidth(); - ImGui::SameLine(); - if (ImGui::Button(STR_C("Slide along the straight"))) - slide_switch(*track, std::clamp(tool.slide_typed, low, high - reach)); - item_tooltip("Moves the switch along its straight by the distance typed, + towards the frog; the straight paths on both sides get shorter or longer. Dragging the switch with the mouse does the same"); - ImGui::TextDisabled(STR_C("Room on the straight: %.2f m back, %.2f m ahead"), -low, high - reach); + if (ImGui::Button(STR_C("Replace the geometry with the template"))) + replace(tool.templates[m_turnout_template], side); + item_tooltip("Keeps the start and the direction of the switch, takes the shape of the chosen template"); + if (slides) + { + ImGui::PushItemWidth(100.0f); + ImGui::InputDouble("##switchslide", &tool.slide_typed, 0.0, 0.0, "%.2f m"); + ImGui::PopItemWidth(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Slide along the straight"))) + slide_switch(*track, std::clamp(tool.slide_typed, low, high - reach)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s\n%s", STR_C("Moves the switch along its straight by the distance typed, + towards the frog; the straight paths on both sides get shorter or longer. Dragging the switch with the mouse does the same"), + format(STR_C("Room on the straight: %.2f m back, %.2f m ahead"), -low, high - reach).c_str()); + } + ImGui::TreePop(); } + ImGui::PopID(); ImGui::Separator(); ImGui::TextDisabled("%s", STR_C("The textures, the speed and the other properties of the switch are in the inspector")); } @@ -1100,6 +1288,25 @@ void editor_mode::render_path_ui() } render_track_name(*track); ImGui::TextDisabled("%s, %.2f m", STR_C(type_name(*track)), track->Length()); + if (track->eType == tt_Normal && false == track->m_paths.empty()) + { + // the track at a glance: tiles of its length, radius, grade, cant and speed, going on to the next row at the edge + auto const &path{track->m_paths.front()}; + geometry::bezier const curve{path}; + auto const length{curve.plan_length()}; + auto const first{curve.first(0.5)}; + auto const second{curve.second(0.5)}; + auto const speed{std::hypot(first.x, first.z)}; + auto const bend{speed > 1e-9 ? std::abs(first.x * second.z - first.z * second.x) / (speed * speed * speed) : 0.0}; + auto const straight{editor_track::is_straight(*track, m_straights.tolerance) || bend < 1e-5}; + auto const rise{path.points[segment_data::point::end].y - path.points[segment_data::point::start].y}; + auto const roll{std::max(std::abs(path.rolls[0]), std::abs(path.rolls[1]))}; + metric_tiles({{format("%.2f m", track->Length()), STR_C("length"), ImGui::GetColorU32(ImGuiCol_Text)}, + {straight ? std::string{STR_C("straight")} : format("R %.0f m", 1.0 / bend), STR_C("plan"), straight ? IM_COL32(255, 255, 255, 230) : overlay_color::grip}, + {format("%+.1f mm/m", length > 1e-6 ? rise / length * 1000.0 : 0.0), STR_C("grade"), ImGui::GetColorU32(ImGuiCol_Text)}, + {format("%.0f mm", 1500.0 * std::sin(glm::radians(static_cast(roll)))), STR_C("cant"), IM_COL32(240, 170, 60, 255)}, + {editor_track::velocity(*track) > 0.0 ? format("%.0f km/h", editor_track::velocity(*track)) : std::string{"-"}, STR_C("speed"), ImGui::GetColorU32(ImGuiCol_Text)}}); + } std::string reason; bool const geometry = editor_track::can_edit_geometry(*track, reason); if (false == geometry) @@ -1188,13 +1395,19 @@ void editor_mode::render_path_ui() } if (ImGui::TreeNode(STR_C("Cant and radius"))) { - ImGui::DragFloat2(STR_C("Roll (deg)"), path.rolls.data(), 0.01f, -15.0f, 15.0f, "%.2f"); - item_tooltip("Cant at the start and at the end as the roll angle of the track"); - finish_edit(rebuild::geometry); - ImGui::InputFloat(STR_C("Radius"), &path.radius, 0.0f, 0.0f, "%.1f"); - item_tooltip("Radius stored with the path in the scenery; the shape itself comes from the control vectors"); - finish_edit(rebuild::geometry); - if (ImGui::Button(STR_C("Straighten"))) + if (begin_fields("##cantradius", 0.3f)) + { + field_row("Roll (deg)"); + ImGui::DragFloat2("##roll", path.rolls.data(), 0.01f, -15.0f, 15.0f, "%.2f"); + item_tooltip("Cant at the start and at the end as the roll angle of the track"); + finish_edit(rebuild::geometry); + field_row("Radius"); + ImGui::InputFloat("##radius", &path.radius, 0.0f, 0.0f, "%.1f"); + item_tooltip("Radius stored with the path in the scenery; the shape itself comes from the control vectors"); + finish_edit(rebuild::geometry); + ImGui::EndTable(); + } + if (ImGui::Button(STR_C("Straighten"), ImVec2(-FLT_MIN, 0.0f))) { push_track_snapshot({{track, before}}); path.points[segment_data::point::control1] = glm::dvec3{}; @@ -1210,14 +1423,15 @@ void editor_mode::render_path_ui() } if (geometry && ImGui::TreeNode(STR_C("Snapping of the ends"))) { - ImGui::PushItemWidth(120.0f); - ImGui::DragFloat(STR_C("Snap radius (m)"), &m_track_snap_radius, 0.05f, 0.0f, 50.0f, "%.2f"); - item_tooltip("A dragged free end within this distance of another free end joins it on release"); - ImGui::PopItemWidth(); - ImGui::Checkbox(STR_C("Align with snapped path"), &m_track_align_tangent); - item_tooltip("On joining, the end also takes over the direction of the other path, so the joint has no kink"); - ImGui::Checkbox(STR_C("Drag connected ends"), &m_track_drag_connected); - item_tooltip("Dragging a joined end, or moving and turning the whole path or switch,\nmoves the ends of the neighbouring paths with it"); + if (begin_fields("##snapfields", 0.45f)) + { + field_row("Snap radius (m)"); + ImGui::DragFloat("##snapradius", &m_track_snap_radius, 0.05f, 0.0f, 50.0f, "%.2f"); + item_tooltip("A dragged free end within this distance of another free end joins it on release"); + ImGui::EndTable(); + } + toggle_chips("snapflags", {{STR_C("Align with snapped path"), &m_track_align_tangent, "On joining, the end also takes over the direction of the other path, so the joint has no kink"}, + {STR_C("Drag connected ends"), &m_track_drag_connected, "Dragging a joined end, or moving and turning the whole path or switch,\nmoves the ends of the neighbouring paths with it"}}); ImGui::TreePop(); } } @@ -1351,15 +1565,15 @@ std::vector editor_mode::track_key_hints(bool const All) else if (m_track_tab == track_tab::turnout) hints = {{"Press+drag on the track", "along: direction, sideways: side"}, {"List in the panel", "the type of the switch"}, {"LMB on another track", "choose it"}}; else if (m_track_tab == track_tab::profile && m_profile.route.spans.empty()) - hints = {{"LMB on a track", "loads the grade line of its line"}}; + hints = {{"LMB on a track", "the grade line of its whole line"}, {"Shift+LMB on more tracks", "the route between the outermost ones"}, {"Shift+drag a box", "the route over the tracks in it"}}; else if (m_track_tab == track_tab::profile) - hints = {{"Drag a point", "changes the grade"}, {"Double click", "adds a point"}, {"Apply", "writes the heights into the tracks"}}; + hints = {{"Drag a point", "changes the grade"}, {"Double click", "adds a point"}, {"LMB on another track", "its whole line"}, {"Shift+LMB on more tracks", "the route between the outermost ones"}, {"Shift+drag a box", "the route over the tracks in it"}, {"Apply", "writes the heights into the tracks"}}; else if (m_track_window_open && m_track_tab == track_tab::speed) hints = {{"LMB on a row", "show the path"}}; else if (m_track_window_open && m_track_tab == track_tab::lineside) hints = {{"LMB on a track", "the line the objects go along"}}; else if (m_track_window_open && m_track_tab == track_tab::parallel) - hints = {{"LMB on a straight", "the new track goes beside it"}}; + hints = {{"LMB on a track", "the new track goes beside it"}, {"Click on a side in the panel", "the side of the new track"}}; else if (m_track_window_open && m_track_tab == track_tab::signals && (m_signal.placing || m_signal.moving)) hints = {{"Drag along", m_signal.moving ? "moves the signal along the track" : "the trains it faces run that way"}, {"Release", "done"}, {"Esc", "cancel"}}; else if (m_track_window_open && m_track_tab == track_tab::signals && signal_armed() == nullptr) @@ -1403,12 +1617,12 @@ std::vector editor_mode::track_key_hints(bool const All) else if (m_track_tab == track_tab::route && m_route.chain.tracks.empty()) hints = {{"LMB on a straight", "its ends and the middle to drag"}, {"LMB on a curve", "its vertex and radius to drag"}}; else if (m_track_tab == track_tab::route) - hints = {{"Drag a vertex", "the curve follows"}, {"Drag the radius grip", "changes the radius"}, {"Apply", "writes the design into the tracks"}, {"Esc", "release the vertex"}}; + hints = {{"Drag a vertex", "the curve follows"}, {"Drag the radius grip", "changes the radius"}, {"Drag in the curvature band", "R up and down, transitions sideways"}, {"Apply", "writes the design into the tracks"}, {"Esc", "release the vertex"}}; else if (selected == nullptr) hints = {{"LMB on a track", "selects it"}, {"Drag on the ground", "selects the tracks in a box"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"RMB on a track", "what can be done with it"}, {"L", "lay a new track"}}; else if (m_track_tab == track_tab::path && m_track_set.size() >= 2 && in_track_set(selected)) { - hints = {{"Inspector", "the properties of all of them at once"}, {"Gizmo", "moves them together"}, {"Shift/Ctrl+LMB", "adds or takes out a track"}, {"Ctrl+Shift+V", "pastes the copied properties"}, {"Esc", "deselects"}, {"Del", "deletes them"}}; + hints = {{"Inspector", "the properties of all of them at once"}, {"Gizmo", "moves them together"}, {"Shift+LMB", "adds or takes out a track"}, {"Ctrl+Shift+V", "pastes the copied properties"}, {"Esc", "deselects"}, {"Del", "deletes them"}}; gizmo = true; } else if (m_point_drag.moved) @@ -1452,6 +1666,7 @@ std::vector editor_mode::track_key_hints(bool const All) } for (auto &hint : hints) { + hint.raw = hint.key; hint.key = STR_C(hint.key); hint.action = STR(hint.action); } @@ -1461,7 +1676,7 @@ std::vector editor_mode::track_key_hints(bool const All) // values of the change in progress, shown next to the cursor std::string editor_mode::track_readout() const { - auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; + auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{STR_C("(no name)")} : Track->name(); }; auto const azimuth = [](glm::dvec3 const &From, glm::dvec3 const &To) { auto const degrees{glm::degrees(std::atan2(To.x - From.x, To.z - From.z))}; return degrees < 0.0 ? degrees + 360.0 : degrees; @@ -1485,7 +1700,7 @@ std::string editor_mode::track_readout() const return link_summary(target, link, editor_track::branch_radius(m_extend.track->m_paths[1])); if (snapped && pieces.empty()) return format(STR_C("no track fits to the end of %s"), name(target.track).c_str()); - auto text{radius > 0.0 ? format("L %.2f m R %.0f m", length, radius) : format("L %.2f m straight", length)}; + auto text{radius > 0.0 ? format("L %.2f m R %.0f m", length, radius) : format("L %.2f m %s", length, STR_C("straight"))}; if (snapped) text += "\n" + format(STR_C("joins %s"), name(target.track).c_str()); if (false == reason.empty()) @@ -1522,7 +1737,7 @@ std::string editor_mode::track_readout() const if (lay.points.size() == 1 && (lay.start.track != nullptr || lay.mouse_snap.track != nullptr)) text += "\n" + track_shaping(lay.mouse_snap.track != nullptr && lay.start.track != nullptr); if (ImGui::GetIO().KeyAlt && false == lay.points.empty()) - text += format("\naz %.0f deg", azimuth(lay.points.back(), lay.mouse)); + text += "\n" + format(STR_C("azimuth %.0f deg"), azimuth(lay.points.back(), lay.mouse)); if (false == lay.status.empty() && ImGui::GetTime() - lay.status_time < 4.0) text += "\n" + lay.status; return text; @@ -1537,19 +1752,19 @@ std::string editor_mode::track_readout() const if (glm::length(offset) < 1e-3) return {}; auto const turn{glm::degrees(signed_angle(line.direction, glm::normalize(offset)))}; - return format("turn %+.3f deg R %.0f m", turn, straight_tool_radius(line)); + return format(STR_C("turn %+.3f deg R %.0f m"), turn, straight_tool_radius(line)); } glm::dvec2 const normal{-line.direction.y, line.direction.x}; - return format("shift %+.3f m over %.2f m", glm::dot(offset, normal), glm::dot(offset, line.direction)); + return format(STR_C("shift %+.3f m over %.2f m"), glm::dot(offset, normal), glm::dot(offset, line.direction)); } if (state.dragging && state.fitting) { auto const &result{m_route.result}; if (false == result.valid) - return result.errors.empty() ? std::string{"no curve fits"} : result.errors.front(); - auto text{format("curve L %.2f m", result.length)}; + return result.errors.empty() ? std::string{STR_C("no curve fits")} : result.errors.front(); + auto text{format(STR_C("curve L %.2f m"), result.length)}; for (auto const &curve : result.curves) - text += format(" deflection %.3f deg", glm::degrees(curve.deflection)); + text += format(STR_C(" deflection %.3f deg"), glm::degrees(curve.deflection)); return text; } if (state.dragging) @@ -1559,7 +1774,7 @@ std::string editor_mode::track_readout() const { glm::dvec2 const normal{-grabbed.direction.y, grabbed.direction.x}; auto const offset{state.preview_start - grabbed.start}; - auto text{format("shift %+.3f m", glm::dot(plan_of(offset), normal))}; + auto text{format(STR_C("shift %+.3f m"), glm::dot(plan_of(offset), normal))}; auto const offer{straight_parallel_offer(grabbed, offset)}; if (offer.snaps) text += "\n" + format(STR_C("parallel %.2f m"), offer.spacing); @@ -1567,7 +1782,7 @@ std::string editor_mode::track_readout() const text += "\n" + format(STR_C("close: %.2f m %.1f°"), offer.spacing, offer.angle); return text; } - return format("L %.3f m (%+.3f) azimuth %.4f deg", plan_distance(state.preview_start, state.preview_end), plan_distance(state.preview_start, state.preview_end) - grabbed.length, + return format(STR_C("L %.3f m (%+.3f) azimuth %.4f deg"), plan_distance(state.preview_start, state.preview_end), plan_distance(state.preview_start, state.preview_end) - grabbed.length, azimuth(state.preview_start, state.preview_end)); } if (m_track_set_using && m_track_gizmo_drag.operation == gizmo_operation::rotate) @@ -1644,6 +1859,54 @@ std::string editor_mode::track_shaping(bool const Joined) const return text + "\n" + STR("Ctrl+wheel: radius, Shift+wheel: another shape"); } +void editor_mode::draw_empty_scenery_hint() +{ + if ((m_track_tab != track_tab::path && m_track_tab != track_tab::lay) || (m_lay.active && false == m_lay.points.empty()) || m_first_steps_open || ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) + return; + auto const &paths{simulation::Paths.sequence()}; + if (std::any_of(paths.begin(), paths.end(), [](TTrack const *Track) { return Track != nullptr && false == Track->m_editorremoved; })) + return; + auto *list{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + auto const low{editor_ui::view_min()}; + auto const high{editor_ui::view_max()}; + auto const font{ImGui::GetFontSize()}; + auto const pad{std::round(font * 1.0f)}; + auto const picture{std::round(ImGui::GetTextLineHeight() * 1.3f)}; + auto const title{STR_C("The scenery has no tracks yet")}; + std::vector> rows; + if (m_track_tab == track_tab::path) + rows.emplace_back("L", "the lay tool"); + rows.emplace_back("LMB", "on the ground: the first point, then each next one lays a piece"); + rows.emplace_back("Ctrl+wheel", "the radius of the curve while laying"); + rows.emplace_back("F1", "all the shortcuts, and the tutorial"); + auto const width{std::min(high.x - low.x - 2.0f * pad, std::round(font * 30.0f))}; + auto const column{std::round(picture * 5.0f)}; + auto const wrap{width - 2.0f * pad - column}; + float height{pad + font * 2.4f + pad * 0.5f}; + for (auto const &row : rows) + height += std::max(picture, ImGui::CalcTextSize(STR_C(row.second), nullptr, false, wrap).y) + pad * 0.45f; + height += pad * 0.6f; + ImVec2 const top{std::round((low.x + high.x - width) * 0.5f), std::round((low.y + high.y - height) * 0.5f)}; + ImVec2 const bottom{top.x + width, top.y + height}; + auto const accent{family_colour(0)}; + list->AddRectFilled(top, bottom, IM_COL32(16, 20, 26, 215), std::round(font * 0.6f)); + list->AddRect(top, bottom, (accent & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 160), std::round(font * 0.6f), 0, 1.5f); + auto const icon{std::round(font * 2.4f)}; + editor_icons::draw(list, editor_icons::icon::lay_track, ImVec2(top.x + pad + icon * 0.5f, top.y + pad + icon * 0.5f), accent); + auto const big{font * 1.25f}; + list->AddText(nullptr, big, ImVec2(top.x + pad + icon + pad * 0.6f, std::round(top.y + pad + (icon - big) * 0.5f)), ImGui::GetColorU32(ImGuiCol_Text), title); + auto y{top.y + pad + icon + pad * 0.5f}; + for (auto const &row : rows) + { + auto const text{STR_C(row.second)}; + auto const size{ImGui::CalcTextSize(text, nullptr, false, wrap)}; + auto const line{std::max(picture, size.y)}; + gesture_draw(*list, ImVec2(top.x + pad, std::round(y + (line - picture) * 0.5f)), picture, gesture_picture(row.first), ImGui::GetColorU32(ImGuiCol_Text)); + list->AddText(nullptr, 0.0f, ImVec2(top.x + pad + column, std::round(y + (line - size.y) * 0.5f)), ImGui::GetColorU32(ImGuiCol_Text, 0.85f), text, nullptr, wrap); + y += line + pad * 0.45f; + } +} + void editor_mode::draw_track_hints() { ImGuiIO const &io = ImGui::GetIO(); @@ -1657,9 +1920,17 @@ void editor_mode::draw_track_hints() drawlist->AddText(ImVec2(At.x + pad, At.y), Foreground, Text); return size.x + 2.0f * pad; }; - auto const width_of = [&](key_hint const &Hint) { return ImGui::CalcTextSize(Hint.key).x + 2.0f * pad + 5.0f + ImGui::CalcTextSize(Hint.action.c_str()).x; }; - auto hints{track_key_hints(false)}; + auto const picture{std::round(ImGui::GetTextLineHeight() * 1.15f)}; + std::vector> pictures; + for (auto const &hint : hints) + pictures.push_back(gesture_picture(hint.raw != nullptr ? hint.raw : hint.key)); + auto const picture_width = [&](std::size_t const Index) { return pictures[Index].empty() ? 0.0f : gesture_width(picture, pictures[Index]) + 5.0f; }; + auto const width_of = [&](std::size_t const Index) { + auto const chip_width{hints[Index].raw != nullptr && gesture_keys_only(hints[Index].raw) ? 0.0f : ImGui::CalcTextSize(hints[Index].key).x + 2.0f * pad + 5.0f}; + return picture_width(Index) + chip_width + ImGui::CalcTextSize(hints[Index].action.c_str()).x; + }; + auto const mode{track_mode_name()}; auto const left{editor_ui::view_min().x + margin}; auto const available{editor_ui::view_max().x - editor_ui::view_min().x - 2.0f * margin}; @@ -1667,24 +1938,32 @@ void editor_mode::draw_track_hints() std::size_t shown{0}; for (; shown < std::min(hints.size(), 3); ++shown) { - auto const next{width + gap + width_of(hints[shown])}; + auto const next{width + gap + width_of(shown)}; if (next > available) break; width = next; } auto const height{ImGui::GetTextLineHeight()}; - auto const y{editor_ui::view_max().y - margin - height}; + auto const bottom{m_profile.strip_top > editor_ui::view_min().y + 4.0f * height ? std::min(editor_ui::view_max().y, m_profile.strip_top) : editor_ui::view_max().y}; + auto const y{bottom - margin - height}; ImVec2 const from{left - 6.0f, y - pad - 2.0f}; ImVec2 const to{left + width + 6.0f, y + height + pad}; auto const hovered{io.MousePos.x > from.x - 40.0f && io.MousePos.x < to.x + 40.0f && io.MousePos.y > from.y - 40.0f && io.MousePos.y < to.y + 40.0f}; auto const fade = [hovered](ImU32 const Colour) { return hovered ? (Colour & ~IM_COL32_A_MASK) | (((Colour >> IM_COL32_A_SHIFT) & 0xFF) / 4 << IM_COL32_A_SHIFT) : Colour; }; drawlist->AddRectFilled(from, to, fade(IM_COL32(0, 0, 0, 160)), 4.0f); auto x{left}; - x += chip(ImVec2(x, y), mode.c_str(), fade(IM_COL32(60, 140, 230, 235)), fade(IM_COL32(255, 255, 255, 255))); + auto const &modes{track_modes()}; + auto const tab{false == m_track_window_open ? track_tab::path : m_track_tab == track_tab::route ? track_tab::straights : m_track_tab}; + auto const found{std::find_if(modes.begin(), modes.end(), [&](track_mode const &Mode) { return Mode.tab == tab; })}; + auto const family{(family_colour(found != modes.end() ? found->family : 0) & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 235)}; + x += chip(ImVec2(x, y), mode.c_str(), fade(family), fade(IM_COL32(255, 255, 255, 255))); for (std::size_t i = 0; i < shown; ++i) { x += gap; - x += chip(ImVec2(x, y), hints[i].key, fade(IM_COL32(255, 210, 60, 220)), fade(IM_COL32(20, 20, 20, 255))) + 5.0f; + if (false == pictures[i].empty()) + x += gesture_draw(*drawlist, ImVec2(x, std::round(y + (height - picture) * 0.5f)), picture, pictures[i], fade(IM_COL32(255, 255, 255, 230))) + 5.0f; + if (hints[i].raw == nullptr || false == gesture_keys_only(hints[i].raw)) + x += chip(ImVec2(x, y), hints[i].key, fade(IM_COL32(255, 210, 60, 220)), fade(IM_COL32(20, 20, 20, 255))) + 5.0f; drawlist->AddText(ImVec2(x, y), fade(IM_COL32(255, 255, 255, 230)), hints[i].action.c_str()); x += ImGui::CalcTextSize(hints[i].action.c_str()).x; } @@ -1753,6 +2032,9 @@ void editor_mode::draw_build_overlay() const } } auto const &display{ImGui::GetIO().DisplaySize}; + // in the 3D view only near the camera, where they can be told apart; from above all of them + glm::dvec3 const eye{Global.pCamera.Pos}; + auto const in_reach = [&](glm::dvec3 const &Point) { return Global.EditorOrtho || glm::distance(Point, eye) <= kDotsRange; }; for (auto const *track : simulation::Paths.sequence()) { if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) @@ -1761,7 +2043,7 @@ void editor_mode::draw_build_overlay() const for (auto const point : {segment_data::point::start, segment_data::point::end}) { ImVec2 screen; - if (projection.project(path.points[point], screen) && screen.x >= 0.0f && screen.y >= 0.0f && screen.x <= display.x && screen.y <= display.y) + if (in_reach(path.points[point]) && projection.project(path.points[point], screen) && screen.x >= 0.0f && screen.y >= 0.0f && screen.x <= display.x && screen.y <= display.y) drawlist->AddCircleFilled(screen, 3.5f, kJointColor); } } diff --git a/application/editormodetrackprops.cpp b/application/editormodetrackprops.cpp index 33dd394d..574c2cda 100644 --- a/application/editormodetrackprops.cpp +++ b/application/editormodetrackprops.cpp @@ -59,7 +59,7 @@ bool rail(TTrack const &Track) std::string name_of(TTrack const &Track) { - return Track.name().empty() ? std::string{"(noname)"} : Track.name(); + return Track.name().empty() ? std::string{STR_C("(no name)")} : Track.name(); } char const *type_of(TTrack const &Track) @@ -71,7 +71,7 @@ char const *type_of(TTrack const &Track) case tt_Cross: return "cross"; case tt_Table: return "turntable"; case tt_Tributary: return "tributary"; - default: return "unknown"; + default: return "unknown type"; } } @@ -237,6 +237,7 @@ struct track_property int group{group::speed}; bool rebuild{false}; // the geometry of the track is made again after a change bool late{false}; // pasted after the others, it depends on them + bool more{false}; int texture{-1}; // which list of the textures on the tracks: 0 rails, 1 trackbed char const *label{""}; char const *tooltip{""}; @@ -322,6 +323,7 @@ std::vector make_properties() { auto &property{add(list, "overhead", field::choice, section::traffic, group::overhead_line, "Overhead line", "State of the overhead line over the track: the trains coast through a section with no current,\nor lower the pantographs and keep to the speed limit given")}; property.applies = any; + property.more = true; property.choices = {"Normal", "No current, coasting", "Pantographs lowered, speed limit"}; property.get = [](TTrack const &Track) { return std::string{Track.fOverhead < 0.f ? "0" : Track.fOverhead == 0.f ? "1" : "2"}; }; property.set = [](TTrack &Track, std::string const &Text) { @@ -338,6 +340,7 @@ std::vector make_properties() } { auto &property{add(list, "overheadspeed", field::number, section::traffic, group::overhead_line, "Speed with the pantographs lowered (km/h)", "Speed limit of the trains with the pantographs lowered")}; + property.more = true; property.applies = [](TTrack const &Track) { return Track.fOverhead > 0.f; }; property.get = [](TTrack const &Track) { return decimal(Track.fOverhead, 0); }; property.set = [](TTrack &Track, std::string const &Text) { @@ -354,6 +357,7 @@ std::vector make_properties() { auto &property{add(list, "circuits", field::text, section::traffic, group::circuits, "Track circuits", "Names of the track circuits (isolated) the track belongs to, separated by spaces.\nA name which doesn't exist yet makes a new circuit. Tracks with vehicles on them keep theirs")}; property.applies = any; + property.more = true; property.empty = "no circuits"; property.get = [](TTrack const &Track) { std::string text; @@ -390,6 +394,30 @@ std::vector make_properties() return true; }; } + { + // the profiles of the rails the game has (models/tory/railprofile_*), the first: none, the default shape + static char const *const profiles[] = {"", "s60", "uic60", "s49", "s42", "s39", "s30pn", "s30din", "s24", "s20", "s18", "s14", "s10", "r-65", "ri60n", "lk1"}; + auto &property{add(list, "railprofile", field::choice, section::look, group::rails, "Rail profile", + "Shape of the rails, the railprofile of the track. It also picks the switch templates of the rail: S60 60E1, S49 49E1.\nNo profile: the default shape, and the templates of all the rails are offered")}; + property.rebuild = true; + property.more = true; + property.applies = [](TTrack const &Track) { return Track.m_visible && (Track.iCategoryFlag & 15) == 1; }; + property.choices = {"No profile", "S60 (60E1)", "UIC60", "S49 (49E1)", "S42", "S39", "S30 PN", "S30 DIN", "S24", "S20", "S18", "S14", "S10", "R65", "Ri60N", "LK1"}; + property.get = [](TTrack const &Track) { + auto const at{std::find_if(std::begin(profiles), std::end(profiles), [&](char const *Name) { return Track.m_profile1.first == Name; })}; + return at != std::end(profiles) ? std::to_string(at - std::begin(profiles)) : Track.m_profile1.first; + }; + property.set = [](TTrack &Track, std::string const &Text) { + double index; + if (false == parse_number(Text, index) || index < 0.0 || index >= static_cast(std::size(profiles))) + return false; + std::string const name{profiles[static_cast(index)]}; + if (name == Track.m_profile1.first) + return false; + Track.rail_profile(name); + return true; + }; + } { auto &property{add(list, "trackbed", field::texture, section::look, group::trackbed, "Trackbed", "Texture of the trackbed (ballast) under the rails.\nA material dragged from the texture browser can be dropped here")}; property.rebuild = true; @@ -409,7 +437,7 @@ std::vector make_properties() }; } { - auto &property{add(list, "frogs", field::check, section::look, group::frogs, "Frog and check rails made from the rails", "Cuts the rails of the switch at its frog and lays wing rails and check rails, for rail profiles S60, UIC60 and S39.\nOff for a switch whose include has its own model of them")}; + auto &property{add(list, "frogs", field::check, section::look, group::frogs, "Frog and check rails made from the rails", "Cuts the rails of the switch at its frog and lays wing rails and check rails.\nOff for a switch whose include has its own model of them")}; property.rebuild = true; property.applies = [](TTrack const &Track) { return track_features::crossings && Track.eType == tt_Switch && Track.SwitchExtension != nullptr; }; property.get = [](TTrack const &Track) { return std::string{Track.SwitchExtension->m_frogs ? "1" : "0"}; }; @@ -1067,10 +1095,12 @@ void editor_mode::render_track_properties(std::vector const &Tracks) ImGui::AlignTextToFramePadding(); auto const partial{cache.applies[Index] < Tracks.size()}; auto const mixed{cache.values[Index].size() > 1}; + ImGui::PushTextWrapPos(0.0f); if (mixed) ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.45f, 1.0f), "%s", STR_C(property.label)); else ImGui::TextUnformatted(STR_C(property.label)); + ImGui::PopTextWrapPos(); if (partial) { ImGui::SameLine(0.0f, 4.0f); @@ -1181,19 +1211,22 @@ void editor_mode::render_track_properties(std::vector const &Tracks) track_property_apply(static_cast(Index), Tracks, dropped); }; - auto const rows = [&](int const Part) { - bool any{false}; + auto const any_in = [&](int const Part, bool const More) { for (std::size_t i = 0; i < list.size(); ++i) - any |= list[i].part == Part && cache.applies[i] > 0; - if (false == any) + if (list[i].part == Part && list[i].more == More && cache.applies[i] > 0) + return true; + return false; + }; + auto const rows = [&](int const Part, bool const More) { + if (false == any_in(Part, More)) return; - if (false == ImGui::BeginTable(format("##trackprops%d", Part).c_str(), 2, ImGuiTableFlags_SizingStretchProp)) + if (false == ImGui::BeginTable(format("##trackprops%d%d", Part, More ? 1 : 0).c_str(), 2, ImGuiTableFlags_SizingStretchProp)) return; ImGui::TableSetupColumn("label", ImGuiTableColumnFlags_WidthStretch, 0.45f); ImGui::TableSetupColumn("value", ImGuiTableColumnFlags_WidthStretch, 0.55f); for (std::size_t i = 0; i < list.size(); ++i) { - if (list[i].part != Part || cache.applies[i] == 0) + if (list[i].part != Part || list[i].more != More || cache.applies[i] == 0) continue; ImGui::PushID(list[i].key); row_label(i); @@ -1208,31 +1241,40 @@ void editor_mode::render_track_properties(std::vector const &Tracks) } ImGui::EndTable(); }; - auto const any_in = [&](int const Part) { - for (std::size_t i = 0; i < list.size(); ++i) - if (list[i].part == Part && cache.applies[i] > 0) - return true; - return false; + auto const more = [&](char const *Id, int const Part) { + if (false == any_in(Part, true)) + return; + ImGui::PushID(Id); + if (ImGui::TreeNode(STR_C("More settings"))) + { + rows(Part, true); + ImGui::TreePop(); + } + ImGui::PopID(); }; if (ImGui::CollapsingHeader(STR_C("Traffic"), ImGuiTreeNodeFlags_DefaultOpen)) - rows(section::traffic); - if ((any_in(section::look) || any_in(section::look_bed) || any_in(section::look_sleepers)) && ImGui::CollapsingHeader(STR_C("Look"), ImGuiTreeNodeFlags_DefaultOpen)) { - rows(section::look); - if (any_in(section::look_bed) && ImGui::TreeNode(STR_C("Shape of the trackbed"))) + rows(section::traffic, false); + more("traffic", section::traffic); + } + if ((any_in(section::look, false) || any_in(section::look, true) || any_in(section::look_bed, false) || any_in(section::look_sleepers, false)) && ImGui::CollapsingHeader(STR_C("Look"), ImGuiTreeNodeFlags_DefaultOpen)) + { + rows(section::look, false); + more("look", section::look); + if (any_in(section::look_bed, false) && ImGui::TreeNode(STR_C("Shape of the trackbed"))) { - rows(section::look_bed); + rows(section::look_bed, false); ImGui::TreePop(); } - if (any_in(section::look_sleepers) && ImGui::TreeNode(STR_C("Sleeper models"))) + if (any_in(section::look_sleepers, false) && ImGui::TreeNode(STR_C("Sleeper models"))) { - rows(section::look_sleepers); + rows(section::look_sleepers, false); ImGui::TreePop(); } } if (ImGui::CollapsingHeader(STR_C("Ride and sound"))) - rows(section::ride); + rows(section::ride, false); } void editor_mode::copy_track_style(TTrack const &Track) @@ -1288,7 +1330,8 @@ void editor_mode::render_track_style(std::vector const &Tracks) { auto &style{m_track_style}; auto const single{Tracks.size() == 1}; - ImGui::SeparatorText(STR_C("Copying properties")); + if (false == ImGui::CollapsingHeader(format("%s###trackstyle%d", STR_C("Copying properties"), single ? 1 : 2).c_str(), single ? 0 : ImGuiTreeNodeFlags_DefaultOpen)) + return; auto const width{button_width(3)}; ImGui::BeginDisabled(false == single); if (ImGui::Button(STR_C("Copy"), ImVec2(width, 0.0f))) @@ -1389,6 +1432,7 @@ void editor_mode::render_track_set_header(std::vector const &Tracks) switches += track->eType == tt_Switch ? 1 : 0; } ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.2f, 1.0f), STR_P("%d track selected, %.1f m", "%d tracks selected, %.1f m", static_cast(Tracks.size())), static_cast(Tracks.size()), length); + item_tooltip("What is set below goes to all of them, in one step of the undo"); ImGui::SameLine(); if (auto const room{ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(STR_C("Deselect")).x - ImGui::GetStyle().FramePadding.x * 2.0f}; room > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + room); @@ -1400,7 +1444,7 @@ void editor_mode::render_track_set_header(std::vector const &Tracks) item_tooltip("Clears the selection (Esc)"); if (switches > 0) ImGui::TextDisabled(STR_P("among them %d switch", "among them %d switches", static_cast(switches)), switches); - ImGui::TextDisabled("%s", STR_C("What is set below goes to all of them, in one step of the undo")); + ImGui::TextDisabled("%s", STR_C("What is set below goes to all of them")); // the list: hovering a row shows the track in the view, a click flies to it, a double click keeps it alone if (ImGui::TreeNode(STR_C("List of the selected tracks"))) @@ -1449,7 +1493,8 @@ void editor_mode::render_track_set_header(std::vector const &Tracks) void editor_mode::render_track_select_tools(std::vector const &Tracks) { - ImGui::SeparatorText(format("%s (?)", STR_C("Selection")).c_str()); + auto const single{Tracks.size() == 1}; + auto const open{ImGui::CollapsingHeader(format("%s (?)###trackselect%d", STR_C("Selection"), single ? 1 : 2).c_str(), single ? 0 : ImGuiTreeNodeFlags_DefaultOpen)}; if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Shift+click: add or take out a track\n" "Ctrl+click: the same\n" @@ -1457,6 +1502,8 @@ void editor_mode::render_track_select_tools(std::vector const &Tracks) "Ctrl+Shift+click: the whole run of joined tracks\n" "Esc: clear the selection, Del: delete the selected tracks\n" "Ctrl+Shift+C / Ctrl+Shift+V: copy and paste the properties")); + if (false == open) + return; auto const width{button_width(2)}; if (ImGui::Button(STR_C("+ Joined"), ImVec2(width, 0.0f))) { diff --git a/application/editormodetrackshared.h b/application/editormodetrackshared.h index 183a9664..c2a45c74 100644 --- a/application/editormodetrackshared.h +++ b/application/editormodetrackshared.h @@ -83,7 +83,7 @@ inline std::string narrowed(std::vector const &Pieces, editor_trac inline std::string describe(editor_track::straight const &Line) { - return format("L %.2f m az %.4f deg i %.2f per mille %zu paths", Line.length, Line.azimuth, Line.grade * 1000.0, Line.tracks.size()); + return format(STR_C("L %.2f m azimuth %.4f deg grade %.2f per mille %zu paths"), Line.length, Line.azimuth, Line.grade * 1000.0, Line.tracks.size()); } inline char const *point_label(editor_track::point_kind const Kind) @@ -107,6 +107,7 @@ double const kLineReach{2000.0}; // how far each way the Curve tab follows the l double const kDigitizeFit{0.5}; double const kDrawnReach{100000.0}; double const kJointRange{400.0}; // the ends of the paths are drawn for the tracks this close to the camera +double const kDotsRange{500.0}; // m, the dots of the ends of the tracks are drawn this close to the camera in the 3D view ImU32 const kJointColor{IM_COL32(255, 255, 255, 200)}; int const kRailCategory{1}; @@ -151,7 +152,7 @@ inline double switch_from(editor_track::straight const &Line, double const Lengt inline std::string link_summary(editor_track::snap_target const &Target, editor_track::branch_link const &Link, double const Switchradius) { - auto const name{Target.track != nullptr && false == Target.track->name().empty() ? Target.track->name() : std::string{"(noname)"}}; + auto const name{Target.track != nullptr && false == Target.track->name().empty() ? Target.track->name() : std::string{STR_C("(no name)")}}; auto text{Link.reverse ? format(STR_C("The branch leads into %s with reverse curves R %.0f m"), name.c_str(), Link.radius) : Link.radius > 0.0 ? format(STR_C("The branch leads into %s with an R %.0f m curve"), name.c_str(), Link.radius) : format(STR_C("The branch leads straight into %s"), name.c_str())}; if (Link.insert > 0.0) @@ -226,7 +227,7 @@ inline char const *type_name(TTrack const &Track) case tt_Cross: return "cross"; case tt_Table: return "turntable"; case tt_Tributary: return "tributary"; - default: return "unknown"; + default: return "unknown type"; } } diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 5257e097..96d5d9ac 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormode.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" +#include "application/editormodeshared.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" @@ -62,7 +63,7 @@ bool rail(TTrack const &Track) std::string name_of(TTrack const &Track) { - return Track.name().empty() ? std::string{"(noname)"} : Track.name(); + return Track.name().empty() ? std::string{STR_C("(no name)")} : Track.name(); } std::string lower(std::string Text) @@ -80,7 +81,7 @@ char const *type_of(TTrack const &Track) case tt_Cross: return "cross"; case tt_Table: return "turntable"; case tt_Tributary: return "tributary"; - default: return "unknown"; + default: return "unknown type"; } } @@ -369,6 +370,13 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) return make(kind::box, track_selected(m_hover.track) ? "Click: take out of the selection" : "Click: add to the selection"); } } + if (m_track_tab == track_tab::profile && shift && false == gizmo_frame::over()) + { + // a box from the empty ground is the route by itself, a click on a track adds it to the route or takes it out + intent.track = m_hover.track; + intent.index = m_hover.track == nullptr ? 1 : 0; + return make(kind::box, m_hover.track == nullptr ? "Drag: the route over the tracks in a box" : track_selected(m_hover.track) ? "Click: take it out of the route, drag: a box" : "Click: add it to the route, drag: a box"); + } if (m_track_tab == track_tab::straights) { auto const &line{current_straight()}; @@ -455,7 +463,7 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) case track_tab::straights: case track_tab::route: return make(kind::select, m_hover.track->eType != tt_Switch && editor_track::is_straight(*m_hover.track, m_straights.tolerance) ? "Click: the whole straight through this track" : "Click: the curve of this track with its straights"); case track_tab::turnout: return make(kind::select, m_hover.track->eType == tt_Switch ? "Click: edit this switch" : m_switch.armed < 0 ? "Click: choose this track, then the type of the switch" : "Click: choose this track, then press and drag on it for the switch"); - case track_tab::profile: return make(kind::select, "Click: the grade line through this track"); + case track_tab::profile: return make(kind::select, "Click: the grade line of the whole line through this track; Shift: the route between the tracks"); case track_tab::lineside: return make(kind::select, "Click: the line the objects go along"); case track_tab::parallel: return make(kind::select, "Click: the straight the new track goes beside"); default: return make(kind::select, "Click: select"); @@ -777,8 +785,11 @@ void editor_mode::render_track_context() show_track_tab(track_tab::turnout); if (ImGui::MenuItem(STR_C("Plan"), "G", false, normal)) show_track_tab(track_tab::straights); - if (ImGui::MenuItem(STR_C("Parallel track"), "L", false, normal)) - show_track_tab(track_tab::lay); + if (ImGui::MenuItem(STR_C("Parallel track"), "N", false, normal)) + { + select_track(track); + show_track_tab(track_tab::parallel); + } if (ImGui::MenuItem(STR_C("Place a vehicle here"))) { show_track_tab(track_tab::lineside); @@ -808,7 +819,7 @@ void editor_mode::render_track_context() speed_start(); } ImGui::Separator(); - if (ImGui::MenuItem(track_selected(track) ? STR_C("Take out of the selection") : STR_C("Add to the selection"), "Shift+LMB")) + if (ImGui::MenuItem(track_selected(track) ? STR_C("Take out of the selection") : STR_C("Add to the selection"), STR_C("Shift+LMB"))) track_set_toggle(*track); if (ImGui::MenuItem(STR_C("Look at it"))) focus_track(*track, context.target.path, point); @@ -1191,22 +1202,49 @@ void editor_mode::track_set_grow() void editor_mode::render_track_spread_ui() { auto &spread{m_spread}; - if (false == ImGui::CollapsingHeader(STR_C("Spread along the joins: Ctrl+Shift+LMB on a path"))) + auto const open{ImGui::CollapsingHeader(STR_C("Spread along the joins"))}; + item_tooltip("Ctrl+Shift+LMB on a track adds the tracks joined to it to the selection.\nHolding Ctrl+Shift shows what the click takes; on a track of the selection it takes them out"); + if (false == open) return; - char const *const stops[] = {STR_C("Stop before the switches"), STR_C("Through the switches, along their main track"), STR_C("Through everything joined")}; - for (int i = 0; i < IM_ARRAYSIZE(stops); ++i) - ImGui::RadioButton(stops[i], &spread.stop, i); - ImGui::TextUnformatted(STR_C("Only to the paths like the clicked one:")); - ImGui::SameLine(); - ImGui::Checkbox(STR_C("rails"), &spread.same_rails); - ImGui::SameLine(); - ImGui::Checkbox(STR_C("trackbed"), &spread.same_bed); - ImGui::SameLine(); - ImGui::Checkbox(STR_C("speed"), &spread.same_speed); - ImGui::SetNextItemWidth(110.f); - ImGui::InputFloat(STR_C("Reach along the track, m (0: no limit)"), &spread.reach, 50.f, 500.f, "%.0f"); + auto const gap{std::round(ImGui::GetFontSize() * 0.3f)}; + auto const width{std::floor((ImGui::GetContentRegionAvail().x - 2.0f * gap) / 3.0f)}; + auto const at = [](ImVec2 const Low, ImVec2 const High, float const X, float const Y) { return ImVec2(Low.x + (High.x - Low.x) * X, Low.y + (High.y - Low.y) * Y); }; + char const *const labels[]{STR_C("Up to a switch"), STR_C("Main track on"), STR_C("Everything joined")}; + char const *const tooltips[]{"Stop before the switches", "Through the switches, along their main track", "Through everything joined"}; + for (int i = 0; i < 3; ++i) + { + if (i > 0) + ImGui::SameLine(0.0f, gap); + ImGui::PushID(i); + if (sketch_tile("##spreadstop", labels[i], spread.stop == i, width, [&](ImDrawList &List, ImVec2 Low, ImVec2 High, ImU32 Line, ImU32 Made) { + auto const dim{(Line & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 70)}; + List.AddLine(at(Low, High, 0.0f, 0.75f), at(Low, High, 1.0f, 0.75f), dim, 2.0f); + List.AddBezierQuadratic(at(Low, High, 0.45f, 0.75f), at(Low, High, 0.7f, 0.75f), at(Low, High, 1.0f, 0.15f), dim, 2.0f); + List.AddLine(at(Low, High, 0.0f, 0.75f), at(Low, High, i == 0 ? 0.4f : 1.0f, 0.75f), Made, 2.5f); + if (i == 2) + List.AddBezierQuadratic(at(Low, High, 0.45f, 0.75f), at(Low, High, 0.7f, 0.75f), at(Low, High, 1.0f, 0.15f), Made, 2.5f); + List.AddCircleFilled(at(Low, High, 0.12f, 0.75f), 3.0f, IM_COL32(230, 90, 70, 255)); + })) + spread.stop = i; + item_tooltip(tooltips[i]); + ImGui::PopID(); + } + ImGui::PushID("spread"); + if (ImGui::TreeNode(STR_C("More settings"))) + { + ImGui::TextDisabled("%s", STR_C("Only to the paths like the clicked one:")); + toggle_chips("spreadsame", {{STR_C("rails"), &spread.same_rails}, {STR_C("trackbed"), &spread.same_bed}, {STR_C("speed"), &spread.same_speed}}); + if (begin_fields("##spreadfields", 0.55f)) + { + field_row("Reach along the track (m)"); + ImGui::InputFloat("##spreadreach", &spread.reach, 50.f, 500.f, "%.0f"); + item_tooltip("How far along the tracks the spread goes from the clicked one; 0: no limit"); + ImGui::EndTable(); + } + ImGui::TreePop(); + } + ImGui::PopID(); spread.reach = std::clamp(spread.reach, 0.f, 100000.f); - ImGui::TextDisabled("%s", STR_C("Holding Ctrl+Shift shows what the click takes; on a path of the set it takes them out")); } void editor_mode::draw_track_spread() const @@ -1305,7 +1343,7 @@ void editor_mode::track_set_delete() ui()->set_node(nullptr); m_track_point = {}; straight_refresh(); - auto status{format(STR_C("%zu paths deleted, Ctrl+Z brings them back"), removed.size())}; + auto status{format(STR_P("%zu track deleted, Ctrl+Z brings it back", "%zu tracks deleted, Ctrl+Z brings them back", static_cast(removed.size())), removed.size())}; if (kept > 0) status += format(STR_C(", %zu with vehicles on them or in the files which can't be changed are left"), kept); ui()->set_status(status, false); diff --git a/application/editormodeturntable.cpp b/application/editormodeturntable.cpp index 182d93bb..c571620e 100644 --- a/application/editormodeturntable.cpp +++ b/application/editormodeturntable.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" +#include "application/editorgestures.h" #include "application/editormodeshared.h" #include "application/editorprojection.h" #include "application/editoruilayer.h" @@ -668,7 +669,7 @@ void editor_mode::turntable_place(glm::dvec3 const &Centre, double const Yaw) m_track_point = {}; ui()->set_node(m_node); turntable_read(table); - tool.status = format(STR_C("Turntable %s: click around it for the tracks out of it, or fit the tracks around. It turns after the scenery is saved and loaded again"), table->name().c_str()); + tool.status = format(STR_C("Turntable %s placed; it turns after the scenery is saved and loaded again"), table->name().c_str()); } void editor_mode::turntable_press(track_intent const &Intent) @@ -826,7 +827,6 @@ void editor_mode::render_turntable_ui() { auto const &kind{tool.templates[tool.kind]}; ImGui::TextDisabled("%s", kind.name.c_str()); - ImGui::SameLine(); ImGui::TextDisabled(kind.angles > 0 ? STR_C("(positions set by the tracks around, up to %d)") : STR_C("(%d positions fixed by the template)"), kind.angles > 0 ? kind.positions : static_cast(kind.fixed.size())); } else @@ -835,7 +835,49 @@ void editor_mode::render_turntable_ui() auto const centre{turntable_centre()}; auto const ends{rim_ends(&table, centre, tool.length * 0.5)}; - if (false == tool.exits.empty() && ImGui::TreeNodeEx(format(STR_C("Positions (%d)"), static_cast(tool.exits.size())).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + if (false == tool.exits.empty()) + { + // the positions round the pit: a track out of the end of a position in green, a free end as a grey tick, the numbers + // past them, the bridge at the first position + auto const occupied = [&](double const Heading) { return std::any_of(ends.begin(), ends.end(), [&](rim_end const &End) { return std::abs(circular(End.heading - Heading)) < 0.05; }); }; + auto const font{ImGui::GetFontSize()}; + auto const width{ImGui::GetContentRegionAvail().x}; + auto const height{std::round(font * 10.0f)}; + auto const top{ImGui::GetCursorScreenPos()}; + ImGui::Dummy(ImVec2(width, height)); + auto *list{ImGui::GetWindowDrawList()}; + list->AddRectFilled(top, ImVec2(top.x + width, top.y + height), ImGui::GetColorU32(ImGuiCol_FrameBg), std::round(font * 0.35f)); + ImVec2 const middle{std::round(top.x + width * 0.5f), std::round(top.y + height * 0.5f)}; + auto const rim{height * 0.22f}; + auto const point = [&](double const Heading, float const Distance) { + auto const direction{heading_vector(Heading)}; + return ImVec2(middle.x + static_cast(direction.x) * Distance, middle.y - static_cast(direction.y) * Distance); + }; + list->AddCircleFilled(middle, rim, IM_COL32(40, 44, 52, 255), 48); + list->AddCircle(middle, rim, ImGui::GetColorU32(ImGuiCol_Text, 0.7f), 48, 2.0f); + auto const bridge{tool.table_yaw + tool.exits.front()}; + list->AddLine(point(bridge, rim * 0.95f), point(bridge + 180.0, rim * 0.95f), IM_COL32(150, 160, 175, 255), std::round(font * 0.55f)); + list->AddLine(point(bridge, rim * 0.95f), point(bridge + 180.0, rim * 0.95f), ImGui::GetColorU32(ImGuiCol_Text), 1.5f); + list->AddCircleFilled(middle, 3.0f, ImGui::GetColorU32(ImGuiCol_Text)); + for (std::size_t i = 0; i < tool.exits.size(); ++i) + for (auto const back : {false, true}) + { + auto const heading{tool.table_yaw + tool.exits[i] + (back ? 180.0 : 0.0)}; + auto const taken{occupied(heading)}; + if (taken) + list->AddLine(point(heading, rim), point(heading, rim * 1.5f), IM_COL32(90, 230, 110, 255), 3.0f); + else + list->AddLine(point(heading, rim), point(heading, rim * 1.15f), ImGui::GetColorU32(ImGuiCol_TextDisabled), 1.5f); + if (back) + continue; + auto const number{std::to_string(i + 1)}; + auto const size{ImGui::CalcTextSize(number.c_str())}; + auto const at{point(heading, rim * (taken ? 1.5f : 1.15f) + font * 0.75f)}; + list->AddText(ImVec2(std::round(at.x - size.x * 0.5f), std::round(at.y - size.y * 0.5f)), taken ? IM_COL32(150, 240, 160, 255) : ImGui::GetColorU32(ImGuiCol_TextDisabled), number.c_str()); + } + ImGui::TextDisabled("%s", STR_C("green: a track out of the position")); + } + if (false == tool.exits.empty() && ImGui::TreeNodeEx(format(STR_C("Positions (%d)"), static_cast(tool.exits.size())).c_str())) { for (std::size_t i = 0; i < tool.exits.size(); ++i) { @@ -843,7 +885,7 @@ void editor_mode::render_turntable_ui() auto const occupied = [&](double const Heading) { return std::any_of(ends.begin(), ends.end(), [&](rim_end const &End) { return std::abs(circular(End.heading - Heading)) < 0.05; }); }; auto const front{occupied(heading)}; auto const back{occupied(heading + 180.0)}; - ImGui::Text("%2d %7.2f deg", static_cast(i + 1), tool.exits[i]); + ImGui::Text("%2d %7.2f°", static_cast(i + 1), tool.exits[i]); ImGui::SameLine(); if (front || back) ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.f), "%s%s%s", front ? STR_C("track") : "", front && back ? " + " : "", back ? STR_C("track on the other end") : ""); @@ -853,29 +895,44 @@ void editor_mode::render_turntable_ui() ImGui::TreePop(); } - ImGui::Separator(); - ImGui::TextUnformatted(STR_C("Tracks around")); - ImGui::PushItemWidth(110.f); - ImGui::InputFloat(STR_C("Reach from the rim, m"), &tool.reach, 10.f, 50.f, "%.0f"); - tool.reach = std::clamp(tool.reach, 5.f, 500.f); - ImGui::InputFloat(STR_C("Radius of the curves, m"), &tool.radius, 10.f, 50.f, "%.0f"); - tool.radius = std::clamp(tool.radius, static_cast(kMinimumRadius), 2000.f); - if (tool.kind < 0 || tool.templates[tool.kind].angles > 0) + ImGui::SeparatorText(STR_C("Tracks around")); + ImGui::PushID("turntablemore"); + if (ImGui::TreeNode(STR_C("More settings"))) { - ImGui::InputFloat(STR_C("Angle step of a new track, deg (0: free)"), &tool.step, 0.5f, 2.5f, "%.2f"); - tool.step = std::clamp(tool.step, 0.f, 45.f); + if (begin_fields("##turntablefields", 0.5f)) + { + field_row("Reach from the rim, m"); + ImGui::InputFloat("##turntablereach", &tool.reach, 10.f, 50.f, "%.0f"); + tool.reach = std::clamp(tool.reach, 5.f, 500.f); + field_row("Radius of the curves, m"); + ImGui::InputFloat("##turntableradius", &tool.radius, 10.f, 50.f, "%.0f"); + tool.radius = std::clamp(tool.radius, static_cast(kMinimumRadius), 2000.f); + if (tool.kind < 0 || tool.templates[tool.kind].angles > 0) + { + field_row("Angle step of a new track, deg (0: free)"); + ImGui::InputFloat("##turntablestep", &tool.step, 0.5f, 2.5f, "%.2f"); + tool.step = std::clamp(tool.step, 0.f, 45.f); + } + ImGui::EndTable(); + } + ImGui::TreePop(); } - ImGui::PopItemWidth(); - ImGui::TextDisabled("%s", STR_C("Click around the turntable: a track out of it, as long as the cursor is far.\nClick a free end near it: that track is led to the rim, straight or with a curve")); - if (ImGui::Button(STR_C("Find the free ends around"))) + ImGui::PopID(); + gesture_rows({{"LMB around the turntable", "a track out of it, as long as the cursor is far"}, {"LMB on a free end near it", "that track is led to the rim, straight or with a curve"}}); + if (ImGui::Button(STR_C("Find the free ends around"), ImVec2(-FLT_MIN, 0.f))) turntable_find_fits(); if (false == tool.fits.empty()) { int chosen{0}; for (auto const &fit : tool.fits) chosen += fit.chosen && fit.issue.empty() ? 1 : 0; - ImGui::SameLine(); - if (ImGui::Button(format(STR_C("Lead the chosen to the turntable (%d)"), chosen).c_str())) + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.42f, 0.24f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.22f, 0.52f, 0.30f, 1.0f)); + ImGui::BeginDisabled(chosen == 0); + auto const lead{ImGui::Button(format(STR_C("Lead the chosen to the turntable (%d)"), chosen).c_str(), ImVec2(-FLT_MIN, 0.f))}; + ImGui::EndDisabled(); + ImGui::PopStyleColor(2); + if (lead) { std::vector fits; for (auto const &fit : tool.fits) @@ -895,7 +952,7 @@ void editor_mode::render_turntable_ui() else ImGui::TextDisabled(" - "); ImGui::SameLine(); - auto const name{fit.end.track->name().empty() ? std::string{"(noname)"} : fit.end.track->name()}; + auto const name{fit.end.track->name().empty() ? std::string{STR_C("(no name)")} : fit.end.track->name()}; if (ImGui::Selectable(name.c_str(), false, ImGuiSelectableFlags_None, ImVec2(ImGui::CalcTextSize(name.c_str()).x, 0.f))) focus_track(*fit.end.track, fit.end.point.path, fit.end.position); if (ImGui::IsItemHovered()) @@ -927,16 +984,26 @@ void editor_mode::render_turntable_ui() for (int i = 0; i < static_cast(tool.templates.size()); ++i) { auto const &kind{tool.templates[i]}; - auto const text{kind.angles > 0 ? format(STR_C("%s %.0f m, up to %d positions set by the tracks"), kind.name.c_str(), kind.length, kind.positions) - : format(STR_C("%s %.0f m, %d fixed positions"), kind.name.c_str(), kind.length, static_cast(kind.fixed.size()))}; - if (ImGui::Selectable((text + "##turntable" + std::to_string(i)).c_str(), tool.chosen == i)) + // the name, and under it the size and the positions + auto const details{kind.angles > 0 ? format(STR_C("%.0f m, up to %d positions set by the tracks"), kind.length, kind.positions) + : format(STR_C("%.0f m, %d fixed positions"), kind.length, static_cast(kind.fixed.size()))}; + auto const line{ImGui::GetTextLineHeight()}; + auto const at{ImGui::GetCursorScreenPos()}; + if (ImGui::Selectable(("##turntable" + std::to_string(i)).c_str(), tool.chosen == i, ImGuiSelectableFlags_None, ImVec2(0.0f, line * 2.0f + 2.0f))) tool.chosen = i; + auto *list{ImGui::GetWindowDrawList()}; + list->AddText(ImVec2(at.x + 4.0f, at.y), ImGui::GetColorU32(ImGuiCol_Text), kind.name.c_str()); + list->AddText(ImVec2(at.x + 4.0f, at.y + line + 1.0f), ImGui::GetColorU32(ImGuiCol_TextDisabled), details.c_str()); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", kind.file.c_str()); } - ImGui::SetNextItemWidth(160.f); - ImGui::InputTextWithHint(STR_C("Name##turntablename"), turntable_name_for_new().c_str(), tool.name, sizeof(tool.name), ImGuiInputTextFlags_CharsNoBlank); - ImGui::TextDisabled("%s", STR_C("Press where the centre goes and drag to turn the bridge.\nClick a free end of a track: the turntable lines up with it, at its end. Shift: no lining up")); + if (begin_fields("##turntablenamefields", 0.3f)) + { + field_row("Name##turntablename"); + ImGui::InputTextWithHint("##turntablename", turntable_name_for_new().c_str(), tool.name, sizeof(tool.name), ImGuiInputTextFlags_CharsNoBlank); + ImGui::EndTable(); + } + gesture_rows({{"Drag", "from where the centre goes, turning the bridge"}, {"LMB on a free end", "the turntable lines up with it, at its end"}, {"Shift+LMB", "no lining up"}}); } if (false == tool.error.empty()) ImGui::TextColored(ImVec4(1.f, 0.45f, 0.35f, 1.f), "%s", tool.error.c_str()); @@ -1045,7 +1112,7 @@ void editor_mode::draw_turntable_overlay() const draw_circle(projection, drawlist, centre, rim, overlay_color::marked, 2.5f); projection.line(drawlist, {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}, {centre.x - direction.x * rim, centre.y, centre.z - direction.y * rim}, overlay_color::marked, 3.f); if (dragged) - label(projection, cursor, format("%.1f deg", yaw)); + label(projection, cursor, format("%.1f°", yaw)); } } diff --git a/application/editormodevehicle.cpp b/application/editormodevehicle.cpp index 431f74be..719d5a01 100644 --- a/application/editormodevehicle.cpp +++ b/application/editormodevehicle.cpp @@ -44,10 +44,45 @@ std::string lower(std::string Text) return Text; } +std::string skin_name(ui::skin_set const &Skin) +{ + return Skin.meta != nullptr && false == Skin.meta->name.empty() ? Skin.meta->name : Skin.skin; +} + +// side view of the vehicle from textures/mini, 0 while there's none; reading one takes a moment, a few of them go in a frame +ImTextureID vehicle_mini(ui::skin_set &Skin, std::set &Asked, int &Loads) +{ + if (false == static_cast(Skin.mini)) + return 0; + if (Asked.count(&Skin) == 0) + { + if (Loads >= 4) + return 0; + ++Loads; + Asked.insert(&Skin); + } + auto const id{Skin.mini.get()}; + return id == deferred_image::invalid_id ? 0 : static_cast(id); +} + +// the side view fitted into the box, kept in proportion and standing on its bottom; mirrored for a vehicle turned around +void draw_mini(ImDrawList &Draw, ImTextureID const Image, glm::ivec2 const Size, ImVec2 const Low, ImVec2 const High, bool const Turned) +{ + if (Image == 0 || Size.x <= 0 || Size.y <= 0) + { + Draw.AddRect(Low, High, ImGui::GetColorU32(ImGuiCol_Border), 3.0f); + return; + } + auto const scale{std::min((High.x - Low.x) / Size.x, (High.y - Low.y) / Size.y)}; + auto const width{std::round(Size.x * scale)}; + auto const height{std::round(Size.y * scale)}; + ImVec2 const low{std::round(Low.x + (High.x - Low.x - width) * 0.5f), High.y - height}; + Draw.AddImage(Image, low, ImVec2(low.x + width, High.y), ImVec2(Turned ? 1.0f : 0.0f, 1.0f), ImVec2(Turned ? 0.0f : 1.0f, 0.0f)); +} + std::string skin_label(ui::vehicle_desc const &Vehicle, ui::skin_set const &Skin) { - auto label{Skin.meta != nullptr && false == Skin.meta->name.empty() ? Skin.meta->name : Skin.skin}; - return label + " (" + Vehicle.path.generic_string() + ")"; + return skin_name(Skin) + " (" + Vehicle.path.generic_string() + ")"; } int path_under(TTrack const &Track, glm::dvec3 const &Point) @@ -176,8 +211,9 @@ bool editor_mode::vehicle_place() } tool.placed = head; first_step_done(first_step::drive); - auto const trackname{tool.track->name().empty() ? std::string{"(noname)"} : tool.track->name()}; - tool.status = format(STR_C("%s and %d vehicle(s) behind it stand on %s"), head.c_str(), static_cast(vehicles.size()) - 1, trackname.c_str()); + auto const trackname{tool.track->name().empty() ? std::string{STR("(no name)")} : tool.track->name()}; + auto const behind{static_cast(vehicles.size()) - 1}; + tool.status = behind > 0 ? format(STR_P("%s and %d vehicle behind it stand on %s", "%s and %d vehicles behind it stand on %s", behind), head.c_str(), behind, trackname.c_str()) : format(STR_C("%s stands on %s"), head.c_str(), trackname.c_str()); if (tool.write && tool.track->name().empty()) tool.status += ". " + std::string{STR_C("The track has no name, the trainset can't be written to the scenery")}; else if (tool.write) @@ -220,7 +256,6 @@ void editor_mode::render_vehicle_ui() tool.bank = std::make_shared(); tool.bank->scan_textures(); } - ImGui::TextDisabled("%s", STR_C("A trainset on the track, placed where the track was clicked, or in the middle of the path")); ImGui::SetNextItemWidth(-1.0f); ImGui::InputTextWithHint("##vehiclesearch", STR_C("search: name, series, skin"), tool.search, sizeof(tool.search)); ImGui::Checkbox(STR_C("Only the ones with a cab"), &tool.controllable_only); @@ -242,78 +277,164 @@ void editor_mode::render_vehicle_ui() listed.emplace_back(vehicle, skin); } } - ImGui::BeginChild("##vehicles", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 8.0f + 6.0f), true); + int loads{0}; + auto const line{ImGui::GetTextLineHeight()}; + auto const box{ImVec2(std::round(ImGui::GetFontSize() * 7.0f), std::round(line * 2.0f))}; + ImGui::BeginChild("##vehicles", ImVec2(0.0f, (box.y + ImGui::GetStyle().ItemSpacing.y) * 5.5f + 6.0f), ImGuiChildFlags_Borders); for (std::size_t i = 0; i < listed.size(); ++i) { auto const &[vehicle, skin] = listed[i]; - if (ImGui::Selectable(format("%s##vehicle%zu", skin_label(*vehicle, *skin).c_str(), i).c_str(), tool.skin == skin)) + ImGui::PushID(static_cast(i)); + auto const low{ImGui::GetCursorScreenPos()}; + if (ImGui::Selectable("##vehicle", tool.skin == skin, ImGuiSelectableFlags_AllowDoubleClick, ImVec2(0.0f, box.y))) { tool.vehicle = vehicle; tool.skin = skin; + if (ImGui::IsMouseDoubleClicked(0)) + tool.consist.push_back({vehicle, skin, false}); } + if (ImGui::IsItemVisible()) + { + auto &draw{*ImGui::GetWindowDrawList()}; + draw_mini(draw, vehicle_mini(*skin, tool.minis, loads), skin->mini.size(), ImVec2(low.x + 2.0f, low.y + 1.0f), ImVec2(low.x + box.x, low.y + box.y - 1.0f), false); + auto const left{low.x + box.x + ImGui::GetStyle().ItemSpacing.x}; + auto const right{ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x}; + draw.PushClipRect(ImVec2(left, low.y), ImVec2(right, low.y + box.y), true); + draw.AddText(ImVec2(left, low.y), ImGui::GetColorU32(ImGuiCol_Text), skin_name(*skin).c_str()); + draw.AddText(ImVec2(left, low.y + line), ImGui::GetColorU32(ImGuiCol_TextDisabled), vehicle->path.generic_string().c_str()); + draw.PopClipRect(); + } + ImGui::PopID(); } ImGui::EndChild(); if (total > listed.size()) ImGui::TextDisabled(STR_C("%zu found, the first %zu listed"), total, listed.size()); - if (tool.vehicle != nullptr && tool.skin != nullptr && ImGui::Button(STR_C("Add to the train"))) - tool.consist.push_back({tool.vehicle, tool.skin, false}); - if (false == tool.consist.empty()) + if (tool.vehicle != nullptr && tool.skin != nullptr) { - ImGui::SameLine(); - ImGui::TextDisabled(STR_C("Train: %d vehicle(s), the first one leads"), static_cast(tool.consist.size())); - std::optional remove; - std::optional raise; + if (ImGui::Button(format(STR_C("Add %s to the train"), skin_name(*tool.skin).c_str()).c_str(), ImVec2(-1.0f, 0.0f))) + tool.consist.push_back({tool.vehicle, tool.skin, false}); + item_tooltip("A double click on the list adds it too"); + } + + // the train as a picture, the head on the left; a click on a vehicle shows its buttons + ImGui::SeparatorText(STR_C("Train")); + if (tool.consist.empty()) + ImGui::TextDisabled("%s", STR_C("No vehicles yet: pick one from the list and add it")); + else + { + auto const height{std::round(line * 1.8f)}; + auto const width_of = [&](ui::skin_set const &Skin) { + auto const size{Skin.mini.size()}; + return size.y > 0 ? std::round(height * size.x / size.y) : height * 3.0f; + }; + float total_width{0.0f}; + for (auto const &item : tool.consist) + total_width += width_of(*item.skin) + 2.0f; + auto const scrolled{total_width > ImGui::GetContentRegionAvail().x}; + ImGui::BeginChild("##train", ImVec2(0.0f, height + 6.0f + (scrolled ? ImGui::GetStyle().ScrollbarSize : 0.0f)), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar); + auto &draw{*ImGui::GetWindowDrawList()}; for (std::size_t i = 0; i < tool.consist.size(); ++i) { auto &item{tool.consist[i]}; - auto const vehicle{item.skin->vehicle.lock()}; + auto const width{width_of(*item.skin)}; + if (i > 0) + ImGui::SameLine(0.0f, 2.0f); ImGui::PushID(static_cast(i)); - ImGui::Checkbox("##turned", &item.turned); + auto const low{ImGui::GetCursorScreenPos()}; + if (ImGui::InvisibleButton("##car", ImVec2(width, height + 4.0f))) + tool.picked = tool.picked == static_cast(i) ? -1 : static_cast(i); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Turned around in the train")); - ImGui::SameLine(); - ImGui::Text("%2d. %s", static_cast(i + 1), vehicle != nullptr ? skin_label(*vehicle, *item.skin).c_str() : item.skin->skin.c_str()); - ImGui::SameLine(); - if (i > 0 && ImGui::SmallButton(STR_C("up"))) - raise = i; - ImGui::SameLine(); - if (ImGui::SmallButton("x")) - remove = i; + { + auto const vehicle{item.skin->vehicle.lock()}; + ImGui::SetTooltip("%d. %s", static_cast(i + 1), vehicle != nullptr ? skin_label(*vehicle, *item.skin).c_str() : item.skin->skin.c_str()); + } + if (tool.picked == static_cast(i) || ImGui::IsItemHovered()) + draw.AddRectFilled(low, ImVec2(low.x + width, low.y + height + 4.0f), tool.picked == static_cast(i) ? family_colour(0, 0.45f) : ImGui::GetColorU32(ImGuiCol_FrameBgHovered), 3.0f); + draw_mini(draw, vehicle_mini(*item.skin, tool.minis, loads), item.skin->mini.size(), ImVec2(low.x, low.y + 2.0f), ImVec2(low.x + width, low.y + height + 2.0f), item.turned); ImGui::PopID(); } - if (raise) - std::swap(tool.consist[*raise], tool.consist[*raise - 1]); - if (remove) - tool.consist.erase(tool.consist.begin() + static_cast(*remove)); + ImGui::EndChild(); + if (tool.picked >= static_cast(tool.consist.size())) + tool.picked = -1; + if (tool.picked >= 0) + { + auto const i{static_cast(tool.picked)}; + auto &item{tool.consist[i]}; + ImGui::Text("%d. %s", tool.picked + 1, skin_name(*item.skin).c_str()); + auto const button = [](char const *Label, bool const Enabled, char const *Tooltip) { + ImGui::BeginDisabled(false == Enabled); + auto const pressed{ImGui::Button(STR_C(Label))}; + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", STR_C(Tooltip)); + ImGui::SameLine(); + return pressed; + }; + auto const forward{button("Forward", i > 0, "Moves it one place towards the head")}; + auto const back{button("Back", i + 1 < tool.consist.size(), "Moves it one place towards the end")}; + auto const turn{button("Turn around", true, "Turned around in the train")}; + auto const remove{button("Remove", true, "Takes it out of the train")}; + ImGui::NewLine(); + if (forward) + { + std::swap(tool.consist[i], tool.consist[i - 1]); + --tool.picked; + } + else if (back) + { + std::swap(tool.consist[i], tool.consist[i + 1]); + ++tool.picked; + } + else if (turn) + item.turned = !item.turned; + else if (remove) + { + tool.consist.erase(tool.consist.begin() + static_cast(i)); + tool.picked = -1; + } + } + else + ImGui::TextDisabled(STR_P("%d vehicle, the head on the left: click it to move it, turn it or remove it", "%d vehicles, the head on the left: click one to move it, turn it or remove it", static_cast(tool.consist.size())), static_cast(tool.consist.size())); if (ImGui::SmallButton(STR_C("Empty the train"))) + { tool.consist.clear(); + tool.picked = -1; + } } + + ImGui::SeparatorText(STR_C("On the track")); if (ImGui::Button(STR_C("Turn the train around"))) tool.facing = -tool.facing; - ImGui::SameLine(); - ImGui::TextDisabled("%s", STR_C("the head goes where the arrow at the marker points")); - ImGui::RadioButton(STR_C("With a driver, to drive"), &tool.driver, 0); - ImGui::SameLine(); - ImGui::RadioButton(STR_C("Nobody, parked"), &tool.driver, 1); - ImGui::Checkbox(STR_C("Write to the scenery as a trainset"), &tool.write); - item_tooltip("On: the trainset is written to the scenery on save (Ctrl+S). Off: only to drive now, gone when the scenery is loaded again"); - if (tool.write) + item_tooltip("The head goes where the arrow at the marker points"); + segmented("driver", tool.driver, + {{0, STR_C("With a driver, to drive"), "The train can be driven or run by the AI at once"}, {1, STR_C("Nobody, parked"), "The vehicles stand braked, without a crew"}}); + toggle_chips("vehiclewrite", {{STR_C("Write to the scenery as a trainset"), &tool.write, "On: the trainset is written to the scenery on save (Ctrl+S). Off: only to drive now, gone when the scenery is loaded again"}}); + if (tool.write && ImGui::TreeNode(STR_C("More settings"))) { - ImGui::PushItemWidth(120.0f); - ImGui::InputText(STR_C("Timetable"), tool.timetable, sizeof(tool.timetable)); - item_tooltip("The timetable the train runs by; none: no timetable, it waits for orders"); - ImGui::DragScalar(STR_C("Speed at the start (km/h)"), ImGuiDataType_Double, &tool.velocity, 1.0f, nullptr, nullptr, "%.0f"); - tool.velocity = std::clamp(tool.velocity, 0.0, 400.0); - ImGui::PopItemWidth(); + if (begin_fields("##vehiclefields", 0.45f)) + { + field_row("Timetable##trainset"); + ImGui::InputText("##vehicletimetable", tool.timetable, sizeof(tool.timetable)); + item_tooltip("The timetable the train runs by; none: no timetable, it waits for orders"); + field_row("Speed at the start (km/h)"); + ImGui::DragScalar("##vehiclevelocity", ImGuiDataType_Double, &tool.velocity, 1.0f, nullptr, nullptr, "%.0f"); + tool.velocity = std::clamp(tool.velocity, 0.0, 400.0); + ImGui::EndTable(); + } + ImGui::TreePop(); } - if (tool.consist.empty() && (tool.vehicle == nullptr || tool.skin == nullptr)) - ImGui::TextDisabled("%s", STR_C("Pick a vehicle from the list")); - else + if (false == tool.consist.empty() || (tool.vehicle != nullptr && tool.skin != nullptr)) { - if (ImGui::Button(STR_C("Place"))) + auto const half{std::floor((ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f)}; + auto const tall{std::round(ImGui::GetFrameHeight() * 1.3f)}; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); + if (ImGui::Button(STR_C("Place"), ImVec2(half, tall))) vehicle_place(); + item_tooltip("A trainset on the track, placed where the track was clicked, or in the middle of the path"); + ImGui::PopStyleColor(2); ImGui::SameLine(); - if (ImGui::Button(STR_C("Place and drive")) && vehicle_place()) + if (ImGui::Button(STR_C("Place and drive"), ImVec2(-1.0f, tall)) && vehicle_place()) { Global.editor_enter_vehicle = tool.placed; tool.leave = true; @@ -324,7 +445,7 @@ void editor_mode::render_vehicle_ui() if (tool.trainsets.empty()) return; ImGui::Separator(); - ImGui::TextDisabled(STR_C("%d trainset(s) to write on save:"), static_cast(tool.trainsets.size())); + ImGui::TextDisabled(STR_P("%d trainset to write on save:", "%d trainsets to write on save:", static_cast(tool.trainsets.size())), static_cast(tool.trainsets.size())); std::optional drop; for (std::size_t i = 0; i < tool.trainsets.size(); ++i) { diff --git a/application/editormodeviewport.cpp b/application/editormodeviewport.cpp index c75e0a77..efa81240 100644 --- a/application/editormodeviewport.cpp +++ b/application/editormodeviewport.cpp @@ -13,6 +13,8 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" #include "application/editoruilayer.h" +#include "application/editormodeshared.h" +#include "editor/editorFormat.hpp" #include "editor/editorIcons.hpp" #include "editor/editorSettings.hpp" #include "rendering/renderer.h" @@ -36,6 +38,7 @@ struct editor_tool_button std::function action; bool group{false}; // a gap before it, it starts another group bool enabled{true}; + ImU32 accent{0}; // the colour of the family of the tool, 0 if it has none }; namespace @@ -49,6 +52,8 @@ float scale() return std::max(1.0f, Global.ui_scale); } +bool terrain_tools_more{false}; + // side of a button, the icon with some room around it float button_side() { @@ -156,21 +161,22 @@ void editor_mode::work_area_tools(std::vector &Buttons) case work_area::tracks: { auto const vehicle{m_track_tab == track_tab::lineside && m_vehicle.open}; - auto const add = [&](track_tab const Tab, icon const Picture, bool const Group) { + auto const add = [&](track_tab const Tab, bool const Group) { auto const &modes{track_modes()}; auto const found{std::find_if(modes.begin(), modes.end(), [&](track_mode const &Mode) { return Mode.tab == Tab; })}; if (found == modes.end()) return; // a tool chosen again gives way to the select one auto const chosen{(m_track_tab == Tab || (Tab == track_tab::straights && track_plan_tab())) && false == (Tab == track_tab::lineside && vehicle)}; - Buttons.push_back({Picture, STR_C(found->label), found->key, STR_C(found->tooltip), chosen, [this, Tab, chosen]() { show_track_tab(chosen ? track_tab::path : Tab); }, Group}); + Buttons.push_back({found->picture, STR_C(found->label), found->key, STR_C(found->tooltip), chosen, [this, Tab, chosen]() { show_track_tab(chosen ? track_tab::path : Tab); }, Group}); + Buttons.back().accent = family_colour(found->family); }; // the tools of laying track first, the others behind More, shown while one of them is in use - add(track_tab::path, icon::select, false); - add(track_tab::lay, icon::lay_track, false); - add(track_tab::straights, icon::curve, false); - add(track_tab::turnout, icon::turnout, false); - add(track_tab::parallel, icon::parallel, false); + add(track_tab::path, false); + add(track_tab::lay, false); + add(track_tab::straights, false); + add(track_tab::turnout, false); + add(track_tab::parallel, false); auto const advanced{m_track_tab == track_tab::turntable || m_track_tab == track_tab::signals || m_track_tab == track_tab::lineside || m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || m_track_tab == track_tab::joints || m_gauge.open}; auto const more{EditorSettings.track_tools_more()}; @@ -180,18 +186,20 @@ void editor_mode::work_area_tools(std::vector &Buttons) }, true}); if (false == more && false == advanced) break; - add(track_tab::turntable, icon::turntable, true); - add(track_tab::signals, icon::signal, false); - add(track_tab::lineside, icon::objects, false); + add(track_tab::turntable, true); + add(track_tab::signals, false); + add(track_tab::lineside, false); Buttons.push_back({icon::vehicle, STR_C("Trainset"), nullptr, STR_C("A trainset on the selected track: written to the scenery on save, and to drive at once"), vehicle, [this, vehicle]() { show_track_tab(vehicle ? track_tab::path : track_tab::lineside); m_vehicle.expand = false == vehicle; }}); - add(track_tab::profile, icon::profile, true); - add(track_tab::speed, icon::speed, false); - add(track_tab::joints, icon::joints, false); + Buttons.back().accent = family_colour(1); + add(track_tab::profile, true); + add(track_tab::speed, false); + add(track_tab::joints, false); Buttons.push_back({icon::gauge, STR_C("Structure gauge"), nullptr, STR_C("Checks which models enter the structure gauge of the tracks and the clearance over the roads (skrajnia budowli)"), m_gauge.open, [this]() { m_gauge.open = !m_gauge.open; }}); + Buttons.back().accent = family_colour(2); break; } case work_area::roads: @@ -222,16 +230,21 @@ void editor_mode::work_area_tools(std::vector &Buttons) chosen(terrain_tool::level), choose(terrain_tool::level)}); Buttons.push_back({icon::brush, STR_C("Paint"), nullptr, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette"), chosen(terrain_tool::paint), choose(terrain_tool::paint)}); Buttons.push_back({icon::chunks, STR_C("Chunks"), nullptr, STR_C("LMB adds a chunk next to the clicked one, Shift+LMB removes it"), chosen(terrain_tool::chunks), choose(terrain_tool::chunks)}); - Buttons.push_back({icon::count, STR_C("Point spacing"), nullptr, STR_C("LMB selects the clicked chunk, Ctrl+LMB adds one or takes it away; the tool options convert the point spacing of the selected"), - chosen(terrain_tool::spacing), choose(terrain_tool::spacing)}); Buttons.push_back({icon::area_fill, STR_C("Water"), nullptr, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back"), chosen(terrain_tool::water), choose(terrain_tool::water)}); Buttons.push_back({icon::count, STR_C("Fill"), nullptr, STR_C("LMB adds a point of the outline of an area to fill with a material, Shift+LMB takes the last one back"), chosen(terrain_tool::fill), choose(terrain_tool::fill)}); - Buttons.push_back({icon::joints, STR_C("Restore"), nullptr, STR_C("LMB takes back the touch-ups made over the tracks: the terrain returns to the shape the tracks give it"), chosen(terrain_tool::restore), - choose(terrain_tool::restore)}); auto const orthophoto{m_orthophoto.enabled()}; Buttons.push_back({icon::orthophoto, STR_C("Orthophoto"), nullptr, STR_C("Aerial imagery of geoportal.gov.pl under the scenery, laid out by the origin of the scenery"), orthophoto, [this, orthophoto]() { m_orthophoto.enabled(false == orthophoto); }, true}); + auto const advanced{m_terrain_tool == terrain_tool::spacing || m_terrain_tool == terrain_tool::restore || m_orthophoto_window}; + Buttons.push_back({icon::more, terrain_tools_more ? STR_C("Less") : STR_C("More"), nullptr, STR_C("Point spacing, restoring the terrain, orthophoto settings"), terrain_tools_more, + []() { terrain_tools_more = false == terrain_tools_more; }, true}); + if (false == terrain_tools_more && false == advanced) + break; + Buttons.push_back({icon::count, STR_C("Point spacing"), nullptr, STR_C("LMB selects the clicked chunk, Ctrl+LMB adds one or takes it away; the tool options convert the point spacing of the selected"), + chosen(terrain_tool::spacing), choose(terrain_tool::spacing), true}); + Buttons.push_back({icon::joints, STR_C("Restore terrain"), nullptr, STR_C("LMB takes back the touch-ups made over the tracks: the terrain returns to the shape the tracks give it"), chosen(terrain_tool::restore), + choose(terrain_tool::restore)}); Buttons.push_back({icon::count, STR_C("Orthophoto settings..."), nullptr, nullptr, m_orthophoto_window, [this]() { m_orthophoto_window = !m_orthophoto_window; }}); break; } @@ -274,7 +287,9 @@ void editor_mode::render_toolbar_tools() auto const buttonheight{height - 2.0f * margin}; auto const left{ImGui::GetCursorScreenPos().x}; auto const top{ImGui::GetWindowPos().y + margin}; - auto const right{ImGui::GetWindowPos().x + ImGui::GetWindowWidth() - 4.0f * s}; + // undo and redo at the right end of the row, the tools before them + auto const history_width{2.0f * buttonheight + gap}; + auto const right{ImGui::GetWindowPos().x + ImGui::GetWindowWidth() - 4.0f * s - history_width - groupgap}; // the names next to the icons if there's room for them; if there isn't, the name of the chosen tool only, or none enum class names { @@ -331,7 +346,9 @@ void editor_mode::render_toolbar_tools() auto const background{button.chosen ? ImGui::GetColorU32(ImGuiCol_Header) : hovered && button.enabled ? ImGui::GetColorU32(ImGuiCol_HeaderHovered) : ImGui::GetColorU32(ImGuiCol_FrameBg)}; list->AddRectFilled(position, end, background, rounding); if (button.chosen) - list->AddRectFilled(ImVec2(position.x, end.y - 2.0f * s), end, ImGui::GetColorU32(ImGuiCol_CheckMark), rounding, ImDrawFlags_RoundCornersBottom); + list->AddRectFilled(ImVec2(position.x, end.y - (button.accent != 0 ? 3.0f : 2.0f) * s), end, button.accent != 0 ? button.accent : ImGui::GetColorU32(ImGuiCol_CheckMark), rounding, ImDrawFlags_RoundCornersBottom); + else if (button.accent != 0) + list->AddRectFilled(ImVec2(position.x + rounding, end.y - 1.0f * s), ImVec2(end.x - rounding, end.y), (button.accent & ~IM_COL32_A_MASK) | (IM_COL32(0, 0, 0, hovered ? 200 : 90) & IM_COL32_A_MASK)); auto const color{button.enabled ? ImGui::GetColorU32(ImGuiCol_Text, button.chosen || hovered ? 1.0f : 0.85f) : ImGui::GetColorU32(ImGuiCol_TextDisabled)}; auto const center{position.y + size.y * 0.5f}; if (named(button, shown)) @@ -339,7 +356,7 @@ void editor_mode::render_toolbar_tools() auto textx{position.x + pad}; if (button.picture != icon::count) { - editor_icons::draw(list, button.picture, ImVec2(textx + iconside * 0.5f, center), color); + editor_icons::draw(list, button.picture, ImVec2(textx + iconside * 0.5f, center), button.chosen && button.accent != 0 ? button.accent : color); textx += iconside + spacing; } list->AddText(ImVec2(textx, std::round(center - ImGui::GetFontSize() * 0.5f)), color, button.label); @@ -352,6 +369,43 @@ void editor_mode::render_toolbar_tools() button.action(); x = end.x; } + + // undo and redo, as Ctrl+Z and Ctrl+Y: not in the middle of a drag, it would go on from the state which was undone + auto const dragging{m_track_gizmo_using || m_track_set_using || m_straights.dragging || m_extend.active || m_switch.placing || m_point_drag.active || m_handle_drag.active}; + auto const history_left{ImGui::GetWindowPos().x + ImGui::GetWindowWidth() - 4.0f * s - history_width}; + for (int index = 0; index < 2; ++index) + { + auto const redo{index == 1}; + auto const steps{redo ? g_redo.size() : m_history.size()}; + auto const enabled{steps > 0 && false == dragging && false == m_history_busy}; + ImVec2 const position{history_left + index * (buttonheight + gap), top}; + ImVec2 const end{position.x + buttonheight, position.y + buttonheight}; + ImGui::SetCursorScreenPos(position); + ImGui::PushID(redo ? "##redo" : "##undo"); + auto const clicked{ImGui::InvisibleButton("##history", ImVec2(buttonheight, buttonheight))}; + ImGui::PopID(); + auto const hovered{ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)}; + list->AddRectFilled(position, end, hovered && enabled ? ImGui::GetColorU32(ImGuiCol_HeaderHovered) : ImGui::GetColorU32(ImGuiCol_FrameBg), rounding); + auto const colour{enabled ? ImGui::GetColorU32(ImGuiCol_Text, hovered ? 1.0f : 0.85f) : ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.6f)}; + // a hook of an arrow: back to the left for undo, on to the right for redo + auto const centre{ImVec2(position.x + buttonheight * 0.5f, position.y + buttonheight * 0.5f)}; + auto const r{buttonheight * 0.22f}; + auto const direction{redo ? -1.0f : 1.0f}; + ImVec2 const from{centre.x + direction * r, centre.y + r * 0.9f}; + ImVec2 const to{centre.x - direction * r * 0.2f, centre.y - r * 0.55f}; + list->AddBezierCubic(from, ImVec2(centre.x + direction * r * 1.6f, centre.y + r * 0.2f), ImVec2(centre.x + direction * r * 1.2f, centre.y - r * 0.6f), to, colour, 1.8f * s); + list->AddLine(to, ImVec2(to.x - direction * r * 0.9f, to.y), colour, 1.8f * s); + list->AddTriangleFilled(ImVec2(to.x - direction * r * 1.45f, to.y), ImVec2(to.x - direction * r * 0.75f, to.y - r * 0.5f), ImVec2(to.x - direction * r * 0.75f, to.y + r * 0.5f), colour); + if (hovered) + ImGui::SetTooltip("%s", (redo ? format(STR_C("Redo (Ctrl+Y): %zu steps"), steps) : format(STR_C("Undo (Ctrl+Z): %zu steps"), steps)).c_str()); + if (clicked && enabled) + { + if (redo) + redo_last(); + else + undo_last(); + } + } } void editor_mode::render_viewport_gizmo() diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index 0d0d4301..faf4df3b 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -634,7 +634,7 @@ void editor_ui::render_() } if (m_node != nullptr) { - auto const name{m_node->name().empty() ? std::string{"(none)"} : m_node->name()}; + auto const name{m_node->name().empty() ? std::string{STR_C("(no name)")} : m_node->name()}; auto const label{std::string{STR_C("Selected:")} + " " + name}; ImGui::SameLine(std::max(ImGui::GetCursorPosX(), ImGui::GetWindowWidth() - ImGui::CalcTextSize(label.c_str()).x - ImGui::GetStyle().ItemSpacing.x * 2.0f)); ImGui::TextUnformatted(label.c_str()); @@ -684,7 +684,7 @@ void editor_ui::render_mode_options(nodebank_panel::edit_mode const Mode) { m_nodebankpanel.set_combo(STR_C("Set##insert"), m_insertset, nullptr); auto const count{m_nodebankpanel.set_entries(m_insertset).size()}; - ImGui::TextDisabled(STR_C("%zu templates in set%s"), count, count == 0 ? ", the node bank selection is used" : ""); + ImGui::TextDisabled(STR_C("%zu templates in set%s"), count, count == 0 ? STR_C(", the node bank selection is used") : ""); } else { @@ -745,7 +745,7 @@ void editor_ui::render_tool_options() break; } if (active == workspace::roads) - name = "Roads"; + name = STR_C("Roads"); else if (active == workspace::terrain) name = STR_C("Terrain"); ImGui::SeparatorText(name); @@ -849,7 +849,7 @@ void editor_ui::scene_row(std::size_t const Entry) ImGui::TextDisabled("%s", entry.name.c_str()); return; } - auto const label{(node->name().empty() ? std::string{"(none)"} : node->name()) + "##" + entry.uuid}; + auto const label{(node->name().empty() ? std::string{STR_C("(no name)")} : node->name()) + "##" + entry.uuid}; if (ImGui::Selectable(label.c_str(), node == m_node, ImGuiSelectableFlags_AllowDoubleClick) && m_sceneselect) m_sceneselect(node, ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)); if (ImGui::IsItemHovered()) diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index bae698a9..9e986067 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -36,15 +36,15 @@ std::string node_name_issue(std::string const &Name) { if (Name.find_first_of(" \t\r\n;\"") != std::string::npos) { - return "a name can't hold spaces, semicolons or quotes"; + return STR_C("a name can't hold spaces, semicolons or quotes"); } if (Name.find("//") != std::string::npos || Name.find("/*") != std::string::npos) { - return "a name can't hold a comment mark"; + return STR_C("a name can't hold a comment mark"); } if (Name == "include") { - return "this word has a meaning of its own in scenery files"; + return STR_C("this word has a meaning of its own in scenery files"); } return {}; } @@ -147,7 +147,7 @@ void itemproperties_panel::update(scene::basic_node *Node) if (node == nullptr) { auto const mouseposition{camera.Pos + GfxRenderer->Mouse_Position()}; - textline = "mouse location: [" + to_string(mouseposition.x, 2) + ", " + to_string(mouseposition.y, 2) + ", " + to_string(mouseposition.z, 2) + "]"; + textline = format(STR_C("Cursor: %.2f, %.2f, %.2f"), mouseposition.x, mouseposition.y, mouseposition.z); text_lines.emplace_back(textline, Global.UITextColor); return; } @@ -166,12 +166,14 @@ void itemproperties_panel::update(scene::basic_node *Node) } } */ - textline = "name: " + (node->name().empty() ? "(none)" : Bezogonkow(node->name())) + "\ntype: " + node->node_type + "\nlocation: [" + to_string(node->location().x, 2) + ", " + to_string(node->location().y, 2) + ", " + - to_string(node->location().z, 2) + "]" + - " (distance: " + to_string(glm::length(glm::dvec3{node->location().x, 0.0, node->location().z} - glm::dvec3{camera.Pos.x, 0.0, camera.Pos.z}), 1) + " m)" + "\nUUID: " + node->uuid.to_string(); + textline = format(STR_C("Name: %s"), (node->name().empty() ? std::string{STR_C("(no name)")} : Bezogonkow(node->name())).c_str()) + "\n" + format(STR_C("Type: %s"), node->node_type.c_str()); + text_lines.emplace_back(textline, Global.UITextColor); + textline = format(STR_C("Location: %.2f, %.2f, %.2f (distance %.1f m)"), node->location().x, node->location().y, node->location().z, + glm::length(glm::dvec3{node->location().x, 0.0, node->location().z} - glm::dvec3{camera.Pos.x, 0.0, camera.Pos.z})) + + "\nUUID: " + node->uuid.to_string(); if (scene::Layers.valid(node->layer())) { - textline += "\nlayer: " + Bezogonkow(scene::Layers.layer(node->layer()).name); + textline += "\n" + format(STR_C("Layer: %s"), Bezogonkow(scene::Layers.layer(node->layer()).name).c_str()); } text_lines.emplace_back(textline, Global.UITextColor); @@ -182,44 +184,44 @@ void itemproperties_panel::update(scene::basic_node *Node) auto const *subnode = static_cast(node); - textline = "angle_x: " + to_string(clamp_circular(subnode->vAngle.x, 360.f), 2) + " deg, " + "angle_y: " + to_string(clamp_circular(subnode->vAngle.y, 360.f), 2) + " deg, " + - "angle_z: " + to_string(clamp_circular(subnode->vAngle.z, 360.f), 2) + " deg"; - textline += ";\nlights: "; + std::string lights; if (subnode->iNumLights > 0) { - textline += '['; + lights += '['; for (int lightidx = 0; lightidx < subnode->iNumLights; ++lightidx) { - textline += std::to_string(subnode->lsLights[lightidx]); + lights += std::to_string(subnode->lsLights[lightidx]); if (lightidx < subnode->iNumLights - 1) { - textline += ", "; + lights += ", "; } } - textline += ']'; + lights += ']'; } else { - textline += "(none)"; + lights = STR_C("(none)"); } + textline = format(STR_C("Rotation (x, y, z): %.2f°, %.2f°, %.2f°"), clamp_circular(subnode->vAngle.x, 360.f), clamp_circular(subnode->vAngle.y, 360.f), clamp_circular(subnode->vAngle.z, 360.f)) + "\n" + + format(STR_C("Lights: %s"), lights.c_str()); text_lines.emplace_back(textline, Global.UITextColor); // 3d shape - auto modelfile{(subnode->pModel != nullptr ? subnode->pModel->NameGet() : "(none)")}; + auto modelfile{(subnode->pModel != nullptr ? subnode->pModel->NameGet() : std::string{STR_C("(none)")})}; if (modelfile.find(paths::models) == 0) { // don't include 'models/' in the path modelfile.erase(0, std::string{paths::models}.size()); } // texture - auto texturefile{(subnode->Material()->replacable_skins[1] != null_handle ? GfxRenderer->Material(subnode->Material()->replacable_skins[1])->GetName() : "(none)")}; + auto texturefile{(subnode->Material()->replacable_skins[1] != null_handle ? GfxRenderer->Material(subnode->Material()->replacable_skins[1])->GetName() : std::string{STR_C("(none)")})}; if (texturefile.find(paths::textures) == 0) { // don't include 'textures/' in the path texturefile.erase(0, std::string{paths::textures}.size()); } - text_lines.emplace_back("mesh: " + modelfile, Global.UITextColor); - text_lines.emplace_back("skin: " + texturefile, Global.UITextColor); + text_lines.emplace_back(format(STR_C("Model: %s"), modelfile.c_str()), Global.UITextColor); + text_lines.emplace_back(format(STR_C("Texture: %s"), texturefile.c_str()), Global.UITextColor); } else if (typeid(*node) == typeid(TTrack)) { @@ -235,24 +237,26 @@ void itemproperties_panel::update(scene::basic_node *Node) } // basic attributes - textline = "isolated: " + (!isolatedlist.empty() ? isolatedlist : "(none)") + "\nvelocity: " + std::to_string(subnode->SwitchExtension ? subnode->SwitchExtension->fVelocity : subnode->fVelocity) + - "\nwidth: " + std::to_string(subnode->fTrackWidth) + " m" + "\nfriction: " + to_string(subnode->fFriction, 2) + "\nquality: " + std::to_string(subnode->iQualityFlag); + textline = format(STR_C("Track circuits: %s"), (!isolatedlist.empty() ? isolatedlist : std::string{STR_C("(none)")}).c_str()) + "\n" + + format(STR_C("Speed limit: %.0f km/h"), static_cast(subnode->SwitchExtension ? subnode->SwitchExtension->fVelocity : subnode->fVelocity)) + "\n" + + format(STR_C("Track gauge: %.0f mm"), subnode->fTrackWidth * 1000.0) + "\n" + format(STR_C("Friction: %.2f"), static_cast(subnode->fFriction)) + "\n" + + format(STR_C("Quality: %d"), static_cast(subnode->iQualityFlag)); text_lines.emplace_back(textline, Global.UITextColor); // textures - auto texturefile{(subnode->m_material1 != null_handle ? GfxRenderer->Material(subnode->m_material1)->GetName() : "(none)")}; + auto texturefile{(subnode->m_material1 != null_handle ? GfxRenderer->Material(subnode->m_material1)->GetName() : std::string{STR_C("(none)")})}; if (texturefile.find(paths::textures) == 0) { texturefile.erase(0, std::string{paths::textures}.size()); } - auto texturefile2{(subnode->m_material2 != null_handle ? GfxRenderer->Material(subnode->m_material2)->GetName() : "(none)")}; + auto texturefile2{(subnode->m_material2 != null_handle ? GfxRenderer->Material(subnode->m_material2)->GetName() : std::string{STR_C("(none)")})}; if (texturefile2.find(paths::textures) == 0) { texturefile2.erase(0, std::string{paths::textures}.size()); } - textline = "skins:\n " + texturefile + "\n " + texturefile2; + textline = format(STR_C("Textures: %s, %s"), texturefile.c_str(), texturefile2.c_str()); text_lines.emplace_back(textline, Global.UITextColor); // paths - textline = "paths: "; + textline = STR_C("Geometry:"); for (auto const &path : subnode->m_paths) { textline += "\n [" + to_string(path.points[segment_data::point::start].x, 3) + ", " + to_string(path.points[segment_data::point::start].y, 3) + ", " + @@ -282,7 +286,7 @@ void itemproperties_panel::update(scene::basic_node *Node) { textline += ", "; } - textline += event.second != nullptr ? Bezogonkow(event.second->m_name) : event.first + " (missing)"; + textline += event.second != nullptr ? Bezogonkow(event.second->m_name) : event.first + STR_C(" (missing)"); } textline += "] "; } @@ -293,9 +297,9 @@ void itemproperties_panel::update(scene::basic_node *Node) auto const *subnode = static_cast(node); - textline = "data: [" + subnode->Text() + "]" + " [" + to_string(subnode->Value1(), 2) + "]" + " [" + to_string(subnode->Value2(), 2) + "]"; + textline = format(STR_C("Data: [%s] [%.2f] [%.2f]"), subnode->Text().c_str(), subnode->Value1(), subnode->Value2()); text_lines.emplace_back(textline, Global.UITextColor); - textline = "track: " + (subnode->asTrackName.empty() ? "(none)" : Bezogonkow(subnode->asTrackName)); + textline = format(STR_C("Track: %s"), (subnode->asTrackName.empty() ? std::string{STR_C("(none)")} : Bezogonkow(subnode->asTrackName)).c_str()); text_lines.emplace_back(textline, Global.UITextColor); } @@ -366,8 +370,8 @@ void itemproperties_panel::update_group() m_grouphandle = grouphandle; } - m_grouplines.emplace_back("nodes: " + std::to_string(nodegroup.nodes.size()) + "\nevents: " + std::to_string(nodegroup.events.size()), Global.UITextColor); - m_grouplines.emplace_back("names prefix: " + (m_groupprefix.empty() ? "(none)" : m_groupprefix), Global.UITextColor); + m_grouplines.emplace_back(format(STR_C("Objects: %zu, events: %zu"), nodegroup.nodes.size(), nodegroup.events.size()), Global.UITextColor); + m_grouplines.emplace_back(format(STR_C("Common start of the names: %s"), (m_groupprefix.empty() ? std::string{STR_C("(none)")} : m_groupprefix).c_str()), Global.UITextColor); } void itemproperties_panel::render() @@ -392,7 +396,7 @@ void itemproperties_panel::render() { ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2(size_max.x, size_max.y)); } - auto const panelname{(title.empty() ? m_name : title) + "###" + m_name}; + auto const panelname{(title.empty() ? std::string{STR_C(m_name.c_str())} : title) + "###" + m_name}; if (true == ImGui::Begin(panelname.c_str(), nullptr, flags)) { render_body(); @@ -408,9 +412,12 @@ void itemproperties_panel::render_body() return; } // header section - for (auto const &line : text_lines) + auto const model{m_node != nullptr && typeid(*m_node) == typeid(TAnimModel)}; + auto details{text_lines.begin()}; + if (false == model && details != text_lines.end()) { - ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str()); + ImGui::TextColored(ImVec4(details->color.r, details->color.g, details->color.b, details->color.a), "%s", details->data.c_str()); + ++details; } // name of the node — TAnimModel only render_name_editor(); @@ -418,6 +425,16 @@ void itemproperties_panel::render_body() render_transform_editor(); // group section render_group(); + ImGui::PushID("nodedetails"); + if (details != text_lines.end() && ImGui::TreeNode(STR_C("More settings"))) + { + for (; details != text_lines.end(); ++details) + { + ImGui::TextColored(ImVec4(details->color.r, details->color.g, details->color.b, details->color.a), "%s", details->data.c_str()); + } + ImGui::TreePop(); + } + ImGui::PopID(); } // Name of a picked TAnimModel. The editor places decorations without names, as each name takes its share of the time @@ -466,7 +483,7 @@ void itemproperties_panel::render_name_editor() m_nameissue = node_name_issue(name); if (m_nameissue.empty() && false == simulation::State.rename_model(picked, name)) { - m_nameissue = "another model instance goes by this name"; + m_nameissue = STR_C("another model instance goes by this name"); } } else @@ -501,7 +518,7 @@ void itemproperties_panel::render_transform_editor() { glm::dvec3 location = picked->location(); double pos[3] = { location.x, location.y, location.z }; - if (ImGui::DragScalarN("position", ImGuiDataType_Double, pos, 3, 0.05f, nullptr, nullptr, "%.6f")) + if (ImGui::DragScalarN(STR_C("position (m)"), ImGuiDataType_Double, pos, 3, 0.05f, nullptr, nullptr, "%.6f")) { picked->location(glm::dvec3(pos[0], pos[1], pos[2])); } @@ -552,7 +569,7 @@ bool itemproperties_panel::render_group() for (auto const &line : m_grouplines) { - ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str()); + ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), "%s", line.data.c_str()); } return true; @@ -587,7 +604,7 @@ void brush_object_list::render_options(nodebank_panel &Bank) else { auto const count{Bank.set_entries(source).size()}; - ImGui::TextDisabled(STR_C("%zu templates in set%s"), count, count == 0 ? ", the node bank selection is used" : ""); + ImGui::TextDisabled(STR_C("%zu templates in set%s"), count, count == 0 ? STR_C(", the node bank selection is used") : ""); } } @@ -876,7 +893,7 @@ void nodebank_panel::render_templates_list(std::string const &Filter) } if (listed == 0) { - ImGui::TextDisabled(EditorIncludes.ready().empty() ? " (none yet, describe the templates in the Include database window)" : " (no match)"); + ImGui::TextDisabled("%s", EditorIncludes.ready().empty() ? STR_C(" (none yet, describe the templates in the Include database window)") : STR_C(" (no match)")); } } @@ -942,7 +959,7 @@ void nodebank_panel::render() ImGui::End(); return; } - auto const panelname{(title.empty() ? name() : title) + "###" + name()}; + auto const panelname{(title.empty() ? std::string{STR_C(m_name.c_str())} : title) + "###" + name()}; if (true == ImGui::Begin(panelname.c_str(), nullptr, flags)) { @@ -950,12 +967,7 @@ void nodebank_panel::render() { m_largepreviews->update(); } - if (ImGui::Button(STR_C("Reload node bank"))) - { - nodebank_reload(); - } - ImGui::SameLine(); - if (ImGui::Button(m_setsopen ? "Close model sets" : "Model sets...")) + if (ImGui::Button(m_setsopen ? STR_C("Close model sets") : STR_C("Model sets..."))) { m_setsopen = !m_setsopen; } @@ -967,19 +979,31 @@ void nodebank_panel::render() EditorSettings.nodebank_previews(previews); EditorSettings.save(); } - if (previews) + if (ImGui::TreeNode(STR_C("More settings"))) { - ImGui::SameLine(); - auto card{EditorSettings.nodebank_card()}; - ImGui::SetNextItemWidth(std::min(ImGui::GetContentRegionAvail().x, 120.0f * Global.ui_scale)); - if (ImGui::SliderInt("##cardsize", &card, 48, 256, STR_C("%d px"))) + if (ImGui::Button(STR_C("Reload node bank"))) { - EditorSettings.nodebank_card(card); + nodebank_reload(); } - if (ImGui::IsItemDeactivatedAfterEdit()) + if (previews) { - EditorSettings.save(); + ImGui::SameLine(); + auto card{EditorSettings.nodebank_card()}; + ImGui::SetNextItemWidth(std::min(ImGui::GetContentRegionAvail().x, 120.0f * Global.ui_scale)); + if (ImGui::SliderInt("##cardsize", &card, 48, 256, STR_C("%d px"))) + { + EditorSettings.nodebank_card(card); + } + if (ImGui::IsItemDeactivatedAfterEdit()) + { + EditorSettings.save(); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("%s", STR_C("Size of the cards with the previews")); + } } + ImGui::TreePop(); } // the edit modes are chosen in the toolbar and set up in the tool options window, the gizmo and the node @@ -1009,7 +1033,7 @@ void nodebank_panel::render() std::vector> const *> entries; int matches{0}; }; - static std::string const ungrouped{"(ungrouped)"}; + std::string const ungrouped{STR_C("(ungrouped)")}; auto const searchfilter{std::string(m_nodesearch)}; std::vector categories; for (auto const &entry : m_nodebank) @@ -1231,7 +1255,7 @@ void nodebank_panel::manual_list(char const *Id, std::vector &List, ImGui::SameLine(); if (ImGui::Button(STR_C("Save as set")) && false == List.empty()) { - open_sets_window(EditorModelSets.create("New set", List)); + open_sets_window(EditorModelSets.create(STR_C("New set"), List)); } ImGui::PopID(); } @@ -1288,15 +1312,15 @@ std::string nodebank_panel::set_name(model_set_ref const &Ref, char const *Manua case model_set_ref::source::user: { auto const *set = EditorModelSets.find(Ref.id); - return set != nullptr ? set->name : "(deleted set)"; + return set != nullptr ? set->name : std::string{STR_C("(deleted set)")}; } case model_set_ref::source::nodebank: { auto const groups{group_names()}; - return Ref.id >= 0 && Ref.id < static_cast(groups.size()) ? "Node bank: " + groups[Ref.id] : "(missing node bank group)"; + return Ref.id >= 0 && Ref.id < static_cast(groups.size()) ? format(STR_C("Node bank: %s"), groups[Ref.id].c_str()) : std::string{STR_C("(missing node bank group)")}; } default: - return Manuallabel != nullptr ? Manuallabel : "(none)"; + return Manuallabel != nullptr ? Manuallabel : STR_C("(none)"); } } @@ -1354,7 +1378,11 @@ void nodebank_panel::render_sets_window() ImGui::EndChild(); if (ImGui::Button(STR_C("New"))) { - open_sets_window(EditorModelSets.create("New set")); + open_sets_window(EditorModelSets.create(STR_C("New set"))); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("%s", STR_C("Makes an empty set. \"Save as set\" on a brush or area fill list makes one too.\nThe sets are chosen in Insert (random model), Brush and Area fill")); } auto const *edited{EditorModelSets.find(m_setsselected)}; if (edited != nullptr) @@ -1362,7 +1390,7 @@ void nodebank_panel::render_sets_window() ImGui::SameLine(); if (ImGui::Button(STR_C("Duplicate"))) { - open_sets_window(EditorModelSets.create(edited->name + " copy", edited->templates)); + open_sets_window(EditorModelSets.create(format(STR_C("%s copy"), edited->name.c_str()), edited->templates)); edited = EditorModelSets.find(m_setsselected); } ImGui::SameLine(); @@ -1393,12 +1421,7 @@ void nodebank_panel::render_sets_window() // right side: content of the selected set ImGui::BeginGroup(); - if (edited == nullptr) - { - ImGui::TextDisabled(STR_C("Create a set with \"New\", or use \"Save as set\" on a brush or area fill list.")); - ImGui::TextDisabled(STR_C("Sets can be chosen in Insert (random model), Brush and Area fill.")); - } - else + if (edited != nullptr) { if (m_setsnameid != edited->id) { @@ -1406,7 +1429,7 @@ void nodebank_panel::render_sets_window() std::snprintf(m_setsname, sizeof(m_setsname), "%s", edited->name.c_str()); m_setsnameid = edited->id; } - ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Rename").x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x); + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(STR_C("Rename")).x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x); auto const entered{ImGui::InputText("##setname", m_setsname, IM_ARRAYSIZE(m_setsname), ImGuiInputTextFlags_EnterReturnsTrue)}; ImGui::PopItemWidth(); ImGui::SameLine(); @@ -1481,10 +1504,10 @@ void nodebank_panel::render_sets_window() } // whole node bank group at once - ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Add group").x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x); + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(STR_C("Add group")).x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x); auto const groups{group_names()}; m_setsgroup.id = groups.empty() ? -1 : std::clamp(m_setsgroup.id, 0, static_cast(groups.size()) - 1); - if (ImGui::BeginCombo("##addgroup", m_setsgroup.id >= 0 ? groups[m_setsgroup.id].c_str() : "(no node bank groups)")) + if (ImGui::BeginCombo("##addgroup", m_setsgroup.id >= 0 ? groups[m_setsgroup.id].c_str() : STR_C("(no node bank groups)"))) { for (int idx = 0; idx < static_cast(groups.size()); ++idx) { @@ -1539,7 +1562,7 @@ std::vector nodebank_panel::group_names() const if (entry.second->empty()) names.emplace_back(entry.first); else if (names.empty()) - names.emplace_back("(ungrouped)"); // templates listed before the first header + names.emplace_back(STR_C("(ungrouped)")); // templates listed before the first header } return names; } @@ -1618,7 +1641,7 @@ void functions_panel::render() { ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2(size_max.x, size_max.y)); } - auto const panelname{(title.empty() ? m_name : title) + "###" + m_name}; + auto const panelname{(title.empty() ? std::string{STR_C(m_name.c_str())} : title) + "###" + m_name}; if (true == ImGui::Begin(panelname.c_str(), nullptr, flags)) { // header section @@ -1671,7 +1694,7 @@ std::string layer_content(scene::basic_layer const &Layer) auto const count{Layer.items[static_cast(itemlabel.second)]}; if (count > 0) { - content += (content.empty() ? "" : "\n") + std::string{itemlabel.first} + ": " + std::to_string(count); + content += (content.empty() ? "" : "\n") + std::string{STR_C(itemlabel.first)} + ": " + std::to_string(count); } } return content; @@ -1679,7 +1702,7 @@ std::string layer_content(scene::basic_layer const &Layer) std::string layer_name(scene::layer_handle const Layer) { - return scene::Layers.valid(Layer) ? Bezogonkow(scene::Layers.layer(Layer).name) : "(none)"; + return scene::Layers.valid(Layer) ? Bezogonkow(scene::Layers.layer(Layer).name) : std::string{STR_C("(none)")}; } // button which can be greyed out, with an explanation shown when it's hovered @@ -1731,7 +1754,7 @@ void itemproperties_panel::render_include() if (false == include.issue.empty()) { ImGui::PushTextWrapPos(0.0f); - ImGui::TextColored(status_errorcolor, "%s", Bezogonkow(include.issue).c_str()); + ImGui::TextColored(status_errorcolor, "%s", include.issue.c_str()); ImGui::PopTextWrapPos(); return; } @@ -1802,7 +1825,7 @@ void layers_panel::render() // initially next to the toolset window ImGui::SetNextWindowPos(ImVec2S(520, 60), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2S(440, 340), ImGuiCond_FirstUseEver); - auto const panelname{(title.empty() ? m_name : title) + "###" + m_name}; + auto const panelname{(title.empty() ? std::string{STR_C(m_name.c_str())} : title) + "###" + m_name}; if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse)) { if (scene::Layers.empty()) @@ -1915,7 +1938,7 @@ void layers_panel::render_list() } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("%s", isactive ? "Active layer is always visible" : "Show or hide models, tracks and traction of the layer"); + ImGui::SetTooltip("%s", isactive ? STR_C("Active layer is always visible") : STR_C("Show or hide models, tracks and traction of the layer")); } ImGui::SameLine(); auto locked{layer.locked}; @@ -1925,7 +1948,7 @@ void layers_panel::render_list() } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("%s", isactive ? "Active layer can't be locked" : "Nodes of locked layer can't be selected in the viewport"); + ImGui::SetTooltip("%s", isactive ? STR_C("Active layer can't be locked") : STR_C("Nodes of locked layer can't be selected in the viewport")); } ImGui::SameLine(); if (ImGui::RadioButton("##active", isactive) && false == isactive) @@ -1944,22 +1967,22 @@ void layers_panel::render_list() ImGui::SetTooltip(STR_C("Active layer: nodes created in the editor are placed in it")); } ImGui::SameLine(); - auto label{std::string(2 * scene::Layers.depth(handle), ' ') + layer_name(handle) + " (" + (layer.binary ? "binary terrain" : std::to_string(layer.item_count())) + ")"}; + auto label{std::string(2 * scene::Layers.depth(handle), ' ') + layer_name(handle) + " (" + (layer.binary ? std::string{STR_C("binary terrain")} : std::to_string(layer.item_count())) + ")"}; if (layer.created) { - label += " [new]"; + label += std::string{" "} + STR_C("[new]"); } if (isremoved) { - label += " [removed]"; + label += std::string{" "} + STR_C("[removed]"); } else if (false == iswritable) { - label += " [read-only]"; + label += std::string{" "} + STR_C("[read-only]"); } else if (layer.late) { - label += " [after FirstInit]"; + label += std::string{" "} + STR_C("[after FirstInit]"); } if (ImGui::Selectable(label.c_str(), handle == m_selected)) { @@ -1970,15 +1993,15 @@ void layers_panel::render_list() std::string content{layer.created ? STR("New layer, its file is created on save") : isremoved ? STR("Dropped from the scenery on save") : STR("Scenery file")}; if (scene::Layers.valid(layer.parent)) { - content += "\nincluded by: " + layer_name(scene::Layers.resolve(layer.parent)); + content += "\n" + format(STR_C("included by: %s"), layer_name(scene::Layers.resolve(layer.parent)).c_str()); } if (false == iswritable) { - content += "\nread-only: " + readonly; + content += "\n" + format(STR_C("read-only: %s"), readonly.c_str()); } if (layer.late) { - content += "\nloaded after the scenario initialization (FirstInit): takes no new nodes, only vehicles belong there"; + content += std::string{"\n"} + STR_C("loaded after the scenario initialization (FirstInit): takes no new nodes, only vehicles belong there"); } if (auto const items{layer_content(layer)}; false == items.empty()) { @@ -2001,7 +2024,7 @@ void layers_panel::render_popups() { ImGui::TextUnformatted(STR_C("File of the new layer, in the scenery directory:")); ImGui::InputTextWithHint("##name", STR_C("name.scm"), m_newname, IM_ARRAYSIZE(m_newname)); - if (ImGui::BeginCombo("Included by", layer_name(m_newparent).c_str())) + if (ImGui::BeginCombo(STR_C("Included by"), layer_name(m_newparent).c_str())) { for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx) { @@ -2058,7 +2081,7 @@ void layers_panel::render_popups() } } ImGui::Text(STR_C("Remove layer \"%s\" from the scenery?"), layer_name(m_selected).c_str()); - ImGui::TextUnformatted(layer.created ? "The layer wasn't saved yet, so it leaves no file behind." : "Its include directive is erased on save. The file itself stays on the disk."); + ImGui::TextUnformatted(layer.created ? STR_C("The layer wasn't saved yet, so it leaves no file behind.") : STR_C("Its include directive is erased on save. The file itself stays on the disk.")); if (included > 0) { ImGui::Text(STR_C("%d layer(s) it includes are removed with it."), included); @@ -2120,11 +2143,15 @@ void layers_panel::render_popups() } ImGui::EndCombo(); } - ImGui::TextDisabled(STR_C("The text of the file is moved on save, the emptied file stays on the disk.\n" - "Merged into the layer which includes it, the content stays where the include was.\n" - "Merged into any other layer it's placed at its end, which changes the load order.")); auto const possible{m_mergetarget != null_handle && scene::Layers.can_merge(m_selected, m_mergetarget, reason)}; - if (action_button(STR_C("Merge"), possible, m_mergetarget != null_handle ? reason : STR("Pick the target layer"))) + auto const merged{action_button(STR_C("Merge"), possible, m_mergetarget != null_handle ? reason : STR("Pick the target layer"))}; + if (possible && ImGui::IsItemHovered()) + { + ImGui::SetTooltip("%s", STR_C("The text of the file is moved on save, the emptied file stays on the disk.\n" + "Merged into the layer which includes it, the content stays where the include was.\n" + "Merged into any other layer it's placed at its end, which changes the load order.")); + } + if (merged) { auto const name{layer_name(m_selected)}; auto const target{m_mergetarget}; @@ -2165,12 +2192,12 @@ void includes_panel::open(std::string const &File) { m_info.parameter(id); } - m_status = "The template uses " + std::to_string(m_parameters) + " parameter(s)."; + m_status = format(STR_P("The template uses %d parameter.", "The template uses %d parameters.", m_parameters), m_parameters); if (false == described) { // starting point for a new description editor_includes::suggest(File, m_info); - m_status += " It has no description yet; the roles are a suggestion based on how the template uses the parameters."; + m_status += std::string{" "} + STR_C("It has no description yet; the roles are a suggestion based on how the template uses the parameters."); } } @@ -2236,7 +2263,7 @@ void includes_panel::render_list() for (auto row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) { auto const &entry{entries[m_listed[row]]}; - auto const marker{entry.complete ? "[ready] " : entry.described ? "[incomplete] " : ""}; + auto const marker{entry.complete ? std::string{STR_C("[ready]")} + " " : entry.described ? std::string{STR_C("[incomplete]")} + " " : std::string{}}; auto const label{marker + Bezogonkow(entry.file) + (entry.name.empty() ? "" : " - " + entry.name) + "##" + std::to_string(m_listed[row])}; if (false == entry.described) { @@ -2249,7 +2276,7 @@ void includes_panel::render_list() } if (false == entry.complete && false == entry.issue.empty() && ImGui::IsItemHovered()) { - ImGui::SetTooltip(STR_C("Not in the node bank: %s"), Bezogonkow(entry.issue).c_str()); + ImGui::SetTooltip(STR_C("Not in the node bank: %s"), entry.issue.c_str()); } if (clicked) { @@ -2259,7 +2286,7 @@ void includes_panel::render_list() } if (m_listed.empty()) { - ImGui::TextDisabled(entries.empty() ? "(no .inc files in the scenery directory)" : "(no match)"); + ImGui::TextDisabled("%s", entries.empty() ? STR_C("(no .inc files in the scenery directory)") : STR_C("(no match)")); } ImGui::EndChild(); } @@ -2268,9 +2295,9 @@ void includes_panel::render_description() { ImGui::Separator(); ImGui::Text("%s", Bezogonkow(m_file).c_str()); - input_text("Name", m_info.name); - input_text("Category", m_info.category); - input_text("Description", m_info.description); + input_text(STR_C("Name"), m_info.name); + input_text(STR_C("Category"), m_info.category); + input_text(STR_C("Description"), m_info.description); ImGui::Columns(4, "##parameters", false); ImGui::SetColumnWidth(0, ImGui::CalcTextSize("(p000) ").x); @@ -2341,25 +2368,11 @@ void includes_panel::render_description() "The other parameters receive their default values.")); } - if (ImGui::Button(STR_C("Suggest roles"))) - { - editor_includes::suggest(m_file, m_info); - } - if (ImGui::IsItemHovered()) - { - ImGui::SetTooltip(STR_C("Sets roles of the parameters still marked as free, where the way the template uses them tells what they are")); - } - ImGui::SameLine(); - if (ImGui::Button(STR_C("Add parameter"))) - { - m_info.parameter(m_info.parameters.empty() ? 1 : m_info.parameters.back().id + 1); - } - ImGui::SameLine(); if (ImGui::Button(STR_C("Save to file"))) { std::string error; m_statuserror = false == editor_includes::save(m_file, m_info, error); - m_status = m_statuserror ? error : "Description saved in \"" + m_file + "\""; + m_status = m_statuserror ? error : format(STR_C("Description saved in \"%s\""), Bezogonkow(m_file).c_str()); if (false == m_statuserror) { EditorIncludes.update(m_file); @@ -2370,6 +2383,23 @@ void includes_panel::render_description() { open(m_file); } + if (ImGui::TreeNode(STR_C("More settings"))) + { + if (ImGui::Button(STR_C("Suggest roles"))) + { + editor_includes::suggest(m_file, m_info); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip(STR_C("Sets roles of the parameters still marked as free, where the way the template uses them tells what they are")); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Add parameter"))) + { + m_info.parameter(m_info.parameters.empty() ? 1 : m_info.parameters.back().id + 1); + } + ImGui::TreePop(); + } } void includes_panel::render() @@ -2384,17 +2414,20 @@ void includes_panel::render() auto const windowsize{ImVec2S(600, 600)}; ImGui::SetNextWindowPos(position.x + windowsize.x <= ImGui::GetIO().DisplaySize.x ? position : ImVec2S(560, 110), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(windowsize, ImGuiCond_FirstUseEver); - auto const panelname{(title.empty() ? m_name : title) + "###" + m_name}; + auto const panelname{(title.empty() ? std::string{STR_C(m_name.c_str())} : title) + "###" + m_name}; if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse)) { - ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); - ImGui::TextWrapped(STR_C("The .inc templates of the scenery directory. Describe a template and its parameters to add it to the node bank; " - "the description is stored in the template file, as //$e comment lines.")); - if (false == Global.editor_session) + ImGui::TextDisabled("%s", STR_C("Describe a template to add it to the node bank")); + if (ImGui::IsItemHovered()) { - ImGui::TextWrapped(STR_C("The node bank offers the templates in a scenery opened for editing (-edit).")); + std::string about{STR_C("The .inc templates of the scenery directory. Describe a template and its parameters to add it to the node bank; " + "the description is stored in the template file, as //$e comment lines.")}; + if (false == Global.editor_session) + { + about += std::string{"\n"} + STR_C("The node bank offers the templates in a scenery opened for editing (-edit)."); + } + ImGui::SetTooltip("%s", about.c_str()); } - ImGui::PopStyleColor(); render_list(); if (m_loaded) @@ -2406,11 +2439,11 @@ void includes_panel::render() ImGui::PushTextWrapPos(0.0f); if (m_statuserror) { - ImGui::TextColored(status_errorcolor, "%s", Bezogonkow(m_status).c_str()); + ImGui::TextColored(status_errorcolor, "%s", m_status.c_str()); } else { - ImGui::TextUnformatted(Bezogonkow(m_status).c_str()); + ImGui::TextUnformatted(m_status.c_str()); } ImGui::PopTextWrapPos(); } diff --git a/application/scenarioloadermode.cpp b/application/scenarioloadermode.cpp index dbdce606..8409e762 100644 --- a/application/scenarioloadermode.cpp +++ b/application/scenarioloadermode.cpp @@ -41,6 +41,10 @@ bool scenarioloader_mode::update() { WriteLog("using simulation seed: " + std::to_string(Global.random_seed), logtype::generic); WriteLog("using simulation starting timestamp: " + std::to_string(Global.starting_timestamp), logtype::generic); + // the command line gives the name lowercased, the file may be named in another case, as a scenery made on Windows; + // the scenery is saved back to its own file then + if (auto const cased{FileCased(Global.asCurrentSceneryPath + Global.SceneryFile)}; cased.size() > Global.asCurrentSceneryPath.size()) + Global.SceneryFile = cased.substr(Global.asCurrentSceneryPath.size()); Application.set_title( Global.AppName + " (" + Global.SceneryFile + ")" ); WriteLog( "\nLoading scenario \"" + Global.SceneryFile + "\"..." ); diff --git a/application/uilayer.cpp b/application/uilayer.cpp index 85d93fec..d7f99d3c 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -568,7 +568,7 @@ void ui_layer::render_hierarchy(){ // fitted to the contents when it first appears; no auto resize, which a docked window can't do ImGui::SetNextWindowSize(ImVec2(0, 0), ImGuiCond_FirstUseEver); ImGui::Begin((std::string(STR_C("Scene Hierarchy")) + "###scenehierarchy").c_str(), &m_editor_hierarchy); - ImGui::Text("Registered nodes: %zu", scene::Hierarchy.size()); + ImGui::Text(STR_C("Registered nodes: %zu"), scene::Hierarchy.size()); ImGui::BeginChild("hierarchy_list", ImVec2(500, 300), true); for (auto &entry : scene::Hierarchy) @@ -599,7 +599,7 @@ void ui_layer::render_hierarchy(){ if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("UUID: %s\nNode type: %s", uuid.c_str(), typeid(*node).name()); + ImGui::SetTooltip(STR_C("UUID: %s\nNode type: %s"), uuid.c_str(), typeid(*node).name()); } } } @@ -709,14 +709,14 @@ void ui_layer::render_menu_contents() GfxRenderer->Debug_Ui_State(ret); } if(EditorModeFlag){ - ImGui::MenuItem("Hierarchy", nullptr, &m_editor_hierarchy); + ImGui::MenuItem(STR_C("Scene Hierarchy"), nullptr, &m_editor_hierarchy); bool change_history_enabled = editor_mode::change_history(); - if (ImGui::MenuItem("Change History", nullptr, &change_history_enabled)) + if (ImGui::MenuItem(STR_C("Change History"), nullptr, &change_history_enabled)) { editor_mode::set_change_history(change_history_enabled); } bool settings_open = editor_mode::settings_open(); - if (ImGui::MenuItem("Editor Settings", nullptr, &settings_open)) + if (ImGui::MenuItem(STR_C("Editor Settings"), nullptr, &settings_open)) { editor_mode::set_settings_open(settings_open); } diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 71a8ebfd..1bfa3f8e 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -11,6 +11,7 @@ http: #include "editor/editorAlignment.hpp" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" +#include "utilities/translation.h" #include #include @@ -296,7 +297,7 @@ result fit_between(design const &Design) auto const end{plan_of(Design.end)}; if (glm::length(Design.start_direction) < 1e-9 || glm::length(Design.end_direction) < 1e-9) { - r.errors.emplace_back("The direction of an end of the fragment is undefined, its end path has zero length"); + r.errors.emplace_back(STR_C("The direction of an end of the fragment is undefined, its end path has zero length")); return r; } auto const startdirection{glm::normalize(Design.start_direction)}; @@ -308,7 +309,7 @@ result fit_between(design const &Design) { auto const offset{end - start}; if (std::abs(cross(startdirection, offset)) > 0.01 || glm::dot(startdirection, enddirection) < std::cos(1e-4) || glm::dot(startdirection, offset) <= 0.0) - r.errors.emplace_back("The ends don't lie on a common straight line, add a vertex"); + r.errors.emplace_back(STR_C("The ends don't lie on a common straight line, add a vertex")); } else if (count == 1) { @@ -316,14 +317,14 @@ result fit_between(design const &Design) auto const determinant{cross(startdirection, enddirection)}; if (std::abs(determinant) < 1e-9) { - r.errors.emplace_back("Tangents of the ends are parallel, use two vertices"); + r.errors.emplace_back(STR_C("Tangents of the ends are parallel, use two vertices")); } else { auto const t{cross(offset, enddirection) / determinant}; auto const u{cross(startdirection, offset) / determinant}; if ((t <= 0.0 || u <= 0.0) && false == Design.vertices.front().reverse_turn) - r.errors.emplace_back("Tangents of the ends intersect outside of the fragment, turn the longer way or use two vertices"); + r.errors.emplace_back(STR_C("Tangents of the ends intersect outside of the fragment, turn the longer way or use two vertices")); r.vertices[0] = start + startdirection * t; } } @@ -344,7 +345,7 @@ result fit_between(design const &Design) { if (glm::distance(polygon[i], polygon[i + 1]) < 1e-3) { - r.errors.emplace_back(format("Tangent %zu has zero length", i + 1)); + r.errors.emplace_back(format(STR_C("Tangent %zu has zero length"), i + 1)); return r; } } @@ -371,9 +372,9 @@ result fit_between(design const &Design) if (Straight >= -1e-4 || -Straight <= startreserve + 1e-4) return; if (startreserve > 0.0) - r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the adjoining straight", -Straight - startreserve)); + r.errors.emplace_back(format(STR_C("Vertex 1: the curve starts %.2f m before the adjoining straight"), -Straight - startreserve)); else - r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the fixed start", -Straight)); + r.errors.emplace_back(format(STR_C("Vertex 1: the curve starts %.2f m before the fixed start"), -Straight)); }; struct fitted @@ -416,7 +417,7 @@ result fit_between(design const &Design) continue; if (geometric > kPi - 1e-3 || (reverse && geometric < 1e-3)) { - r.errors.emplace_back(format("Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex", k + 1)); + r.errors.emplace_back(format(STR_C("Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex"), k + 1)); return r; } if (vertex.compound && false == vertex.arcs.empty()) @@ -451,7 +452,7 @@ result fit_between(design const &Design) auto const middle{(low + high) * 0.5}; (angles(middle) > deflection ? high : low) = middle; } - r.warnings.emplace_back(format("Vertex %zu: transition curves shortened to %.1f / %.1f m to fit the deflection angle", k + 1, fit.transition_in * low, fit.transition_out * low)); + r.warnings.emplace_back(format(STR_C("Vertex %zu: transition curves shortened to %.1f / %.1f m to fit the deflection angle"), k + 1, fit.transition_in * low, fit.transition_out * low)); fit.transition_in *= low; fit.transition_out *= low; } @@ -525,7 +526,8 @@ result fit_between(design const &Design) fit.transition_in *= scales[k]; fit.transition_out *= scales[k]; } - r.warnings.emplace_back(format("Vertex %zu: R reduced from %.1f to %.1f m, transitions to %.1f / %.1f m, to fit between the ends", k + 1, Design.vertices[k].radius, fit.radius, fit.transition_in, fit.transition_out)); + r.warnings.emplace_back(format(STR_C("Vertex %zu: R reduced from %.1f to %.1f m, transitions to %.1f / %.1f m, to fit between the ends"), k + 1, Design.vertices[k].radius, fit.radius, fit.transition_in, fit.transition_out)); + r.radius_warnings.push_back(r.warnings.back()); } for (std::size_t k = 0; k < count; ++k) { @@ -550,7 +552,7 @@ result fit_between(design const &Design) } fit.transition_in *= low; fit.transition_out *= low; - r.warnings.emplace_back(format("Vertex %zu: transition curves shortened to %.1f / %.1f m, the circular arc keeps %.0f m", k + 1, fit.transition_in, fit.transition_out, minimum)); + r.warnings.emplace_back(format(STR_C("Vertex %zu: transition curves shortened to %.1f / %.1f m, the circular arc keeps %.0f m"), k + 1, fit.transition_in, fit.transition_out, minimum)); } if (fit.transition_in < 1.0) fit.transition_in = 0.0; @@ -564,7 +566,7 @@ result fit_between(design const &Design) if (ramp >= fit.cant) continue; fit.cant = ramp < 5.0 ? 0.0 : std::floor(ramp / 5.0) * 5.0; - r.warnings.emplace_back(format("Vertex %zu: cant lowered to %.0f mm, the shortened transition curves can't ramp more", k + 1, fit.cant)); + r.warnings.emplace_back(format(STR_C("Vertex %zu: cant lowered to %.0f mm, the shortened transition curves can't ramp more"), k + 1, fit.cant)); } std::vector vertexchainage(count, 0.0); @@ -586,7 +588,7 @@ result fit_between(design const &Design) if (deflection < 1e-7) { if (lead < -1e-4) - r.errors.emplace_back(format("Vertex %zu lies inside the curve of the previous vertex", k + 1)); + r.errors.emplace_back(format(STR_C("Vertex %zu lies inside the curve of the previous vertex"), k + 1)); push_straight(cursor, directionin, lead); vertexchainage[k] = chainage; cursor = position; @@ -594,14 +596,14 @@ result fit_between(design const &Design) } if (geometric > kPi - 1e-3 || (reverse && geometric < 1e-3)) { - r.errors.emplace_back(format("Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex", k + 1)); + r.errors.emplace_back(format(STR_C("Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex"), k + 1)); return r; } if (fit.compound) { auto const shape{build_compound(deflection, fit.arcs, fit.transition_in, fit.transition_out)}; if (shape.shortened < 1.0) - r.warnings.emplace_back(format("Vertex %zu: transition curves shortened to fit the deflection angle", k + 1)); + r.warnings.emplace_back(format(STR_C("Vertex %zu: transition curves shortened to fit the deflection angle"), k + 1)); auto const normalin{perpendicular(directionin) * static_cast(side)}; auto const endoffset{directionin * shape.end.x + normalin * shape.end.y}; auto const along{cross(directionout, endoffset) / cross(directionout, directionin)}; @@ -610,7 +612,7 @@ result fit_between(design const &Design) if (k == 0) starts_early(straight); else if (straight < -1e-4) - r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight)); + r.errors.emplace_back(format(STR_C("Curves of vertices %zu and %zu overlap by %.2f m"), k, k + 1, -straight)); push_straight(cursor, directionin, straight); auto const startchainage{chainage}; glm::dvec2 origin{curvestart}; @@ -657,9 +659,9 @@ result fit_between(design const &Design) report.recommended = recommend(Design.speed, sharpest, Design.norms); r.curves.push_back(report); if (Design.norms.checked && sharpest < report.recommended.radius_min - 1e-6) - r.warnings.emplace_back(format("Vertex %zu: R %.0f m is below the minimum %.0f m for %.0f km/h", k + 1, sharpest, report.recommended.radius_min, Design.speed)); + r.warnings.emplace_back(format(STR_C("Vertex %zu: R %.0f m is below the minimum %.0f m for %.0f km/h"), k + 1, sharpest, report.recommended.radius_min, Design.speed)); if (Design.norms.checked && report.unbalanced > Design.norms.unbalanced + 1e-6) - r.warnings.emplace_back(format("Vertex %zu: unbalanced acceleration %.2f m/s2 exceeds %.2f m/s2", k + 1, report.unbalanced, Design.norms.unbalanced)); + r.warnings.emplace_back(format(STR_C("Vertex %zu: unbalanced acceleration %.2f m/s2 exceeds %.2f m/s2"), k + 1, report.unbalanced, Design.norms.unbalanced)); continue; } auto const radius{fit.radius}; @@ -683,7 +685,7 @@ result fit_between(design const &Design) auto const arcangle{deflection - endin.angle - endout.angle}; if (arcangle < -1e-9) { - r.errors.emplace_back(format("Vertex %zu: transition curves are too long for the deflection angle", k + 1)); + r.errors.emplace_back(format(STR_C("Vertex %zu: transition curves are too long for the deflection angle"), k + 1)); return r; } @@ -691,7 +693,7 @@ result fit_between(design const &Design) if (k == 0) starts_early(straight); else if (straight < -1e-4) - r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight)); + r.errors.emplace_back(format(STR_C("Curves of vertices %zu and %zu overlap by %.2f m"), k, k + 1, -straight)); push_straight(cursor, directionin, straight); element curve; @@ -759,16 +761,16 @@ result fit_between(design const &Design) if (false == norms.checked) continue; if (radius < report.recommended.radius_min - 1e-6) - r.warnings.emplace_back(format("Vertex %zu: R %.0f m is below the minimum %.0f m for %.0f km/h", k + 1, radius, report.recommended.radius_min, Design.speed)); + r.warnings.emplace_back(format(STR_C("Vertex %zu: R %.0f m is below the minimum %.0f m for %.0f km/h"), k + 1, radius, report.recommended.radius_min, Design.speed)); if (curve.cant > norms.cant_max + 1e-6) - r.warnings.emplace_back(format("Vertex %zu: cant %.0f mm exceeds %.0f mm", k + 1, curve.cant, norms.cant_max)); + r.warnings.emplace_back(format(STR_C("Vertex %zu: cant %.0f mm exceeds %.0f mm"), k + 1, curve.cant, norms.cant_max)); if (report.unbalanced > norms.unbalanced + 1e-6) - r.warnings.emplace_back(format("Vertex %zu: unbalanced acceleration %.2f m/s2 exceeds %.2f m/s2 (cant at least %.0f mm)", k + 1, report.unbalanced, norms.unbalanced, report.recommended.cant_min)); + r.warnings.emplace_back(format(STR_C("Vertex %zu: unbalanced acceleration %.2f m/s2 exceeds %.2f m/s2 (cant at least %.0f mm)"), k + 1, report.unbalanced, norms.unbalanced, report.recommended.cant_min)); auto const required{transition_length(Design.speed, radius, curve.cant, norms)}; if (lengthin + 1e-6 < required || lengthout + 1e-6 < required) - r.warnings.emplace_back(format("Vertex %zu: transition curves shorter than the required %.0f m", k + 1, required)); + r.warnings.emplace_back(format(STR_C("Vertex %zu: transition curves shorter than the required %.0f m"), k + 1, required)); if (curve.cant > 0.0 && (lengthin <= 0.0 || lengthout <= 0.0)) - r.warnings.emplace_back(format("Vertex %zu: cant without a transition curve to ramp it", k + 1)); + r.warnings.emplace_back(format(STR_C("Vertex %zu: cant without a transition curve to ramp it"), k + 1)); } auto const lastdirection{glm::normalize(polygon.back() - polygon[polygon.size() - 2])}; auto const tail{glm::dot(end - cursor, lastdirection)}; @@ -777,9 +779,9 @@ result fit_between(design const &Design) if (tail < -1e-4 && -tail > endreserve + 1e-4) { if (endreserve > 0.0) - r.errors.emplace_back(format("Vertex %zu: the curve ends %.2f m past the adjoining straight", count, -tail - endreserve)); + r.errors.emplace_back(format(STR_C("Vertex %zu: the curve ends %.2f m past the adjoining straight"), count, -tail - endreserve)); else - r.errors.emplace_back(format("Vertex %zu: the curve ends %.2f m past the fixed end", count, -tail)); + r.errors.emplace_back(format(STR_C("Vertex %zu: the curve ends %.2f m past the fixed end"), count, -tail)); } push_straight(cursor, lastdirection, tail); r.length = chainage; @@ -791,7 +793,7 @@ result fit_between(design const &Design) { auto const &e{r.elements[i]}; if (e.kind == element_kind::straight && e.length < minimum && r.elements[i - 1].kind != element_kind::straight && r.elements[i + 1].kind != element_kind::straight) - r.warnings.emplace_back(format("Straight between curves is %.1f m long, less than %.0f m", e.length, minimum)); + r.warnings.emplace_back(format(STR_C("Straight between curves is %.1f m long, less than %.0f m"), e.length, minimum)); } } @@ -815,9 +817,9 @@ result fit_between(design const &Design) point.tangent = point.radius * std::abs(gradeafter - gradebefore) / 2.0; } if (Design.start_radius > 0.0 && Design.start_radius < 10000.0) - r.warnings.emplace_back(format("The adjoining track at the start is curved (R %.0f m), the curvature changes abruptly", Design.start_radius)); + r.warnings.emplace_back(format(STR_C("The adjoining track at the start is curved (R %.0f m), the curvature changes abruptly"), Design.start_radius)); if (Design.end_radius > 0.0 && Design.end_radius < 10000.0) - r.warnings.emplace_back(format("The adjoining track at the end is curved (R %.0f m), the curvature changes abruptly", Design.end_radius)); + r.warnings.emplace_back(format(STR_C("The adjoining track at the end is curved (R %.0f m), the curvature changes abruptly"), Design.end_radius)); r.valid = r.errors.empty() && r.length > 1e-3; return r; @@ -1045,6 +1047,7 @@ std::optional fitted_join(join_frame const &Frame, join_kind const curve.length = r.length; curve.speed = shape.speed; curve.warnings = r.warnings; + curve.radius_warnings = r.radius_warnings; curve.radius = r.curves.empty() ? 0.0 : std::numeric_limits::max(); for (auto const &report : r.curves) { @@ -1329,6 +1332,19 @@ std::vector pieces(result const &Result, design const &Design, std auto longest = std::max_element(spans.begin(), spans.end(), [&](span const &A, span const &B) { return extent(A) < extent(B); }); split(longest, (chainage_from(*longest) + chainage_to(*longest)) * 0.5); } + // no path longer than a track may be + for (std::size_t i = 0; i < spans.size();) + { + auto const from{chainage_from(spans[i])}; + auto const length{chainage_to(spans[i]) - from}; + if (length <= geometry::longest_path) + { + ++i; + continue; + } + auto const count{static_cast(std::ceil(length / geometry::longest_path))}; + split(spans.begin() + static_cast(i), from + length / count); + } if (Intervals != nullptr) { Intervals->clear(); diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index 7067055b..171c8ac3 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -155,6 +155,7 @@ struct result double gauge{1500.0}; std::vector errors; std::vector warnings; + std::vector radius_warnings; std::vector vertices; std::vector vertex_chainages; std::vector vertex_elevations; @@ -233,6 +234,7 @@ struct join_result double speed{0.0}; int variants{0}; std::vector warnings; + std::vector radius_warnings; }; join_result join(join_request const &Request); diff --git a/editor/editorGeometry.hpp b/editor/editorGeometry.hpp index 271178f1..a9ec6667 100644 --- a/editor/editorGeometry.hpp +++ b/editor/editorGeometry.hpp @@ -20,6 +20,10 @@ http://mozilla.org/MPL/2.0/. namespace geometry { +// m, the longest path of a track the editor lays or leaves after a change: longer ones are cut into equal pieces +double constexpr longest_path{400.0}; + + // points of the paths closer than this are taken for the same point double constexpr same_point{0.05}; diff --git a/editor/editorIncludeInfo.cpp b/editor/editorIncludeInfo.cpp index 62ec5a63..fb27243b 100644 --- a/editor/editorIncludeInfo.cpp +++ b/editor/editorIncludeInfo.cpp @@ -12,6 +12,8 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Globals.h" #include "utilities/utilities.h" +#include "utilities/translation.h" +#include "editor/editorFormat.hpp" #include @@ -96,7 +98,7 @@ bool read_template(std::string const &File, template_text &Text, std::string &Er std::ifstream input{Global.asCurrentSceneryPath + File, std::ios_base::binary}; if (false == input.is_open()) { - Error = "can't open file \"" + File + "\""; + Error = format(STR_C("can't open file \"%s\""), File.c_str()); return false; } auto &content{Text.content}; @@ -248,7 +250,7 @@ bool parse(template_text const &Text, include_info &Info, std::string &Error) } if (false == document.IsMap()) { - Error = "the description isn't a list of named values"; + Error = STR_C("the description isn't a list of named values"); return false; } Info.name = document["name"].as(""); @@ -293,7 +295,7 @@ bool parse(template_text const &Text, include_info &Info, std::string &Error) } catch (YAML::Exception const &exception) { - Error = std::string{"the description is malformed: "} + exception.what(); + Error = format(STR_C("the description is malformed: %s"), exception.what()); return false; } return true; @@ -355,7 +357,7 @@ bool save(std::string const &File, include_info const &Info, std::string &Error) auto document{YAML::Load(text.yaml)}; if (false == document.IsNull() && false == document.IsMap()) { - Error = "the existing description isn't a list of named values"; + Error = STR_C("the existing description isn't a list of named values"); return false; } if (false == document["version"].IsDefined()) @@ -427,7 +429,7 @@ bool save(std::string const &File, include_info const &Info, std::string &Error) } catch (YAML::Exception const &exception) { - Error = std::string{"the existing description is malformed, fix or remove it first: "} + exception.what(); + Error = format(STR_C("the existing description is malformed, fix or remove it first: %s"), exception.what()); return false; } if (false == text.utf8) @@ -458,7 +460,7 @@ bool save(std::string const &File, include_info const &Info, std::string &Error) if (output.fail()) { std::filesystem::remove(filepath + ".tmp", error); - Error = "can't write file \"" + File + "\""; + Error = format(STR_C("can't write file \"%s\""), File.c_str()); return false; } // the first save of a file leaves its original content next to it @@ -469,7 +471,7 @@ bool save(std::string const &File, include_info const &Info, std::string &Error) std::filesystem::rename(filepath + ".tmp", filepath, error); if (error) { - Error = "can't replace file \"" + File + "\": " + error.message(); + Error = format(STR_C("can't replace file \"%s\": %s"), File.c_str(), error.message().c_str()); std::filesystem::remove(filepath + ".tmp", error); return false; } @@ -555,14 +557,14 @@ bool complete(include_info const &Info, int const Parameters, std::string *Issue std::string issue; if (Info.name.empty()) { - issue = "the template needs a name"; + issue = STR_C("the template needs a name"); } for (auto id = 1; id <= Parameters && issue.empty(); ++id) { auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })}; if (lookup == std::end(Info.parameters) || (false == automatic(lookup->role) && lookup->value.empty())) { - issue = "(p" + std::to_string(id) + ") needs a default value, or a role filled in by the editor (name, pos.*, rot.*)"; + issue = format(STR_C("(p%d) needs a default value, or a role filled in by the editor (name, pos.*, rot.*)"), id); } } if (false == issue.empty() && Issue != nullptr) @@ -763,7 +765,7 @@ include_entry examine(std::string const &File) entry.described = false == text.yaml.empty(); if (false == entry.described) { - entry.issue = "the template has no description"; + entry.issue = STR_C("the template has no description"); return entry; } include_info info; @@ -791,8 +793,9 @@ void editorIncludeBank::scan() m_scanned = true; std::error_code error; auto const root{std::filesystem::path(Global.asCurrentSceneryPath)}; - // NOTE: on an error the iterator turns into the end one - for (std::filesystem::recursive_directory_iterator file{root, std::filesystem::directory_options::skip_permission_denied, error}, end; file != end; file.increment(error)) + // NOTE: on an error the iterator turns into the end one. Folders linked into the scenery folder are a part of it + auto const options{std::filesystem::directory_options::skip_permission_denied | std::filesystem::directory_options::follow_directory_symlink}; + for (std::filesystem::recursive_directory_iterator file{root, options, error}, end; file != end; file.increment(error)) { if (false == file->is_regular_file(error)) { diff --git a/editor/editorInfra.cpp b/editor/editorInfra.cpp index ba38961f..0d8f9906 100644 --- a/editor/editorInfra.cpp +++ b/editor/editorInfra.cpp @@ -104,13 +104,25 @@ char const *name(category const Category) case category::catenary: return "Catenary"; case category::logic: return "Logic"; case category::other: return "Other"; + case category::equipment: return "Track equipment"; + case category::environment: return "Surroundings"; } return ""; } +std::optional category_named(std::string const &Category) +{ + static std::map const named{{"signal", category::signal}, {"sign", category::sign}, {"pole", category::pole}, {"catenary", category::catenary}, {"equipment", category::equipment}, {"environment", category::environment}, + {"switch drive", category::equipment}, {"turntable", category::equipment}, {"logic", category::logic}}; + auto const found{named.find(lower(Category))}; + if (found == named.end()) + return std::nullopt; + return found->second; +} + std::vector const &categories() { - static std::vector const result{category::signal, category::sign, category::pole, category::catenary, category::logic, category::other}; + static std::vector const result{category::signal, category::sign, category::pole, category::catenary, category::equipment, category::logic, category::environment, category::other}; return result; } @@ -261,9 +273,11 @@ std::vector default_rules() { return { {category::signal, "semafor sem_ sem- tarcza tarcz_ tz_ tm_ tzw powtarz sygnaliz sygnal signal"}, - {category::sign, "wskaz wsk_ wsk- w4 w8 w11 w24 w27 kilometr hektometr slupek_km znak tablic sign"}, + {category::equipment, "equipment rezonator shp przejazd plyty zwrotni zwrl zwrp wzm -wz_ 2-5-13/ 6-9-9/ 1435mm/ podsypk most wiadukt przepust cbp- kozio przejscie szafa"}, + {category::environment, "environment tree grass krzak plants/ posers/ drzew trawa las_ lawka plot_ wiata bud/ nastawni budyn lamp pojazdy/ peronowe/ kladka dworzec magazyn /dom reklama blok kwietnik"}, + {category::sign, "wskaz wsk_ wsk- w4 w8 w11 w24 w27 ip/pkp/w pkp/w /we kilometr hektometr hekt slhm slupek_km znak tablic napis sign"}, {category::catenary, "trakcj siec_ siec- catenary przewod lina_nosna"}, - {category::pole, "slup maszt wysieg bramk konstrukcj kotw odciag pole gantry"}, + {category::pole, "slup maszt wysieg bramk konstrukcj kotw odciag stb1 sts1 zwis tra/sb tr/-p- tr/-l- pole gantry"}, }; } diff --git a/editor/editorInfra.hpp b/editor/editorInfra.hpp index 64d32b25..a2f34c32 100644 --- a/editor/editorInfra.hpp +++ b/editor/editorInfra.hpp @@ -11,6 +11,8 @@ http://mozilla.org/MPL/2.0/. #include #include +#include +#include #include #include @@ -39,10 +41,16 @@ enum class category pole, catenary, logic, - other + other, + // after the others, so the numbers kept in the scenery files keep their meaning + equipment, // of the track itself, on its axis: the magnets of the train protection, the plates of a level crossing, the + // ballast and the lanterns of the switches + environment // greenery, people, small architecture: by the track, but not a part of the railway, not bound by default }; char const *name(kind Kind); char const *name(category Category); +// the kind of an object by the category in the description of its template, if it names one +std::optional category_named(std::string const &Category); std::vector const &categories(); // point of a path, measured in the plan from the start of the path diff --git a/editor/editorNmt.cpp b/editor/editorNmt.cpp index 484b34b9..64f12308 100644 --- a/editor/editorNmt.cpp +++ b/editor/editorNmt.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorNmt.hpp" #include "editor/editorHttp.hpp" #include "utilities/Logs.h" +#include "utilities/translation.h" #include #include @@ -321,7 +322,7 @@ std::shared_ptr editor_nmt::fetch(key const &Key, std: } if (false == can_download()) { - Error = "this build can't fetch from the internet"; + Error = STRN("This build can't fetch from the internet"); result->failed = true; return result; } @@ -365,7 +366,7 @@ std::shared_ptr editor_nmt::fetch(key const &Key, std: if (stop()) break; if (Error.empty()) - Error = "the service gave no terrain model"; + Error = STRN("The service gave no terrain model"); } WriteLog("Editor: NMT square " + std::to_string(Key.first) + "," + std::to_string(Key.second) + " not fetched: " + Error, logtype::generic); result->failed = true; diff --git a/editor/editorProfile.cpp b/editor/editorProfile.cpp index b4d889b1..6e610958 100644 --- a/editor/editorProfile.cpp +++ b/editor/editorProfile.cpp @@ -10,11 +10,13 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "editor/editorProfile.hpp" #include "editor/editorFormat.hpp" +#include "utilities/translation.h" #include #include #include #include +#include namespace profile { @@ -83,14 +85,40 @@ void complete(line &Line, context const &Context) if (end) points.back().elevation = Context.end_elevation; - // the vertical curve of the joint starts right at the end of the route, off the adjoining track + // the vertical curve of the joint starts at the end of the route, off the adjoining track, or past the switches and the shared + // timbers the end lies on: over them the grade of the adjoining track goes on unchanged auto const joint = [&](pvi const &End, pvi const &Neighbour, double const Grade, double const Direction) -> std::optional { - auto const run{(Neighbour.chainage - End.chainage) * Direction}; + auto lead{0.0}; + for (bool grown{true}; grown;) + { + grown = false; + for (auto const &zone : Context.switches) + { + auto const to_start{Direction > 0.0 ? zone.from - End.chainage : End.chainage - zone.to}; + auto const to_end{Direction > 0.0 ? zone.to - End.chainage : End.chainage - zone.from}; + if (to_start <= lead + 0.5 && to_end > lead) + { + lead = to_end; + grown = true; + } + } + } + auto const run{(Neighbour.chainage - End.chainage) * Direction - lead}; if (run < 1.0) return std::nullopt; - auto const offset{std::abs(Neighbour.elevation - (End.elevation + Grade * run * Direction))}; + auto const offset{std::abs(Neighbour.elevation - (End.elevation + Grade * (lead + run) * Direction))}; if (offset / run * 1000.0 <= Line.norms.curve_threshold + 1e-6) - return std::nullopt; + { + // no curve needed, but the grade breaks only past the timbers the end lies on + if (lead < 0.5) + return std::nullopt; + pvi result; + result.chainage = End.chainage + lead * Direction; + result.elevation = End.elevation + Grade * lead * Direction; + result.automatic = false; + result.joint = true; + return result; + } auto radius{std::ceil(required_radius(Line) / 10.0) * 10.0}; double tangent; if (run * run >= 2.0 * radius * offset) @@ -101,8 +129,8 @@ void complete(line &Line, context const &Context) radius = run * run / (2.0 * offset); } pvi result; - result.chainage = End.chainage + tangent * Direction; - result.elevation = End.elevation + Grade * tangent * Direction; + result.chainage = End.chainage + (lead + tangent) * Direction; + result.elevation = End.elevation + Grade * (lead + tangent) * Direction; result.radius = radius; result.automatic = false; result.joint = true; @@ -192,16 +220,16 @@ std::vector check(line const &Line, context const &Context) }; if (points.size() < 2) { - add(true, -1, 0.0, "The grade line needs at least the two ends"); + add(true, -1, 0.0, STR_C("The grade line needs at least the two ends")); return result; } for (std::size_t k = 0; k + 1 < points.size(); ++k) { if (points[k + 1].chainage - points[k].chainage < 0.5) - add(true, static_cast(k + 1), points[k + 1].chainage, format("Points %zu and %zu are closer than 0.5 m", k + 1, k + 2)); + add(true, static_cast(k + 1), points[k + 1].chainage, format(STR_C("Points %zu and %zu are closer than 0.5 m"), k + 1, k + 2)); auto const slope{grade_after(Line, k) * 1000.0}; if (std::abs(slope) > norms.grade_max + 1e-6) - add(false, static_cast(k), points[k].chainage, format("Grade %.2f per mille between points %zu and %zu exceeds %.2f", slope, k + 1, k + 2, norms.grade_max)); + add(false, static_cast(k), points[k].chainage, format(STR_C("Grade %.2f per mille between points %zu and %zu exceeds %.2f"), slope, k + 1, k + 2, norms.grade_max)); } auto const list{curves(Line)}; @@ -211,25 +239,26 @@ std::vector check(line const &Line, context const &Context) auto const change{std::abs(grade_after(Line, k) - grade_after(Line, k - 1)) * 1000.0}; auto const radius{points[k].radius}; if (radius <= 0.0 && change > norms.curve_threshold + 1e-6) - add(false, static_cast(k), points[k].chainage, format("Point %zu: change of grade %.2f per mille needs a vertical curve", k + 1, change)); + add(false, static_cast(k), points[k].chainage, format(STR_C("Point %zu: change of grade %.2f per mille needs a vertical curve"), k + 1, change)); else if (radius > 0.0 && radius < required - 1e-6) - add(false, static_cast(k), points[k].chainage, format("Point %zu: R %.0f m is below the required %.0f m for %.0f km/h", k + 1, radius, required, Line.speed)); + add(false, static_cast(k), points[k].chainage, format(STR_C("Point %zu: R %.0f m is below the required %.0f m for %.0f km/h"), k + 1, radius, required, Line.speed)); if (radius <= 0.0 && change > 1e-3) for (auto const &zone : Context.switches) - if (points[k].chainage > zone.from + 1e-3 && points[k].chainage < zone.to - 1e-3) - add(true, static_cast(k), points[k].chainage, format("Point %zu: the grade changes inside switch %s", k + 1, zone.name.c_str())); + // a break on the bound of a switch is let be, within what the chainages of routes laid over each other differ by + if (points[k].chainage > zone.from + 0.05 && points[k].chainage < zone.to - 0.05) + add(true, static_cast(k), points[k].chainage, format(STR_C("Point %zu: the grade changes inside switch %s"), k + 1, zone.name.c_str())); } for (std::size_t i = 0; i < list.size(); ++i) { auto const &c{list[i]}; auto const point{static_cast(c.point)}; if (c.start < points.front().chainage - 1e-6 || c.end > points.back().chainage + 1e-6) - add(true, point, points[c.point].chainage, format("Point %zu: the vertical curve reaches past the end of the route", c.point + 1)); + add(true, point, points[c.point].chainage, format(STR_C("Point %zu: the vertical curve reaches past the end of the route"), c.point + 1)); if (i + 1 < list.size() && c.end > list[i + 1].start + 1e-6) - add(true, point, c.end, format("Vertical curves of points %zu and %zu overlap by %.2f m", c.point + 1, list[i + 1].point + 1, c.end - list[i + 1].start)); + add(true, point, c.end, format(STR_C("Vertical curves of points %zu and %zu overlap by %.2f m"), c.point + 1, list[i + 1].point + 1, c.end - list[i + 1].start)); for (auto const &zone : Context.switches) if (overlaps(c.start, c.end, zone)) - add(true, point, points[c.point].chainage, format("Point %zu: the vertical curve lies on switch %s", c.point + 1, zone.name.c_str())); + add(true, point, points[c.point].chainage, format(STR_C("Point %zu: the vertical curve lies on switch %s"), c.point + 1, zone.name.c_str())); } if (norms.element_min > 0.0) { @@ -243,21 +272,21 @@ std::vector check(line const &Line, context const &Context) }; auto const free{(points[k + 1].chainage - tangent(k + 1)) - (points[k].chainage + tangent(k))}; if (free < norms.element_min - 1e-6) - add(false, static_cast(k), points[k].chainage, format("Constant grade between points %zu and %zu is %.1f m long, less than %.0f m", k + 1, k + 2, std::max(0.0, free), norms.element_min)); + add(false, static_cast(k), points[k].chainage, format(STR_C("Constant grade between points %zu and %zu is %.1f m long, less than %.0f m"), k + 1, k + 2, std::max(0.0, free), norms.element_min)); } } for (auto const &zone : Context.switches) { auto const slope{std::abs(grade(Line, (zone.from + zone.to) * 0.5)) * 1000.0}; - if (slope > norms.grade_switch_max + 1e-6) - add(false, -1, zone.from, format("Grade %.2f per mille over switch %s exceeds %.2f", slope, zone.name.c_str(), norms.grade_switch_max)); + if (norms.grade_switch_max > 0.0 && slope > norms.grade_switch_max + 1e-6) + add(false, -1, zone.from, format(STR_C("Grade %.2f per mille over switch %s exceeds %.2f"), slope, zone.name.c_str(), norms.grade_switch_max)); } for (auto const &fixed : Context.fixed) { auto const departure{elevation(Line, fixed.first) - fixed.second}; if (std::abs(departure) > norms.fixed_tolerance) { - add(true, -1, fixed.first, format("At %s the grade line departs %.3f m from a fixed point", format_chainage(fixed.first).c_str(), departure)); + add(true, -1, fixed.first, format(STR_C("At %s the grade line departs %.3f m from a fixed point"), format_chainage(fixed.first).c_str(), departure)); break; } } @@ -268,11 +297,11 @@ std::vector check(line const &Line, context const &Context) auto const chainage{Atend ? points.back().chainage : points.front().chainage}; auto const departure{elevation(Line, chainage) - (Atend ? Context.end_elevation : Context.start_elevation)}; if (std::abs(departure) > norms.fixed_tolerance) - add(false, Atend ? static_cast(points.size()) - 1 : 0, chainage, format("The %s of the grade line is %.3f m off the adjoining track", Atend ? "end" : "start", departure)); + add(false, Atend ? static_cast(points.size()) - 1 : 0, chainage, format(Atend ? STR_C("The end of the grade line is %.3f m off the adjoining track") : STR_C("The start of the grade line is %.3f m off the adjoining track"), departure)); auto const slope{grade(Line, chainage)}; auto const change{std::abs(slope - (Atend ? Context.end_grade : Context.start_grade)) * 1000.0}; if (change > norms.curve_threshold + 1e-6) - add(false, Atend ? static_cast(points.size()) - 1 : 0, chainage, format("Change of grade %.2f per mille against the adjoining track at the %s", change, Atend ? "end" : "start")); + add(false, Atend ? static_cast(points.size()) - 1 : 0, chainage, format(Atend ? STR_C("Change of grade %.2f per mille against the adjoining track at the end") : STR_C("Change of grade %.2f per mille against the adjoining track at the start"), change)); }; ends(false); ends(true); @@ -330,9 +359,10 @@ line recognize(std::vector const &Chainages, std::vector const & else ++i; } + auto const apart = [&](part const &A, part const &B, double const Chainage) { return std::abs((A.slope - B.slope) * Chainage + A.offset - B.offset); }; for (std::size_t i = 0; i + 1 < parts.size();) { - if (std::abs(parts[i].slope - parts[i + 1].slope) < Options.grade_tolerance * 0.5) + if (std::abs(parts[i].slope - parts[i + 1].slope) < Options.grade_tolerance * 0.5 && apart(parts[i], parts[i + 1], (Chainages[parts[i].last] + Chainages[parts[i + 1].first]) * 0.5) <= Options.step) { parts[i].last = parts[i + 1].last; fit(parts[i]); @@ -352,6 +382,20 @@ line recognize(std::vector const &Chainages, std::vector const & auto const change{b.slope - a.slope}; auto chainage{std::abs(change) > 1e-9 ? (a.offset - b.offset) / change : (from + to) * 0.5}; chainage = std::clamp(chainage, from, std::max(from, to)); + // the grades don't meet between the parts, as at a stretch lifted with the grade it had: a break where each ends + if (apart(a, b, chainage) > Options.step) + { + for (auto const &[at, slope, offset] : {std::tuple{from, a.slope, a.offset}, std::tuple{to, b.slope, b.offset}}) + if (at - result.points.back().chainage >= 0.5) + { + pvi point; + point.automatic = false; + point.chainage = at; + point.elevation = slope * at + offset; + result.points.push_back(point); + } + continue; + } pvi point; point.automatic = false; point.chainage = chainage; @@ -382,72 +426,26 @@ line recognize(std::vector const &Chainages, std::vector const & if (radius > 0.0) point.radius = std::round(radius); } - return result; -} - -line fit_ground(std::vector const &Chainages, std::vector const &Ground, context const &Context, line const &Settings, ground_fit const &Options) -{ - line result; - result.speed = Settings.speed; - result.norms = Settings.norms; - result.join_start = Settings.join_start; - result.join_end = Settings.join_end; - auto const count{std::min(Chainages.size(), Ground.size())}; - if (count < 2) + if (Options.detail <= 0.0) return result; - - // gaps in the ground bridged straight, past the outermost samples it stays level - std::vector ground(Ground.begin(), Ground.begin() + count); - std::vector known; + std::vector off(count, 0); for (std::size_t i = 0; i < count; ++i) - if (std::isfinite(ground[i])) - known.push_back(i); - if (known.empty()) - return result; - for (std::size_t i = 0, k = 0; i < count; ++i) + off[i] = std::abs(elevation(result, Chainages[i]) - Elevations[i]) > Options.detail ? 1 : 0; + std::vector bends; + for (std::size_t i = 0; i < count;) { - if (std::isfinite(ground[i])) + if (off[i] == 0) + { + ++i; continue; - while (k + 1 < known.size() && known[k + 1] < i) - ++k; - if (i < known.front()) - ground[i] = Ground[known.front()]; - else if (k + 1 >= known.size()) - ground[i] = Ground[known.back()]; - else - { - auto const a{known[k]}, b{known[k + 1]}; - ground[i] = Ground[a] + (Ground[b] - Ground[a]) * (Chainages[i] - Chainages[a]) / std::max(1e-9, Chainages[b] - Chainages[a]); } - } - if (Options.smoothing > 1.0) - { - std::vector sums(count + 1, 0.0); - for (std::size_t i = 0; i < count; ++i) - sums[i + 1] = sums[i] + ground[i]; - std::vector smooth(count); - std::size_t low{0}, high{0}; - for (std::size_t i = 0; i < count; ++i) - { - while (Chainages[i] - Chainages[low] > Options.smoothing * 0.5) - ++low; - while (high + 1 < count && Chainages[high + 1] - Chainages[i] <= Options.smoothing * 0.5) - ++high; - high = std::max(high, i); - smooth[i] = (sums[high + 1] - sums[low]) / static_cast(high - low + 1); - } - ground.swap(smooth); - } - - bool const start_fixed{Settings.join_start && Context.start_joined}; - bool const end_fixed{Settings.join_end && Context.end_joined}; - - // first guess of the points: the ground simplified to within the tolerance - std::vector knots{0, count - 1}; - { + auto const first{i > 0 ? i - 1 : i}; + while (i < count && off[i] != 0) + ++i; + auto const last{std::min(i, count - 1)}; std::vector keep(count, 0); - keep.front() = keep.back() = 1; - std::vector> stack{{0, count - 1}}; + keep[first] = keep[last] = 1; + std::vector> stack{{first, last}}; while (false == stack.empty()) { auto const [a, b]{stack.back()}; @@ -457,179 +455,412 @@ line fit_ground(std::vector const &Chainages, std::vector const auto const run{std::max(1e-9, Chainages[b] - Chainages[a])}; double worst{0.0}; std::size_t index{a}; - for (auto i = a + 1; i < b; ++i) + for (auto k = a + 1; k < b; ++k) { - auto const chord{ground[a] + (ground[b] - ground[a]) * (Chainages[i] - Chainages[a]) / run}; - auto const departure{std::abs(ground[i] - chord)}; + auto const departure{std::abs(Elevations[k] - (Elevations[a] + (Elevations[b] - Elevations[a]) * (Chainages[k] - Chainages[a]) / run))}; if (departure > worst) { worst = departure; - index = i; + index = k; } } - if (worst > Options.tolerance) + if (worst > Options.detail * 0.5) { keep[index] = 1; stack.push_back({a, index}); stack.push_back({index, b}); } } - knots.clear(); - for (std::size_t i = 0; i < count; ++i) - if (keep[i] != 0) - knots.push_back(i); + auto const low{Chainages[first]}, high{Chainages[last]}; + result.points.erase(std::remove_if(result.points.begin() + 1, result.points.end() - 1, [&](pvi const &Point) { return Point.chainage >= low - 0.5 && Point.chainage <= high + 0.5; }), result.points.end() - 1); + for (auto k = first; k <= last; ++k) + if (keep[k] != 0 && k > 0 && k + 1 < count) + { + pvi point; + point.automatic = false; + point.chainage = Chainages[k]; + point.elevation = Elevations[k]; + bends.push_back(point); + } } + if (bends.empty()) + return result; + result.points.insert(result.points.end() - 1, bends.begin(), bends.end()); + std::sort(result.points.begin(), result.points.end(), [](pvi const &Left, pvi const &Right) { return Left.chainage < Right.chainage; }); + std::vector points; + for (auto const &point : result.points) + if (points.empty() || point.chainage - points.back().chainage >= 0.5 || &point == &result.points.back()) + { + if (false == points.empty() && point.chainage - points.back().chainage < 0.5) + points.pop_back(); + points.push_back(point); + } + result.points = std::move(points); + return result; +} - auto const segment_limit = [&](double const From, double const To) { - auto limit{result.norms.grade_max}; +line fit_ground(std::vector const &Chainages, std::vector const &Ground, context const &Context, line const &Settings, ground_fit const &Options, std::vector const &Tolerances, + std::vector const &Reaches) +{ + line result; + result.speed = Settings.speed; + result.norms = Settings.norms; + result.join_start = Settings.join_start; + result.join_end = Settings.join_end; + auto const count{std::min(Chainages.size(), Ground.size())}; + if (count < 2 || std::none_of(Ground.begin(), Ground.begin() + count, [](double const Height) { return std::isfinite(Height); })) + return result; + auto const origin{Chainages.front()}; + auto const length{Chainages[count - 1] - origin}; + if (length < 1.0) + return result; + + // the line is sought on nodes a few metres apart, straight between them: the grade can change only at a node + auto const nodes{std::max(3, static_cast(std::ceil(length / 5.0)) + 1)}; + auto const step{length / static_cast(nodes - 1)}; + std::vector cell(count); + std::vector part(count); + for (std::size_t i = 0; i < count; ++i) + { + auto const at{std::clamp((Chainages[i] - origin) / step, 0.0, static_cast(nodes - 1))}; + cell[i] = std::min(nodes - 2, static_cast(at)); + part[i] = at - static_cast(cell[i]); + } + auto const node = [&](std::size_t const K) { return origin + step * static_cast(K); }; + // a change of grade at a node is the curve of the required radius around it, which keeps off the switches: none on a + // switch, and beside one no more than a curve reaching up to it + auto const required{std::ceil(required_radius(result) / 10.0) * 10.0}; + auto const sharpest{step / (required * 1.001)}; + std::vector bend_max(nodes, sharpest); + for (std::size_t k = 1; k + 1 < nodes; ++k) for (auto const &zone : Context.switches) - if (overlaps(From, To, zone)) - limit = std::min(limit, result.norms.grade_switch_max); - return limit / 1000.0; + bend_max[k] = std::min(bend_max[k], std::max(0.0, 2.0 * std::max(zone.from - node(k), node(k) - zone.to) / (required * 1.001))); + struct held + { + std::size_t cell; + double part; + double low; + double high; }; - // elevations of the points with the least squares of the departures from the ground - auto const solve = [&](std::vector const &Knots) { - auto const n{Knots.size()}; - std::vector lower(n, 0.0), diagonal(n, 1e-9), upper(n, 0.0), right(n, 0.0); - auto const add = [&](std::size_t const Interval, double const T, double const Value, double const Weight) { - diagonal[Interval] += Weight * (1.0 - T) * (1.0 - T); - diagonal[Interval + 1] += Weight * T * T; - upper[Interval] += Weight * (1.0 - T) * T; - lower[Interval + 1] += Weight * (1.0 - T) * T; - right[Interval] += Weight * (1.0 - T) * Value; - right[Interval + 1] += Weight * T * Value; - }; - auto const interval_of = [&](double const Chainage) { - std::size_t k{0}; - while (k + 2 < n && Chainage >= Chainages[Knots[k + 1]]) - ++k; - return k; - }; - for (std::size_t k = 0; k + 1 < n; ++k) - { - auto const from{Chainages[Knots[k]]}; - auto const run{std::max(1e-9, Chainages[Knots[k + 1]] - from)}; - auto const last{k + 2 == n ? Knots[k + 1] : Knots[k + 1] - 1}; - for (auto i = Knots[k]; i <= last; ++i) - add(k, (Chainages[i] - from) / run, ground[i], 1.0); - } - for (auto const &fixed : Context.fixed) - { - auto const k{interval_of(fixed.first)}; - auto const from{Chainages[Knots[k]]}; - auto const t{std::clamp((fixed.first - from) / std::max(1e-9, Chainages[Knots[k + 1]] - from), 0.0, 1.0)}; - add(k, t, fixed.second, 1e6); - } - auto const pin = [&](std::size_t const Index, double const Value) { - lower[Index] = upper[Index] = 0.0; - diagonal[Index] = 1.0; - right[Index] = Value; - }; - if (start_fixed) - pin(0, Context.start_elevation); - if (end_fixed) - pin(n - 1, Context.end_elevation); - for (std::size_t k = 1; k < n; ++k) - { - auto const factor{lower[k] / diagonal[k - 1]}; - diagonal[k] -= factor * upper[k - 1]; - right[k] -= factor * right[k - 1]; - } - std::vector z(n); - z[n - 1] = right[n - 1] / diagonal[n - 1]; - for (auto k = n - 1; k-- > 0;) - z[k] = (right[k] - upper[k] * z[k + 1]) / diagonal[k]; + std::vector bounds; + for (auto const &bound : Options.bounds) + { + auto const at{std::clamp((bound.chainage - origin) / step, 0.0, static_cast(nodes - 1))}; + auto const cell{std::min(nodes - 2, static_cast(at))}; + bounds.push_back({cell, at - static_cast(cell), std::min(bound.low, bound.high), std::max(bound.low, bound.high)}); + } + bool const start_fixed{Settings.join_start && Context.start_joined}; + bool const end_fixed{Settings.join_end && Context.end_joined}; - // grades kept within the limits, the fixed ends don't move - for (int pass = 0; pass < 4; ++pass) + // a change of grade costs the price times its size, so the line breaks where the ground keeps away from it long enough to + // be worth it. The ground costs the square of its distance over its tolerance; past its reach ever less, up to a bound: + // what parts from the line, a bridge, a viaduct, a wall, a track on another level, is gone past rather than followed. The + // bends are no sharper than the radius of the norms and the grades no steeper than the limit + auto const tolerance{std::max(0.01, Options.tolerance)}; + auto const own = [&](std::size_t const I) { return I < Tolerances.size() && Tolerances[I] > 0.0 ? std::max(0.005, Tolerances[I]) : tolerance; }; + auto const reach = [&](std::size_t const I) { return I < Reaches.size() && Reaches[I] > 0.0 ? std::max(0.005, Reaches[I]) : own(I); }; + auto const price{24000.0 * tolerance * tolerance}; + auto const grade_max{(Options.grade_max > 0.0 ? Options.grade_max : result.norms.grade_max) / 1000.0}; + // what has to hold, the heights of the joined ends and the switches in one plane, holds before the grades of the ends, and + // these before the limits of the norms + double constexpr stiff{1e11}; + double constexpr firm{1e9}; + + std::vector x(nodes); + { + std::size_t i{0}; + for (std::size_t k = 0; k < nodes; ++k) { - for (std::size_t k = 0; k + 1 < n; ++k) - { - if (k + 2 == n && end_fixed) - continue; - auto const run{Chainages[Knots[k + 1]] - Chainages[Knots[k]]}; - auto const step{segment_limit(Chainages[Knots[k]], Chainages[Knots[k + 1]]) * run}; - z[k + 1] = std::clamp(z[k + 1], z[k] - step, z[k] + step); - } - for (auto k = n - 1; k-- > 0;) - { - if (k == 0 && start_fixed) - continue; - auto const run{Chainages[Knots[k + 1]] - Chainages[Knots[k]]}; - auto const step{segment_limit(Chainages[Knots[k]], Chainages[Knots[k + 1]]) * run}; - z[k] = std::clamp(z[k], z[k + 1] - step, z[k + 1] + step); - } + while (i + 1 < count && Chainages[i] < node(k)) + ++i; + auto j{i}; + while (j > 0 && false == std::isfinite(Ground[j])) + --j; + while (j + 1 < count && false == std::isfinite(Ground[j])) + ++j; + x[k] = Ground[j]; } + } + // the cost of a line, with its slope and the band of its curvature when asked. The distance of the ground counts up to + // its tolerance times the root of the spread, the size of a change of grade is rounded off within the flat + auto spread{256.0}; + auto flat{sharpest}; + std::vector gradient(nodes), diagonal(nodes), first(nodes), second(nodes); + auto const cost = [&](std::vector const &Line, bool const Derivatives) { + if (Derivatives) + { + std::fill(gradient.begin(), gradient.end(), 0.0); + std::fill(diagonal.begin(), diagonal.end(), 1e-9); + std::fill(first.begin(), first.end(), 0.0); + std::fill(second.begin(), second.end(), 0.0); + } + auto total{0.0}; + auto const add = [&](std::initializer_list> Terms, double const Value, double const Slope, double const Curvature) { + total += Value; + if (false == Derivatives) + return; + for (auto const &[a, ca] : Terms) + { + gradient[a] += Slope * ca; + for (auto const &[b, cb] : Terms) + { + if (b == a) + diagonal[a] += Curvature * ca * cb; + else if (b == a + 1) + first[a] += Curvature * ca * cb; + else if (b == a + 2) + second[a] += Curvature * ca * cb; + } + } + }; + auto const square = [&](std::initializer_list> Terms, double const Off, double const Weight) { add(Terms, Weight * Off * Off, 2.0 * Weight * Off, 2.0 * Weight); }; + for (std::size_t i = 0; i < count; ++i) + { + if (false == std::isfinite(Ground[i])) + continue; + auto const off{Line[cell[i]] * (1.0 - part[i]) + Line[cell[i] + 1] * part[i] - Ground[i]}; + auto const agreeing{spread * reach(i) * reach(i)}; + auto const share{agreeing / (agreeing + off * off)}; + auto const weight{tolerance * tolerance / (own(i) * own(i))}; + add({{cell[i], 1.0 - part[i]}, {cell[i] + 1, part[i]}}, weight * off * off * share, weight * 2.0 * off * share * share, weight * 2.0 * share * share); + } + for (std::size_t k = 1; k + 1 < nodes; ++k) + { + auto const terms = {std::pair{k - 1, 1.0 / step}, std::pair{k, -2.0 / step}, std::pair{k + 1, 1.0 / step}}; + auto const bend{(Line[k - 1] - 2.0 * Line[k] + Line[k + 1]) / step}; + if (bend_max[k] <= 0.0) + { + square(terms, bend, stiff); + continue; + } + auto const root{std::sqrt(bend * bend + flat * flat)}; + add(terms, price * root, price * bend / root, price * flat * flat / (root * root * root)); + if (std::abs(bend) > bend_max[k]) + square(terms, bend - std::copysign(bend_max[k], bend), firm); + } + for (std::size_t k = 0; k + 1 < nodes; ++k) + { + auto const grade{(Line[k + 1] - Line[k]) / step}; + if (std::abs(grade) > grade_max) + square({{k, -1.0 / step}, {k + 1, 1.0 / step}}, grade - std::copysign(grade_max, grade), firm); + } + if (start_fixed) + { + square({{0, 1.0}}, Line[0] - Context.start_elevation, stiff); + square({{0, -1.0 / step}, {1, 1.0 / step}}, (Line[1] - Line[0]) / step - Context.start_grade, firm * 10.0); + } + if (end_fixed) + { + square({{nodes - 1, 1.0}}, Line[nodes - 1] - Context.end_elevation, stiff); + square({{nodes - 2, -1.0 / step}, {nodes - 1, 1.0 / step}}, (Line[nodes - 1] - Line[nodes - 2]) / step - Context.end_grade, firm * 10.0); + } + for (auto const &bound : bounds) + { + auto const height{Line[bound.cell] * (1.0 - bound.part) + Line[bound.cell + 1] * bound.part}; + if (height < bound.low || height > bound.high) + square({{bound.cell, 1.0 - bound.part}, {bound.cell + 1, bound.part}}, height - std::clamp(height, bound.low, bound.high), firm * 10.0); + } + return total; + }; + // the step of Newton, the band factored as L D L^T with two diagonals below the main one + auto const newton = [&]() { + std::vector d(nodes), l1(nodes, 0.0), l2(nodes, 0.0), z(nodes); + for (std::size_t i = 0; i < nodes; ++i) + { + d[i] = diagonal[i] - (i >= 1 ? l1[i - 1] * l1[i - 1] * d[i - 1] : 0.0) - (i >= 2 ? l2[i - 2] * l2[i - 2] * d[i - 2] : 0.0); + if (i + 1 < nodes) + l1[i] = (first[i] - (i >= 1 ? l2[i - 1] * l1[i - 1] * d[i - 1] : 0.0)) / d[i]; + if (i + 2 < nodes) + l2[i] = second[i] / d[i]; + } + for (std::size_t i = 0; i < nodes; ++i) + z[i] = -gradient[i] - (i >= 1 ? l1[i - 1] * z[i - 1] : 0.0) - (i >= 2 ? l2[i - 2] * z[i - 2] : 0.0); + for (std::size_t i = 0; i < nodes; ++i) + z[i] /= d[i]; + for (auto i = nodes - 1; i-- > 0;) + z[i] -= l1[i] * z[i + 1] + (i + 2 < nodes ? l2[i] * z[i + 2] : 0.0); return z; }; - - auto const radius{std::ceil(required_radius(result) / 10.0) * 10.0}; - auto const threshold{result.norms.curve_threshold / 1000.0}; - std::vector z; - for (int iteration = 0; iteration < 1000; ++iteration) + // the spread and the flat narrow down level by level, each level going on from the line of the one before: from nearly + // all the ground agreeing and changes of grade costing their square, to the tolerance and their size + for (;;) { - z = solve(knots); - auto const n{knots.size()}; - if (n <= 2) - break; - auto const chainage = [&](std::size_t const K) { return Chainages[knots[K]]; }; - auto const slope = [&](std::size_t const K) { return (z[K + 1] - z[K]) / std::max(1e-9, chainage(K + 1) - chainage(K)); }; - std::vector change(n, 0.0), tangent(n, 0.0); - for (std::size_t k = 1; k + 1 < n; ++k) + auto current{cost(x, true)}; + for (int pass = 0; pass < 100; ++pass) { - change[k] = std::abs(slope(k) - slope(k - 1)); - tangent[k] = change[k] > threshold + 1e-9 ? radius * change[k] * 0.5 : 0.0; - } - // the curve of a joint starts at the end of the route and takes its whole length - if (start_fixed) - { - change[0] = std::abs(slope(0) - Context.start_grade); - tangent[0] = change[0] > threshold + 1e-9 ? radius * change[0] : 0.0; - } - if (end_fixed) - { - change[n - 1] = std::abs(slope(n - 2) - Context.end_grade); - tangent[n - 1] = change[n - 1] > threshold + 1e-9 ? radius * change[n - 1] : 0.0; - } - - std::size_t removed{0}; - for (std::size_t k = 1; k + 1 < n && removed == 0; ++k) - for (auto const &zone : Context.switches) - if (overlaps(chainage(k) - tangent[k] - 1.0, chainage(k) + tangent[k] + 1.0, zone)) - { - removed = k; - break; - } - if (removed == 0) - { - double worst{0.0}; - for (std::size_t k = 0; k + 1 < n; ++k) + auto const direction{newton()}; + auto descent{0.0}; + for (std::size_t k = 0; k < nodes; ++k) + descent += gradient[k] * direction[k]; + std::vector trial(nodes); + auto length{1.0}; + for (;; length *= 0.5) { - bool const inner{(k > 0 || tangent[0] > 0.0) && (k + 2 < n || tangent[n - 1] > 0.0)}; - auto const needed{tangent[k] + tangent[k + 1] + (inner ? result.norms.element_min : 0.0)}; - auto const shortage{needed - (chainage(k + 1) - chainage(k))}; - if (shortage <= 1e-6 || shortage <= worst) - continue; - worst = shortage; - if (k == 0) - removed = 1; - else if (k + 2 == n) - removed = n - 2; - else - removed = change[k] <= change[k + 1] ? k : k + 1; + for (std::size_t k = 0; k < nodes; ++k) + trial[k] = x[k] + length * direction[k]; + if (cost(trial, false) <= current + 1e-4 * length * descent || length < 1e-6) + break; } + auto moved{0.0}; + for (std::size_t k = 0; k < nodes; ++k) + moved = std::max(moved, std::abs(length * direction[k])); + x = std::move(trial); + current = cost(x, true); + if (moved < 1e-5) + break; } - if (removed == 0) + if (spread <= 1.0 && flat <= 1e-6) break; - knots.erase(knots.begin() + removed); + spread = std::max(1.0, spread * 0.5); + flat = std::max(1e-6, flat * 0.5); } - for (std::size_t k = 0; k < knots.size(); ++k) + // the design: the changes of grade along a bend, one after another the same way, are one point of intersection where + // the grades before and after them meet. No change at a node is sharper than the required radius, so the curve of that + // radius stays within the nodes of its bend; where the ends held the line sharper, the curve is as sharp as the line + auto const change = [&](std::size_t const K) { return (x[K - 1] - 2.0 * x[K] + x[K + 1]) / step; }; + auto const significant{sharpest * 0.01}; + auto const between = [&](std::size_t const K, std::size_t const L) { + return std::any_of(Context.switches.begin(), Context.switches.end(), [&](auto const &Zone) { return Zone.to > node(K) && Zone.from < node(L); }); + }; + auto const bends = [&](std::size_t const K) { return bend_max[K] > 0.0 && std::abs(change(K)) >= significant; }; + // an end whose grade the line couldn't take over keeps only its height, the grade breaks at the joint + if (start_fixed && std::abs((x[1] - x[0]) / step - Context.start_grade) > significant) + result.join_start = false; + if (end_fixed && std::abs((x[nodes - 1] - x[nodes - 2]) / step - Context.end_grade) > significant) + result.join_end = false; + result.points.push_back({origin, x[0]}); + std::vector rooms{0.0}; + for (std::size_t k = 1; k + 1 < nodes;) { - pvi point; - point.chainage = Chainages[knots[k]]; - point.elevation = z[k]; + if (false == bends(k)) + { + ++k; + continue; + } + auto const first{k}; + auto const sign{change(k) > 0.0}; + double sum{0.0}, moment{0.0}; + for (; k + 1 < nodes && bends(k) && (change(k) > 0.0) == sign && (k == first || false == between(k - 1, k)); ++k) + { + sum += change(k); + moment += change(k) * node(k); + } + auto const at{moment / sum}; + pvi point{at, x[first] + (x[first] - x[first - 1]) / step * (at - node(first))}; + auto room{std::min(at - node(first) + step * 0.5, node(k - 1) + step * 0.5 - at)}; + for (auto const &zone : Context.switches) + room = std::min(room, std::max(0.0, std::max(zone.from - at, at - zone.to))); result.points.push_back(point); + rooms.push_back(room); + } + result.points.push_back({node(nodes - 1), x[nodes - 1]}); + rooms.push_back(0.0); + // the curve of the required radius where it has room, off the switches and the curves beside; where it hasn't, as sharp as + // the room lets it be + for (std::size_t i = 1; i + 1 < result.points.size(); ++i) + { + auto const turn{std::abs(grade_after(result, i) - grade_after(result, i - 1))}; + if (turn * 1000.0 > result.norms.curve_threshold + 1e-6 && required * turn * 0.5 > rooms[i] - 1e-4) + { + result.points[i].radius = 2.0 * std::max(0.0, rooms[i] - 1e-4) / turn; + result.points[i].automatic = false; + } + } + assign_radii(result); + + // then the simplest design which keeps within half the tolerance of the line found, the one of the ground agreeing with + // it: a point is dropped, or two bending the same way become one, as long as that departs the least and adds no fault + std::vector limit(nodes, tolerance); + for (std::size_t i = 0; i < count; ++i) + if (std::isfinite(Ground[i]) && std::abs(Ground[i] - (x[cell[i]] * (1.0 - part[i]) + x[cell[i] + 1] * part[i])) <= reach(i)) + for (auto const k : {cell[i], cell[i] + 1}) + limit[k] = std::min(limit[k], own(i)); + for (auto const &bound : bounds) + for (auto const k : {bound.cell, bound.cell + 1}) + limit[k] = std::min(limit[k], std::max(0.001, (bound.high - bound.low) * 0.5)); + auto const errors = [&](line const &Line) { + auto completed{Line}; + complete(completed, Context); + auto const found{check(completed, Context)}; + return std::count_if(found.begin(), found.end(), [](issue const &Issue) { return Issue.error; }); + }; + auto const departure = [&](line const &Line, double const From, double const To) { + auto worst{0.0}; + for (std::size_t k = 0; k < nodes; ++k) + if (node(k) >= From && node(k) <= To) + worst = std::max(worst, std::abs(elevation(Line, node(k)) - x[k]) / limit[k]); + return worst; + }; + auto faults{errors(result)}; + // an end whose grade the curve of the joint can't take over without a fault breaks at the joint + for (auto *join : {&result.join_start, &result.join_end}) + if (faults > 0 && *join) + { + *join = false; + if (auto const fewer{errors(result)}; fewer < faults) + faults = fewer; + else + *join = true; + } + for (;;) + { + auto const &points{result.points}; + auto const last{points.size() - 1}; + std::vector> candidates; + for (std::size_t i = 1; i < last; ++i) + { + auto const from{points[i >= 2 ? i - 2 : 0].chainage}; + auto dropped{result}; + dropped.points.erase(dropped.points.begin() + i); + assign_radii(dropped); + candidates.emplace_back(departure(dropped, from, points[std::min(last, i + 2)].chainage), std::move(dropped)); + if (i + 1 >= last) + continue; + auto const before{grade_after(result, i - 1)}; + auto const between{grade_after(result, i)}; + auto const after{grade_after(result, i + 1)}; + if ((between - before) * (after - between) <= 0.0) + continue; + auto const at{(points[i + 1].elevation - points[i].elevation + before * points[i].chainage - after * points[i + 1].chainage) / (before - after)}; + auto merged{result}; + merged.points[i] = {at, points[i].elevation + before * (at - points[i].chainage)}; + merged.points.erase(merged.points.begin() + i + 1); + assign_radii(merged); + candidates.emplace_back(departure(merged, from, points[std::min(last, i + 3)].chainage), std::move(merged)); + } + std::sort(candidates.begin(), candidates.end(), [](auto const &A, auto const &B) { return A.first < B.first; }); + auto const chosen{std::find_if(candidates.begin(), candidates.end(), [&](auto const &Candidate) { return Candidate.first <= 0.5 && errors(Candidate.second) <= faults; })}; + if (chosen == candidates.end()) + break; + result = std::move(chosen->second); + faults = errors(result); + } + return result; +} + +std::vector departures(line const &Line, std::vector const &Chainages, std::vector const &Ground, double const Tolerance) +{ + std::vector result; + auto const count{std::min(Chainages.size(), Ground.size())}; + auto const off = [&](std::size_t const I) { return std::isfinite(Ground[I]) ? Ground[I] - elevation(Line, Chainages[I]) : 0.0; }; + for (std::size_t i = 0; i < count;) + { + if (std::abs(off(i)) <= Tolerance) + { + ++i; + continue; + } + departure stretch; + stretch.from = Chainages[i]; + auto const below{off(i) < 0.0}; + for (; i < count && std::abs(off(i)) > Tolerance && (off(i) < 0.0) == below; ++i) + { + if (std::abs(off(i)) > std::abs(stretch.depth)) + stretch.depth = off(i); + stretch.to = Chainages[i]; + } + result.push_back(stretch); } return result; } diff --git a/editor/editorProfile.hpp b/editor/editorProfile.hpp index 9e0712c8..b2314086 100644 --- a/editor/editorProfile.hpp +++ b/editor/editorProfile.hpp @@ -30,7 +30,8 @@ struct pvi struct limits { double grade_max{6.0}; // per mille - double grade_switch_max{2.5}; // per mille, over switches + double grade_switch_max{0.0}; // per mille, over switches and their shared timbers; 0: the limit of the line, as a switch may lie on + // the grade of the line. A lower one for the tracks of a station where vehicles stand double radius_factor{0.4}; // R >= k * V^2 double radius_min{2000.0}; double curve_threshold{2.0}; // per mille, change of grade which needs a vertical curve @@ -107,18 +108,43 @@ struct recognition double grade_tolerance{0.0004}; double minimum_length{15.0}; double sharp_gap{2.0}; + double step{0.02}; + // where the line read departs from the track by more than this, the bends of the track itself; 0 leaves the line as read + double detail{0.0}; }; // grade line of existing track, from its elevations and grades sampled along the chainage line recognize(std::vector const &Chainages, std::vector const &Elevations, std::vector const &Grades, recognition const &Options); struct ground_fit { - double tolerance{1.0}; // m, departure from the ground which doesn't yet need a change of grade - double smoothing{20.0}; // m, length over which the ground is averaged + double tolerance{1.0}; // m, the ground this near the line agrees with it; farther, the line doesn't lie on it there + double grade_max{0.0}; // per mille, the steepest grade the line may take; 0: the limit of the line + // heights the line keeps between before it lies on the ground, as where the tracks off a switch of it lead into another line: + // they can't climb more than their length lets them + struct bound + { + double chainage{0.0}; + double low{0.0}; + double high{0.0}; + }; + std::vector bounds; }; -// grade line which follows the ground within the limits of Settings, with the least earthworks. -// Ground is NaN where there's none; the joined ends of the route keep the elevation of the adjoining track -line fit_ground(std::vector const &Chainages, std::vector const &Ground, context const &Context, line const &Settings, ground_fit const &Options); +// grade line which lies on the ground where it agrees, within the limits of Settings, with the fewest and least changes of +// grade; where the ground parts from it, at a bridge, a viaduct, a cutting wall, the line goes on past it. Chainages ascend, +// Ground is NaN where there's none. Each sample may have its own tolerance, how closely it tells the height, and its own +// reach, how far from the line it still belongs to it (empty: the tolerance of Options). The joined ends of the route keep +// the elevation of the adjoining track, and its grade where the line can take it over +line fit_ground(std::vector const &Chainages, std::vector const &Ground, context const &Context, line const &Settings, ground_fit const &Options, std::vector const &Tolerances = {}, + std::vector const &Reaches = {}); +// stretches where the ground departs from the line by over Tolerance, one way all along: depth below the line (a gap, under +// a bridge, a viaduct), above it (a wall, a structure over the track) +struct departure +{ + double from{0.0}; + double to{0.0}; + double depth{0.0}; // m, the ground against the line where they part the most +}; +std::vector departures(line const &Line, std::vector const &Chainages, std::vector const &Ground, double const Tolerance); // chainages where the paths have to end so that each of them carries a single grade or a single curve std::vector breaks(line const &Line); diff --git a/editor/editorRoad.cpp b/editor/editorRoad.cpp index 8e2113d1..136f607e 100644 --- a/editor/editorRoad.cpp +++ b/editor/editorRoad.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "editor/editorRoad.hpp" +#include "editor/editorFormat.hpp" #include "editor/editorTrack.hpp" #include "world/Track.h" @@ -17,6 +18,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "utilities/Globals.h" #include "utilities/utilities.h" +#include "utilities/translation.h" #include #include @@ -1067,7 +1069,7 @@ bool editor_road::dissolve(glm::dvec3 const &Joint, record &Record, std::string { if (false == can_edit(*end.road, &reason)) { - Error = "\"" + end.road->name() + "\" can't be changed: " + reason; + Error = format(STR_C("\"%s\" can't be changed: %s"), end.road->name().c_str(), STR(reason).c_str()); return false; } } @@ -1664,7 +1666,6 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) float lanewidth{0.f}; auto const evenlanes{lane_width(layout, atend ? 1.0 : 0.0, lanewidth)}; auto const gap{static_cast(layout.median_width(atend ? 1.0 : 0.0))}; - char const *unevenlanes{" differ in width, and a junction takes roads with lanes of the same width. "}; std::string reason; auto const junctions{arms_at(position)}; @@ -1680,12 +1681,12 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) continue; if (false == evenlanes) { - Notes += "Lanes of \"" + Road.name() + "\"" + unevenlanes; + Notes += format(STR_C("Lanes of \"%s\" differ in width, and a junction takes roads with lanes of the same width"), Road.name().c_str()) + ". "; continue; } if (false == can_edit(*junction, &reason)) { - Notes += "\"" + junction->name() + "\" wasn't fitted: " + reason + ". "; + Notes += format(STR_C("\"%s\" wasn't fitted: %s"), junction->name().c_str(), STR(reason).c_str()) + ". "; continue; } arm.incoming = arriving; @@ -1717,7 +1718,7 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) continue; if (false == can_edit(*other, &reason)) { - Notes += "\"" + other->name() + "\" wasn't fitted: " + reason + ". "; + Notes += format(STR_C("\"%s\" wasn't fitted: %s"), other->name().c_str(), STR(reason).c_str()) + ". "; continue; } auto state{otherlayout}; @@ -1743,12 +1744,12 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) float otherlanewidth{0.f}; if (false == evenlanes || false == lane_width(otherlayout, otheratend ? 1.0 : 0.0, otherlanewidth)) { - Notes += "Lanes of \"" + (evenlanes ? other->name() : Road.name()) + "\" differ in width, and a road changes the number of its lanes only if they're all as wide. "; + Notes += format(STR_C("Lanes of \"%s\" differ in width, and a road changes the number of its lanes only if they're all as wide"), (evenlanes ? other->name() : Road.name()).c_str()) + ". "; continue; } if (false == can_edit(*other, &reason)) { - Notes += "The lanes next to \"" + Road.name() + "\" weren't fitted: " + reason + ". "; + Notes += format(STR_C("The lanes next to \"%s\" weren't fitted: %s"), Road.name().c_str(), STR(reason).c_str()) + ". "; continue; } auto *stretch{other}; @@ -1758,7 +1759,7 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) auto *rest{split(*other, parameter_at(lengths, otheratend ? lengths.back() - kTransitionLength : kTransitionLength))}; if (rest == nullptr) { - Notes += "\"" + other->name() + "\" couldn't be cut for the lanes to change. "; + Notes += format(STR_C("\"%s\" couldn't be cut for the lanes to change"), other->name().c_str()) + ". "; continue; } // the neighbour goes on record the way it was before the cut @@ -1781,7 +1782,7 @@ void editor_road::settle(road_node &Road, record &Record, std::string &Notes) state.lanes_between(counts[0][0], counts[0][1], counts[1][0], counts[1][1]); if (state.lanes_at(end, true) != counts[end][0] || state.lanes_at(end, false) != counts[end][1]) { - Notes += "The lanes of \"" + Road.name() + "\" can't be led on to the road next to it. "; + Notes += format(STR_C("The lanes of \"%s\" can't be led on to the road next to it"), Road.name().c_str()) + ". "; continue; } // and it's as wide there as the piece is, with its directions as far apart @@ -1829,13 +1830,13 @@ bool editor_road::branch_from(road_node &Road, double const T, glm::dvec2 const } if (layout.changing()) { - Error = "\"" + Road.name() + "\" changes its lanes along the way, which leaves no place for a junction. Lead the road out of a piece next to it"; + Error = format(STR_C("\"%s\" changes its lanes along the way, which leaves no place for a junction. Lead the road out of a piece next to it"), Road.name().c_str()); return false; } float lanewidth{0.f}; if (false == lane_width(layout, T, lanewidth)) { - Error = "Lanes of \"" + Road.name() + "\" differ in width, and a junction takes roads with lanes of the same width"; + Error = format(STR_C("Lanes of \"%s\" differ in width, and a junction takes roads with lanes of the same width"), Road.name().c_str()); return false; } auto const halfwidth{0.5 * (layout.forward + layout.backward) * lanewidth + 0.5 * layout.median_width(T)}; @@ -1878,7 +1879,7 @@ bool editor_road::branch_from(road_node &Road, double const T, glm::dvec2 const { if (false == can_edit(*change.first, &reason)) { - Error = "\"" + change.first->name() + "\" can't be changed: " + reason; + Error = format(STR_C("\"%s\" can't be changed: %s"), change.first->name().c_str(), STR(reason).c_str()); return false; } } @@ -1886,7 +1887,7 @@ bool editor_road::branch_from(road_node &Road, double const T, glm::dvec2 const { if (false == can_edit(*piece, &reason)) { - Error = "\"" + piece->name() + "\" can't be changed: " + reason; + Error = format(STR_C("\"%s\" can't be changed: %s"), piece->name().c_str(), STR(reason).c_str()); return false; } } @@ -1899,7 +1900,7 @@ bool editor_road::branch_from(road_node &Road, double const T, glm::dvec2 const junction_node::arm_data arm; if (false == cut_arm(cut, arm)) { - Error = "Lanes of \"" + cut.road->name() + "\" differ in width, and a junction takes roads with lanes of the same width"; + Error = format(STR_C("Lanes of \"%s\" differ in width, and a junction takes roads with lanes of the same width"), cut.road->name().c_str()); return false; } junction.arms.emplace_back(arm); @@ -1936,7 +1937,7 @@ bool editor_road::branch_from(junction_node &Junction, glm::dvec2 const &Directi std::string reason; if (false == can_edit(Junction, &reason)) { - Error = "\"" + Junction.name() + "\" can't be changed: " + reason; + Error = format(STR_C("\"%s\" can't be changed: %s"), Junction.name().c_str(), STR(reason).c_str()); return false; } auto const direction{glm::length(Direction) > 1e-9 ? glm::normalize(Direction) : glm::dvec2{0.0, 1.0}}; diff --git a/editor/editorSpeed.cpp b/editor/editorSpeed.cpp index c9746369..249d43d7 100644 --- a/editor/editorSpeed.cpp +++ b/editor/editorSpeed.cpp @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorTrack.hpp" #include "editor/editorSwitchStore.hpp" +#include "utilities/translation.h" #include "world/Track.h" #include @@ -141,56 +142,58 @@ bool verdict::warned(options const &Options) const std::string explain(verdict const &Verdict, options const &Options) { if (Verdict.safe >= Options.top) - return format("The geometry allows any speed up to %.0f km/h", Options.top); + return format(STR_C("The geometry allows any speed up to %.0f km/h"), Options.top); auto const gauge{Options.norms.gauge / 1000.0}; // the numbers are given at the speed set, or at the safe one when none is set auto const speed{Verdict.set > 0.0 ? Verdict.set : Verdict.safe}; auto const radius_text = [](double const Radius) { - return Radius == 0.0 ? std::string{"straight"} : format("R %.0f m%s", std::abs(Radius), Radius < 0.0 ? " the other way" : ""); + return Radius == 0.0 ? std::string{STR_C("a straight")} : format("R %.0f m%s", std::abs(Radius), Radius < 0.0 ? STR_C(" the other way") : ""); }; auto const unbalanced = [&](double const Radius, double const Cant) { return (Radius != 0.0 ? speed * speed / (12.96 * Radius) : 0.0) - kGravity * Cant / 1000.0 / gauge; }; std::string text; if (Verdict.warned(Options)) - text = "Warning only, the limit of a switch branch: "; + text = STR_C("Warning only, the limit of a switch branch: "); if (Verdict.set <= 0.0) - text += format("No speed limit is set, so the trains run at the line speed, while the geometry allows %.0f km/h.\n", Verdict.safe); + text += format(STR_C("No speed limit is set, so the trains run at the line speed, while the geometry allows %.0f km/h.\n"), Verdict.safe); + else if (Verdict.set > Verdict.safe + 0.5) + text += format(STR_C("The speed set, %.0f km/h, is above the %.0f km/h the geometry allows.\n"), Verdict.set, Verdict.safe); else - text += format("The speed set, %.0f km/h, is %s the %.0f km/h the geometry allows.\n", Verdict.set, Verdict.set > Verdict.safe + 0.5 ? "above" : "within", Verdict.safe); + text += format(STR_C("The speed set, %.0f km/h, is within the %.0f km/h the geometry allows.\n"), Verdict.set, Verdict.safe); if (Verdict.why == cause::branch) - text += Verdict.hard < Options.top ? format("Without the switch branch limit the geometry allows %.0f km/h (%s).\n", Verdict.hard, describe(Verdict.hard_why)) - : std::string{"Without the switch branch limit the geometry allows any speed.\n"}; + text += Verdict.hard < Options.top ? format(STR_C("Without the switch branch limit the geometry allows %.0f km/h (%s).\n"), Verdict.hard, STR_C(describe(Verdict.hard_why))) + : std::string{STR_C("Without the switch branch limit the geometry allows any speed.\n")}; switch (Verdict.why) { case cause::radius: case cause::branch: { bool const branch{Verdict.why == cause::branch}; - text += format("%s, %s, cant %.0f mm.\n", branch ? "Diverging track of the switch" : "Curve", radius_text(Verdict.radius).c_str(), Verdict.cant); - text += format("Unbalanced acceleration V^2/(12.96 R) - g h/s at %.0f km/h: %.2f m/s2, the limit is %.2f m/s2%s.\n", speed, unbalanced(Verdict.radius, Verdict.cant), - branch ? Options.branch_unbalanced : Options.norms.unbalanced, branch ? " for the switch branches, which have no cant" : ""); - text += branch ? "A switch of a larger radius would allow more" : "More cant or a larger radius would allow more"; + text += format(STR_C("%s, %s, cant %.0f mm.\n"), branch ? STR_C("Diverging track of the switch") : STR_C("Curve"), radius_text(Verdict.radius).c_str(), Verdict.cant); + text += format(STR_C("Unbalanced acceleration V²/(12.96 R) - g h/s at %.0f km/h: %.2f m/s², the limit is %.2f m/s²%s.\n"), speed, unbalanced(Verdict.radius, Verdict.cant), + branch ? Options.branch_unbalanced : Options.norms.unbalanced, branch ? STR_C(" for the switch branches, which have no cant") : ""); + text += branch ? STR_C("A switch of a larger radius would allow more") : STR_C("More cant or a larger radius would allow more"); break; } case cause::cant_rate: - text += format("The cant changes from %.0f to %.0f mm over %.0f m, %.2f mm per m.\n", Verdict.cant_from, Verdict.cant_to, Options.reference_length, Verdict.cant_slope); - text += format("A train at %.0f km/h feels it change by %.0f mm/s, the limit is %.0f mm/s.\n", speed, Verdict.cant_slope * speed / 3.6, Options.norms.cant_rate); - text += "A longer cant ramp, along a longer transition curve, would allow more"; + text += format(STR_C("The cant changes from %.0f to %.0f mm over %.0f m, %.2f mm per m.\n"), Verdict.cant_from, Verdict.cant_to, Options.reference_length, Verdict.cant_slope); + text += format(STR_C("A train at %.0f km/h feels it change by %.0f mm/s, the limit is %.0f mm/s.\n"), speed, Verdict.cant_slope * speed / 3.6, Options.norms.cant_rate); + text += STR_C("A longer cant ramp, along a longer transition curve, would allow more"); break; case cause::cant_ramp: - text += format("The cant changes from %.0f to %.0f mm over %.0f m, a ramp of 1:%.0f.\n", Verdict.cant_from, Verdict.cant_to, Options.reference_length, 1000.0 / std::max(1e-6, Verdict.cant_slope)); - text += format("At %.0f km/h the ramp can't be steeper than 1:(k V) = 1:%.0f, k = %.1f.\n", speed, Options.norms.ramp_factor * speed, Options.norms.ramp_factor); - text += "A longer cant ramp would allow more"; + text += format(STR_C("The cant changes from %.0f to %.0f mm over %.0f m, a ramp of 1:%.0f.\n"), Verdict.cant_from, Verdict.cant_to, Options.reference_length, 1000.0 / std::max(1e-6, Verdict.cant_slope)); + text += format(STR_C("At %.0f km/h the ramp can't be steeper than 1:(k V) = 1:%.0f, k = %.1f.\n"), speed, Options.norms.ramp_factor * speed, Options.norms.ramp_factor); + text += STR_C("A longer cant ramp would allow more"); break; case cause::jerk: { auto const from{unbalanced(Verdict.radius_from, Verdict.cant_from)}; auto const to{unbalanced(Verdict.radius_to, Verdict.cant_to)}; auto const time{Options.reference_length / std::max(1.0, speed / 3.6)}; - text += format("Within %.0f m the track goes from %s, cant %.0f mm, to %s, cant %.0f mm.\n", Options.reference_length, radius_text(Verdict.radius_from).c_str(), Verdict.cant_from, + text += format(STR_C("Within %.0f m the track goes from %s, cant %.0f mm, to %s, cant %.0f mm.\n"), Options.reference_length, radius_text(Verdict.radius_from).c_str(), Verdict.cant_from, radius_text(Verdict.radius_to).c_str(), Verdict.cant_to); - text += format("At %.0f km/h the unbalanced acceleration changes from %.2f to %.2f m/s2 in %.1f s: %.2f m/s3, the limit is %.2f m/s3.\n", speed, from, to, time, std::abs(to - from) / time, - Options.norms.jerk); - text += "The curve lacks a transition, or it is too short"; + text += format(STR_C("At %.0f km/h the unbalanced acceleration changes from %.2f to %.2f m/s² in %.1f s: %.2f m/s³, the limit is %.2f m/s³.\n"), speed, from, to, time, + std::abs(to - from) / time, Options.norms.jerk); + text += STR_C("The curve lacks a transition, or it is too short"); break; } default: break; diff --git a/editor/editorSwitchDesign.cpp b/editor/editorSwitchDesign.cpp index 8a5b5a7d..d4ef0a4f 100644 --- a/editor/editorSwitchDesign.cpp +++ b/editor/editorSwitchDesign.cpp @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorFormat.hpp" #include "world/RailLayout.h" +#include "utilities/translation.h" #include "utilities/utilities.h" #include @@ -706,7 +707,7 @@ bool save_own(std::vector const &Designs, std::string &Error) std::ofstream file{path, std::ios::trunc}; if (false == file.is_open()) { - Error = "Can't write " + path.string(); + Error = format(STR_C("Can't write %s"), path.string().c_str()); return false; } file << "# switch templates made in the track editor of MaSzyna\n"; @@ -730,7 +731,7 @@ bool save_own(std::vector const &Designs, std::string &Error) } if (file.fail()) { - Error = "Can't write " + path.string(); + Error = format(STR_C("Can't write %s"), path.string().c_str()); return false; } return true; diff --git a/editor/editorSwitchStore.cpp b/editor/editorSwitchStore.cpp index 58aa80fe..0851a87f 100644 --- a/editor/editorSwitchStore.cpp +++ b/editor/editorSwitchStore.cpp @@ -124,6 +124,12 @@ void mark(scene::layer_handle const Layer) text += " number " + word(entry.number) + " source " + word(entry.source); if (entry.middle > 0.0) text += format(" middle %.3f", entry.middle); + if (entry.lift != 0.0) + { + auto const from{context.to_local(entry.lift_from)}; + auto const to{context.to_local(entry.lift_to)}; + text += format(" lift %.4f %.4f %.4f %.4f %.4f %.4f %.4f ramp %.3f", entry.lift, from.x, from.y, from.z, to.x, to.y, to.z, entry.ramp); + } lines.push_back(text); } scene::Layers.mark(Layer, kMark, std::move(lines)); @@ -179,8 +185,18 @@ void load() line >> start.x >> start.y >> start.z >> keyword >> direction.x >> direction.y >> keyword >> km >> keyword >> entry.number >> keyword >> entry.source; if (line.fail()) continue; - if (line >> keyword && keyword == "middle") - line >> entry.middle; + while (line >> keyword) + { + if (keyword == "middle") + line >> entry.middle; + else if (keyword == "lift") + { + glm::dvec3 from{0.0}, to{0.0}; + line >> entry.lift >> from.x >> from.y >> from.z >> to.x >> to.y >> to.z >> keyword >> entry.ramp; + entry.lift_from = context.to_world(from); + entry.lift_to = context.to_world(to); + } + } entry.start = context.to_world(start); ahead = context.to_world(start + glm::dvec3{direction.x, 0.0, direction.y}); entry.direction = plan_of(ahead - entry.start); diff --git a/editor/editorSwitchStore.hpp b/editor/editorSwitchStore.hpp index 38962610..7b5f29f4 100644 --- a/editor/editorSwitchStore.hpp +++ b/editor/editorSwitchStore.hpp @@ -47,6 +47,11 @@ struct record std::string number{"-"}; std::string source{"designed"}; // designed: from the catalogue, measured: copied from a switch of the scenery double middle{0.0}; // the radius in the middle of the slip curves of a double slip with the blades outside of the diamond + // a crossover laid along a curve: the outer track went up by lift over it, level between the two points, with ramps of + // ramp m on both sides + double lift{0.0}; + glm::dvec3 lift_from{0.0}, lift_to{0.0}; + double ramp{0.0}; }; void reset(); diff --git a/editor/editorTerrainStreamer.cpp b/editor/editorTerrainStreamer.cpp index 0c0a8161..52fa7995 100644 --- a/editor/editorTerrainStreamer.cpp +++ b/editor/editorTerrainStreamer.cpp @@ -1370,17 +1370,34 @@ void terrain_streamer::water_material(std::string const &Material) m_waterchanged = true; } +std::vector> terrain_streamer::modifier_bounds() const +{ + std::vector> result; + result.reserve(m_manifest.modifiers.size()); + for (auto const &modifier : m_manifest.modifiers) + result.push_back(modifier.bounds()); + return result; +} + void terrain_streamer::apply_modifiers(editor_terrain &Chunk, glm::dvec2 const &Min, glm::dvec2 const &Max) +{ + apply_modifiers(Chunk, Min, Max, modifier_bounds()); +} + +void terrain_streamer::apply_modifiers(editor_terrain &Chunk, glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector> const &Bounds) { auto const ¤t{Chunk.data()}; auto const low{current.corner()}; auto const high{low + glm::dvec2{heightmap::chunk_size}}; std::vector shapes; - shapes.reserve(m_manifest.modifiers.size()); bool reachesany{false}; - for (auto const &modifier : m_manifest.modifiers) + for (std::size_t i = 0; i < m_manifest.modifiers.size(); ++i) { - heightmap::modifier_shape shape{modifier}; + // the shape of a modifier far away isn't worth making + auto const &[from, to]{Bounds[i]}; + if (to.x < low.x || from.x > high.x || to.y < low.y || from.y > high.y) + continue; + heightmap::modifier_shape shape{m_manifest.modifiers[i]}; if (false == shape.reaches(low, high)) continue; reachesany = true; @@ -1418,18 +1435,84 @@ void terrain_streamer::apply_modifiers(editor_terrain &Chunk, glm::dvec2 const & for (int ix = xs.first; ix <= xs.second; ++ix) { auto const index{static_cast(iz) * data.side() + ix}; - auto height{data.base[index]}; + auto const ground{static_cast(data.base[index])}; auto const x{low.x + ix * step}, z{low.y + iz * step}; + // the same for any order of the modifiers: within the formation of a track its formation, of the nearest one where two + // overlap; elsewhere the slopes, the highest fill and the lowest cut, the nearer of the two where both reach + std::optional bed, fill, cut; for (auto const &shape : shapes) - shape.apply(x, z, height); - data.heights[index] = touchups ? height + data.adjust[index] : height; + { + auto const effect{shape.evaluate(x, z, ground)}; + if (false == effect.has_value()) + continue; + if (effect->outside <= 0.0) + { + if (false == bed.has_value() || effect->distance < bed->distance) + bed = effect; + } + else if (effect->height > ground) + { + if (false == fill.has_value() || effect->height > fill->height) + fill = effect; + } + else if (effect->height < ground && (false == cut.has_value() || effect->height < cut->height)) + cut = effect; + } + auto height{ground}; + if (bed.has_value()) + height = bed->height; + else if (fill.has_value() && cut.has_value()) + height = fill->outside <= cut->outside ? fill->height : cut->height; + else if (fill.has_value()) + height = fill->height; + else if (cut.has_value()) + height = cut->height; + data.heights[index] = static_cast(touchups ? height + data.adjust[index] : height); } } void terrain_streamer::apply_modifiers(glm::dvec2 const &Min, glm::dvec2 const &Max) { + auto const bounds{modifier_bounds()}; for (auto *chunk : chunks_in(Min, Max, true)) - apply_modifiers(*chunk, Min, Max); + apply_modifiers(*chunk, Min, Max, bounds); +} + +void terrain_streamer::modifiers(std::vector Modifiers) +{ + if (Modifiers.empty()) + return; + remember_manifest(); + std::vector> areas; + for (auto &modifier : Modifiers) + { + auto bounds{modifier.bounds()}; + auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == modifier.name; })}; + if (existing != m_manifest.modifiers.end()) + { + auto const old{existing->bounds()}; + bounds.first = glm::min(bounds.first, old.first); + bounds.second = glm::max(bounds.second, old.second); + *existing = std::move(modifier); + } + else + m_manifest.modifiers.push_back(std::move(modifier)); + areas.push_back(bounds); + } + m_manifestchanged = true; + // every chunk reached, once, whole + std::vector chunks; + for (auto const &[from, to] : areas) + for (auto *chunk : chunks_in(from, to, true)) + chunks.push_back(chunk); + std::sort(chunks.begin(), chunks.end()); + chunks.erase(std::unique(chunks.begin(), chunks.end()), chunks.end()); + auto const bounds{modifier_bounds()}; + for (auto *chunk : chunks) + { + auto const low{chunk->data().corner()}; + apply_modifiers(*chunk, low, low + glm::dvec2{heightmap::chunk_size}, bounds); + } } void terrain_streamer::modifier(heightmap::modifier Modifier) @@ -1451,6 +1534,19 @@ void terrain_streamer::modifier(heightmap::modifier Modifier) apply_modifiers(bounds.first, bounds.second); } +std::size_t terrain_streamer::drop_modifiers(std::string const &Prefix) +{ + auto &list{m_manifest.modifiers}; + auto const matches = [&](heightmap::modifier const &Item) { return Item.name.rfind(Prefix, 0) == 0; }; + auto const count{static_cast(std::count_if(list.begin(), list.end(), matches))}; + if (count == 0) + return 0; + remember_manifest(); + list.erase(std::remove_if(list.begin(), list.end(), matches), list.end()); + m_manifestchanged = true; + return count; +} + bool terrain_streamer::remove_modifier(std::string const &Name) { auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Name; })}; diff --git a/editor/editorTerrainStreamer.hpp b/editor/editorTerrainStreamer.hpp index d29451e7..ab76ca81 100644 --- a/editor/editorTerrainStreamer.hpp +++ b/editor/editorTerrainStreamer.hpp @@ -132,6 +132,10 @@ class terrain_streamer void water_material(std::string const &Material); // modifiers: added or replaced by name void modifier(heightmap::modifier Modifier); + // many at once, as the formation of all the tracks: each chunk they reach is led anew once + void modifiers(std::vector Modifiers); + // the modifiers whose names start so are forgotten, the chunks keep their heights till they're made anew; returns how many + std::size_t drop_modifiers(std::string const &Prefix); bool remove_modifier(std::string const &Name); // the ground takes the shape the modifier gives it for good, and the modifier is removed: from then on the // terrain there is shaped by hand like anywhere else @@ -249,7 +253,10 @@ class terrain_streamer std::vector chunks_in(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const Load); // leads the heights of the chunk from its base heights through the modifiers, within the rectangle void apply_modifiers(editor_terrain &Chunk, glm::dvec2 const &Min, glm::dvec2 const &Max); + // the same with the rectangles the modifiers reach, as modifier_bounds() gives them, worked out once for many chunks + void apply_modifiers(editor_terrain &Chunk, glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector> const &Bounds); void apply_modifiers(glm::dvec2 const &Min, glm::dvec2 const &Max); + std::vector> modifier_bounds() const; void build_water(); void clear_water(bool const Touch); std::string pack_path(heightmap::pack_key const &Key) const; diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 7bafe19e..4f30765c 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -157,6 +157,7 @@ editor_track::state editor_track::capture(TTrack const &Track) result.sleeper_ballast = Track.m_sleeper_ballast_z; result.timber_model = Track.m_timber_model_name; result.timber_skin = Track.m_timber_skin_name; + result.rail_profile = Track.m_profile1; for (int i = 0; i < 6; ++i) result.events[i] = events(const_cast(Track), i); result.name = Track.name(); @@ -261,6 +262,7 @@ void editor_track::apply(TTrack &Track, state const &State) Track.m_sleeper_ballast_z = State.sleeper_ballast; Track.m_timber_model_name = State.timber_model; Track.m_timber_skin_name = State.timber_skin; + Track.m_profile1 = State.rail_profile; for (int i = 0; i < 6; ++i) events(Track, i) = State.events[i]; Track.m_events = std::any_of(State.events.begin(), State.events.end(), [](auto const &Events) { @@ -2118,9 +2120,9 @@ std::vector route_next(route_node const &Node) for (int i = 0; i < static_cast(next->m_paths.size()); ++i) { auto const &candidate{next->m_paths[i]}; - if (glm::distance(candidate.points[segment_data::point::start], exit) <= kSamePoint) + if (geometry::plan_distance(candidate.points[segment_data::point::start], exit) <= kSamePoint) result.emplace_back(next, i, true); - else if (glm::distance(candidate.points[segment_data::point::end], exit) <= kSamePoint) + else if (geometry::plan_distance(candidate.points[segment_data::point::end], exit) <= kSamePoint) result.emplace_back(next, i, false); } return result; @@ -2337,6 +2339,60 @@ void plane_heights(segment_data &Path, glm::dvec3 const &Origin, glm::dvec2 cons control2.y = height(second) - end.y; } +// a path of a switch on the plane of the cant of the path the route runs along: across from each of its points the height of +// that path less its cant times the distance to the left of it. The plane follows the curve of a switch bent into a curve +void cone_heights(segment_data &Path, segment_data const &Along) +{ + bezier const along{Along}; + auto const nearest = [&](glm::dvec3 const &Point) { + auto const distance = [&](double const T) { return geometry::plan_distance(along.point(T), Point); }; + double best{0.0}; + for (int i = 0; i <= 64; ++i) + if (distance(i / 64.0) < distance(best)) + best = i / 64.0; + double step{1.0 / 64.0}; + for (int i = 0; i < 30; ++i) + { + step *= 0.5; + if (best - step >= 0.0 && distance(best - step) < distance(best)) + best -= step; + else if (best + step <= 1.0 && distance(best + step) < distance(best)) + best += step; + } + return best; + }; + auto const height = [&](glm::dvec3 const &Point) { + auto const t{nearest(Point)}; + auto const on{along.point(t)}; + auto direction{plan_of(along.first(t))}; + direction = glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{1.0, 0.0}; + glm::dvec2 const left{-direction.y, direction.x}; + auto const lateral{glm::dot(plan_of(Point) - plan_of(on), left)}; + auto const roll{glm::radians(static_cast(Along.rolls[0] + (Along.rolls[1] - Along.rolls[0]) * t))}; + return on.y - lateral * std::tan(roll); + }; + auto &start{Path.points[segment_data::point::start]}; + auto &end{Path.points[segment_data::point::end]}; + auto &control1{Path.points[segment_data::point::control1]}; + auto &control2{Path.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + { + control1 = (end - start) / 3.0; + control2 = (start - end) / 3.0; + } + start.y = height(start); + end.y = height(end); + control1.y = 0.0; + control2.y = 0.0; + // the inner points of the curve on the plane at a third and two thirds of it + auto const h1{height(bezier{Path}.point(1.0 / 3.0))}; + auto const h2{height(bezier{Path}.point(2.0 / 3.0))}; + auto const y1{3.0 * h1 - 1.5 * h2 - 5.0 / 6.0 * start.y + end.y / 3.0}; + auto const y2{3.0 * h2 - 1.5 * h1 + start.y / 3.0 - 5.0 / 6.0 * end.y}; + control1.y = y1 - start.y; + control2.y = y2 - end.y; +} + glm::dvec2 start_direction(segment_data const &Path) { auto const &start{Path.points[segment_data::point::start]}; @@ -2364,7 +2420,7 @@ glm::dvec3 editor_track::sampled_position(std::vector const &Sampl return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.position; }); } -bool editor_track::adjoining_grade(route const &Route, bool const Atend, double &Grade) +bool editor_track::adjoining_grade(route const &Route, bool const Atend, double &Grade, TTrack **Adjoining, int *Path, bool *Entered) { if (Route.spans.empty()) return false; @@ -2374,6 +2430,12 @@ bool editor_track::adjoining_grade(route const &Route, bool const Atend, double if (next.empty()) return false; auto const &[track, path, forward]{next.front()}; + if (Adjoining != nullptr) + *Adjoining = track; + if (Path != nullptr) + *Path = path; + if (Entered != nullptr) + *Entered = forward; auto const tangent{path_tangent(track->m_paths[path], false == forward)}; auto const plan{std::hypot(tangent.x, tangent.z)}; if (plan < 1e-9) @@ -2390,6 +2452,12 @@ std::vector editor_track::apply_profile(route &Route, if (std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == Track; }) && std::find(Created.begin(), Created.end(), Track) == Created.end()) States.emplace_back(Track, capture(*Track)); }; + // the parameter of the split at a distance along the path in the plan, as split_path takes it: a straight is the cubic with + // the controls at its ends there, not with the parameter spread evenly + auto const parameter = [](TTrack const &Track, double const Distance) { + bezier const curve{Track.m_paths.front()}; + return nearest_parameter(Track, curve.point(curve.parameter(Distance))); + }; sample_route(Route, 1.0); std::vector spans; for (auto const &span : Route.spans) @@ -2399,16 +2467,35 @@ std::vector editor_track::apply_profile(route &Route, spans.push_back(span); continue; } + // a piece shorter than the margin, between two breaks, goes with the one before it std::vector cuts; for (auto const chainage : Breaks) - if (chainage > span.from + margin && chainage < span.to - margin) + if (chainage > span.from + margin && chainage < span.to - margin && (cuts.empty() || chainage > cuts.back() + margin)) cuts.push_back(chainage); + // and no piece longer than a track may be + { + auto bounds{cuts}; + bounds.insert(bounds.begin(), span.from); + bounds.push_back(span.to); + for (std::size_t i = 0; i + 1 < bounds.size(); ++i) + { + auto const length{bounds[i + 1] - bounds[i]}; + auto const count{static_cast(std::ceil(length / geometry::longest_path))}; + for (int k = 1; k < count; ++k) + cuts.push_back(bounds[i] + length * k / count); + } + std::sort(cuts.begin(), cuts.end()); + } if (cuts.empty()) { spans.push_back(span); continue; } remember(span.track); + // the pieces named after the path: name_1, name_2... + auto const original{span.track->name()}; + int number{0}; + auto const next_name = [&]() { return original.empty() || original == "none" ? std::string{} : original + "_" + std::to_string(++number); }; if (span.forward) { auto *piece{span.track}; @@ -2416,8 +2503,8 @@ std::vector editor_track::apply_profile(route &Route, spans.push_back({piece, 0, true}); for (auto const cut : cuts) { - auto const t{bezier(piece->m_paths.front()).parameter(cut - origin)}; - auto *created{split_path(*piece, t)}; + auto const t{parameter(*piece, cut - origin)}; + auto *created{split_path(*piece, t, next_name())}; if (created == nullptr) continue; Created.push_back(created); @@ -2431,8 +2518,8 @@ std::vector editor_track::apply_profile(route &Route, std::vector order; for (auto cut = cuts.begin(); cut != cuts.end(); ++cut) { - auto const t{bezier(span.track->m_paths.front()).parameter(span.to - *cut)}; - auto *created{split_path(*span.track, t)}; + auto const t{parameter(*span.track, span.to - *cut)}; + auto *created{split_path(*span.track, t, next_name())}; if (created == nullptr) continue; Created.push_back(created); @@ -2457,6 +2544,10 @@ std::vector editor_track::apply_profile(route &Route, TTrack *neighbour; }; std::vector joints; + if (false == Route.spans.empty()) + for (auto const &[span, atend] : {std::pair{Route.spans.front(), false == Route.spans.front().forward}, std::pair{Route.spans.back(), Route.spans.back().forward}}) + if (auto *neighbour{neighbour_at(*span.track, span.path, atend)}; neighbour != nullptr && false == on_route(neighbour)) + joints.push_back({span.track, span.path, atend, neighbour}); std::vector changed; for (auto const &span : Route.spans) { @@ -2500,6 +2591,14 @@ std::vector editor_track::apply_profile(route &Route, } if (i == span.path) continue; + // in a cant the other path lies on its plane, else on the plane of the grade + if (std::abs(path.rolls[0]) > 1e-3f || std::abs(path.rolls[1]) > 1e-3f) + { + cone_heights(span.track->m_paths[i], path); + for (auto &piece : span.track->m_paths[i].pieces) + cone_heights(piece, path); + continue; + } plane_heights(span.track->m_paths[i], path.points[segment_data::point::start], start_direction(path), grade); for (auto &piece : span.track->m_paths[i].pieces) plane_heights(piece, path.points[segment_data::point::start], start_direction(path), grade); @@ -2530,7 +2629,7 @@ std::vector editor_track::apply_profile(route &Route, } if (nearest > kSamePoint || std::abs(gap) <= 0.001) continue; - // a regular path off the route takes the height of the switch at the joint, keeping its other end + // a regular path off the route takes the height of the route at the joint, keeping its other end std::string reason; bool const closed{joint.neighbour->eType == tt_Normal && can_edit_geometry(*joint.neighbour, reason)}; if (closed) @@ -2571,63 +2670,71 @@ void editor_track::keep_heights(chain const &Chain, std::vector &P } } +std::optional editor_track::template_of_switch(TTrack *Track) +{ + if (Track == nullptr || Track->m_editorremoved || Track->eType != tt_Switch || Track->m_paths.size() < 2 || (Track->iCategoryFlag & 0x80) != 0) + return std::nullopt; + auto const &main{Track->m_paths[0]}; + auto const origin{main.points[segment_data::point::start]}; + auto const chord{plan_of(main.points[segment_data::point::end] - origin)}; + if (glm::length(chord) < 1.0) + return std::nullopt; + auto const axis{glm::normalize(chord)}; + glm::dvec2 const normal{-axis.y, axis.x}; + auto const tolocal = [&](glm::dvec3 const &Vector) { + auto const planar{plan_of(Vector)}; + return glm::dvec3{glm::dot(planar, normal), Vector.y, glm::dot(planar, axis)}; + }; + switch_template entry; + entry.source = Track; + for (int i = 0; i < 2; ++i) + { + auto const &path{Track->m_paths[i]}; + auto &local{entry.local[i]}; + local = path; + local.transform([&](glm::dvec3 const &Point) { return tolocal(Point - origin); }, tolocal); + } + if (glm::length(plan_of(entry.local[1].points[segment_data::point::start])) > kSamePoint) + return std::nullopt; + if (entry.local[1].points[segment_data::point::end].x < 0.0) + { + for (auto &path : entry.local) + { + auto const mirror = [](glm::dvec3 const &Point) { return glm::dvec3{-Point.x, Point.y, Point.z}; }; + path.transform(mirror, mirror); + path.rolls = {-path.rolls[0], -path.rolls[1]}; + for (auto &piece : path.pieces) + piece.rolls = {-piece.rolls[0], -piece.rolls[1]}; + } + } + entry.length = entry.local[0].points[segment_data::point::end].z; + auto const tangent{path_tangent(entry.local[1], true)}; + auto const angle{std::atan2(std::abs(tangent.x), tangent.z)}; + entry.ratio = angle > 1e-6 ? 1.0 / std::tan(angle) : 0.0; + entry.radius = path_radius(entry.local[1], 0.25); + entry.count = 1; + entry.label = format("L %.2f m 1:%.1f R %.0f m %s", entry.length, entry.ratio, entry.radius, material_name(Track->m_material1).c_str()); + return entry; +} + std::vector editor_track::find_switch_templates() { std::vector result; std::map lookup; for (auto *track : simulation::Paths.sequence()) { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->m_paths.size() < 2 || (track->iCategoryFlag & 0x80) != 0) + auto entry{template_of_switch(track)}; + if (false == entry.has_value()) continue; - auto const &main{track->m_paths[0]}; - auto const origin{main.points[segment_data::point::start]}; - auto const chord{plan_of(main.points[segment_data::point::end] - origin)}; - if (glm::length(chord) < 1.0) - continue; - auto const axis{glm::normalize(chord)}; - glm::dvec2 const normal{-axis.y, axis.x}; - auto const tolocal = [&](glm::dvec3 const &Vector) { - auto const planar{plan_of(Vector)}; - return glm::dvec3{glm::dot(planar, normal), Vector.y, glm::dot(planar, axis)}; - }; - switch_template entry; - entry.source = track; - for (int i = 0; i < 2; ++i) - { - auto const &path{track->m_paths[i]}; - auto &local{entry.local[i]}; - local = path; - local.transform([&](glm::dvec3 const &Point) { return tolocal(Point - origin); }, tolocal); - } - if (glm::length(plan_of(entry.local[1].points[segment_data::point::start])) > kSamePoint) - continue; - if (entry.local[1].points[segment_data::point::end].x < 0.0) - { - for (auto &path : entry.local) - { - auto const mirror = [](glm::dvec3 const &Point) { return glm::dvec3{-Point.x, Point.y, Point.z}; }; - path.transform(mirror, mirror); - path.rolls = {-path.rolls[0], -path.rolls[1]}; - for (auto &piece : path.pieces) - piece.rolls = {-piece.rolls[0], -piece.rolls[1]}; - } - } - entry.length = entry.local[0].points[segment_data::point::end].z; - auto const tangent{path_tangent(entry.local[1], true)}; - auto const angle{std::atan2(std::abs(tangent.x), tangent.z)}; - entry.ratio = angle > 1e-6 ? 1.0 / std::tan(angle) : 0.0; - entry.radius = path_radius(entry.local[1], 0.25); - auto const key{format("%.2f|%.2f|%.2f|%s|%s", entry.length, entry.local[1].points[segment_data::point::end].x, entry.local[1].points[segment_data::point::end].z, material_name(track->m_material1).c_str(), material_name(track->m_material2).c_str())}; + auto const key{format("%.2f|%.2f|%.2f|%s|%s", entry->length, entry->local[1].points[segment_data::point::end].x, entry->local[1].points[segment_data::point::end].z, material_name(track->m_material1).c_str(), material_name(track->m_material2).c_str())}; auto const found{lookup.find(key)}; if (found != lookup.end()) { ++result[found->second].count; continue; } - entry.count = 1; - entry.label = format("L %.2f m 1:%.1f R %.0f m %s", entry.length, entry.ratio, entry.radius, material_name(track->m_material1).c_str()); lookup.emplace(key, result.size()); - result.push_back(entry); + result.push_back(std::move(*entry)); } std::sort(result.begin(), result.end(), [](switch_template const &A, switch_template const &B) { return A.count > B.count; }); return result; @@ -2769,6 +2876,20 @@ editor_track::slip_curve editor_track::slip_of(double const Radius, double const return make((low + high) * 0.5); } +std::string editor_track::rail_of(TTrack const *Track) +{ + if (Track == nullptr || (Track->iCategoryFlag & 15) != 1) + return {}; + auto const &profile{Track->m_profile1.first}; + if (profile == "s60" || profile == "uic60") + return "60E1"; + if (profile == "s49") + return "49E1"; + if (profile == "s42") + return "S42"; + return {}; +} + std::vector editor_track::standard_switch_templates() { std::vector result; @@ -2788,7 +2909,7 @@ std::vector editor_track::standard_switch_templat result.push_back(std::move(*entry)); } // the slips and crossings of Id-4: the angle comes from the straights clicked, the half length from the crossing to the joint - // only to the label; the slips with the blades outside of the diamond have a straight in the middle of their curves, along which + // to a and the label; the slips with the blades outside of the diamond have a straight in the middle of their curves, along which // the near rails touch. The asymmetric slips of Id-4 (1:9/1:7,5, 1:9/1:6,6) are left out, both of their curves being the same here struct slipping { @@ -2831,6 +2952,7 @@ std::vector editor_track::standard_switch_templat slip.type = entry.type; slip.radius = entry.radius; slip.middle = entry.middle; + slip.a = entry.half; slip.ratio = ratio_of(entry.name); slip.rail = std::string{entry.name}.substr(0, std::string{entry.name}.find('-')); slip.label = std::string{entry.type} + ' ' + entry.name + format(" a %.3f", entry.half) + (slip.type == "Rkp" ? " single slip" : " double slip") + @@ -2949,7 +3071,7 @@ TTrack *editor_track::create_switch(switch_template const &Template, std::vector TTrack *track{nullptr}; if (Template.source != nullptr) { - track = clone(*Template.source); + track = clone(*Template.source, Name); } else { @@ -3207,7 +3329,7 @@ std::vector editor_track::relay(chain const &Chain, std::vectorm_friction = Template.m_friction; return track; } -TTrack *editor_track::create_path(TTrack const &Style, segment_data const &Path) +TTrack *editor_track::create_path(TTrack const &Style, segment_data const &Path, std::string const &Name) { auto const category{Style.iCategoryFlag & 15}; std::ostringstream text; @@ -3230,7 +3352,7 @@ TTrack *editor_track::create_path(TTrack const &Style, segment_data const &Path) if (auto const speed{velocity(Style)}; speed > 0.0) text << "velocity " << speed << ' '; write_ending(text, Style); - auto *track{load_path(text.str(), Style)}; + auto *track{load_path(text.str(), Style, Name)}; track->m_paths = {Path}; return track; } @@ -3267,7 +3389,7 @@ double editor_track::nearest_parameter(TTrack const &Track, glm::dvec3 const &Po return best; } -TTrack *editor_track::split_path(TTrack &Track, double const T) +TTrack *editor_track::split_path(TTrack &Track, double const T, std::string const &Name) { if (Track.eType != tt_Normal || Track.m_paths.empty() || T <= 0.001 || T >= 0.999) return nullptr; @@ -3304,7 +3426,12 @@ TTrack *editor_track::split_path(TTrack &Track, double const T) } first.rolls[1] = roll; second.rolls[0] = roll; - auto *created{create_path(Track, second)}; + auto *created{create_path(Track, second, Name)}; + created->iDamageFlag = Track.iDamageFlag; + created->iAction |= Track.iAction & 0x80; + created->fVelocity = Track.fVelocity; + created->fVerticalRadius = Track.fVerticalRadius; + created->m_friction = Track.m_friction; Track.m_paths.front() = first; // the events stay where the trains run into them: those of the runs towards the end at the start, in the original, // those of the runs towards the start at the end, in the new piece; those of a stop in both. the ones bound @@ -3382,6 +3509,8 @@ void editor_track::retire(TTrack &Track) disconnect(Track); simulation::Region->erase_and_unregister(&Track); Track.m_editorremoved = true; + // the name is free for the path laid in its place, as the switches of a crossover laid again keep theirs + simulation::Paths.rename(&Track, std::string{}); for (auto *neighbour : adjoining) { update_transition(*neighbour); @@ -3399,6 +3528,13 @@ void editor_track::retire(TTrack &Track) s_observer->track_retired(Track); } +void editor_track::revive(TTrack &Track) +{ + Track.m_editorremoved = false; + if (false == simulation::Paths.rename(&Track, Track.name())) + WriteLog("Editor: the path " + Track.name() + " came back, but another one has its name now", logtype::generic); +} + void editor_track::rebuild_geometry(TTrack &Track) { auto const rebuild = [](TTrack &Path) { diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index cebde773..43136197 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -70,6 +70,7 @@ class editor_track float sleeper_ballast{0.f}; std::string timber_model; std::string timber_skin; + std::pair rail_profile; std::array>, 6> events; std::string name; }; @@ -383,7 +384,8 @@ class editor_track static double sampled_grade(std::vector const &Samples, double const Chainage); static glm::dvec3 sampled_position(std::vector const &Samples, double const Chainage); // grade of the path which adjoins the end of the route, positive rising along the route - static bool adjoining_grade(route const &Route, bool const Atend, double &Grade); + // Adjoining, Path, Entered: the path met, and whether it's met at its start + static bool adjoining_grade(route const &Route, bool const Atend, double &Grade, TTrack **Adjoining = nullptr, int *Path = nullptr, bool *Entered = nullptr); // path off the route whose end no longer met the adjoining path in height struct height_gap { @@ -456,10 +458,14 @@ class editor_track // touching in its middle: Pass from the crossing, half of the gauge and the width of the head; a straight there when Middle < 0 static slip_curve slip_of(double const Radius, double const Middle, double const Angle, double const Pass, double const Lead = 0.0, double const Entry = 0.0); static std::vector standard_switch_templates(); + // the rail of the switch templates fitting the track, by the rail profile it's laid with; empty where the profile doesn't tell + static std::string rail_of(TTrack const *Track); static std::optional template_of(switch_design::design const &Design, std::string &Error); static std::vector find_switch_templates(); + // the switch of the scenery as a template of its own shape; none when its tracks don't start at one point + static std::optional template_of_switch(TTrack *Track); static TTrack *load_path(std::string const &Text, TTrack const &Template, std::string const &Name = {}); - static TTrack *create_path(TTrack const &Style, segment_data const &Path); + static TTrack *create_path(TTrack const &Style, segment_data const &Path, std::string const &Name = {}); // parameters of a path laid where there's no other path to copy them from struct path_style { @@ -470,7 +476,8 @@ class editor_track }; // the path goes to the layer receiving the items created in the editor static TTrack *create_path(path_style const &Style, segment_data const &Path); - static TTrack *split_path(TTrack &Track, double const T); + // Name: of the new piece, else after the track + static TTrack *split_path(TTrack &Track, double const T, std::string const &Name = {}); static double nearest_parameter(TTrack const &Track, glm::dvec3 const &Point); // point of the first path at the parameter of nearest_parameter() static glm::dvec3 point_at(TTrack const &Track, double const T); @@ -480,6 +487,8 @@ class editor_track static void move_straights(std::vector const &Lines, std::vector> const &Ends); static std::vector relay(chain const &Chain, std::vector const &Pieces, TTrack **Last = nullptr); static void retire(TTrack &Track); + // a retired path back in the scenery, found by its name again; it's committed by the caller + static void revive(TTrack &Track); static std::string material_name(material_handle const Material); static material_handle fetch_material(std::string const &Name); @@ -514,7 +523,7 @@ class editor_track static void bind_switch_path(TTrack &Switch, int const Path); static void store_switch_path(TTrack &Switch, int const Path); static void rebuild_geometry(TTrack &Track); - static TTrack *clone(TTrack const &Template); + static TTrack *clone(TTrack const &Template, std::string const &Name = {}); static TTrack *load_text(std::string const &Text, TTrack const *Template, std::string const &Name); static void move_straight(straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End, std::function const &Member); }; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index e219e72f..2d79a2ba 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -2,6 +2,7 @@ msgid "" msgstr "" "Language: pl_PL\n" "Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" # scenery editor: track tools @@ -11,48 +12,15 @@ msgstr "Proste w scenerii" msgid "Build" msgstr "Budowa" -msgid "Select Esc" -msgstr "Wybór Esc" - -msgid "Lay track L" -msgstr "Układanie toru L" - -msgid "Switch T" -msgstr "Rozjazd T" - -msgid "Edit and check (inspector)" -msgstr "Edycja i kontrola (inspektor)" - -msgid "Show the inspector" -msgstr "Pokaż inspektor" - msgid "The curve can't be read from this path, edit it as a single path" msgstr "Z tego toru nie da się odczytać łuku, edytuj go jako pojedynczy tor" -msgid "New path from the free end" -msgstr "Nowy tor z wolnego końca" - -msgid "Switch: move and turn it with the gizmo" -msgstr "Rozjazd: przesuwaj i obracaj gizmem" - -msgid "Path: drag its points" -msgstr "Tor: przeciągaj jego punkty" - -msgid "Vertical profile: edit the grade line in the strip below" -msgstr "Profil podłużny: edytuj niweletę na pasku na dole" - msgid "Track" msgstr "Tor" -msgid "LMB on a path in the view selects it. L lays new track, T puts a switch into the selected one." -msgstr "LPM na torze w widoku wybiera go. L układa nowy tor, T wstawia rozjazd w wybrany." - msgid "New track" msgstr "Nowy tor" -msgid "Click the points the track goes through, the curves go at the inner points. Start or finish at a free end (magenta ring) to join it." -msgstr "Klikaj punkty, przez które ma iść tor, łuki powstaną w punktach pośrednich. Zacznij lub skończ na wolnym końcu (purpurowy pierścień), aby się z nim połączyć." - msgid "Curve radius (m)" msgstr "Promień łuku (m)" @@ -63,44 +31,20 @@ msgid "Transition curves and cant" msgstr "Krzywe przejściowe i przechyłka" msgid "Longest path (m)" -msgstr "Najdłuższy tor (m)" - -msgid "Parameters of the selected track" -msgstr "Parametry wybranego toru" +msgstr "Maks. długość toru (m)" msgid "Switch template" msgstr "Wzorzec rozjazdu" -msgid "Press on the selected straight or curve where the switch starts and drag: along sets its direction, sideways its side" -msgstr "Wciśnij na wybranej prostej lub łuku w miejscu początku rozjazdu i przeciągnij: wzdłuż ustala kierunek, w bok stronę" - msgid "Collect from the scenery" msgstr "Zbierz ze scenerii" -msgid "Select a path in the view, then build the route along the line, or set its start and end paths (switches are passed through)" -msgstr "Wybierz tor w widoku, potem zbuduj trasę wzdłuż linii albo wskaż jej tor początkowy i końcowy (rozjazdy są przechodzone)" - -msgid "Start: %s" -msgstr "Początek: %s" - -msgid "Route: %zu paths, %.2f m, %s ... %s" -msgstr "Trasa: %zu torów, %.2f m, %s ... %s" - msgid "Vertical profile" msgstr "Profil podłużny" -msgid "Along the line from the selected path" -msgstr "Wzdłuż linii od wybranego toru" - msgid "m each way" msgstr "m w każdą stronę" -msgid "Start: selected path" -msgstr "Początek: wybrany tor" - -msgid "End: selected path" -msgstr "Koniec: wybrany tor" - msgid "Chainage of the start (m)" msgstr "Kilometraż początku (m)" @@ -110,42 +54,24 @@ msgstr "Prędkość projektowa (km/h)" msgid "Exaggeration" msgstr "Przewyższenie" -msgid "Shown:" -msgstr "Widoczne:" - msgid "Terrain" msgstr "Teren" msgid "Plan curvature" msgstr "Krzywizna w planie" -msgid "Fit the view" -msgstr "Dopasuj widok" - -msgid "Recognize from the track" -msgstr "Rozpoznaj z toru" - msgid "Apply to the scenery" msgstr "Zastosuj w scenerii" -msgid "LMB on a path selects it, the inspector on the right shows what can be done with it" -msgstr "LPM na torze wybiera go, inspektor po prawej pokazuje, co można z nim zrobić" - msgid "Lays new track through the clicked points, on the ground or from a free end of a track" msgstr "Układa nowy tor przez klikane punkty, na terenie albo od wolnego końca toru" -msgid "Puts a switch into the selected straight or curve: choose the template, then press and drag on the track" -msgstr "Wstawia rozjazd w wybraną prostą lub łuk: wybierz wzorzec, potem wciśnij i przeciągnij na torze" - msgid "Keeps the start and the direction of the switch, takes the shape of the chosen template" -msgstr "Zachowuje początek i kierunek rozjazdu, przyjmuje kształt wybranego wzorca" +msgstr "Zachowuje początek i kierunek rozjazdu, przyjmuje kształt wybranego szablonu" msgid "Mirrors the switch: the diverging track goes to the other side" msgstr "Odbija rozjazd: tor zwrotny przechodzi na drugą stronę" -msgid "Speed limit of the path for the AI drivers and the timetable, -1: no limit of its own" -msgstr "Ograniczenie prędkości toru dla maszynistów AI i rozkładu, -1: bez własnego ograniczenia" - msgid "Friction coefficient of the rails, 0.15 for the dry rails; lower values make the wheels slip" msgstr "Współczynnik tarcia szyn, 0.15 dla suchych; mniejsze wartości powodują poślizg kół" @@ -158,15 +84,6 @@ msgstr "Jakość toru, 20 to nominalna; mniejsze wartości dają mniej spokojną msgid "0: track in order; any value adds jolts, 128: the vehicles derail on the path" msgstr "0: tor sprawny; inne wartości dodają szarpnięcia, 128: pojazdy wykolejają się na torze" -msgid "Surroundings of the path, they affect the sound of the passing vehicles" -msgstr "Otoczenie toru, wpływa na dźwięk przejeżdżających pojazdów" - -msgid "Gauge of the track (1.435 m for the standard gauge), width of the surface for the roads" -msgstr "Rozstaw toru (1.435 m dla normalnotorowego), szerokość nawierzchni dla dróg" - -msgid "Length in metres after which the texture of the rails and the trackbed repeats" -msgstr "Długość w metrach, po której powtarza się tekstura szyn i podsypki" - msgid "Height of the trackbed edge below the rail top, in metres" msgstr "Wysokość krawędzi podsypki poniżej główki szyny, w metrach" @@ -185,14 +102,11 @@ msgstr "Stan sieci trakcyjnej nad torem: pociągi przejeżdżają bez prądu prz msgid "Repeats a model along the path, the sleepers in place of the drawn trackbed" msgstr "Powtarza model wzdłuż toru, podkłady zamiast rysowanej podsypki" -msgid "Shift of each model: sideways, along the path, up" -msgstr "Przesunięcie każdego modelu: w bok, wzdłuż toru, w górę" - msgid "event0: a vehicle with a crew stands on the path, event1: runs along it towards its start, event2: towards its end.\neventall0/1/2: the same for any vehicle" msgstr "event0: pojazd z obsadą stoi na torze, event1: jedzie po nim w stronę początku, event2: w stronę końca.\neventall0/1/2: to samo dla dowolnego pojazdu" msgid "Name of the event to add to the chosen list. An event which doesn't exist is kept, and reported" -msgstr "Nazwa zdarzenia do dodania do wybranej listy. Nieistniejące zdarzenie zostaje zachowane i zgłoszone" +msgstr "Nazwa eventu do dodania do wybranej listy. Event, którego nie ma, zostaje zachowany i zgłoszony" msgid "A dragged free end within this distance of another free end joins it on release" msgstr "Przeciągany wolny koniec w tej odległości od innego wolnego końca łączy się z nim po puszczeniu" @@ -203,12 +117,6 @@ msgstr "Przy łączeniu koniec przejmuje też kierunek drugiego toru, żeby styk msgid "Dragging a joined end, or moving and turning the whole path or switch,\nmoves the ends of the neighbouring paths with it" msgstr "Przeciąganie połączonego końca albo przesuwanie i obracanie całego toru lub rozjazdu\nprzesuwa razem z nim końce sąsiednich torów" -msgid "Control vector of the start, relative to the start point; zero for a straight" -msgstr "Wektor sterujący początku, względem punktu początkowego; zero dla prostej" - -msgid "Control vector of the end, relative to the end point; zero for a straight" -msgstr "Wektor sterujący końca, względem punktu końcowego; zero dla prostej" - msgid "Cant at the start and at the end as the roll angle of the track" msgstr "Przechyłka na początku i na końcu jako kąt przechylenia toru" @@ -218,20 +126,14 @@ msgstr "Promień zapisany z torem w scenerii; sam kształt wynika z wektorów st msgid "Removes the control vectors and the radius, the path becomes a straight between its ends" msgstr "Usuwa wektory sterujące i promień, tor staje się prostą między swoimi końcami" -msgid "The line through the selected path, from switch to switch or to a free end:\na vertex for each curve, the straights between them follow the vertices" -msgstr "Linia przez wybrany tor, od rozjazdu do rozjazdu lub do wolnego końca:\nwierzchołek dla każdego łuku, proste między nimi podążają za wierzchołkami" - -msgid "Only the curve under the selected path, between the straights adjoining it,\nwhich it takes over or gives back as its radius changes" -msgstr "Tylko łuk pod wybranym torem, między przylegającymi prostymi,\nktóre przejmuje lub oddaje przy zmianie promienia" - msgid "Speed the cant, the transitions and the recommended radius are designed for" msgstr "Prędkość, dla której projektowane są przechyłka, krzywe przejściowe i zalecany promień" -msgid "Cubic parabola: the usual one on the Polish lines; clothoid: curvature growing linearly with the length" -msgstr "Parabola sześcienna: zwykła na polskich liniach; klotoida: krzywizna rośnie liniowo z długością" +msgid "The usual one on the Polish lines" +msgstr "Zwykła na polskich liniach" -msgid "Curve made of two arcs of different radii, joined with a transition" -msgstr "Łuk z dwóch łuków o różnych promieniach, połączonych krzywą przejściową" +msgid "Curvature growing linearly with the length" +msgstr "Krzywizna rośnie liniowo z długością" msgid "Height of the outer rail above the inner one" msgstr "Wysokość szyny zewnętrznej ponad wewnętrzną" @@ -240,7 +142,7 @@ msgid "For the loops: the curve turns by more than a half circle" msgstr "Dla pętli: łuk skręca o więcej niż pół okręgu" msgid "Cant and transitions recommended for the design speed and the radius" -msgstr "Przechyłka i krzywe przejściowe zalecane dla prędkości projektowej i promienia" +msgstr "Ustawia przechyłkę i krzywe przejściowe zalecane dla prędkości projektowej i promienia" msgid "Largest change of the direction between the paths of one straight" msgstr "Największa zmiana kierunku między torami jednej prostej" @@ -255,7 +157,7 @@ msgid "Shorter straights aren't listed" msgstr "Krótsze proste nie są wypisywane" msgid "Radius of the curves made by the break and the S-curve; automatic: 1.5 times the minimum radius for the speed of the line" -msgstr "Promień łuków tworzonych przez załamanie i esowanie; automatyczny: 1.5 raza promień minimalny dla prędkości linii" +msgstr "Promień łuków przy załamaniu i w esownicy; automatyczny: 1.5 raza promień minimalny dla prędkości linii" msgid "Lengths of the transition curves from the speed of the paths and the limits of the curve design" msgstr "Długości krzywych przejściowych z prędkości torów i ograniczeń projektowania łuków" @@ -266,41 +168,32 @@ msgstr "Przeciągnij, aby zmienić, Ctrl+klik, aby wpisać wartość" msgid "Speed limit of the new paths, and the design speed of the transition curves and of the cant" msgstr "Ograniczenie prędkości nowych torów oraz prędkość projektowa krzywych przejściowych i przechyłki" -msgid "Lengths and the cant recommended for the speed and the radius; off: plain circular arcs with no cant" -msgstr "Długości i przechyłka zalecane dla prędkości i promienia; wyłączone: zwykłe łuki kołowe bez przechyłki" - msgid "Takes the textures and the other parameters from the selected track.\nA track started or finished at a free end always takes them from the track it joins" -msgstr "Bierze tekstury i pozostałe parametry z wybranego toru.\nTor zaczęty lub zakończony na wolnym końcu zawsze bierze je z toru, do którego dochodzi" +msgstr "Bierze tekstury i pozostałe parametry z zaznaczonego toru.\nTor zaczęty albo skończony na wolnym końcu zawsze bierze je od toru, do którego dochodzi" msgid "Looks for the switches of the scenery, to use their shapes as the templates" -msgstr "Szuka rozjazdów w scenerii, aby użyć ich kształtów jako wzorców" +msgstr "Szuka rozjazdów w scenerii, aby użyć ich kształtów jako szablonów" msgid "stop laying" -msgstr "zakończ układanie" - -msgid "next point, a curve goes at it" -msgstr "następny punkt, w nim powstanie łuk" +msgstr "kończy układanie" msgid "join and lay" -msgstr "połącz i ułóż" +msgstr "łączy i układa" msgid "lay" msgstr "ułóż" msgid "take back the point" -msgstr "cofnij punkt" +msgstr "cofa punkt" msgid "cancel" -msgstr "anuluj" - -msgid "curve through the cursor, along the end: straight" -msgstr "łuk przez kursor, wzdłuż końca: prosta" +msgstr "anuluje" msgid "straight" msgstr "prosta" msgid "build" -msgstr "zbuduj" +msgstr "układa tor" msgid "direction of the switch" msgstr "kierunek rozjazdu" @@ -308,26 +201,11 @@ msgstr "kierunek rozjazdu" msgid "side of the diverging track" msgstr "strona toru zwrotnego" -msgid "on the selected straight or curve where the switch starts" -msgstr "na wybranej prostej lub łuku, gdzie zaczyna się rozjazd" - -msgid "put the switch away" -msgstr "odłóż rozjazd" - msgid "select a path" -msgstr "wybierz tor" - -msgid "drag the points of the grade line, wheel zooms" -msgstr "przeciągaj punkty niwelety, kółko przybliża" +msgstr "zaznacza tor" msgid "show the path" -msgstr "pokaż tor" - -msgid "select a straight or a curve" -msgstr "wybierz prostą lub łuk" - -msgid "the ends or the middle" -msgstr "końce lub środek" +msgstr "pokazuje tor" msgid "break" msgstr "załamanie" @@ -350,12 +228,6 @@ msgstr "przesuń rozjazd" msgid "turn" msgstr "obróć" -msgid "leave the main track" -msgstr "zostaw tor zasadniczy" - -msgid "select a curve" -msgstr "wybierz łuk" - msgid "vertex" msgstr "wierzchołek" @@ -366,13 +238,7 @@ msgid "apply" msgstr "zastosuj" msgid "release the vertex" -msgstr "puść wierzchołek" - -msgid "select a path or a point handle" -msgstr "wybierz tor lub uchwyt punktu" - -msgid "release the point" -msgstr "puść punkt" +msgstr "puszcza wierzchołek" msgid "split the path under the cursor" msgstr "podziel tor pod kursorem" @@ -389,60 +255,24 @@ msgstr "ponów" msgid "lay, switch, straight, curve, profile, speed, infra" msgstr "układanie, rozjazd, prosta, łuk, profil, prędkość, infrastruktura" -msgid "Straight G" -msgstr "Prosta G" - -msgid "The whole straight through the selected path" -msgstr "Cała prosta przez wybrany tor" - -msgid "Curve C" -msgstr "Łuk C" - -msgid "Curves of the line through the selected path: radii, transitions, cant" -msgstr "Łuki linii przez wybrany tor: promienie, krzywe przejściowe, przechyłka" - msgid "Path" msgstr "Tor" -msgid "Points, control vectors and parameters of the selected path" -msgstr "Punkty, wektory sterujące i parametry wybranego toru" - -msgid "Profile P" -msgstr "Profil P" - msgid "Vertical profile (grade line) along the line" msgstr "Profil podłużny (niweleta) wzdłuż linii" -msgid "Speed V" -msgstr "Prędkość V" - msgid "Speed limits of the paths against what their geometry allows" msgstr "Ograniczenia prędkości torów wobec tego, na co pozwala ich geometria" -msgid "Infra I" -msgstr "Infra I" - -msgid "Infrastructure bound to the track, which follows its changes" -msgstr "Infrastruktura powiązana z torem, podążająca za jego zmianami" - msgid "Straight" msgstr "Prosta" -msgid "The whole straight through the selected path: drag its ends or the middle, break it, shift it (G)" -msgstr "Cała prosta przez wybrany tor: przeciągaj końce lub środek, załamuj, przesuwaj (G)" - msgid "Curve" msgstr "Łuk" -msgid "The line through the path, switch to switch: vertices with the radii, transitions and cant of the curves (C)" -msgstr "Linia przez tor, od rozjazdu do rozjazdu: wierzchołki z promieniami, krzywymi przejściowymi i przechyłką łuków (C)" - msgid "Switch" msgstr "Rozjazd" -msgid "Geometry of the switch from a template, side of the diverging track" -msgstr "Geometria rozjazdu ze wzorca, strona toru zwrotnego" - msgid "Profile" msgstr "Profil" @@ -458,24 +288,6 @@ msgstr "Ograniczenia prędkości torów wobec prędkości dopuszczalnej przez ge msgid "Infra" msgstr "Infra" -msgid "Infrastructure along the track, bound to follow its changes (I)" -msgstr "Infrastruktura wzdłuż toru, powiązana tak, by podążała za jego zmianami (I)" - -msgid "Select a path in the view first" -msgstr "Najpierw wybierz tor w widoku" - -msgid "The selected path isn't a switch" -msgstr "Wybrany tor nie jest rozjazdem" - -msgid "The selected path isn't straight" -msgstr "Wybrany tor nie jest prosty" - -msgid "Switches and paths of other kinds aren't a part of a line" -msgstr "Rozjazdy i tory innych rodzajów nie są częścią linii" - -msgid "The selected path is straight, the single curve mode needs a curve" -msgstr "Wybrany tor jest prosty, tryb pojedynczego łuku wymaga łuku" - msgid "normal" msgstr "zwykły" @@ -515,25 +327,13 @@ msgid "Limits" msgstr "Ograniczenia" msgid "Automatic R at all points" -msgstr "Automatyczny R we wszystkich punktach" +msgstr "Ustaw automatyczny R wszędzie" msgid "Required R for %.0f km/h: %.0f m" msgstr "Wymagany R dla %.0f km/h: %.0f m" -msgid "%d errors, %d warnings: see the Remarks in the strip" -msgstr "Błędy: %d, ostrzeżenia: %d - zobacz Uwagi na dolnym pasku" - -msgid "%d warnings: see the Remarks in the strip" -msgstr "Ostrzeżenia: %d - zobacz Uwagi na dolnym pasku" - msgid "The grade line meets the limits" -msgstr "Niweleta spełnia ograniczenia" - -msgid "Largest departure from the track: %.3f m" -msgstr "Największa odchyłka od toru: %.3f m" - -msgid "Sets the points of the grade line from the existing track" -msgstr "Ustawia punkty niwelety na podstawie istniejącego toru" +msgstr "Niweleta zgodna z normami" msgid "Sets the heights of the track along the route to the grade line, Ctrl+Z takes it back" msgstr "Ustawia wysokości toru wzdłuż trasy według niwelety, Ctrl+Z cofa" @@ -542,19 +342,19 @@ msgid "LMB on a remark shows its place" msgstr "LPM na uwadze pokazuje jej miejsce" msgid "sag" -msgstr "wklęsły" +msgstr "załom wklęsły" msgid "crest" -msgstr "wypukły" +msgstr "załom wypukły" msgid "No point selected. Click a point of the grade line in the plot, or double click the line to add one." -msgstr "Nie wybrano punktu. Kliknij punkt niwelety na wykresie albo kliknij dwukrotnie linię, aby dodać nowy." +msgstr "Nie wybrano punktu. Kliknij punkt niwelety na wykresie albo kliknij dwukrotnie linię, aby dodać nowy" msgid "Point %d: joint with the adjoining track" msgstr "Punkt %d: styk z przylegającym torem" msgid "Grade %+.2f -> %+.2f per mille (%s)" -msgstr "Pochylenie %+.2f -> %+.2f promila (%s)" +msgstr "Pochylenie %+.2f -> %+.2f ‰ (%s)" msgid "Placed automatically, moves with the neighbouring point; untick the joining in the inspector to edit the end freely" msgstr "Ustawiany automatycznie, porusza się z sąsiednim punktem; odznacz dowiązanie w inspektorze, aby swobodnie edytować koniec" @@ -568,17 +368,14 @@ msgstr ", dowiązany do przylegającego toru" msgid ", end of the route" msgstr ", koniec trasy" -msgid "Elevation %.3f m" -msgstr "Wysokość %.3f m" - msgid "auto" msgstr "auto" msgid "R follows the limits: the required radius where the change of grade needs a curve; 0: no curve" -msgstr "R wynika z ograniczeń: wymagany promień tam, gdzie zmiana pochylenia wymaga łuku; 0: bez łuku" +msgstr "R wynika z norm: wymagany promień tam, gdzie zmiana pochylenia wymaga łuku; 0: bez łuku" msgid "Change of grade %+.2f per mille, %s" -msgstr "Zmiana pochylenia %+.2f promila, %s" +msgstr "Zmiana pochylenia %+.2f ‰, %s" msgid "Tangent T %.2f m, sagitta f %.3f m" msgstr "Styczna T %.2f m, strzałka f %.3f m" @@ -599,25 +396,25 @@ msgid "Exaggeration of the heights against the lengths, also Ctrl+wheel over the msgstr "Przewyższenie wysokości względem długości, także Ctrl+kółko nad wykresem" msgid "Fit" -msgstr "Dopasuj" +msgstr "Pokaż całość" msgid "Shows the whole route" msgstr "Pokazuje całą trasę" msgid "before %+.2f per mille" -msgstr "przed %+.2f promila" +msgstr "przed %+.2f ‰" msgid "after %+.2f per mille" -msgstr "za %+.2f promila" +msgstr "za %+.2f ‰" msgid "plan: curvature (up: left)" msgstr "plan: krzywizna (w górę: w lewo)" msgid "Grade line %.3f m, %+.2f per mille" -msgstr "Niweleta %.3f m, %+.2f promila" +msgstr "Niweleta %.3f m, %+.2f ‰" msgid "Track %.3f m (%+.3f), %+.2f per mille" -msgstr "Tor %.3f m (%+.3f), %+.2f promila" +msgstr "Tor %.3f m (%+.3f), %+.2f ‰" msgid "Terrain %.3f m, %s %.2f m" msgstr "Teren %.3f m, %s %.2f m" @@ -632,10 +429,10 @@ msgid " (switch, tilted as a whole with its branch)" msgstr " (rozjazd, pochylany w całości z odgałęzieniem)" msgid "Maximum grade (per mille)" -msgstr "Największe pochylenie (promile)" +msgstr "Największe pochylenie (‰)" msgid "Maximum grade over switches (per mille)" -msgstr "Największe pochylenie na rozjazdach (promile)" +msgstr "Największe pochylenie na rozjazdach (‰)" msgid "Vertical curve R >= k*V^2, k" msgstr "Łuk pionowy R >= k*V^2, k" @@ -644,10 +441,10 @@ msgid "Vertical curve R min (m)" msgstr "Najmniejszy R łuku pionowego (m)" msgid "Vertical curve needed above a change of (per mille)" -msgstr "Łuk pionowy potrzebny powyżej zmiany o (promile)" +msgstr "Łuk pionowy potrzebny od zmiany pochylenia (‰)" msgid "Constant grade between curves at least (m)" -msgstr "Stałe pochylenie między łukami co najmniej (m)" +msgstr "Najkrótszy odcinek stałego pochylenia (m)" msgid "Tolerance at fixed points (m)" msgstr "Tolerancja w punktach stałych (m)" @@ -659,10 +456,10 @@ msgid "Elevation (m)" msgstr "Wysokość (m)" msgid "Grade before (per mille)" -msgstr "Pochylenie przed (promile)" +msgstr "Pochylenie przed (‰)" msgid "Grade after (per mille)" -msgstr "Pochylenie za (promile)" +msgstr "Pochylenie za (‰)" msgid "Vertical curve R (m)" msgstr "R łuku pionowego (m)" @@ -676,103 +473,49 @@ msgstr "Styczne" msgid "Switches" msgstr "Rozjazdy" -msgid "wheel: zoom Ctrl+wheel: heights RMB/MMB drag: pan double click on the line: add a point" -msgstr "kółko: przybliżenie Ctrl+kółko: wysokości PPM/ŚPM: przesuwanie dwuklik na linii: nowy punkt" - -msgid "Ctrl: only the height Shift: keep the grade before" -msgstr "Ctrl: tylko wysokość Shift: zachowaj pochylenie przed" - -msgid "drag up or down: radius of the vertical curve" -msgstr "przeciągnij w górę lub w dół: promień łuku pionowego" - -msgid "drag: move the point Ctrl+drag: only the height Shift+drag: keep the grade before Del: remove" -msgstr "przeciągnij: przesuń punkt Ctrl: tylko wysokość Shift: zachowaj pochylenie przed Del: usuń" - -msgid "double click: add a point here" -msgstr "dwuklik: dodaj tu punkt" - # scenery editor: track modes -msgid "The selected path isn't straight: C edits it as a curve, Esc goes back to Select" -msgstr "Wybrany tor nie jest prosty: C edytuje go jako łuk, Esc wraca do Wyboru" - msgid "Switches aren't a part of a line: T edits the switch, Esc goes back to Select" msgstr "Rozjazdy nie są częścią linii: T edytuje rozjazd, Esc wraca do Wyboru" -msgid "The track tools are in the Track window on the right: choose the mode at its top, or press its key in the view." -msgstr "Narzędzia torów są w oknie Tor po prawej: wybierz tryb u jego góry albo naciśnij jego klawisz w widoku." - -msgid "Show the Track window" -msgstr "Pokaż okno Tor" - msgid "Tracks" msgstr "Tory" -msgid "Track window" -msgstr "Okno Tor" - -msgid "The selected path isn't straight: LMB on a straight" -msgstr "Wybrany tor nie jest prosty: LPM na prostej" - msgid "Switches aren't a part of a line: LMB on a plain path" msgstr "Rozjazdy nie są częścią linii: LPM na zwykłym torze" -msgid "Selected switch" -msgstr "Wybrany rozjazd" - msgid "back to Select" msgstr "powrót do Wyboru" -msgid "select the straight or the curve for the switch" -msgstr "wybierz prostą lub łuk dla rozjazdu" - msgid "move the selected switch" -msgstr "przesuń wybrany rozjazd" - -msgid "on the selected track where the switch starts" -msgstr "na wybranym torze, gdzie zaczyna się rozjazd" - -msgid "select another track" -msgstr "wybierz inny tor" +msgstr "przesuwa zaznaczony rozjazd" msgid "modes: lay, switch, straight, curve, profile, speed, infra" msgstr "tryby: układanie, rozjazd, prosta, łuk, profil, prędkość, infrastruktura" msgid "Select" -msgstr "Wybór" +msgstr "Zaznaczanie" msgid "LMB on a path selects it: its points, control vectors and parameters" -msgstr "LPM na torze wybiera go: jego punkty, wektory sterujące i parametry" +msgstr "LPM na torze zaznacza go: jego punkty, wektory sterujące i parametry" msgid "Lay track" msgstr "Układanie" msgid "Puts switches into the selected straight or curve, edits the geometry of the selected switch" -msgstr "Wstawia rozjazdy w wybraną prostą lub łuk, edytuje geometrię wybranego rozjazdu" - -msgid "The whole straight through the selected path: drag its ends or the middle, break it, shift it" -msgstr "Cała prosta przez wybrany tor: przeciągaj końce lub środek, załamuj, przesuwaj" - -msgid "The line through the path, switch to switch: vertices with the radii, transitions and cant of the curves" -msgstr "Linia przez tor, od rozjazdu do rozjazdu: wierzchołki z promieniami, krzywymi przejściowymi i przechyłką łuków" +msgstr "Wstawia rozjazdy w zaznaczoną prostą lub łuk, edytuje geometrię zaznaczonego rozjazdu" msgid "Speed limits of the paths against the speed their geometry allows" msgstr "Ograniczenia prędkości torów wobec prędkości dopuszczalnej przez geometrię" -msgid "Infrastructure along the track, bound to follow its changes" -msgstr "Infrastruktura wzdłuż toru, powiązana tak, by podążała za jego zmianami" - -msgid "Track editor" -msgstr "Edytor torów" - msgid "Structure gauge" msgstr "Skrajnia" msgid "Checks which models enter the structure gauge of the tracks and the clearance over the roads (skrajnia budowli)" -msgstr "Sprawdza, które modele wchodzą w skrajnię budowli torów i w skrajnię nad drogami" +msgstr "Sprawdza, które modele wchodzą w skrajnię, czyli wolną przestrzeń wokół toru potrzebną pociągom, i w skrajnię nad drogami" msgid "The geometry of this switch is degenerate, it can't be edited here" -msgstr "Geometria tego rozjazdu jest zdegenerowana, nie da się jej tu edytować" +msgstr "Geometria tego rozjazdu jest błędna, nie da się jej tu edytować" msgid "Length %.3f m, diverging %s, end offset %.3f m" msgstr "Długość %.3f m, tor zwrotny %s, odsunięcie końca %.3f m" @@ -790,14 +533,11 @@ msgid "Too far from the new ends of the switch, left unjoined: " msgstr "Za daleko od nowych końców rozjazdu, zostawione niepołączone: " msgid "Replace the geometry with the template" -msgstr "Zastąp geometrię wzorcem" +msgstr "Zastąp geometrię szablonem" msgid "Flip the side" msgstr "Odwróć stronę" -msgid "Velocity (km/h, -1: none)" -msgstr "Prędkość (km/h, -1: brak)" - msgid "Ride and sound" msgstr "Jazda i dźwięk" @@ -816,12 +556,6 @@ msgstr "Flagi uszkodzeń" msgid "Width" msgstr "Szerokość" -msgid "Textures and trackbed" -msgstr "Tekstury i podsypka" - -msgid "The texture changes on Enter or leaving the field, \"none\" removes it" -msgstr "Tekstura zmienia się po Enter lub wyjściu z pola, \"none\" ją usuwa" - msgid "Texture length" msgstr "Długość tekstury" @@ -834,9 +568,6 @@ msgstr "Szerokość podsypki" msgid "Trackbed slope" msgstr "Skarpa podsypki" -msgid "Track circuit, overhead line, sleepers, events" -msgstr "Odcinek izolowany, sieć, podkłady, zdarzenia" - msgid "Track circuits" msgstr "Odcinki izolowane" @@ -870,9 +601,6 @@ msgstr "Rozstaw podkładów (m)" msgid "Distance between the sleepers along the path" msgstr "Odległość między podkładami wzdłuż toru" -msgid "Sleeper offset (m)" -msgstr "Przesunięcie podkładu (m)" - msgid "Trackbed shift (m)" msgstr "Przesunięcie podsypki (m)" @@ -880,22 +608,22 @@ msgid "Vertical shift of the drawn trackbed under the sleepers, negative lowers msgstr "Pionowe przesunięcie rysowanej podsypki pod podkładami, ujemne ją obniża" msgid "Events" -msgstr "Zdarzenia" +msgstr "Eventy" msgid "%s %s (by the name of the path)" msgstr "%s %s (po nazwie toru)" msgid "%s %s (no such event)" -msgstr "%s %s (nie ma takiego zdarzenia)" +msgstr "%s %s (nie ma takiego eventu)" msgid "Add" msgstr "Dodaj" msgid "The events can't change while vehicles stand on the path" -msgstr "Zdarzenia nie mogą się zmienić, gdy na torze stoją pojazdy" +msgstr "Eventów nie można zmienić, gdy na torze stoją pojazdy" msgid "No such events: %s" -msgstr "Nie ma takich zdarzeń: %s" +msgstr "Nie ma takich eventów: %s" msgid "No path selected" msgstr "Nie zaznaczono toru" @@ -910,7 +638,7 @@ msgid "Geometry" msgstr "Geometria" msgid "Path %d" -msgstr "Odcinek %d" +msgstr "Tor %d" msgid "(+) joined with another track, (-) a free end" msgstr "(+) połączony z innym torem, (-) wolny koniec" @@ -933,23 +661,20 @@ msgstr "To rozjazd: geometrię, stronę i napęd ustawia narzędzie Rozjazd" msgid "Edit the switch (T)" msgstr "Edytuj rozjazd (T)" -msgid "Opens the turnout tool with this switch" -msgstr "Otwiera narzędzie Rozjazd z tym rozjazdem" - msgid "Geometry of the paths" -msgstr "Geometria odcinków" +msgstr "Geometria torów" msgid "Start (%.2f, %.2f, %.2f) End (%.2f, %.2f, %.2f)" msgstr "Początek (%.2f, %.2f, %.2f) Koniec (%.2f, %.2f, %.2f)" msgid "Drag to move, Ctrl+click to type the value" -msgstr "Przeciągnij, by zmienić, Ctrl+klik, by wpisać wartość" +msgstr "Przeciągnij, by przesunąć, Ctrl+LPM, by wpisać wartość" msgid "vector##point" msgstr "wektor##point" msgid "Control vector, relative to its end; zero for a straight" -msgstr "Wektor kontrolny, względem swojego końca; zero dla prostej" +msgstr "Wektor sterujący, względem swojego końca; zero dla prostej" msgid "Cant and radius" msgstr "Przechyłka i promień" @@ -958,7 +683,7 @@ msgid "Roll (deg)" msgstr "Przechył (°)" msgid "Radius" -msgstr "Promień" +msgstr "Promień (m)" msgid "Straighten" msgstr "Wyprostuj" @@ -970,10 +695,10 @@ msgid "Snap radius (m)" msgstr "Zasięg przyciągania (m)" msgid "Align with snapped path" -msgstr "Dopasuj kierunek do przyciągniętego" +msgstr "Dopasuj kierunek" msgid "Drag connected ends" -msgstr "Ciągnij połączone końce" +msgstr "Przeciągnij połączone końce" msgid "The fragment runs through %zu switches along their main tracks. They move as a whole with the line, so they have to fall on its straights" msgstr "Fragment przechodzi przez %zu rozjazdów torem zasadniczym. Przesuwają się w całości z linią, więc muszą wypaść na jej prostych" @@ -1020,17 +745,11 @@ msgstr "Torów między rozjazdami nie da się odczytać jako ciągu" msgid "The designed line between the switches is too short for the paths there" msgstr "Projektowana linia między rozjazdami jest za krótka na tamtejsze tory" -msgid "Whole line" -msgstr "Cała linia" - -msgid "Single curve" -msgstr "Pojedynczy łuk" - msgid "%zu paths, %.2f m" msgstr "%zu torów, %.2f m" msgid "Reset design" -msgstr "Resetuj projekt" +msgstr "Przywróć projekt" msgid "Reads the curves of the line from the scenery again, dropping the changes not applied" msgstr "Odczytuje łuki linii ze scenerii od nowa, porzucając niezastosowane zmiany" @@ -1038,23 +757,11 @@ msgstr "Odczytuje łuki linii ze scenerii od nowa, porzucając niezastosowane zm msgid "Design speed and limits" msgstr "Prędkość projektowa i normy" -msgid "First path: %s" -msgstr "Pierwszy tor: %s" - -msgid "Use selected##routefrom" -msgstr "Użyj zaznaczonego##routefrom" - -msgid "Last path: %s" -msgstr "Ostatni tor: %s" - -msgid "Use selected##routeto" -msgstr "Użyj zaznaczonego##routeto" - msgid "Rail axes distance (mm)" msgstr "Rozstaw osi szyn (mm)" msgid "Unbalanced acceleration (m/s2)" -msgstr "Przyspieszenie niezrównoważone (m/s2)" +msgstr "Przyspieszenie niezrównoważone (m/s²)" msgid "Maximum cant (mm)" msgstr "Największa przechyłka (mm)" @@ -1066,7 +773,7 @@ msgid "Cant ramp 1:(k*V), k" msgstr "Rampa przechyłkowa 1:(k*V), k" msgid "Unbalanced acc. change rate (m/s3)" -msgstr "Przyrost przysp. niezrównoważonego (m/s3)" +msgstr "Przyrost przysp. niezrównoważonego (m/s³)" msgid "Straight between curves (s of travel)" msgstr "Prosta między łukami (s jazdy)" @@ -1074,56 +781,11 @@ msgstr "Prosta między łukami (s jazdy)" msgid "Add vertex" msgstr "Dodaj wierzchołek" -msgid "Remove vertex" -msgstr "Usuń wierzchołek" - -msgid "LMB unfolds the values of the vertex, the same as a click on it in the view" -msgstr "LPM rozwija wartości wierzchołka, tak samo jak kliknięcie go w widoku" - -msgid "At the intersection of the end tangents" -msgstr "Na przecięciu stycznych końców" - -msgid "Distance from the start (m)" -msgstr "Odległość od początku (m)" - -msgid "Distance from the end (m)" -msgstr "Odległość od końca (m)" - -msgid "Position (x, z)" -msgstr "Położenie (x, z)" - -msgid "Radius R1 (m)" -msgstr "Promień R1 (m)" - -msgid "Radius R (m)" -msgstr "Promień R (m)" - -msgid "Compound curve (two radii)" -msgstr "Łuk koszowy (dwa promienie)" - -msgid "Radius R2 (m)" -msgstr "Promień R2 (m)" - -msgid "Transition R1-R2 (m)" -msgstr "Przejściowa R1-R2 (m)" - -msgid "Share of R1" -msgstr "Udział R1" - -msgid "Transition in (m)" -msgstr "Przejściowa na wejściu (m)" - -msgid "Transition out (m)" -msgstr "Przejściowa na wyjściu (m)" - -msgid "Cant (mm)" -msgstr "Przechyłka (mm)" - msgid "Turn the longer way (over 180 deg)" msgstr "Skręt dłuższą drogą (ponad 180°)" msgid "Apply recommended for V" -msgstr "Zalecane dla V" +msgstr "Ustaw zalecane" msgid "For %.0f km/h: R min %.0f m, cant %.0f mm (equilibrium %.0f, min %.0f), transition %.0f m" msgstr "Dla %.0f km/h: R min %.0f m, przechyłka %.0f mm (równowagowa %.0f, min %.0f), przejściowa %.0f m" @@ -1156,7 +818,7 @@ msgid "Correct the errors to apply the design" msgstr "Popraw błędy, by zastosować projekt" msgid "Straights recognized from chains of collinear straight paths.\nLMB: select a path to see the straight it belongs to" -msgstr "Proste rozpoznane z ciągów współliniowych prostych torów.\nLPM: zaznacz tor, by zobaczyć jego prostą" +msgstr "Prosta to kilka prostych torów w jednej linii.\nLPM na torze pokazuje jego prostą" msgid "Angle tolerance (deg)" msgstr "Tolerancja kąta (°)" @@ -1174,13 +836,13 @@ msgid "Recognize straights in the scenery" msgstr "Rozpoznaj proste w scenerii" msgid "%zu straights" -msgstr "%zu prostych" +msgstr "Znalezione proste: %zu" msgid "path name" msgstr "nazwa toru" msgid "Break" -msgstr "Złam" +msgstr "Załamanie" msgid "Turns the rest of the straight from a point, a curve goes at the break (Ctrl+drag in the view does the same)" msgstr "Obraca resztę prostej od punktu, w załomie wstawia łuk (to samo robi Ctrl+przeciągnięcie w widoku)" @@ -1195,19 +857,28 @@ msgid "Move with others" msgstr "Przesuwaj z innymi" msgid "Straights of a group move together and stay parallel; Alt+click in the view adds another one" -msgstr "Proste z grupy przesuwają się razem i zostają równoległe; Alt+klik w widoku dodaje kolejną" +msgstr "Proste z grupy przesuwają się razem i zostają równoległe; Alt+LPM w widoku dodaje kolejną" msgid "Curve R (m, 0: automatic)" msgstr "R łuku (m, 0: automatycznie)" -msgid "Transitions from the line speed" -msgstr "Przejściowe z prędkości linii" +msgid "What counts as one straight" +msgstr "Co liczy się jako jedna prosta" -msgid "Transition (m)" -msgstr "Przejściowa (m)" +msgid "Transition curves" +msgstr "Krzywe przejściowe" + +msgid "From the line speed" +msgstr "Z prędkości linii" + +msgid "Given length" +msgstr "Zadana długość" + +msgid "The same length of the transition curves at every curve" +msgstr "Ta sama długość krzywych przejściowych przy każdym łuku" msgid "Group: %zu straights move together" -msgstr "Grupa: %zu prostych przesuwa się razem" +msgstr "Grupa przesuwanych razem prostych: %zu" msgid "Clear" msgstr "Wyczyść" @@ -1240,13 +911,10 @@ msgid "Placing of the switch cancelled" msgstr "Wstawianie rozjazdu anulowane" msgid "The curve would run through a switch" -msgstr "Łuk przechodziłby przez rozjazd" +msgstr "Łuk przechodziłby przez rozjazd: wybierz punkt dalej od rozjazdu" msgid "The curve can't be fitted" -msgstr "Nie da się dopasować łuku" - -msgid "Done, select a curve to adjust it in the Curve mode" -msgstr "Gotowe, zaznacz łuk, by poprawić go w trybie Łuk" +msgstr "Łuk się nie mieści: zmniejsz R łuku albo wybierz inne miejsce" msgid "The track can't be deleted while vehicles stand on it" msgstr "Toru nie można usunąć, gdy stoją na nim pojazdy" @@ -1255,7 +923,7 @@ msgid "The switch is replaced by its main track, Shift+Del removes all of it" msgstr "Rozjazd zastąpiono jego torem zasadniczym, Shift+Del usuwa całość" msgid "Continue from the free end" -msgstr "Dalej od wolnego końca" +msgstr "Przedłużanie od wolnego końca" msgid "Length (m)##extend" msgstr "Długość (m)##extend" @@ -1266,23 +934,14 @@ msgstr "Długość kolejnego odcinka, dla łuków mierzona po łuku" msgid "Radius of the curves added" msgstr "Promień dodawanych łuków" -msgid "Straight##extend" -msgstr "Prosta##extend" - msgid "Adds a straight at the free end; the new piece gets selected, so the next click goes on from it" msgstr "Dokłada prostą na wolnym końcu; nowy tor zostaje zaznaczony, więc kolejne kliknięcie idzie dalej od niego" -msgid "Curve left##extend" -msgstr "Łuk w lewo##extend" +msgid "Curve left" +msgstr "Łuk w lewo" -msgid "Curve right##extend" -msgstr "Łuk w prawo##extend" - -msgid "Freehand" -msgstr "Z ręki" - -msgid "Dragging the red ring of a free end draws a curve through the cursor; off: it draws a straight along the end" -msgstr "Przeciągnięcie czerwonego pierścienia wolnego końca rysuje łuk przez kursor; wyłączone: rysuje prostą wzdłuż końca" +msgid "Curve right" +msgstr "Łuk w prawo" msgid "Two points lie on one another" msgstr "Dwa punkty leżą na sobie" @@ -1293,9 +952,6 @@ msgstr "Nie da się ułożyć toru przez te punkty" msgid "Click at least two points" msgstr "Kliknij co najmniej dwa punkty" -msgid "Laid %zu paths, %.1f m" -msgstr "Ułożone odcinki: %zu, razem %.1f m" - msgid "The curvature changes abruptly where the track joins, there's no room for a transition curve" msgstr "Na styku z torem krzywizna zmienia się skokowo, nie ma miejsca na krzywą przejściową" @@ -1308,14 +964,11 @@ msgstr "Więcej ustawień" msgid "Texture of the rails" msgstr "Tekstura szyn" -msgid "Texture of the ballast" -msgstr "Tekstura podsypki" - msgid "The scenery isn't opened for editing: new track won't be saved" msgstr "Sceneria nie jest otwarta do edycji: nowy tor nie zostanie zapisany" msgid "A double slip goes only into a straight crossing another one" -msgstr "Rozjazd krzyżowy wstawia się tylko w prostą przecinającą inną" +msgstr "Rozjazd krzyżowy i skrzyżowanie stawia się tylko na przecięciu dwóch prostych" msgid "The selected track is neither a recognized straight nor a curve" msgstr "Zaznaczony tor nie jest ani rozpoznaną prostą, ani łukiem" @@ -1336,10 +989,10 @@ msgid " inserted" msgstr " wstawiony" msgid "The switch couldn't be created from this template" -msgstr "Nie udało się utworzyć rozjazdu z tego wzorca" +msgstr "Nie udało się utworzyć rozjazdu z tego szablonu" msgid "Scenery: " -msgstr "Sceneria: " +msgstr "Ze scenerii: " msgid "No straight crossing the selected one near the click" msgstr "W pobliżu kliknięcia żadna prosta nie przecina zaznaczonej" @@ -1368,9 +1021,6 @@ msgstr "Nie rozpoznano geometrii rozjazdu krzyżowego" msgid "R limited to " msgstr "R ograniczony do " -msgid " m, a neighbouring path isn't straight" -msgstr " m, sąsiedni tor nie jest prosty" - msgid " m to fit the straights" msgstr " m, by zmieścić się na prostych" @@ -1386,18 +1036,6 @@ msgstr "płasko" msgid "mountains" msgstr "góry" -msgid "Texture 1 (rails/surface)" -msgstr "Tekstura 1 (szyny/nawierzchnia)" - -msgid "Texture 2 (trackbed/side)" -msgstr "Tekstura 2 (podsypka/bok)" - -msgid "Rails P1-P2" -msgstr "Szyny P1-P2" - -msgid "Rails P3-P4" -msgstr "Szyny P3-P4" - msgid "Cubic parabola" msgstr "Parabola 3. stopnia" @@ -1425,14 +1063,11 @@ msgstr "Przeciągnij" msgid "Release" msgstr "Puść" -msgid "straight along the end, to a free end: a curve joining it" -msgstr "prosta wzdłuż końca, do wolnego końca: łuk łączący" - msgid "Drag along" -msgstr "Ciągnij wzdłuż" +msgstr "Przeciągnij wzdłuż" msgid "Drag sideways" -msgstr "Ciągnij w bok" +msgstr "Przeciągnij w bok" msgid "LMB on a track" msgstr "LPM na torze" @@ -1440,11 +1075,8 @@ msgstr "LPM na torze" msgid "remove the switch, its main track stays" msgstr "usuwa rozjazd, tor zasadniczy zostaje" -msgid "loads the grade line of its line" -msgstr "wczytuje niweletę jego linii" - msgid "Drag a point" -msgstr "Ciągnij punkt" +msgstr "Przeciągnij punkt" msgid "Double click" msgstr "Podwójny klik" @@ -1461,27 +1093,12 @@ msgstr "LPM na wierszu" msgid "LMB on the straight" msgstr "LPM na prostej" -msgid "LMB on a straight track" -msgstr "LPM na prostym torze" - -msgid "selects the whole straight" -msgstr "zaznacza całą prostą" - -msgid "shifts the straight sideways" -msgstr "przesuwa prostą w bok" - msgid "Near another straight" msgstr "Przy innej prostej" -msgid "keep shifting: snaps parallel at %.2f m" -msgstr "ciągnij dalej: przyleganie równoległe na %.2f m" - msgid "snaps parallel at %.2f m" msgstr "przyleganie równoległe na %.2f m" -msgid "close: parallel snap at %.2f m" -msgstr "blisko: przyleganie równoległe na %.2f m" - msgid "parallel %.2f m" msgstr "równolegle %.2f m" @@ -1489,47 +1106,44 @@ msgid "parallel at %.2f m" msgstr "równolegle na %.2f m" msgid "snap at %.2f m (%.1f°)" -msgstr "przyleganie na %.2f m (%.1f°)" +msgstr "przyciąga na %.2f m (%.1f°)" msgid "snaps parallel at 4.00 / 4.50 / 4.75 m" -msgstr "łapie równolegle na 4.00 / 4.50 / 4.75 m" +msgstr "przyciąga równolegle na 4.00 / 4.50 / 4.75 m" msgid "Nearby straight: the diamond snaps parallel at 4.00 / 4.50 / 4.75 m" -msgstr "Sąsiednia prosta: romb łapie równolegle na 4.00 / 4.50 / 4.75 m" +msgstr "Obok jest inna prosta: romb przyciąga do rozstawu 4.00 / 4.50 / 4.75 m" msgid "close: %.2f m %.1f°" msgstr "blisko: %.2f m %.1f°" msgid "Drag: shift the straight sideways — close: parallel snap at %.2f m" -msgstr "Ciągnij: przesuń prostą w bok — blisko: przyleganie równoległe na %.2f m" +msgstr "Przeciągnij: przesuwa prostą w bok, blisko innej przylega równolegle na %.2f m" msgid "Drag: shift the straight sideways — close: %.2f m %.1f°" -msgstr "Ciągnij: przesuń prostą w bok — blisko: %.2f m %.1f°" +msgstr "Przeciągnij: przesuń prostą w bok — blisko: %.2f m %.1f°" msgid "Drag: shift the straight sideways — snaps parallel at %.2f m" -msgstr "Ciągnij: przesuń prostą w bok — przyleganie równoległe na %.2f m" +msgstr "Przeciągnij: przesuń prostą w bok — przyciąga równolegle na %.2f m" msgid "Ctrl+drag" -msgstr "Ctrl+ciągnij" +msgstr "Ctrl+przeciągnij" msgid "breaks the straight" msgstr "łamie prostą" msgid "Shift+click x4" -msgstr "Shift+klik x4" +msgstr "Shift+LPM x4" msgid "shifts a piece around an obstacle" msgstr "przesuwa kawałek, omijając przeszkodę" msgid "Alt+click" -msgstr "Alt+klik" +msgstr "Alt+LPM" msgid "adds another straight to move together" msgstr "dodaje kolejną prostą do wspólnego przesuwania" -msgid "loads its line: the curves become vertices W1, W2..." -msgstr "wczytuje jego linię: łuki stają się wierzchołkami W1, W2..." - msgid "changes the radius" msgstr "zmienia promień" @@ -1540,7 +1154,7 @@ msgid "selects it" msgstr "zaznacza go" msgid "lay a new track" -msgstr "ułóż nowy tor" +msgstr "układa nowy tor" msgid "moves the point" msgstr "przesuwa punkt" @@ -1548,21 +1162,6 @@ msgstr "przesuwa punkt" msgid "releases the point" msgstr "puszcza punkt" -msgid "LMB on a handle" -msgstr "LPM na uchwycie" - -msgid "picks a point: an end or a control vector" -msgstr "wybiera punkt: koniec lub wektor kontrolny" - -msgid "moves the whole track, W turns it" -msgstr "przesuwa cały tor, W obraca" - -msgid "Drag the red ring" -msgstr "Ciągnij czerwony pierścień" - -msgid "leads a new track out of a free end, or use Continue below" -msgstr "wyprowadza nowy tor z wolnego końca, albo użyj „Dalej” niżej" - msgid "deletes the track" msgstr "usuwa tor" @@ -1606,7 +1205,7 @@ msgid ", changes go there on save" msgstr ", zmiany trafią tam przy zapisie" msgid "Forget the design" -msgstr "Porzuć projekt" +msgstr "Usuń zapisany projekt" msgid "The design goes to the scenery file of the route on save" msgstr "Projekt trafi przy zapisie do pliku scenerii z tą trasą" @@ -1627,16 +1226,16 @@ msgid "Select a track first" msgstr "Najpierw zaznacz tor" msgid "%zu paths checked, %d with the speed above what the geometry allows, %d switch branch warnings" -msgstr "Sprawdzono %zu torów, %d z prędkością ponad dopuszczalną przez geometrię, %d ostrzeżeń dla torów zwrotnych" +msgstr "Sprawdzone tory: %zu, z prędkością ponad dopuszczalną przez geometrię: %d, ostrzeżenia dla torów zwrotnych: %d" msgid "Nothing changed" msgstr "Nic się nie zmieniło" msgid "Speed limits of %zu paths lowered to the safe ones, Ctrl+Z brings them back" -msgstr "Obniżono ograniczenia %zu torów do bezpiecznych, Ctrl+Z je przywraca" +msgstr "Ograniczenia obniżone do bezpiecznych na torach: %zu, Ctrl+Z cofa" msgid "Speed limits of the paths against the speed their geometry allows:\nradius and cant, cant ramps, curves without the transitions, switch branches" -msgstr "Ograniczenia prędkości torów wobec prędkości dopuszczalnej przez geometrię:\npromień i przechyłka, rampy przechyłkowe, łuki bez przejściowych, tory zwrotne" +msgstr "Sprawdza, czy prędkość na torach nie jest większa,\nniż pozwala ich geometria" msgid "Line through the selected path" msgstr "Linia przez zaznaczony tor" @@ -1654,10 +1253,10 @@ msgid "Check" msgstr "Sprawdź" msgid "Switch branches (m/s2)" -msgstr "Tory zwrotne (m/s2)" +msgstr "Tory zwrotne (m/s²)" msgid "Unbalanced acceleration allowed on the diverging tracks of the switches, which have no cant.\n0.65 m/s2 gives 40 km/h for R 190 m and 100 km/h for R 1200 m" -msgstr "Dopuszczalne przyspieszenie niezrównoważone na torach zwrotnych rozjazdów, które nie mają przechyłki.\n0,65 m/s2 daje 40 km/h dla R 190 m i 100 km/h dla R 1200 m" +msgstr "Dopuszczalne przyspieszenie niezrównoważone na torach zwrotnych rozjazdów, które nie mają przechyłki.\n0.65 m/s² daje 40 km/h dla R 190 m i 100 km/h dla R 1200 m" msgid "Reference length (m)" msgstr "Długość odniesienia (m)" @@ -1665,20 +1264,14 @@ msgstr "Długość odniesienia (m)" msgid "Length over which an abrupt change of the curvature or the cant is felt,\nabout the length of a car. Shorter: stricter for the curves without the transitions" msgstr "Długość, na której odczuwa się nagłą zmianę krzywizny lub przechyłki,\nmniej więcej długość wagonu. Krótsza: surowiej dla łuków bez przejściowych" -msgid "Change the limits, then Check again" -msgstr "Zmień normy i sprawdź ponownie" - msgid "The scenery changed since the check" msgstr "Sceneria zmieniła się od sprawdzenia" msgid "Paths without a speed limit of their own run at the line speed,\nthose are listed whenever the geometry limits the speed at all" -msgstr "Tory bez własnego ograniczenia jadą z prędkością linii,\nsą wypisywane, gdy tylko geometria w ogóle ogranicza prędkość" +msgstr "Na torach bez własnego ograniczenia obowiązuje prędkość linii;\nsą wypisywane, gdy tylko geometria w ogóle ogranicza prędkość" msgid "Within their limits too" -msgstr "Także mieszczące się w ograniczeniu" - -msgid "Set the safe speed on %zu paths" -msgstr "Ustaw bezpieczną prędkość na %zu torach" +msgstr "Także bez przekroczeń" msgid "Lowers the speed limit of each listed path to its safe speed, rounded down to 10 km/h (5 km/h below 40).\nThe limits of the switch branches are only warnings and aren't applied here, the button of a single path does.\nA switch has one velocity for both its tracks: negative limits only the diverging one, the main one keeping no limit.\nWhen the main track needs a limit of its own, the lower of the two applies to both" msgstr "Obniża ograniczenie każdego wypisanego toru do bezpiecznej prędkości, zaokrąglonej w dół do 10 km/h (5 km/h poniżej 40).\nOgraniczenia torów zwrotnych są tylko ostrzeżeniami i nie są tu stosowane, robi to przycisk pojedynczego toru.\nRozjazd ma jedną prędkość dla obu torów: ujemna ogranicza tylko zwrotny, zasadniczy zostaje bez ograniczenia.\nGdy tor zasadniczy potrzebuje własnego ograniczenia, niższe z dwóch obowiązuje oba" @@ -1702,13 +1295,13 @@ msgid "Select a path first" msgstr "Najpierw zaznacz tor" msgid "%d object(s) found along %d path(s)" -msgstr "Znaleziono obiektów: %d wzdłuż torów: %d" +msgstr "Znalezione obiekty: %d (wzdłuż torów: %d)" msgid "%d object(s) bound, they follow the changes of the track. The bindings are kept in the scenery files on save" msgstr "Powiązano obiektów: %d, podążają za zmianami toru. Powiązania są zapisywane w plikach scenerii" msgid "Bound objects follow the changes of the track" -msgstr "Powiązane obiekty podążają za zmianami toru" +msgstr "Przesuwaj powiązane obiekty z torem" msgid "Bound objects keep their chainage along the path, the offset from its axis and the height above the rail top;\nmodels and includes turn with the path and stay upright. The move is a part of the undo step of the change." msgstr "Powiązane obiekty zachowują kilometraż wzdłuż toru, odsunięcie od osi i wysokość nad główką szyny;\nmodele i include obracają się z torem i zostają pionowo. Przesunięcie jest częścią kroku cofania zmiany." @@ -1723,10 +1316,10 @@ msgid "(%d stored binding(s) not found, kept as they are)" msgstr "(nie znaleziono zapisanych powiązań: %d, zostają bez zmian)" msgid "Reach each way, m" -msgstr "Zasięg w każdą stronę, m" +msgstr "Zasięg w każdą stronę (m)" msgid "Corridor each side of the axis, m" -msgstr "Pas z każdej strony osi, m" +msgstr "Pas z każdej strony osi (m)" msgid "Rules for names of the files" msgstr "Reguły dla nazw plików" @@ -1735,10 +1328,10 @@ msgid "Parts of the names, separated by spaces; the first rule which matches dec msgstr "Fragmenty nazw rozdzielone spacjami; decyduje pierwsza pasująca reguła" msgid "Default rules" -msgstr "Reguły domyślne" +msgstr "Przywróć domyślne" msgid "Includes are matched by the template name and its //$e description, models by the file and node name;\ntraction is always the catenary, memory cells and event launchers the logic" -msgstr "Include są dopasowywane po nazwie szablonu i opisie //$e, modele po nazwie pliku i węzła;\ntraction to zawsze sieć, komórki pamięci i wyzwalacze zdarzeń to logika" +msgstr "Include są dopasowywane po nazwie szablonu i opisie //$e, modele po nazwie pliku i obiektu;\nobiekty traction to zawsze Sieć trakcyjna, komórki pamięci i wyzwalacze eventów to Logika" msgid "Find objects" msgstr "Znajdź obiekty" @@ -1765,13 +1358,13 @@ msgid "%s, %s\n%.2f m %s of the axis, %.2f m above the rail top\n%s" msgstr "%s, %s\n%.2f m %s od osi, %.2f m nad główką szyny\n%s" msgid "Bound objects along the scope" -msgstr "Powiązane obiekty w zasięgu" +msgstr "Powiązane obiekty w zakresie" msgid "%d of %d" msgstr "%d z %d" msgid "Unbind all of these" -msgstr "Odwiąż wszystkie te" +msgstr "Odwiąż wszystkie z listy" msgid "unbind" msgstr "odwiąż" @@ -1801,23 +1394,17 @@ msgid "GPL: unified structure gauge, widened only in the curves under 250 m.\nG1 msgstr "GPL: ujednolicona skrajnia budowli, poszerzana tylko w łukach poniżej 250 m.\nG1, G2, GA, GB, GC: graniczne (GSZ) i nominalne (NSZ) skrajnie instalacyjne, poszerzane o 3750/R we wszystkich łukach." msgid "Clearance over the roads, from 0.5 m beyond the edges of the carriageway" -msgstr "Skrajnia nad drogami, od 0,5 m poza krawędziami jezdni" +msgstr "Skrajnia nad drogami, od 0.5 m poza krawędziami jezdni" msgid "Stops the scan and keeps the models found so far" msgstr "Przerywa skanowanie i zachowuje znalezione dotąd modele" -msgid "Find the violations on the whole map" -msgstr "Znajdź naruszenia na całej mapie" - msgid "Checks the models along all tracks of the scenery against the chosen gauge\nand lists them, the deepest first.\nRuns in the background, the list fills up as it goes" msgstr "Sprawdza modele wzdłuż wszystkich torów scenerii względem wybranej skrajni\ni wypisuje je, najgłębsze najpierw.\nDziała w tle, lista zapełnia się na bieżąco" msgid "The scenery changed since the scan (%s)" msgstr "Sceneria zmieniła się od skanowania (%s)" -msgid "No model enters the gauge on the map" -msgstr "Żaden model nie wchodzi w skrajnię na mapie" - msgid "The gauge is clear along this line" msgstr "Skrajnia na tej linii jest wolna" @@ -1825,7 +1412,7 @@ msgid "%zu models enter the gauge%s" msgstr "Modeli wchodzących w skrajnię: %zu%s" msgid " on the map" -msgstr " na mapie" +msgstr " (cała sceneria)" msgid "Outline of %s (right half)" msgstr "Obrys %s (prawa połowa)" @@ -1842,7 +1429,6 @@ msgstr "Edycja" msgid "Analysis" msgstr "Analiza" - msgid "The adjoining straights can't be cut at the ends of the curve: the start misses by %.1f m, the end by %.1f m" msgstr "Nie da się przyciąć przyległych prostych na końcach łuku: początek chybia o %.1f m, koniec o %.1f m" @@ -1852,144 +1438,80 @@ msgstr "Tu nie ma łuku: LPM na łuku" msgid "LMB on a curve" msgstr "LPM na łuku" -msgid "loads it with the straights at its ends" -msgstr "wczytuje go razem z prostymi na końcach" - msgid "Curve %d of the S-curve" msgstr "Łuk %d esownicy" msgid "Details" msgstr "Szczegóły" - msgid "There's no ground under the route" msgstr "Pod trasą nie ma terenu" msgid "Grade line fitted to the ground: %zu points, embankment up to %.2f m, cutting up to %.2f m" msgstr "Niweleta dopasowana do terenu: %zu punktów, nasyp do %.2f m, wykop do %.2f m" -msgid "Fit to the ground" -msgstr "Dopasuj do terenu" - msgid "Places the breaks of the grade line along the ground under the route,\nwithin the limits and with the least earthworks; the ground itself stays as it is" msgstr "Rozmieszcza załamania niwelety wzdłuż terenu pod trasą,\nw granicach norm i z najmniejszymi robotami ziemnymi; sam teren się nie zmienia" -msgid "m off the ground##fittolerance" -msgstr "m od terenu##fittolerance" - msgid "How far the grade line may depart from the ground before it breaks;\nmore gives fewer breaks and higher embankments and deeper cuttings" msgstr "Jak daleko niweleta może odejść od terenu, zanim się załamie;\nwięcej to mniej załamań, ale wyższe nasypy i głębsze wykopy" -msgid "There's no editor terrain to shape" -msgstr "Nie ma terenu edytora do przekształcenia" - -msgid "The editor terrain doesn't reach the route, or it's shaped already" -msgstr "Teren edytora nie sięga trasy albo jest już przekształcony" - -msgid "Terrain shaped at %zu points: embankment up to %.2f m, cutting up to %.2f m" -msgstr "Teren przekształcony w %zu punktach: nasyp do %.2f m, wykop do %.2f m" - msgid "The terrain is back as it was before the shaping" -msgstr "Teren przywrócony do stanu sprzed przekształcenia" - -msgid "The shaped terrain isn't loaded any more" -msgstr "Przekształcony teren nie jest już wczytany" +msgstr "Teren przywrócony do stanu sprzed kształtowania" msgid "Shape the terrain to the grade line" -msgstr "Przekształć teren pod niweletę" - -msgid "Only the editor terrain changes: a formation under the track and slopes to the ground; the triangles and models of the scenery stay as they are." -msgstr "Zmienia się tylko teren edytora: korona podtorza pod torem i skarpy do terenu; trójkąty i modele scenerii zostają bez zmian." - -msgid "Formation below the rail top (m)" -msgstr "Korona podtorza pod główką szyny (m)" +msgstr "Kształtowanie terenu pod niweletę" msgid "Half width of the formation (m)" -msgstr "Połowa szerokości korony (m)" +msgstr "Połowa szerokości torowiska (m)" msgid "From the axis of the route to the edge of the formation;\nfor two tracks add the distance between them" -msgstr "Od osi trasy do krawędzi korony;\ndla dwóch torów dodaj rozstaw między nimi" +msgstr "Od osi trasy do krawędzi torowiska;\ndla dwóch torów dodaj rozstaw torów" msgid "Slopes 1 : n, n" -msgstr "Skarpy 1 : n, n" +msgstr "Pochylenie skarp 1:n, n" msgid "Slopes reach at most (m)" -msgstr "Skarpy sięgają najwyżej (m)" +msgstr "Największy zasięg skarp (m)" msgid "Correct the errors of the grade line first" msgstr "Najpierw popraw błędy niwelety" msgid "Shape the terrain" -msgstr "Przekształć teren" +msgstr "Ukształtuj teren" msgid "Restore the terrain" msgstr "Przywróć teren" -msgid "Puts back the terrain as it was before the last shaping" -msgstr "Przywraca teren sprzed ostatniego przekształcenia" - -msgid "There's no editor terrain to shape: convert the scenery ground first" -msgstr "Nie ma terenu edytora do przekształcenia: najpierw zamień grunt scenerii" - -msgid "The chosen materials have no triangles" -msgstr "Wybrane materiały nie mają trójkątów" - msgid "Ground converted: %zu chunks of editor terrain made, %zu shapes taken out of the scene, %zu triangle nodes go from the scenery files on save" -msgstr "Grunt zamieniony: %zu chunków terenu edytora, %zu brył zdjętych ze sceny, %zu węzłów trójkątów zniknie z plików scenerii przy zapisie" +msgstr "Grunt zamieniony. Kafle terenu edytora: %zu, bryły zdjęte ze sceny: %zu, obiekty z trójkątów usuwane z plików scenerii przy zapisie: %zu" msgid ", %zu chunks kept as there was editor terrain already" -msgstr ", %zu chunków pominiętych, bo był tam już teren edytora" +msgstr ", pominięte kafle, bo był tam już teren edytora: %zu" msgid ", %zu nodes stay as their files can't be rewritten" -msgstr ", %zu węzłów zostaje, bo ich plików nie da się zapisać" - -msgid "Convert the scenery ground to editor terrain..." -msgstr "Zamień grunt scenerii na teren edytora..." +msgstr ", obiekty, które zostają, bo ich plików nie da się zapisać: %zu" msgid "Replaces the triangles of the chosen materials with editor terrain of the same shape,\nwhich can be shaped and sculpted; the scenery files lose these triangles on save" msgstr "Zastępuje trójkąty wybranych materiałów terenem edytora o tym samym kształcie,\nktóry można przekształcać i rzeźbić; pliki scenerii tracą te trójkąty przy zapisie" -msgid "Materials of the scenery triangles: tick the ones which are the ground" -msgstr "Materiały trójkątów scenerii: zaznacz te, które są gruntem" - -msgid "The scenery has no triangles" -msgstr "Sceneria nie ma trójkątów" - msgid "%s (%zu triangles)" msgstr "%s (%zu trójkątów)" -msgid "The terrain gets a single texture: that of the material which covers the most. Until saved, loading the scenery again takes it all back." -msgstr "Teren dostaje jedną teksturę: materiału, który pokrywa najwięcej. Do zapisu ponowne wczytanie scenerii cofa wszystko." - -msgid "Tick at least one material" -msgstr "Zaznacz co najmniej jeden materiał" - msgid "Convert" msgstr "Zamień" -msgid "Sculpt mode (LMB smooth)" -msgstr "Rzeźbienie (LPM wygładza)" - -msgid "Sculpt mode (LMB raise / Shift = lower)" -msgstr "Rzeźbienie (LPM podnosi / Shift obniża)" - -msgid "Raise / lower" -msgstr "Podnoś / obniżaj" - msgid "Smooth" -msgstr "Wygładzaj" - -msgid "Evens out the ground under the brush: bumps and sharp edges go, the shape stays.\nThe strength sets how fast" -msgstr "Wyrównuje teren pod pędzlem: znikają nierówności i ostre krawędzie, kształt zostaje.\nSiła ustala, jak szybko" +msgstr "Wygładzanie" msgid "Rounding of the edges (m)" msgstr "Zaokrąglenie krawędzi (m)" msgid "Length over which the slope bends into the formation and into the ground; 0: sharp edges" -msgstr "Długość, na której skarpa przechodzi w koronę i w teren; 0: ostre krawędzie" +msgstr "Długość, na której skarpa przechodzi w torowisko i w teren; 0: ostre krawędzie" msgid "Sculpting..." -msgstr "Rzeźbienie..." +msgstr "Rzeźb teren..." msgid "Opens the terrain tools: raising, lowering and smoothing with a brush" msgstr "Otwiera narzędzia terenu: podnoszenie, obniżanie i wygładzanie pędzlem" @@ -2002,63 +1524,45 @@ msgstr " (zwrotny)" msgid " (main)" msgstr " (zasadniczy)" -msgid "%s set on %zu paths, Ctrl+Z takes it back" -msgstr "%s ustawiono na %zu torach, Ctrl+Z cofa" - msgid "%zu fouling point markers moved to their places, Ctrl+Z takes it back" -msgstr "Przestawiono %zu wskaźników ukresu na ich miejsca, Ctrl+Z cofa" +msgstr "Przestawione wskaźniki W17: %zu, Ctrl+Z cofa" msgid "%zu fouling point markers placed, Ctrl+Z takes them back" -msgstr "Postawiono %zu wskaźników ukresu, Ctrl+Z je usuwa" +msgstr "Postawione wskaźniki W17: %zu, Ctrl+Z je usuwa" msgid "%zu found, the first %zu listed" msgstr "Znaleziono %zu, pokazano pierwsze %zu" msgid "%zu hectometre posts moved to their places, Ctrl+Z takes it back" -msgstr "Przestawiono %zu słupków hektometrowych na ich miejsca, Ctrl+Z cofa" +msgstr "Przestawione słupki hektometrowe: %zu, Ctrl+Z cofa" msgid "%zu hectometre posts placed, Ctrl+Z takes them back" -msgstr "Postawiono %zu słupków hektometrowych, Ctrl+Z je usuwa" +msgstr "Postawione słupki hektometrowe: %zu, Ctrl+Z je usuwa" msgid "%zu joints fixed, Ctrl+Z takes them back" -msgstr "Naprawiono %zu styków, Ctrl+Z cofa" +msgstr "Naprawione styki: %zu, Ctrl+Z cofa" msgid "%zu paths checked: %d gaps, %d steps, %d kinks, %d cant jumps, %d grade breaks" -msgstr "Sprawdzono %zu torów: %d szczelin, %d uskoków, %d załamań, %d skoków przechyłki, %d załamań pochylenia" - -msgid "%zu paths deleted, Ctrl+Z brings them back" -msgstr "Usunięto %zu torów, Ctrl+Z je przywraca" - -msgid "%zu paths selected, %.1f m" -msgstr "Zaznaczono %zu torów, %.1f m" +msgstr "Sprawdzone tory: %zu. Szczeliny: %d, uskoki: %d, załamania: %d, skoki przechyłki: %d, załamania pochylenia: %d" msgid "%zu switches, %zu fouling points: %d without a marker, %d markers standing wrong" -msgstr "%zu rozjazdów, %zu ukresów: %d bez wskaźnika, %d wskaźników stoi źle" - -msgid "(of the direction of the line, see the preview)" -msgstr "(względem kierunku linii, patrz podgląd)" +msgstr "Rozjazdów: %zu, ukresów: %zu. Bez wskaźnika: %d, wskaźników stojących źle: %d" msgid ", %d free ends" -msgstr ", %d wolnych końców" +msgstr ", wolne końce: %d" msgid ", %zu in the files which can't be changed are left as they are" -msgstr ", %zu w plikach, których nie można zmieniać, zostaje bez zmian" +msgstr ", pominięte (pliki, których nie można zmieniać): %zu" msgid ", %zu with vehicles on them or in the files which can't be changed are left" -msgstr ", %zu z pojazdami albo w plikach, których nie można zmieniać, zostaje" +msgstr ", nieusunięte: %zu (stoją na nich pojazdy albo są w plikach, których nie można zmieniać)" msgid "3.5 m on the straight track; more on the curves, where the cars lean out" -msgstr "3,5 m na prostej; na łukach więcej, bo wagony się wychylają" +msgstr "3.5 m na prostej; na łukach więcej, bo wagony się wychylają" msgid "A free end closer than this to the end of another path is a gap: the trains don't go across it.\nThe simulation joins the ends no more than 2 cm apart" msgstr "Wolny koniec bliżej niż tyle od końca innego toru to szczelina: pociągi przez nią nie przejadą.\nSymulacja łączy końce oddalone najwyżej o 2 cm" -msgid "A marker off by more (m) is wrong" -msgstr "Wskaźnik dalej niż tyle (m) stoi źle" - -msgid "A post standing off by more (m) is wrong" -msgstr "Słupek przesunięty o więcej (m) stoi źle" - msgid "A switch has no line of its own: select a plain path" msgstr "Rozjazd nie ma własnej linii: zaznacz zwykły tor" @@ -2069,7 +1573,7 @@ msgid "Along the line each way (m)" msgstr "Wzdłuż linii w każdą stronę (m)" msgid "Along the line of the selected path" -msgstr "Wzdłuż linii zaznaczonego toru" +msgstr "Użyj linii zaznaczonego toru" msgid "Around the camera" msgstr "Wokół kamery" @@ -2077,9 +1581,6 @@ msgstr "Wokół kamery" msgid "Around the camera (m)" msgstr "Wokół kamery (m)" -msgid "Build %zu paths, %.1f m" -msgstr "Zbuduj %zu torów, %.1f m" - msgid "Cant jump (mm)" msgstr "Skok przechyłki (mm)" @@ -2092,17 +1593,14 @@ msgstr "Kopiuj nazwę" msgid "Delete" msgstr "Usuń" -msgid "Delete %zu paths" -msgstr "Usuń %zu torów" - msgid "Ends of the paths which almost meet, steps, kinks, jumps of the cant and of the grade at the joints" msgstr "Końce torów, które prawie się stykają, uskoki, załamania, skoki przechyłki i pochylenia na stykach" msgid "Ends of the paths which almost meet, steps, kinks,\njumps of the cant and of the grade at the joints" -msgstr "Końce torów, które prawie się stykają, uskoki, załamania,\nskoki przechyłki i pochylenia na stykach" +msgstr "Szuka błędów na stykach torów: szczelin, uskoków,\nzałamań, skoków przechyłki i pochylenia" msgid "Environment" -msgstr "Otoczenie" +msgstr "Okolica toru" msgid "Find a track by its name (Ctrl+F)" msgstr "Znajdź tor po nazwie (Ctrl+F)" @@ -2117,7 +1615,7 @@ msgid "Fix this one" msgstr "Napraw ten" msgid "Fouling point markers" -msgstr "Wskaźniki ukresu (W17)" +msgstr "Postaw wskaźniki ukresu (W17)..." msgid "Free ends too" msgstr "Także wolne końce" @@ -2138,7 +1636,7 @@ msgid "Hectometre posts" msgstr "Słupki hektometrowe" msgid "Hectometre posts from here" -msgstr "Słupki hektometrowe od tego miejsca" +msgstr "Postaw stąd słupki hektometrowe..." msgid "Joints" msgstr "Styki" @@ -2153,14 +1651,11 @@ msgid "Kilometre at the point (km)" msgstr "Kilometr w tym miejscu (km)" msgid "Kink (deg)" -msgstr "Załamanie (st.)" +msgstr "Załamanie (°)" msgid "Left" msgstr "Lewa" -msgid "Line %.0f m, km %s - %s: %d posts to place, %zu standing, %d of them wrong" -msgstr "Linia %.0f m, km %s - %s: %d słupków do postawienia, %zu stoi, %d z nich źle" - msgid "Look at it" msgstr "Pokaż" @@ -2168,10 +1663,10 @@ msgid "Marker" msgstr "Wskaźnik" msgid "Move %d wrong markers to their places" -msgstr "Przestaw %d źle stojących wskaźników" +msgstr "Przestaw źle stojące wskaźniki (%d)" msgid "Move %d wrong posts to their places" -msgstr "Przestaw %d źle stojących słupków" +msgstr "Przestaw źle stojące (%d)" msgid "No line through this path" msgstr "Przez ten tor nie biegnie żadna linia" @@ -2188,36 +1683,9 @@ msgstr "Tor równoległy" msgid "Parallel" msgstr "Równoległy" -msgid "First select a straight path (LMB). Then a second track is laid beside it.\nAlong the line the curves are included: R minus or plus the spacing, same transitions and cant." -msgstr "Najpierw zaznacz tor prosty (LPM). Potem drugi tor kładzie się obok.\nWzdłuż linii łuki wchodzą: R minus albo plus rozstaw, te same krzywe przejściowe i przechyłka." - -msgid "First select a straight path in the view." -msgstr "Najpierw zaznacz tor prosty w widoku." - -msgid "The selected path isn't a straight. Select a straight first." -msgstr "Zaznaczony tor nie jest prostą. Najpierw zaznacz prostą." - -msgid "First select a straight. Creates a new track beside it; the selected path stays." -msgstr "Najpierw zaznacz prostą. Tworzy nowy tor obok; zaznaczony tor zostaje." - -msgid "Two paths selected: set the spacing of the other in Select. The first must be a straight." -msgstr "Dwa tory zaznaczone: rozstaw drugiego ustawiasz w Wyborze. Pierwszy musi być prostą." - -msgid "First selected must be a straight. It stays; the other is moved to this spacing." -msgstr "Pierwszy zaznaczony musi być prostą. On zostaje; drugi jest przestawiany na ten rozstaw." - -msgid "Lay %zu paths, %.1f m" -msgstr "Ułóż %zu torów, %.1f m" - -msgid "first select a straight, then a second track beside it in the panel" -msgstr "najpierw zaznacz prostą, potem drugi tor obok niej w panelu" - msgid "Make parallel" msgstr "Ustaw równolegle" -msgid "%s stays. %s is moved to this spacing, taking its curves." -msgstr "%s zostaje. %s jest przestawiany na ten rozstaw, z jego łukami." - msgid "Now %.2f m on the right" msgstr "Teraz %.2f m po prawej" @@ -2231,16 +1699,16 @@ msgid "%s set parallel to %s, %.2f m away; Ctrl+Z takes it back" msgstr "%s ustawiony równolegle do %s, rozstaw %.2f m; Ctrl+Z cofa" msgid "Two selected paths: set the scope to this path, so the other takes its shape at the spacing" -msgstr "Dwa zaznaczone tory: zakres na ten tor, wtedy drugi bierze jego kształt w rozstawie" +msgstr "Dwa zaznaczone tory: ustaw zakres „Ten tor”, wtedy drugi przejmie kształt pierwszego w rozstawie" msgid "Parallel track of %zu paths, %.1f m, %.2f m away; Ctrl+Z takes it back" -msgstr "Tor równoległy z %zu odcinków, %.1f m, w rozstawie %.2f m; Ctrl+Z cofa" +msgstr "Ułożony tor równoległy (torów: %zu, %.1f m) w rozstawie %.2f m; Ctrl+Z cofa" msgid "Place %d markers" -msgstr "Postaw %d wskaźników" +msgstr "Postaw wskaźniki W17 (%d)" msgid "Place %d posts" -msgstr "Postaw %d słupków" +msgstr "Postaw słupki (%d)" msgid "Posts" msgstr "Słupki" @@ -2257,11 +1725,8 @@ msgstr "Odczytaj ze słupków" msgid "Right" msgstr "Prawa" -msgid "Second track alongside, curves included: radius R minus or plus the spacing,\nthe same transitions and cant, heights on the plane of the cant" -msgstr "Drugi tor obok, także na łukach: promień R minus albo plus rozstaw,\nte same krzywe przejściowe i przechyłka, wysokości na płaszczyźnie przechyłki" - msgid "Select a path of the line, or RMB on it: Hectometre posts" -msgstr "Zaznacz tor linii albo PPM na nim: Słupki hektometrowe" +msgstr "Zaznacz tor linii albo kliknij go PPM i wybierz „Słupki hektometrowe od tego miejsca”" msgid "Select a switch, or check around the camera" msgstr "Zaznacz rozjazd albo sprawdź wokół kamery" @@ -2272,11 +1737,8 @@ msgstr "Shift+LPM" msgid "Side, looking where it grows" msgstr "Strona, patrząc w kierunku wzrostu" -msgid "Spacing (m)" -msgstr "Rozstaw (m)" - msgid "Spacing of the axes (m)" -msgstr "Odległość osi torów (m)" +msgstr "Rozstaw torów (m)" msgid "Speed check of the line" msgstr "Sprawdź prędkość linii" @@ -2294,22 +1756,19 @@ msgid "Templates can be placed only in a scenery opened for editing (-edit)" msgstr "Szablony można stawiać tylko w scenerii otwartej do edycji (-edit)" msgid "The active layer can't take them" -msgstr "Aktywna warstwa ich nie przyjmie" +msgstr "Aktywna warstwa nie przyjmie obiektów" msgid "The active layer can't take them: " -msgstr "Aktywna warstwa ich nie przyjmie: " +msgstr "Aktywna warstwa nie przyjmie obiektów: " msgid "The file of the active layer would receive them with a rotation or a scale in effect, pick another layer" -msgstr "Plik aktywnej warstwy dostałby je z obrotem albo skalą, wybierz inną warstwę" - -msgid "The gizmo moves them together, W turns them; Shift+LMB adds or takes out a path" -msgstr "Gizmo przesuwa je razem, W obraca; Shift+LPM dodaje albo usuwa tor" +msgstr "Plik aktywnej warstwy dostałby obiekty z obrotem albo skalą, wybierz inną warstwę" msgid "The kilometrage grows" msgstr "Kilometraż rośnie" msgid "The line, up to the switches" -msgstr "Linia, do rozjazdów" +msgstr "Cała linia" msgid "The numbers of the %zu posts along the line don't go by 100 m, the kilometrage wasn't read from them" msgstr "Numery %zu słupków przy linii nie idą co 100 m, kilometrażu z nich nie odczytano" @@ -2318,7 +1777,7 @@ msgid "The scenery isn't opened for editing, the new track won't be saved" msgstr "Sceneria nie jest otwarta do edycji, nowy tor nie zostanie zapisany" msgid "The selected switch" -msgstr "Zaznaczony rozjazd" +msgstr "Zaznaczony" msgid "The spacing is larger than the radius of %s" msgstr "Rozstaw jest większy niż promień toru %s" @@ -2330,13 +1789,13 @@ msgid "Trackbed" msgstr "Podsypka" msgid "Turned by (deg)" -msgstr "Obrót (st.)" +msgstr "Obrót (°)" msgid "Velocity" msgstr "Prędkość" msgid "Vertical profile of the line" -msgstr "Profil podłużny linii" +msgstr "Pokaż profil podłużny linii" msgid "W17 %.1f m off" msgstr "W17 o %.1f m za daleko" @@ -2344,11 +1803,8 @@ msgstr "W17 o %.1f m za daleko" msgid "W17 where the axes of the tracks meeting at a switch are the spacing apart,\nhalf way between them" msgstr "W17 tam, gdzie osie torów zbiegających się na rozjeździe są o rozstaw od siebie,\nw połowie międzytorza" -msgid "Whole straight" -msgstr "Cała prosta" - msgid "adds a track to the selection, drag: a box" -msgstr "dodaje tor do zaznaczenia, przeciągnięcie: prostokąt" +msgstr "dodaje tor do zaznaczenia, przeciągnięcie: ramka" msgid "against it" msgstr "przeciwnie" @@ -2377,12 +1833,6 @@ msgstr "łączy końce, wyrównuje je" msgid "kink" msgstr "załamanie" -msgid "km %s stands %+.1f m off##%u" -msgstr "km %s stoi %+.1f m od miejsca##%u" - -msgid "km %s, %+.1f m off" -msgstr "km %s, %+.1f m od miejsca" - msgid "modes: lay, switch, straight, curve, profile, speed, joints, infra" msgstr "tryby: układanie, rozjazd, prosta, łuk, profil, prędkość, styki, infrastruktura" @@ -2395,38 +1845,23 @@ msgstr "uskok" msgid "texture of the rails" msgstr "tekstura szyn" -msgid "texture of the trackbed" -msgstr "tekstura podsypki" - msgid "there's no active layer" msgstr "nie ma aktywnej warstwy" -msgid "track circuits, separated by spaces" -msgstr "obwody torowe, oddzielone spacjami" - msgid "type the length" msgstr "wpisz długość" msgid "type the length of the next leg, Enter adds the point" msgstr "wpisz długość następnego toru, Enter dodaje punkt" -msgid "type the radius, a space, the length" -msgstr "wpisz promień, spację, długość" - -msgid "type the shift, + to the left" -msgstr "wpisz przesunięcie, + w lewo" - msgid "type the turn in degrees, + to the left" msgstr "wpisz obrót w stopniach, + w lewo" msgid "what can be done with it" msgstr "co można z nim zrobić" -msgid "Vehicle on the track" -msgstr "Pojazd na torze" - msgid "A trainset on the track, placed where the track was clicked, or in the middle of the path" -msgstr "Skład na torze, stawiany tam, gdzie kliknięto tor, albo w jego połowie" +msgstr "Skład staje tam, gdzie klikniesz tor, albo w połowie toru" msgid "search: name, series, skin" msgstr "szukaj: nazwa, seria, malowanie" @@ -2435,13 +1870,10 @@ msgid "Only the ones with a cab" msgstr "Tylko z kabiną" msgid "With a driver, to drive" -msgstr "Z maszynistą, do jazdy" +msgstr "Z maszynistą" msgid "Nobody, parked" -msgstr "Bez obsady, odstawiony" - -msgid "Pick a vehicle from the list" -msgstr "Wybierz pojazd z listy" +msgstr "Bez obsady" msgid "Place" msgstr "Postaw" @@ -2452,9 +1884,6 @@ msgstr "Postaw i jedź" msgid "The vehicle couldn't be placed, the log says why" msgstr "Nie udało się postawić pojazdu, przyczyna jest w logu" -msgid "%s stands on %s. It isn't saved with the scenery" -msgstr "%s stoi na %s. Nie zapisuje się razem ze scenerią" - msgid "Place a vehicle here" msgstr "Postaw tu pojazd" @@ -2476,18 +1905,9 @@ msgstr "w pliku scenerii: Y (wschód) %.0f m, X (północ) %.0f m" msgid "Use it" msgstr "Użyj" -msgid "Model along the track" -msgstr "Model wzdłuż toru" - msgid "A model bent to follow the track, or repeated along it: platforms, walls, fences, barriers, lamps" msgstr "Model wygięty wzdłuż toru albo powtarzany przy nim: perony, mury, płoty, bariery, latarnie" -msgid "By this track:" -msgstr "Przy tym torze:" - -msgid "New one" -msgstr "Nowy" - msgid "model, e.g. peronowe/1k96w200m" msgstr "model, np. peronowe/1k96w200m" @@ -2495,7 +1915,7 @@ msgid "search the models" msgstr "szukaj modeli" msgid "Skin" -msgstr "Skóra" +msgstr "Malowanie" msgid "The model is %.2f m long along the track" msgstr "Model ma %.2f m długości wzdłuż toru" @@ -2512,20 +1932,11 @@ msgstr "Powtarzany" msgid "Copy length (m, 0: the model's)" msgstr "Długość kopii (m, 0: jak model)" -msgid "Step (m, 0: the model's length)" -msgstr "Krok (m, 0: długość modelu)" - -msgid "Sideways (m, + left)" -msgstr "W bok (m, + w lewo)" - -msgid "Up (m)" -msgstr "W górę (m)" - msgid "Axis of the model along the track" msgstr "Oś modelu wzdłuż toru" msgid "Turned around" -msgstr "Odwrócony" +msgstr "Obrócony o 180°" msgid "Mirrored" msgstr "Lustrzany" @@ -2539,9 +1950,6 @@ msgstr "Wzdłuż" msgid "this path" msgstr "tego toru" -msgid "the line to the switches" -msgstr "linii do rozjazdów" - msgid "through the switches" msgstr "przez rozjazdy" @@ -2551,26 +1959,14 @@ msgstr "Od (m)" msgid "To (m, -1: the end)" msgstr "Do (m, -1: koniec)" -msgid "LMB where it starts, the distance from the track sets the offset; drag to where it ends" -msgstr "LPM tam, gdzie się zaczyna (odległość od toru ustawia przesunięcie), przeciągnij do końca" - -msgid "Mark it in the view" -msgstr "Zaznacz w widoku" - -msgid "line %.0f m" -msgstr "linia %.0f m" - msgid "Lay it" msgstr "Ułóż" -msgid "Lay it again along the selected line" -msgstr "Ułóż ponownie wzdłuż zaznaczonej linii" - msgid "%s laid along %.1f m, Ctrl+Z takes it back" msgstr "%s ułożony na %.1f m, Ctrl+Z cofa" msgid "The scenery isn't opened for editing, it won't be saved" -msgstr "Sceneria nie jest otwarta do edycji, to nie zostanie zapisane" +msgstr "Sceneria nie jest otwarta do edycji, model nie zostanie zapisany" msgid "type the shift, + to the right" msgstr "wpisz przesunięcie, + w prawo" @@ -2582,10 +1978,10 @@ msgid "Sideways (m, + right)" msgstr "W bok (m, + w prawo)" msgid "model origin" -msgstr "początek modelu" +msgstr "punkt zerowy modelu" msgid "side facing the track" -msgstr "strona od toru" +msgstr "strona przy torze" msgid "far side" msgstr "dalsza strona" @@ -2602,11 +1998,8 @@ msgstr "spód" msgid "top" msgstr "wierzch" -msgid "Platform edge:" -msgstr "Krawędź peronu:" - msgid "The edge of the platform %.3f m from the axis of the track, its top this high over the rail top" -msgstr "Krawędź peronu %.3f m od osi toru, wierzch tyle nad główką szyny" +msgstr "Krawędź peronu %.3f m od osi toru, wierzch na tej wysokości nad główką szyny" msgid "Parameters of the template" msgstr "Parametry szablonu" @@ -2615,7 +2008,7 @@ msgid "The location and the rotation are zeros, the template is laid out along t msgstr "Położenie i obrót są zerami, szablon układa się wzdłuż toru" msgid "top of the edge by the track" -msgstr "wierzch krawędzi od toru" +msgstr "wierzch krawędzi przy torze" msgid "Edge to the track" msgstr "Krawędź do toru" @@ -2636,19 +2029,31 @@ msgid "the whole stretch" msgstr "cały zakres" msgid "%.1f m: %d copies of %.2f m (the original %.2f m)" -msgstr "%.1f m: %d kopii po %.2f m (oryginał %.2f m)" +msgstr "%.1f m, kopii: %d po %.2f m (oryginał %.2f m)" msgid "%.1f m: %d copies every %.2f m" -msgstr "%.1f m: %d kopii co %.2f m" +msgstr "%.1f m, kopii: %d co %.2f m" -msgid "The line ends before that, it's cut there" -msgstr "Linia kończy się wcześniej, tam zostanie ucięte" +msgid "Gauge widening in the curves" +msgstr "Poszerzenie skrajni na łukach" -msgid "Set back by the widening of the structure gauge in the curves" -msgstr "Odsunięty o poszerzenie skrajni na łukach" +msgid "Reference point" +msgstr "Punkt odniesienia" + +msgid "The edge of the platform from the axis of the track" +msgstr "Krawędź peronu od osi toru" + +msgid "Platform height:" +msgstr "Wysokość peronu:" + +msgid "One model stretched and bent along the curve of the track" +msgstr "Jeden model rozciągnięty i wygięty wzdłuż łuku toru" + +msgid "Copies of the model one after another, each one straight" +msgstr "Kopie modelu jedna za drugą, każda prosta" msgid "The offset grows by the widening of the gauge below the platforms: 3.75/R in the curves of R >= 250 m,\nmore in the sharper ones, and on the inner side of a canted curve by the cant times the height of the edge / 1.5;\nit begins 20 m (inner side) and 26 m (outer side) ahead of the curve, as in the gauge check" -msgstr "Przesunięcie rośnie o poszerzenie skrajni poniżej peronów: 3,75/R na łukach R >= 250 m,\nwięcej na ciaśniejszych, a po wewnętrznej stronie łuku z przechyłką o przechyłkę razy wysokość krawędzi / 1,5;\nzaczyna się 20 m (strona wewnętrzna) i 26 m (zewnętrzna) przed łukiem, jak w sprawdzaniu skrajni" +msgstr "Przesunięcie rośnie o poszerzenie skrajni poniżej peronów: 3.75/R na łukach R >= 250 m,\nwięcej na ciaśniejszych, a po wewnętrznej stronie łuku z przechyłką o przechyłkę razy wysokość krawędzi / 1.5;\nzaczyna się 20 m (strona wewnętrzna) i 26 m (zewnętrzna) przed łukiem, jak w sprawdzaniu skrajni" msgid "On the ground" msgstr "Na gruncie" @@ -2657,13 +2062,10 @@ msgid "Over the rail heads" msgstr "Nad główką szyny" msgid "by (m)" -msgstr "o (m)" - -msgid "A marker higher or lower by more (m) is wrong" -msgstr "Wskaźnik wyżej lub niżej o więcej (m) stoi źle" +msgstr "Wysokość nad szyną (m)" msgid "W17 %.1f m off, %+.2f m high" -msgstr "W17 %.1f m od miejsca, %+.2f m wysokości" +msgstr "W17 %.1f m od miejsca, %+.2f m w pionie" msgid "alternately" msgstr "naprzemiennie" @@ -2677,42 +2079,18 @@ msgstr "Obiekty" msgid "Along the track: hectometre posts, fouling point markers, trainsets" msgstr "Przy torze: słupki hektometrowe, wskaźniki ukresu, składy" -msgid "Model along the track: release where it ends" -msgstr "Model wzdłuż toru: puść tam, gdzie się kończy" - -msgid "Model along the track: LMB by the track where it starts, drag to the end" -msgstr "Model wzdłuż toru: LPM przy torze tam, gdzie się zaczyna, przeciągnij do końca" - msgid "Objects along the track" msgstr "Obiekty przy torze" -msgid "LMB by the track" -msgstr "LPM przy torze" - -msgid "where it starts, the side of the track it goes on" -msgstr "tam, gdzie się zaczyna, po tej stronie toru" - -msgid "to where it ends, a click: one as long as the original" -msgstr "do końca; samo kliknięcie: jeden jak oryginał" - msgid "lays it" msgstr "układa" -msgid "LMB away from the track" -msgstr "LPM z dala od toru" - -msgid "selects another track" -msgstr "zaznacza inny tor" - msgid "the line the objects go along" msgstr "linia, wzdłuż której idą obiekty" msgid "modes: lay, switch, straight, curve, objects, profile, speed, joints, infra" msgstr "tryby: układanie, rozjazd, prosta, łuk, obiekty, profil, prędkość, styki, infrastruktura" -msgid "Platforms and models" -msgstr "Perony i modele" - msgid "Fouling points" msgstr "Ukresy W17" @@ -2723,13 +2101,13 @@ msgid "LMB on a track: the line the objects go along" msgstr "LPM na torze: linia, wzdłuż której idą obiekty" msgid "%zu duplicate hectometre posts removed, Ctrl+Z brings them back" -msgstr "Usunięto %zu zdublowanych słupków hektometrowych, Ctrl+Z je przywraca" +msgstr "Usunięte zdublowane słupki hektometrowe: %zu, Ctrl+Z je przywraca" msgid "Remove %d duplicate posts" -msgstr "Usuń %d zdublowanych słupków" +msgstr "Usuń zdublowane (%d)" msgid "Posts with a number another post nearer to its place already has" -msgstr "Słupki z numerem, który ma już inny słupek stojący bliżej swojego miejsca" +msgstr "Usuwa słupki z numerem, który ma już inny słupek stojący bliżej właściwego miejsca" msgid "km %s duplicate##%u" msgstr "km %s zdublowany##%u" @@ -2746,31 +2124,28 @@ msgstr "km %s, %.1f m od swojego miejsca" # scenery editor: what a click does msgid "Click: start from this free end, drag: lead a track out of it" -msgstr "Klik: zacznij od tego wolnego końca, przeciągnij: wyprowadź z niego tor" +msgstr "LPM: zacznij od tego wolnego końca, przeciągnij: wyprowadź z niego tor" msgid "Click: join this end and lay" -msgstr "Klik: dołącz do tego końca i ułóż" +msgstr "LPM: dołącz do tego końca i ułóż" msgid "Click: start of the new track" -msgstr "Klik: początek nowego toru" +msgstr "LPM: początek nowego toru" msgid "Click: lay the track to here" -msgstr "Klik: połóż tor do tego miejsca" - -msgid "Press: where the model starts, drag to its end" -msgstr "Wciśnij: początek modelu, przeciągnij do jego końca" +msgstr "LPM: ułóż tor do tego miejsca" msgid "Drag: the tracks in a box join the selection" msgstr "Przeciągnij: tory w ramce dołączą do zaznaczenia" msgid "Click: take out of the selection, drag: a box" -msgstr "Klik: usuń z zaznaczenia, przeciągnij: ramka" +msgstr "LPM: usuń z zaznaczenia, przeciągnij: ramka" msgid "Click: add to the selection, drag: a box" -msgstr "Klik: dodaj do zaznaczenia, przeciągnij: ramka" +msgstr "LPM: dodaj do zaznaczenia, przeciągnij: ramka" msgid "Drag: the control vector, the curve follows" -msgstr "Przeciągnij: wektor kontrolny, krzywa podąża" +msgstr "Przeciągnij: wektor sterujący, łuk podąża" msgid "Drag: the joint, the neighbours follow" msgstr "Przeciągnij: styk, sąsiednie tory podążają" @@ -2779,55 +2154,40 @@ msgid "Drag: the end, dropped on a free end it joins it" msgstr "Przeciągnij: koniec, upuszczony na wolny koniec łączy się z nim" msgid "Click: move this straight together with the others" -msgstr "Klik: przesuwaj tę prostą razem z pozostałymi" +msgstr "LPM: przesuwaj tę prostą razem z pozostałymi" msgid "Press: break the straight here, drag to turn the rest" -msgstr "Wciśnij: załam prostą tutaj, przeciągnij, by obrócić resztę" +msgstr "Wciśnij LPM: załam prostą tutaj, przeciągnij, by obrócić resztę" msgid "Click: next point of the shift around an obstacle" -msgstr "Klik: następny punkt objazdu przeszkody" +msgstr "LPM: następny punkt objazdu przeszkody" msgid "Click: start of the shift" -msgstr "Klik: początek przesunięcia" - -msgid "Click: shift the straight sideways" -msgstr "Klik: przesuń prostą w bok" - -msgid "Click: move this end of the straight" -msgstr "Klik: przesuń ten koniec prostej" - -msgid "Click: move this vertex" -msgstr "Klik: przesuń ten wierzchołek" - -msgid "Click: change the radius of this curve" -msgstr "Klik: zmień promień tego łuku" +msgstr "LPM: początek przesunięcia" msgid "Press and drag: a switch here, along sets its direction, sideways its side" -msgstr "Wciśnij i przeciągnij: rozjazd tutaj, wzdłuż ustala kierunek, w bok stronę" +msgstr "Wciśnij LPM i przeciągnij: rozjazd tutaj, wzdłuż ustala kierunek, w bok stronę" msgid "Click: the whole straight through this track" -msgstr "Klik: cała prosta przez ten tor" +msgstr "LPM: cała prosta przez ten tor" msgid "Click: the curve of this track with its straights" -msgstr "Klik: łuk tego toru z prostymi" +msgstr "LPM: łuk tego toru z prostymi" msgid "Click: edit this switch" -msgstr "Klik: edytuj ten rozjazd" +msgstr "LPM: edytuj ten rozjazd" msgid "Click: choose this track, then the type of the switch" -msgstr "Klik: wybierz ten tor, potem typ rozjazdu" +msgstr "LPM: wybierz ten tor, potem typ rozjazdu" msgid "Click: choose this track, then press and drag on it for the switch" -msgstr "Klik: wybierz ten tor, potem wciśnij i przeciągnij na nim, by wstawić rozjazd" - -msgid "Click: the grade line through this track" -msgstr "Klik: niweleta przez ten tor" +msgstr "LPM: wybierz ten tor, potem wciśnij LPM i przeciągnij na nim, by wstawić rozjazd" msgid "Click: the line the objects go along" -msgstr "Klik: linia, wzdłuż której idą obiekty" +msgstr "LPM: linia, wzdłuż której idą obiekty" msgid "Click: select" -msgstr "Klik: zaznacz" +msgstr "LPM: zaznacz" msgid "Drag from a free end" msgstr "Przeciągnij z wolnego końca" @@ -2865,14 +2225,11 @@ msgstr "Esc" msgid "deselects" msgstr "odznacza" -msgid "Dragging from a free end draws a curve through the cursor; off: it draws a straight along the end" -msgstr "Przeciąganie z wolnego końca rysuje łuk przez kursor; wyłączone: prostą wzdłuż końca" - msgid "Enter" msgstr "Enter" msgid "lay up to the cursor" -msgstr "ułóż do kursora" +msgstr "układa do kursora" msgid "Shift" msgstr "Shift" @@ -2880,15 +2237,6 @@ msgstr "Shift" msgid "moves the whole track" msgstr "przesuwa cały tor" -msgid "The gizmo moves them together; Shift+LMB adds or takes out a path" -msgstr "Gizmo przesuwa je razem; Shift+LPM dodaje albo usuwa tor" - -msgid "Double slip switches" -msgstr "Rozjazdy krzyżowe podwójne" - -msgid "Only at a crossing of two straights" -msgstr "Tylko na skrzyżowaniu dwóch prostych" - msgid "Drag: shift the straight sideways" msgstr "Przeciągnij: przesuń prostą w bok" @@ -2926,32 +2274,26 @@ msgid "Another arc after the last one, the angle of the curve is shared out by t msgstr "Kolejny łuk za ostatnim; kąt zwrotu dzielony według udziałów" msgid "Formation under the ballast of the track" -msgstr "Korona pod podsypką toru" +msgstr "Torowisko według podsypki" msgid "The terrain goes as deep under the track as its ballast reaches and as wide as its foot,\nread from each path of the route; off: the values given below" msgstr "Teren schodzi pod torem tak głęboko, jak sięga jego podsypka, i tak szeroko, jak jej podstawa,\nodczytane z każdego toru trasy; wyłączone: wartości podane niżej" msgid "Ballast %.2f m deep, its foot %.2f m from the axis" -msgstr "Podsypka %.2f m, jej podstawa %.2f m od osi" +msgstr "Grubość podsypki %.2f m, jej podstawa %.2f m od osi" msgid "Formation below the track (m)" -msgstr "Korona poniżej toru (m)" +msgstr "Torowisko poniżej toru (m)" msgid "Name##switchname" msgstr "Nazwa##switchname" msgid "Name of the next switch, its drive events are named after it; empty: the next free z1, z2...; a number at the end goes up with each switch" -msgstr "Nazwa następnej zwrotnicy, od niej nazywają się zdarzenia napędu; puste: kolejna wolna z1, z2...; liczba na końcu rośnie z każdą zwrotnicą" - -msgid "No switch drives in scenery/tory/napedy" -msgstr "Brak napędów zwrotnicowych w scenery/tory/napedy" +msgstr "Nazwa następnej zwrotnicy, od niej nazywają się eventy napędu; puste: kolejna wolna z1, z2...; liczba na końcu rośnie z każdą zwrotnicą" msgid "Drive" msgstr "Napęd" -msgid "Drive of the switch from scenery/tory/napedy: its events + and - switch it with the keys by the switch and in the map during the game; none: the switch can't be switched" -msgstr "Napęd zwrotnicy ze scenery/tory/napedy: jego zdarzenia + i - przestawiają ją klawiszami przy zwrotnicy i w podglądzie mapy podczas gry; brak: zwrotnicy nie da się przestawić" - msgid "diverging side" msgstr "od toru zwrotnego" @@ -2959,13 +2301,13 @@ msgid "other side" msgstr "z drugiej strony" msgid "The drive couldn't be placed: " -msgstr "Nie udało się wstawić napędu: " +msgstr "Nie udało się postawić napędu: " msgid "Switch %s with the drive %s: events %s+ and %s- switch it, also in the map" -msgstr "Zwrotnica %s z napędem %s: zdarzenia %s+ i %s- ją przestawiają, także w mapie" +msgstr "Zwrotnica %s z napędem %s: eventy %s+ i %s- ją przestawiają, także na mapie" msgid "Drive: events %s+ and %s- switch it, also in the map" -msgstr "Napęd: zdarzenia %s+ i %s- ją przestawiają, także w mapie" +msgstr "Napęd: eventy %s+ i %s- przestawiają zwrotnicę, także z mapy" msgid "No drive: the switch can't be switched during the game" msgstr "Brak napędu: zwrotnicy nie da się przestawić podczas gry" @@ -2979,14 +2321,8 @@ msgstr "bez napędu" msgid "The drive %s has no variant for a %s switch with the drive on the %s" msgstr "Napęd %s nie ma wariantu dla rozjazdu %s z napędem z %s" -msgid "No switch drives: no template is described with category: switch drive" -msgstr "Brak napędów: żaden wzorzec nie ma opisu z category: switch drive" - msgid "Templates described with category: switch drive, the side of the switch and of the drive, and a parameter with the role track for the name of the switch.\nIts events + and - switch it with the keys by the switch and in the map during the game" -msgstr "Wzorce opisane jako category: switch drive, ze stroną rozjazdu i napędu oraz parametrem o roli track na nazwę zwrotnicy.\nIch zdarzenia + i - przestawiają zwrotnicę klawiszami przy niej i w podglądzie mapy podczas gry" - -msgid "Track editor: the window with its modes opens or closes" -msgstr "Edytor torów: otwiera albo zamyka okno z jego trybami" +msgstr "Model napędu stawiany przy zwrotnicy; w grze eventy + i - przestawiają ją klawiszami przy niej i na mapie.\nNa liście są szablony opisane jako category: switch drive, ze stroną rozjazdu i napędu oraz parametrem o roli track na nazwę zwrotnicy" msgid "Look along" msgstr "Szukaj wzdłuż" @@ -2994,23 +2330,14 @@ msgstr "Szukaj wzdłuż" msgid "Route of the vertical profile" msgstr "Trasa profilu podłużnego" -msgid "%d read by the paths" -msgstr "%d odczytywanych przez tory" - msgid "The memory cell of the object is read by a getvalues event of a path, listed by the path, inside a multiple\nevent or written loose as :event. The object is bound to the thread of that path, not to the nearest one" -msgstr "Komórkę pamięci obiektu odczytuje zdarzenie getvalues toru: wpisane w torze, wewnątrz zdarzenia multiple\nalbo dopisane luzem jako :event. Obiekt zostaje przypisany do nitki tego toru, nie do najbliższej" - -msgid "%d of them nearer to another path" -msgstr "w tym %d bliżej innego toru" +msgstr "Komórkę pamięci obiektu odczytuje event getvalues toru: wpisany w torze, wewnątrz eventu multiple\nalbo dopisany luzem jako :event. Obiekt zostaje przypisany do nitki tego toru, nie do najbliższej" msgid "By the distance alone these would go to the wrong thread. Check them in 3D: the dashed line goes to the nearer path" msgstr "Po samej odległości trafiłyby do złej nitki. Sprawdź je w 3D: przerywana linia prowadzi do bliższego toru" -msgid "%d signal(s) not read by any path" -msgstr "%d sygnalizator(ów) bez odczytu przez tor" - msgid "The signal has a memory cell, but no event of a path reads it: the vehicles won't see it.\nBound to the nearest path" -msgstr "Sygnalizator ma komórkę pamięci, ale żadne zdarzenie toru jej nie odczytuje: pojazdy go nie zobaczą.\nPrzypisany do najbliższego toru" +msgstr "Sygnalizator ma komórkę pamięci, ale żaden event toru jej nie odczytuje: pojazdy go nie zobaczą.\nPrzypisany do najbliższego toru" msgid "Read by a path" msgstr "Odczytywane przez tor" @@ -3022,10 +2349,10 @@ msgid "Not read" msgstr "Bez odczytu" msgid "\nRead by the path %s: event %s, getvalues of the memory cell %s" -msgstr "\nOdczytywany przez tor %s: zdarzenie %s, getvalues komórki pamięci %s" +msgstr "\nOdczytywany przez tor %s: event %s, getvalues komórki pamięci %s" msgid " (written loose, the path takes it by the name)" -msgstr " (dopisane luzem, tor bierze je po nazwie)" +msgstr " (dopisany luzem, tor bierze go po nazwie)" msgid "\nThe path %s is nearer, but it doesn't read the object" msgstr "\nTor %s jest bliżej, ale nie odczytuje obiektu" @@ -3055,13 +2382,13 @@ msgid "(can't be changed: %s)" msgstr "(nie można zmienić: %s)" msgid "Renamed from %s" -msgstr "Zmieniono z %s" +msgstr "Zmieniono nazwę z %s" msgid "Name##trackname" msgstr "Nazwa##trackname" msgid "Enter renames the path, Esc while typing takes the text back.\nThe events, memory cells and the map of the game find the path by its name" -msgstr "Enter zmienia nazwę toru, Esc w trakcie pisania przywraca tekst.\nZdarzenia, komórki pamięci i mapa w grze znajdują tor po nazwie" +msgstr "Enter zmienia nazwę toru, Esc w trakcie pisania przywraca tekst.\nEventy, komórki pamięci i mapa w grze znajdują tor po nazwie" msgid "Rename anyway" msgstr "Zmień mimo to" @@ -3085,16 +2412,13 @@ msgid "Saved as %s: the scenery keeps the names in lower case" msgstr "Zapisana jako %s: sceneria trzyma nazwy małymi literami" msgid "The new name goes to %d memory cell(s) and %d include(s), the drives of a switch among them" -msgstr "Nowa nazwa trafi do %d komórek pamięci i %d includów, w tym napędów zwrotnicy" +msgstr "Nowa nazwa trafi też do komórek pamięci (%d) i plików include (%d), w tym do napędów zwrotnic" msgid "These events aim at the path by the old name and the editor doesn't write them: %s" -msgstr "Te zdarzenia wskazują tor starą nazwą, a edytor ich nie zapisuje: %s" +msgstr "Te eventy wskazują tor starą nazwą, a edytor ich nie zapisuje: %s" msgid "These events belong to the path by the old name: %s" -msgstr "Te zdarzenia należą do toru przez starą nazwę: %s" - -msgid "They work until the scenery is loaded again; then they need the new name in their files" -msgstr "Działają do ponownego wczytania scenerii; potem trzeba im wpisać nową nazwę w plikach" +msgstr "Te eventy należą do toru po starej nazwie: %s" msgid "(no name)" msgstr "(bez nazwy)" @@ -3118,46 +2442,43 @@ msgid "No free ends within %.0f m of the rim" msgstr "Brak wolnych końców w promieniu %.0f m od krawędzi" msgid "%d track(s) led to the turntable" -msgstr "Doprowadzono %d tor(ów) do obrotnicy" +msgstr "Tory doprowadzone do obrotnicy: %d" msgid "The turntable has %d positions, %d track lines lead to it: the rest won't be reached" -msgstr "Obrotnica ma %d położeń, a dochodzi do niej %d linii torów: do reszty nie dojedzie" +msgstr "Położeń obrotnicy: %d, linii torów do niej: %d; do nadmiarowych linii pomost nie dojedzie" msgid "The name %s is taken by another path" msgstr "Nazwę %s ma już inny tor" -msgid "Turntable %s: click around it for the tracks out of it, or fit the tracks around. It turns after the scenery is saved and loaded again" -msgstr "Obrotnica %s: klikaj dookoła, żeby wyprowadzić tory, albo dopasuj tory wokół. Obraca się po zapisaniu i ponownym wczytaniu scenerii" - msgid "Track out of the turntable at %.1f deg, %.1f m" -msgstr "Tor z obrotnicy pod kątem %.1f st., %.1f m" +msgstr "Tor z obrotnicy pod kątem %.1f°, %.1f m" msgid "Click: lead this track to the turntable" msgstr "Kliknij: doprowadź ten tor do obrotnicy" msgid "Click: a track out of the turntable, %.1f deg, %.1f m" -msgstr "Kliknij: tor z obrotnicy, %.1f st., %.1f m" +msgstr "Kliknij: tor z obrotnicy, %.1f°, %.1f m" msgid "Click: this turntable" -msgstr "Kliknij: ta obrotnica" +msgstr "Kliknij: zaznacz tę obrotnicę" msgid "Click: a turntable at the end of this track" -msgstr "Kliknij: obrotnica na końcu tego toru" +msgstr "Kliknij: postaw obrotnicę na końcu tego toru" msgid "Press: the centre of a new turntable, drag: the direction of its bridge" -msgstr "Wciśnij: środek nowej obrotnicy, przeciągnij: kierunek jej mostu" +msgstr "Wciśnij LPM: środek nowej obrotnicy, przeciągnij: kierunek pomostu" msgid "Turntable %s, bridge %.1f m" -msgstr "Obrotnica %s, most %.1f m" +msgstr "Obrotnica %s, pomost %.1f m" msgid "(positions set by the tracks around, up to %d)" msgstr "(położenia według torów dookoła, do %d)" msgid "(%d positions fixed by the template)" -msgstr "(%d położeń ustalonych we wzorcu)" +msgstr "(położenia ustalone w szablonie: %d)" msgid "No turntable template takes this path as its track: the positions aren't known" -msgstr "Żaden wzorzec obrotnicy nie bierze tego toru: położenia nie są znane" +msgstr "Żaden szablon obrotnicy nie pasuje do tego toru, więc położenia nie są znane" msgid "Positions (%d)" msgstr "Położenia (%d)" @@ -3175,16 +2496,34 @@ msgid "Tracks around" msgstr "Tory dookoła" msgid "Reach from the rim, m" -msgstr "Zasięg od krawędzi, m" +msgstr "Zasięg od krawędzi (m)" msgid "Radius of the curves, m" -msgstr "Promień łuków, m" +msgstr "Promień łuków (m)" msgid "Angle step of a new track, deg (0: free)" -msgstr "Krok kąta nowego toru, st. (0: dowolny)" +msgstr "Krok kąta nowego toru (°, 0: dowolny)" -msgid "Click around the turntable: a track out of it, as long as the cursor is far.\nClick a free end near it: that track is led to the rim, straight or with a curve" -msgstr "Kliknij dookoła obrotnicy: tor z niej, tak długi, jak daleko jest kursor.\nKliknij wolny koniec w pobliżu: ten tor zostanie doprowadzony do krawędzi, prosto albo łukiem" +msgid "LMB around the turntable" +msgstr "LPM dookoła obrotnicy" + +msgid "a track out of it, as long as the cursor is far" +msgstr "tor z niej, tak długi, jak daleko jest kursor" + +msgid "LMB on a free end near it" +msgstr "LPM na wolnym końcu obok" + +msgid "that track is led to the rim, straight or with a curve" +msgstr "ten tor zostanie doprowadzony do krawędzi, prosto albo łukiem" + +msgid "from where the centre goes, turning the bridge" +msgstr "od środka nowej obrotnicy, kierunek obraca pomost" + +msgid "the turntable lines up with it, at its end" +msgstr "obrotnica ustawia się w linii z nim, na jego końcu" + +msgid "no lining up" +msgstr "bez ustawiania w linii" msgid "Find the free ends around" msgstr "Znajdź wolne końce dookoła" @@ -3193,10 +2532,10 @@ msgid "Lead the chosen to the turntable (%d)" msgstr "Doprowadź wybrane do obrotnicy (%d)" msgid "%.1f deg, curve R %.0f m, %.1f m" -msgstr "%.1f st., łuk R %.0f m, %.1f m" +msgstr "%.1f°, łuk R %.0f m, %.1f m" msgid "%.1f deg, straight %.1f m" -msgstr "%.1f st., prosta %.1f m" +msgstr "%.1f°, prosta %.1f m" msgid "in place of its last straight %s" msgstr "w miejsce jego ostatniej prostej %s" @@ -3208,52 +2547,19 @@ msgid "New turntable" msgstr "Nowa obrotnica" msgid "No turntables: no template is described with category: turntable and its turntable: {length}" -msgstr "Brak obrotnic: żaden wzorzec nie ma opisu category: turntable z turntable: {length}" - -msgid "%s %.0f m, up to %d positions set by the tracks" -msgstr "%s %.0f m, do %d położeń według torów" - -msgid "%s %.0f m, %d fixed positions" -msgstr "%s %.0f m, %d stałych położeń" +msgstr "Brak obrotnic: żaden szablon nie ma opisu category: turntable z turntable: {length}" msgid "Name##turntablename" msgstr "Nazwa##turntablename" -msgid "Press where the centre goes and drag to turn the bridge.\nClick a free end of a track: the turntable lines up with it, at its end. Shift: no lining up" -msgstr "Wciśnij w miejscu środka i przeciągnij, żeby obrócić most.\nKliknij wolny koniec toru: obrotnica ustawi się w jego osi, na jego końcu. Shift: bez dopasowania" - -msgid "The scenery has its own ground under the route: convert it to editor terrain first" -msgstr "Sceneria ma pod trasą własny teren: najpierw zamień go na teren edytora" - -msgid "; %d chunk(s) of terrain made under the route" -msgstr "; utworzono %d kawałków terenu pod trasą" - -msgid "; %d chunk(s) left out: the scenery has its own ground there, convert it to shape it" -msgstr "; pominięto %d kawałków: sceneria ma tam własny teren, zamień go, żeby go kształtować" - msgid "Make terrain where the route has none" -msgstr "Utwórz teren tam, gdzie trasa go nie ma" - -msgid "Chunks of editor terrain are made along the route where there's none, from the ground around,\nthen shaped. Where the scenery has its own ground triangles the chunk is left out: convert them instead" -msgstr "Wzdłuż trasy, tam gdzie go brak, powstają kawałki terenu edytora z wysokości otoczenia,\npotem są kształtowane. Tam, gdzie sceneria ma własne trójkąty terenu, kawałek jest pomijany: zamień je zamiast tego" - -msgid "Puts back the terrain as it was before the last shaping, and takes away the terrain it made" -msgstr "Przywraca teren sprzed ostatniego kształtowania i usuwa teren, który przy tym powstał" +msgstr "Teren tam, gdzie go brak" msgid "Click: start from this free end, drag: the curve of the switch goes on" -msgstr "Kliknij: zacznij od tego wolnego końca, przeciągnij: łuk rozjazdu biegnie dalej" - -msgid "%s and %d vehicle(s) behind it stand on %s" -msgstr "%s i %d pojazd(ów) za nim stoją na %s" - -msgid "Add to the train" -msgstr "Dodaj do składu" - -msgid "Train: %d vehicle(s), the first one leads" -msgstr "Skład: %d pojazd(ów), pierwszy prowadzi" +msgstr "LPM: zacznij od tego wolnego końca, przeciągnij: łuk rozjazdu biegnie dalej" msgid "Turned around in the train" -msgstr "Obrócony w składzie" +msgstr "Obraca go w składzie" msgid "up" msgstr "w górę" @@ -3264,9 +2570,6 @@ msgstr "Wyczyść skład" msgid "Turn the train around" msgstr "Obróć skład" -msgid "the head goes where the arrow at the marker points" -msgstr "czoło jedzie tam, gdzie wskazuje strzałka przy znaczniku" - msgid "Trainset turned around" msgstr "Skład odwrócony" @@ -3274,16 +2577,13 @@ msgid "The first vehicle stays where the trainset starts and leads the other way msgstr "Pierwszy pojazd zostaje w miejscu początku składu i prowadzi w drugą stronę,\nreszta składu stoi za nim" msgid "Click: take out the %zu joined paths, %.0f m" -msgstr "Kliknij: wyjmij %zu połączonych torów, %.0f m" +msgstr "LPM: wyjmij połączone tory (%zu, %.0f m)" msgid "Click: add the %zu joined paths, %.0f m" -msgstr "Kliknij: dodaj %zu połączonych torów, %.0f m" - -msgid "Spread along the joins: Ctrl+Shift+LMB on a path" -msgstr "Rozlewanie po połączeniach: Ctrl+Shift+LPM na torze" +msgstr "LPM: dodaj połączone tory (%zu, %.0f m)" msgid "Stop before the switches" -msgstr "Zatrzymaj przed rozjazdami" +msgstr "Zatrzymuje się przed rozjazdami" msgid "Through the switches, along their main track" msgstr "Przez rozjazdy, po ich torze zasadniczym" @@ -3291,6 +2591,15 @@ msgstr "Przez rozjazdy, po ich torze zasadniczym" msgid "Through everything joined" msgstr "Przez wszystko, co połączone" +msgid "Up to a switch" +msgstr "Do rozjazdu" + +msgid "Main track on" +msgstr "Dalej zasadniczym" + +msgid "Everything joined" +msgstr "Wszystko połączone" + msgid "Only to the paths like the clicked one:" msgstr "Tylko do torów takich jak kliknięty:" @@ -3303,17 +2612,8 @@ msgstr "podsypka" msgid "speed" msgstr "prędkość" -msgid "Reach along the track, m (0: no limit)" -msgstr "Zasięg wzdłuż toru, m (0: bez limitu)" - msgid "Holding Ctrl+Shift shows what the click takes; on a path of the set it takes them out" -msgstr "Trzymając Ctrl+Shift widać, co kliknięcie zabierze; na torze ze zbioru je wyjmuje" - -msgid "Grow by the neighbours" -msgstr "Poszerz o sąsiadów" - -msgid "Adds the paths joined to the set, by the rules of the spread" -msgstr "Dodaje tory połączone ze zbiorem, według zasad rozlewania" +msgstr "Przytrzymany Ctrl+Shift pokazuje, co zabierze kliknięcie; na zaznaczonym torze kliknięcie je wyjmuje" msgid "New scenery" msgstr "Nowa sceneria" @@ -3330,9 +2630,6 @@ msgstr "Serwis mapy nie odesłał obrazu" msgid "No map: " msgstr "Brak mapy: " -msgid "Find the place on the map: the wheel zooms, dragging moves the map. A click sets the centre of the scenery, its point 0 0 0." -msgstr "Znajdź miejsce na mapie: kółko przybliża, przeciąganie przesuwa mapę. Kliknięcie ustala środek scenerii, jej punkt 0 0 0." - msgid "file name, e.g. my_line" msgstr "nazwa pliku, np. moja_linia" @@ -3352,10 +2649,10 @@ msgid "Centre of the scenery, PUWG 1992" msgstr "Środek scenerii, PUWG 1992" msgid "E, km##east" -msgstr "E, km##east" +msgstr "E (km)##east" msgid "N, km##north" -msgstr "N, km##north" +msgstr "N (km)##north" msgid "latitude, longitude" msgstr "szerokość, długość geograficzna" @@ -3373,7 +2670,7 @@ msgid "%.5f N, %.5f E" msgstr "%.5f N, %.5f E" msgid "The scenery keeps the centre as its geographic reference (//$g PUWG1992): the orthophoto of the editor lies under it, in place." -msgstr "Sceneria zapamiętuje środek jako odniesienie geograficzne (//$g PUWG1992): ortofotomapa edytora leży pod nią na swoim miejscu." +msgstr "Sceneria zapamięta ten środek, więc ortofotomapa w edytorze trafi pod nią na swoje miejsce" msgid "Give the file a name" msgstr "Nadaj plikowi nazwę" @@ -3390,20 +2687,14 @@ msgstr "Nie da się zapisać pliku" msgid "Map: (c) OpenStreetMap contributors, served by geoportal.gov.pl" msgstr "Mapa: (c) współtwórcy OpenStreetMap, udostępniana przez geoportal.gov.pl" -msgid "Along the selected path; dragged past its end the model goes on along the joined paths, at a switch along the branch nearer the cursor" -msgstr "Wzdłuż zaznaczonego toru; przeciągnięty za jego koniec model idzie dalej po połączonych torach, na rozjeździe gałęzią bliższą kursorowi" - msgid "every (m, 0: its length)" -msgstr "co (m, 0: jego długość)" +msgstr "Odstęp (m, 0: długość modelu)" msgid "%.2f m %s of the axis, %.2f m over the rail top" msgstr "%.2f m %s od osi, %.2f m nad główką szyny" -msgid "Press beside the track where the model begins and drag along to where it ends; the side of the press is the side of the model. Past the end of the path it goes on along the joined ones. A click lays one copy." -msgstr "Wciśnij obok toru tam, gdzie model się zaczyna, i przeciągnij wzdłuż do jego końca; strona wciśnięcia to strona modelu. Za końcem toru idzie dalej po połączonych. Kliknięcie kładzie jedną kopię." - msgid "Lay it along the selected path" -msgstr "Połóż wzdłuż zaznaczonego toru" +msgstr "Ułóż wzdłuż zaznaczonego toru" msgid "New scenery..." msgstr "Nowa sceneria..." @@ -3437,17 +2728,11 @@ msgstr "Gięcie wzdłuż toru" msgid "Bend it along the track" msgstr "Wygnij wzdłuż toru" -msgid "Select a model or an include standing by a track" -msgstr "Zaznacz model albo include stojący przy torze" - msgid "No track within %.0f m of the model" msgstr "Brak toru w promieniu %.0f m od modelu" -msgid "axis of the model along the track" -msgstr "oś modelu wzdłuż toru" - msgid "the one nearest the track" -msgstr "najbliższa kierunkowi toru" +msgstr "auto" msgid "Reference point: the model bends about it, the line through it keeps its length, the offsets are its place" msgstr "Punkt odniesienia: wokół niego model się wygina, linia przez niego zachowuje długość, odsunięcia podają jego położenie" @@ -3467,9 +2752,6 @@ msgstr "koniec" msgid "picked point" msgstr "wskazany punkt" -msgid "point (x, y, z of the model)" -msgstr "punkt (x, y, z modelu)" - msgid "Pick the point on the model" msgstr "Wskaż punkt na modelu" @@ -3485,9 +2767,6 @@ msgstr "Punkt odniesienia %.2f %.2f %.2f w modelu" msgid "The model stays where it stands and bends with the track; Ctrl+Z gives it back" msgstr "Model zostaje tam, gdzie stoi, i wygina się z torem; Ctrl+Z go przywraca" -msgid "The model stands %.1f deg off the track, bent it'll follow the track" -msgstr "Model stoi %.1f° w stosunku do toru, po wygięciu pójdzie wzdłuż toru" - msgid "The model has no triangles to bend, or it can't be loaded" msgstr "Model nie ma trójkątów do wygięcia albo nie da się go wczytać" @@ -3500,12 +2779,6 @@ msgstr "%s wygięty na %.1f m toru, Ctrl+Z cofa" msgid "%.2f m along the reference line, %d copies (the model is %.2f m)" msgstr "%.2f m wzdłuż linii odniesienia, kopii: %d (model ma %.2f m)" -msgid "from the track axis (m, + right)" -msgstr "od osi toru (m, + w prawo)" - -msgid "over the rail head (m)" -msgstr "nad główką szyny (m)" - msgid "along the track (m)" msgstr "wzdłuż toru (m)" @@ -3513,7 +2786,7 @@ msgid "length (m)" msgstr "długość (m)" msgid "As long as the model" -msgstr "Długość modelu" +msgstr "Długość jak model" msgid "Along the whole track" msgstr "Na całym torze" @@ -3521,9 +2794,6 @@ msgstr "Na całym torze" msgid "Bent" msgstr "Wygięty" -msgid "copy every (m, 0: its length)" -msgstr "kopia co (m, 0: jej długość)" - msgid "Done" msgstr "Gotowe" @@ -3569,25 +2839,19 @@ msgstr "Shift: wysokość Ctrl: tylko wzdłuż toru" msgid "%.2f m from the track, %+.2f m along" msgstr "%.2f m od toru, %+.2f m wzdłuż" -msgid "Drag: offset from the track; Shift: height; Ctrl: along the track" -msgstr "Przeciągnij: odsunięcie od toru; Shift: wysokość; Ctrl: wzdłuż toru" - msgid "%.2f m over the rail head" msgstr "%.2f m nad główką szyny" -msgid "%.2f m from the track axis" -msgstr "%.2f m od osi toru" - msgid "%+.2f m along the track" msgstr "%+.2f m wzdłuż toru" # scenery editor: general mode, toolset, layers, settings msgid "LMB: select node F: focus End: drop to ground Del: delete" -msgstr "LPM: zaznacz węzeł F: pokaż End: opuść na ziemię Del: usuń" +msgstr "LPM: zaznacz obiekt F: pokaż End: opuść na ziemię Del: usuń" msgid "LMB: copy the clicked model to the node bank" -msgstr "LPM: skopiuj kliknięty model do banku węzłów" +msgstr "LPM: skopiuj kliknięty model do banku obiektów" msgid "LMB: insert a model at the cursor" msgstr "LPM: wstaw model w miejscu kursora" @@ -3596,19 +2860,22 @@ msgid "Random model from set" msgstr "Losowy model z zestawu" msgid "%zu templates in set%s" -msgstr "%zu szablonów w zestawie%s" +msgstr "Szablonów w zestawie: %zu%s" msgid "Inserts the template selected in the node bank" -msgstr "Wstawia szablon zaznaczony w banku węzłów" +msgstr "Wstawia szablon zaznaczony w banku obiektów" msgid "Hold LMB: paint models along the cursor path" msgstr "Przytrzymaj LPM: maluj modele wzdłuż drogi kursora" msgid "Node properties" -msgstr "Właściwości węzła" +msgstr "Właściwości obiektu" + +msgid "Cursor: %.2f, %.2f, %.2f" +msgstr "Kursor: %.2f, %.2f, %.2f" msgid "Array" -msgstr "Tablica" +msgstr "Szereg kopii" msgid "Ctrl+S" msgstr "Ctrl+S" @@ -3617,28 +2884,28 @@ msgid "Ctrl+Shift+F11" msgstr "Ctrl+Shift+F11" msgid "name" -msgstr "nazwa" +msgstr "Nazwa" msgid "(none)" msgstr "(brak)" msgid "Name the events can refer to the node by.\nNodes without a name load quicker, leave it empty unless something needs it" -msgstr "Nazwa, po której zdarzenia mogą odwołać się do węzła.\nWęzły bez nazwy wczytują się szybciej, zostaw puste, jeśli nic jej nie potrzebuje" +msgstr "Nazwa, po której eventy mogą odwołać się do obiektu.\nObiekty bez nazwy wczytują się szybciej, zostaw puste, jeśli nic jej nie potrzebuje" msgid "Transform" msgstr "Przekształcenie" msgid "rotation (deg)" -msgstr "obrót (°)" +msgstr "Obrót (°)" msgid "scale (x,y,z)" -msgstr "skala (x,y,z)" +msgstr "Skala (x,y,z)" msgid "reset rotation" -msgstr "zeruj obrót" +msgstr "Przywróć obrót" msgid "reset scale" -msgstr "zeruj skalę" +msgstr "Przywróć skalę" msgid "Parent Group" msgstr "Grupa nadrzędna" @@ -3647,16 +2914,16 @@ msgid "Spacing" msgstr "Odstęp" msgid "Paints the template selected in the node bank" -msgstr "Maluje szablonem zaznaczonym w banku węzłów" +msgstr "Maluje szablonem zaznaczonym w banku obiektów" msgid "Reload node bank" -msgstr "Wczytaj bank węzłów ponownie" +msgstr "Wczytaj ponownie bank obiektów" msgid "Search" msgstr "Szukaj" msgid "Search node bank" -msgstr "Szukaj w banku węzłów" +msgstr "Szukaj w banku obiektów" msgid "Scenery templates (.inc)" msgstr "Szablony scenerii (.inc)" @@ -3668,7 +2935,7 @@ msgid " (none yet, see Model sets...)" msgstr " (jeszcze żadnych, zobacz Zestawy modeli...)" msgid "Node bank groups" -msgstr "Grupy banku węzłów" +msgstr "Grupy banku obiektów" msgid "(empty)" msgstr "(pusto)" @@ -3677,7 +2944,7 @@ msgid "Add selected" msgstr "Dodaj zaznaczony" msgid "Adds the template selected in the node bank list" -msgstr "Dodaje szablon zaznaczony na liście banku węzłów" +msgstr "Dodaje szablon zaznaczony na liście banku obiektów" msgid "Remove" msgstr "Usuń" @@ -3703,14 +2970,8 @@ msgstr "Usunąć zestaw \"%s\"?" msgid "Delete##confirm" msgstr "Usuń##confirm" -msgid "Create a set with \"New\", or use \"Save as set\" on a brush or area fill list." -msgstr "Utwórz zestaw przyciskiem \"Nowy\" albo użyj \"Zapisz jako zestaw\" na liście pędzla lub wypełniania obszaru." - -msgid "Sets can be chosen in Insert (random model), Brush and Area fill." -msgstr "Zestawy można wybrać we Wstawianiu (losowy model), Pędzlu i Wypełnianiu obszaru." - msgid "(empty) select a template in the node bank and press \"Add selected\"" -msgstr "(pusto) zaznacz szablon w banku węzłów i naciśnij \"Dodaj zaznaczony\"" +msgstr "(pusto) zaznacz szablon w banku obiektów i naciśnij \"Dodaj zaznaczony\"" msgid "Remove all templates from this set?" msgstr "Usunąć wszystkie szablony z tego zestawu?" @@ -3737,10 +2998,10 @@ msgid "include: %s" msgstr "include: %s" msgid "The template takes no parameters." -msgstr "Szablon nie ma parametrów." +msgstr "Szablon nie ma parametrów" msgid "Models of the template follow the changes at once, the rest of it once the scenery is saved and loaded again." -msgstr "Modele szablonu od razu idą za zmianami, reszta po zapisaniu i ponownym wczytaniu scenerii." +msgstr "Modele szablonu zmieniają się od razu, reszta po zapisaniu i ponownym wczytaniu scenerii" msgid "Scenery layers are available in an edit session.\nStart the simulator with: -edit " msgstr "Warstwy scenerii są dostępne w sesji edycji.\nUruchom symulator z: -edit " @@ -3764,13 +3025,13 @@ msgid "Save" msgstr "Zapisz" msgid "Writes changed nodes and layers to the scenery files (Ctrl+S)" -msgstr "Zapisuje zmienione węzły i warstwy do plików scenerii (Ctrl+S)" +msgstr "Zapisuje zmienione obiekty i warstwy do plików scenerii (Ctrl+S)" msgid "visible, locked, active, layer file" msgstr "widoczna, zablokowana, aktywna, plik warstwy" msgid "Active layer: nodes created in the editor are placed in it" -msgstr "Aktywna warstwa: tu trafiają węzły tworzone w edytorze" +msgstr "Aktywna warstwa: tu trafiają obiekty tworzone w edytorze" msgid "File of the new layer, in the scenery directory:" msgstr "Plik nowej warstwy, w katalogu scenerii:" @@ -3791,7 +3052,7 @@ msgid "The layer holds:" msgstr "Warstwa zawiera:" msgid "Make sure the rest of the scenery doesn't refer to its tracks, events or memory cells." -msgstr "Upewnij się, że reszta scenerii nie odwołuje się do jej torów, zdarzeń ani komórek pamięci." +msgstr "Upewnij się, że reszta scenerii nie odwołuje się do jej torów, eventów ani komórek pamięci" msgid "Move content of layer \"%s\" to:" msgstr "Przenieś zawartość warstwy \"%s\" do:" @@ -3818,10 +3079,10 @@ msgid "Looks through the scenery directory again, for the templates added or cha msgstr "Przegląda katalog scenerii jeszcze raz, w poszukiwaniu szablonów dodanych lub zmienionych poza edytorem" msgid "%d template(s) in the scenery directory, %d described, %d ready for the node bank" -msgstr "Szablonów w katalogu scenerii: %d, opisanych: %d, gotowych do banku węzłów: %d" +msgstr "Szablonów w katalogu scenerii: %d, opisanych: %d, gotowych do banku obiektów: %d" msgid "Not in the node bank: %s" -msgstr "Nie ma w banku węzłów: %s" +msgstr "Nie ma w banku obiektów: %s" msgid "param" msgstr "param." @@ -3836,13 +3097,13 @@ msgid "default" msgstr "domyślna" msgid "Complete: once saved, the template is offered by the node bank (Insert tab, Scenery templates)." -msgstr "Kompletny: po zapisaniu szablon pojawi się w banku węzłów (zakładka Wstaw, Szablony scenerii)." +msgstr "Kompletny: po zapisaniu szablon pojawi się w banku obiektów (Wstawianie, Szablony scenerii)" msgid "Not for the node bank yet: %s." -msgstr "Jeszcze nie do banku węzłów: %s." +msgstr "Jeszcze nie do banku obiektów: %s" msgid "A template placed from the node bank gets the cursor location in the pos.* parameters, the rotation set in the\nInsert tab in rot.y, and a unique name (the default value followed by a random suffix) in the name parameter.\nThe other parameters receive their default values." -msgstr "Szablon wstawiony z banku obiektów dostaje położenie kursora w parametrach pos.*, obrót ustawiony w zakładce\nWstaw w rot.y, a w parametrze name niepowtarzalną nazwę (wartość domyślna z losowym przyrostkiem).\nPozostałe parametry dostają wartości domyślne." +msgstr "Szablon wstawiony z banku obiektów dostaje położenie kursora w parametrach pos.*, obrót ustawiony w narzędziu\nWstawianie w rot.y, a w parametrze name niepowtarzalną nazwę (wartość domyślna z losowym przyrostkiem).\nPozostałe parametry dostają wartości domyślne" msgid "Suggest roles" msgstr "Podpowiedz role" @@ -3860,10 +3121,10 @@ msgid "Reload" msgstr "Wczytaj ponownie" msgid "The .inc templates of the scenery directory. Describe a template and its parameters to add it to the node bank; the description is stored in the template file, as //$e comment lines." -msgstr "Szablony .inc z katalogu scenerii. Opisz szablon i jego parametry, żeby dodać go do banku węzłów; opis jest zapisywany w pliku szablonu jako linie komentarza //$e." +msgstr "Szablony .inc z katalogu scenerii. Opisz szablon i jego parametry, żeby dodać go do banku obiektów; opis trafia do pliku szablonu jako komentarze //$e" msgid "The node bank offers the templates in a scenery opened for editing (-edit)." -msgstr "Bank węzłów oferuje szablony w scenerii otwartej do edycji (-edit)." +msgstr "Bank obiektów oferuje szablony w scenerii otwartej do edycji (-edit)" msgid "Editor Settings" msgstr "Ustawienia edytora" @@ -3875,106 +3136,25 @@ msgid "Camera movement" msgstr "Ruch kamery" msgid "Transform gizmo (ImGuizmo)" -msgstr "Gizmo przekształceń (ImGuizmo)" - -msgid "Grid cells" -msgstr "Komórki siatki" - -msgid "Cell size (m)" -msgstr "Rozmiar komórki (m)" +msgstr "Gizmo (uchwyty przesuwania, obrotu i skali)" msgid "Base height (m)" msgstr "Wysokość bazowa (m)" -msgid "Texture (optional)" -msgstr "Tekstura (opcjonalnie)" - -msgid "Create flat terrain" -msgstr "Utwórz płaski teren" - -msgid "Chunks / side" -msgstr "Fragmenty / bok" - -msgid "Create chunked terrain" -msgstr "Utwórz teren z fragmentów" - -msgid "total %d x %d m, %d chunks" -msgstr "razem %d x %d m, fragmentów: %d" - -msgid "Chunk edit mode (LMB add neighbour / Shift = delete)" -msgstr "Tryb edycji fragmentów (LPM dodaje sąsiedni / Shift = usuń)" - -msgid "Grid chunks: %zu" -msgstr "Fragmenty siatki: %zu" - -msgid "Streaming (open world, follows camera)" -msgstr "Strumieniowanie (otwarty świat, za kamerą)" - -msgid "Stream radius" -msgstr "Promień strumieniowania" - -msgid "Persist edits to disk (16-bit)" -msgstr "Zapisuj zmiany na dysku (16-bit)" - -msgid "Stream terrain" -msgstr "Strumieniuj teren" - -msgid "Resident chunks: %zu (dir: %s)" -msgstr "Wczytane fragmenty: %zu (katalog: %s)" - -msgid "Patches: %zu" -msgstr "Łaty: %zu" - -msgid "Capture selected model as terrain" -msgstr "Zamień zaznaczony model na teren" - -msgid "Capture: select a model instance first" -msgstr "Zamiana: najpierw zaznacz instancję modelu" - -msgid "Brush radius" -msgstr "Promień pędzla" - -msgid "Brush strength" -msgstr "Siła pędzla" - -msgid "Raise centre" -msgstr "Podnieś środek" - -msgid "Lower centre" -msgstr "Obniż środek" - -msgid "Optimize (mesh simplification, all patches)" -msgstr "Optymalizacja (upraszczanie siatki, wszystkie łaty)" - -msgid "Flatness tol (m)" -msgstr "Tolerancja płaskości (m)" - -msgid "Auto-optimize after sculpt" -msgstr "Optymalizuj po rzeźbieniu" - -msgid "Optimize all" -msgstr "Optymalizuj wszystko" - -msgid "Full-res all" -msgstr "Pełna rozdzielczość wszędzie" - -msgid "Triangles: %zu / %zu" -msgstr "Trójkąty: %zu / %zu" - msgid "Show orthophoto (geoportal.gov.pl)" msgstr "Pokaż ortofotomapę (geoportal.gov.pl)" msgid "Scenery origin (0,0,0) in PUWG 1992 / EPSG:2180" -msgstr "Środek scenerii (0,0,0) w PUWG 1992 / EPSG:2180" +msgstr "Punkt odniesienia scenerii (0,0,0) w układzie PUWG 1992 / EPSG:2180" msgid "X (northing, m)" -msgstr "X (northing, m)" +msgstr "X (północ, m)" msgid "Y (easting, m)" -msgstr "Y (easting, m)" +msgstr "Y (wschód, m)" msgid "Enter the origin of this scenery first" -msgstr "Najpierw podaj środek tej scenerii" +msgstr "Najpierw podaj punkt odniesienia tej scenerii" msgid "Camera at X %.1f Y %.1f" msgstr "Kamera w X %.1f Y %.1f" @@ -3992,7 +3172,7 @@ msgid "Refit" msgstr "Dopasuj ponownie" msgid "Samples the ground again, after it was edited" -msgstr "Ponownie próbkuje grunt, po jego edycji" +msgstr "Dopasowuje zdjęcia od nowa, po zmianach terenu" msgid "Lift above ground (m)" msgstr "Uniesienie nad grunt (m)" @@ -4004,7 +3184,7 @@ msgid "Height (m)" msgstr "Wysokość (m)" msgid "Covered by objects and terrain" -msgstr "Zasłaniane przez obiekty i teren" +msgstr "Chowaj za obiektami i terenem" msgid "Draws the imagery as part of the scene, so tracks, models and terrain in front of it hide it.\nIt is lit like the scene. Fully opaque, mouse placement lands on it as on any surface;\ntranslucent, it doesn't catch the mouse.\nNot available with the Better Renderer, which doesn't pick up geometry added in the editor." msgstr "Rysuje zdjęcia jako część sceny, więc tory, modele i teren przed nimi je zasłaniają.\nJest oświetlone jak scena. W pełni kryjące łapie mysz przy wstawianiu jak każda powierzchnia;\nprzezroczyste nie łapie myszy.\nNiedostępne z Better Renderer, który nie widzi geometrii dodanej w edytorze." @@ -4019,7 +3199,7 @@ msgid "Newest" msgstr "Najnowsze" msgid "4K tiles where available" -msgstr "Kafle 4K, gdzie są dostępne" +msgstr "Użyj kafli 4K, gdzie są" msgid "4096 px tiles (~6 cm/px) from the high-resolution orthophoto, for the tiles around the camera.\nAreas it doesn't cover use the standard imagery. Each 4K tile takes ~85 MB of video memory." msgstr "Kafle 4096 px (~6 cm/px) z ortofotomapy wysokiej rozdzielczości, dla kafli wokół kamery.\nTam, gdzie jej nie ma, zwykłe zdjęcia. Każdy kafel 4K zajmuje ~85 MB pamięci karty." @@ -4028,35 +3208,32 @@ msgid "Tiles: %d shown, %d loading, %d failed" msgstr "Kafle: wyświetlane %d, wczytywane %d, nieudane %d" msgid "Retry" -msgstr "Ponów" +msgstr "Pobierz ponownie" msgid "This build has no HTTP client, only cached tiles are shown" -msgstr "Ten build nie ma klienta HTTP, widać tylko kafle z pamięci podręcznej" +msgstr "Ta wersja nie umie pobierać kafli, widać tylko te z pamięci podręcznej" msgid "Cache: %s" msgstr "Pamięć podręczna: %s" msgid "Clear cache" -msgstr "Wyczyść cache" +msgstr "Wyczyść pamięć podręczną" msgid "Deletes downloaded tiles from disk. They are fetched again around the camera." -msgstr "Usuwa pobrane kafle z dysku. Zostaną ponownie pobrane wokół kamery." +msgstr "Usuwa pobrane kafle z dysku. Wokół kamery pobierają się od nowa" msgid "Imagery: GUGiK, geoportal.gov.pl" msgstr "Zdjęcia: GUGiK, geoportal.gov.pl" msgid "Put all instances of the selected model on the ground" -msgstr "Postaw wszystkie instancje zaznaczonego modelu na ziemi" +msgstr "Postaw na ziemi wszystkie kopie zaznaczonego modelu" msgid "Select a model in the scene. Every instance of the same model file is moved straight up or down\nto the ground under it: terrain, large models and terrain made in the editor." -msgstr "Zaznacz model w scenie. Każda instancja tego samego pliku modelu zostanie przesunięta pionowo\nna grunt pod nią: teren, duże modele i teren zrobiony w edytorze." +msgstr "Zaznacz model w scenie. Każda kopia tego samego pliku modelu przesuwa się pionowo\nna grunt pod nią: teren, duże modele i teren zrobiony w edytorze" msgid "LMB: add outline point Backspace: remove last point" msgstr "LPM: dodaj punkt obrysu Backspace: usuń ostatni punkt" -msgid "Points: %zu Area: %.0f m2 (%.2f ha)" -msgstr "Punkty: %zu Powierzchnia: %.0f m2 (%.2f ha)" - msgid "Remove last point" msgstr "Usuń ostatni punkt" @@ -4064,7 +3241,7 @@ msgid "Clear outline" msgstr "Wyczyść obrys" msgid "%zu templates in set" -msgstr "%zu szablonów w zestawie" +msgstr "Modele w zestawie: %zu" msgid "Density (objects/ha)" msgstr "Gęstość (obiektów/ha)" @@ -4091,13 +3268,13 @@ msgid "Fixed count" msgstr "Stała liczba" msgid "Fit length" -msgstr "Dopasuj do długości" +msgstr "Stała długość" msgid "Count" msgstr "Liczba" msgid "Number of the models in the array, the selected one included" -msgstr "Liczba modeli w tablicy, łącznie z zaznaczonym" +msgstr "Liczba modeli w szeregu, łącznie z zaznaczonym" msgid "As many copies are made as fit in this length, measured along the row" msgstr "Powstaje tyle kopii, ile mieści się w tej długości, mierzonej wzdłuż rzędu" @@ -4124,31 +3301,31 @@ msgid "Each copy is turned by this much around the vertical axis against the one msgstr "Każda kopia jest obrócona o tyle wokół osi pionowej względem poprzedniej,\na rząd wygina się razem z nimi w łuk" msgid "Follow the ground" -msgstr "Za gruntem" +msgstr "Po terenie" msgid "The copies are put as high over the ground as the model is, instead of level with it.\nNot for the models large enough to count as ground themselves" msgstr "Kopie stoją tak wysoko nad gruntem jak model, zamiast na jego poziomie.\nNie dla modeli tak dużych, że same liczą się jako grunt" msgid "Model %.2f x %.2f x %.2f m, %d in the array, %.2f m apart" -msgstr "Model %.2f x %.2f x %.2f m, w tablicy %d, co %.2f m" +msgstr "Model %.2f x %.2f x %.2f m, w szeregu %d, co %.2f m" msgid "The array follows the settings" -msgstr "Tablica idzie za ustawieniami" +msgstr "Szereg idzie za ustawieniami" msgid "The copies stay the way they are. Selecting or changing anything else does the same" msgstr "Kopie zostają tak, jak są. To samo daje zaznaczenie lub zmiana czegokolwiek innego" msgid "Make array" -msgstr "Utwórz tablicę" +msgstr "Utwórz szereg" msgid "Select a model to make an array of" -msgstr "Zaznacz model, z którego ma powstać tablica" +msgstr "Zaznacz model, z którego ma powstać szereg" msgid "Extent (m), wheel" msgstr "Zasięg (m), kółko" msgid "Extent (m), wheel / MMB pan" -msgstr "Zasięg (m), kółko / przesuwanie kółkiem" +msgstr "Zasięg (m)" msgid "Wheel zooms. Hold the wheel and drag to pan." msgstr "Kółko przybliża. Wciśnij kółko i przeciągnij, żeby przesunąć widok." @@ -4178,7 +3355,7 @@ msgid "Scale (E)" msgstr "Skaluj (E)" msgid "Local space (R)" -msgstr "Układ lokalny (R)" +msgstr "Lokalne osie (R)" msgid "Snap (hold Ctrl)" msgstr "Krok (przytrzymaj Ctrl)" @@ -4186,24 +3363,18 @@ msgstr "Krok (przytrzymaj Ctrl)" msgid "No node selected" msgstr "Nic nie zaznaczono" -msgid "Editor History" -msgstr "Historia edytora" - msgid "Max history size" msgstr "Maks. rozmiar historii" msgid "Max placement distance" -msgstr "Maks. odległość wstawiania" +msgstr "Maks. odległość stawiania" msgid "History (newest at end): %zu entries" -msgstr "Historia (najnowsze na końcu): %zu wpisów" +msgstr "Wpisy historii (najnowsze na końcu): %zu" msgid "Clear History" msgstr "Wyczyść historię" -msgid "Undo Selected" -msgstr "Cofnij zaznaczone" - # scenery editor: modes, layers and messages msgid "Set##insert" @@ -4224,9 +3395,6 @@ msgstr "Zapisuje całą scenę do plików _export obok scenariusza, pliki scener msgid "Mode windows" msgstr "Okna trybów" -msgid "Toolset" -msgstr "Przybornik" - msgid "Layers" msgstr "Warstwy" @@ -4240,7 +3408,7 @@ msgid "Manual list" msgstr "Lista ręczna" msgid "Insert" -msgstr "Wstaw" +msgstr "Wstawianie" msgid "Brush" msgstr "Pędzel" @@ -4276,7 +3444,7 @@ msgid "The scenario file can't be merged into another layer" msgstr "Pliku scenariusza nie da się scalić z inną warstwą" msgid "Layer \"%s\" can't take new nodes" -msgstr "Warstwa \"%s\" nie może przyjąć nowych węzłów" +msgstr "Warstwa \"%s\" nie może przyjąć nowych obiektów" msgid "New layer, its file is created on save" msgstr "Nowa warstwa, jej plik powstanie przy zapisie" @@ -4288,7 +3456,7 @@ msgid "Scenery file" msgstr "Plik scenerii" msgid "Layer \"%s\" created and made active. Its file is written on save." -msgstr "Warstwa \"%s\" utworzona i ustawiona jako aktywna. Jej plik zostanie zapisany przy zapisie." +msgstr "Warstwa \"%s\" utworzona i ustawiona jako aktywna. Jej plik powstanie przy zapisie scenerii" msgid "Layer \"%s\" removed. It can be restored until the scenery is saved." msgstr "Warstwa \"%s\" usunięta. Można ją przywrócić, dopóki sceneria nie zostanie zapisana." @@ -4324,10 +3492,10 @@ msgid "Selected model package is empty" msgstr "Wybrany zestaw modeli jest pusty" msgid "Density too low for this area, nothing to place" -msgstr "Za mała gęstość na ten obszar, nie ma czego wstawić" +msgstr "Za mała gęstość na ten obszar, nie ma czego postawić" msgid "Placed %zu of %zu objects" -msgstr "Wstawiono %zu z %zu obiektów" +msgstr "Postawiono %zu z %zu obiektów" msgid "Model set" msgstr "Zestaw modeli" @@ -4342,7 +3510,7 @@ msgid "The offsets come to nothing, the copies would sit in the model" msgstr "Odsunięcia dają zero, kopie siedziałyby w modelu" msgid "An array of one is the model itself, nothing to make" -msgstr "Tablica z jednego to sam model, nie ma czego tworzyć" +msgstr "Szereg z jednego to sam model, nie ma czego tworzyć" msgid "The length takes no copy, nothing to make" msgstr "Ta długość nie mieści żadnej kopii, nie ma czego tworzyć" @@ -4354,7 +3522,7 @@ msgid "No copies with these settings" msgstr "Przy tych ustawieniach nie ma kopii" msgid "The model and %zu copies; Ctrl+Z takes the array back" -msgstr "Model i kopie: %zu; Ctrl+Z cofa tablicę" +msgstr "Model i kopie: %zu; Ctrl+Z cofa szereg" msgid "Include selected, no gizmo under rotate or scale" msgstr "Zaznaczony include, bez gizma przy obrocie lub skali" @@ -4363,19 +3531,13 @@ msgid "Include selected, axes as its template allows" msgstr "Zaznaczony include, osie na tyle, na ile pozwala szablon" msgid "B: bend along %s" -msgstr "B: wygnij wzdłuż %s" +msgstr "B: wygnij wzdłuż toru %s" msgid "Click: the reference point, where it lands on the model" -msgstr "Klik: punkt odniesienia tam, gdzie trafi w model" - -msgid "Drag: offset from the track" -msgstr "Przeciągnij: odsunięcie od toru" - -msgid "Shift: height Ctrl: along the track" -msgstr "Shift: wysokość Ctrl: wzdłuż toru" +msgstr "LPM: punkt odniesienia tam, gdzie trafi w model" msgid "Click: edit the bent model" -msgstr "Klik: edytuj wygięty model" +msgstr "LPM: edytuj wygięty model" msgid "Bend it along the track (B)" msgstr "Wygnij wzdłuż toru (B)" @@ -4390,16 +3552,16 @@ msgid "Platform" msgstr "Peron" msgid "Puts the edge facing the track at the distance chosen from its axis, set back by the widening of the gauge in the curves.\nStands on the plane of the rail heads carried on to its edge: lower on the inner side of a canted curve,\nhigher on the outer one; on the inner side also set back by the cant times the height of the edge / 1.5" -msgstr "Ustawia krawędź od strony toru w wybranej odległości od jego osi, odsuniętą o poszerzenie skrajni na łukach.\nStoi na płaszczyźnie główek szyn przedłużonej do krawędzi: niżej po wewnętrznej stronie łuku z przechyłką,\nwyżej po zewnętrznej; po wewnętrznej dodatkowo odsunięty o przechyłkę razy wysokość krawędzi / 1,5" +msgstr "Ustawia krawędź od strony toru w wybranej odległości od jego osi, odsuniętą o poszerzenie skrajni na łukach.\nStoi na płaszczyźnie główek szyn przedłużonej do krawędzi: niżej po wewnętrznej stronie łuku z przechyłką,\nwyżej po zewnętrznej; po wewnętrznej dodatkowo odsunięty o przechyłkę razy wysokość krawędzi / 1.5" msgid "1.725 m: lines of out-of-gauge loads, older platforms" -msgstr "1,725 m: linie przesyłek z przekroczoną skrajnią, starsze perony" +msgstr "1.725 m: linie dla przesyłek z przekroczoną skrajnią, starsze perony" msgid "1.675 m: nominal by the 0.55 and 0.76 m platforms" -msgstr "1,675 m: nominalnie przy peronach 0,55 i 0,76 m" +msgstr "1.675 m: nominalna przy peronach 0.55 i 0.76 m" msgid "1.650 m: the least allowed in service" -msgstr "1,650 m: najmniej dopuszczalne w eksploatacji" +msgstr "1.650 m: najmniejsza dopuszczalna w eksploatacji" msgid "edge of the platform from the axis" msgstr "krawędź peronu od osi toru" @@ -4436,7 +3598,7 @@ msgid "removes the selected signal with its event" msgstr "usuwa zaznaczony sygnalizator razem z jego eventem" msgid "Click: select the signal %s, drag: move it along the track" -msgstr "Klik: zaznacz sygnalizator %s, przeciągnij: przesuń go wzdłuż toru" +msgstr "LPM: zaznacz sygnalizator %s, przeciągnij: przesuń go wzdłuż toru" msgid "Choose the type of the signal in the Tracks window first" msgstr "Najpierw wybierz typ sygnalizatora w oknie Tory" @@ -4445,7 +3607,7 @@ msgid "Semaphore" msgstr "Semafor" msgid "Automatic block" -msgstr "SBL" +msgstr "Semafor samoczynny" msgid "Warning signal" msgstr "Tarcza ostrzegawcza" @@ -4454,7 +3616,7 @@ msgid "Shunting signal" msgstr "Tarcza manewrowa" msgid "Stop signal" -msgstr "Zaporowy" +msgstr "Tarcza zaporowa" msgid "Substitute signal" msgstr "Sygnał zastępczy" @@ -4463,19 +3625,19 @@ msgid "Repeater" msgstr "Powtarzacz" msgid " on the right of the trains" -msgstr " po prawej pociągów" +msgstr " po prawej w kierunku jazdy" msgid " on the left of the trains" -msgstr " po lewej pociągów" +msgstr " po lewej w kierunku jazdy" msgid "Click: %s %s (%s)%s, the train doesn't read it" -msgstr "Klik: %s %s (%s)%s, pociąg go nie odczytuje" +msgstr "LPM: %s %s (%s)%s, pociąg go nie odczytuje" msgid "Drag along: the trains it faces run the way of the drag" msgstr "Przeciągnij wzdłuż: pociągi, do których jest zwrócony, jadą w stronę przeciągnięcia" msgid "Click: %s %s (%s)%s, read by %s %s of %s" -msgstr "Klik: %s %s (%s)%s, pociąg odczytuje go przez %s %s toru %s" +msgstr "LPM: %s %s (%s)%s, pociąg odczytuje go przez %s %s toru %s" msgid "Signal %s moved" msgstr "Sygnalizator %s przesunięty" @@ -4511,7 +3673,7 @@ msgid "No template of this kind matches the filter" msgstr "Żaden szablon tego rodzaju nie pasuje do filtra" msgid "%zu variants, e.g. %s" -msgstr "wariantów: %zu, np. %s" +msgstr "Wariantów: %zu, np. %s" msgid "The train doesn't read it" msgstr "Pociąg go nie odczytuje" @@ -4523,7 +3685,7 @@ msgid "Variant of the chosen type. A head leaning to one side is swapped for its msgstr "Wariant wybranego typu. Głowica odchylona w jedną stronę zostaje zamieniona na odpowiednik odchylony w stronę toru, gdy sygnalizator stanie po drugiej stronie" msgid "The train doesn't read it: it repeats what the signal ahead of it shows" -msgstr "Pociąg go nie odczytuje: powtarza to, co pokazuje sygnalizator przed nim" +msgstr "Pociąg go nie odczytuje: powtarza to, co pokazuje następny sygnalizator" msgid "The train reads it by %s on the path it stands by" msgstr "Pociąg odczytuje go przez %s na torze, przy którym stoi" @@ -4535,10 +3697,10 @@ msgid "Read by %s %s of %s" msgstr "Odczytywany przez %s %s toru %s" msgid "No path has %s among its events: the train doesn't see it" -msgstr "Żaden tor nie ma %s wśród swoich eventów: pociąg go nie widzi" +msgstr "Żaden tor nie ma %s wśród swoich eventów: pociąg go nie odczytuje" msgid "Name, plate and the linked signal: in the node properties. Del removes it with its event" -msgstr "Nazwa, tablica i powiązany sygnalizator: we właściwościach węzła. Del usuwa go razem z eventem" +msgstr "Nazwę, tablicę i powiązany sygnalizator zmienisz w Inspektorze, Del usuwa go razem z eventem" msgid "No signal templates: no template of the scenery directory is described with category: signal. tools/editor/signal_headers.py writes the descriptions into the signal templates." msgstr "Brak szablonów sygnalizatorów: żaden szablon katalogu scenerii nie ma opisu z category: signal. tools/editor/signal_headers.py dopisuje opisy do szablonów sygnalizatorów." @@ -4547,7 +3709,7 @@ msgid "Name of the next signal; it counts up after each one placed. Its events a msgstr "Nazwa następnego sygnalizatora; po każdym postawionym rośnie o jeden. Od niej biorą nazwy jego eventy i komórka pamięci" msgid "auto: %s" -msgstr "auto: %s" +msgstr "domyślnie: %s" msgid "Plate" msgstr "Tablica" @@ -4562,10 +3724,10 @@ msgid "Linked signal" msgstr "Powiązany sygnalizator" msgid "(p7) of the template: the warning signal a semaphore drives, the previous block signal, \"none\" for no link" -msgstr "(p7) szablonu: tarcza ostrzegawcza, którą steruje semafor, poprzedni semafor SBL, \"none\": bez powiązania" +msgstr "Semafor: tarcza ostrzegawcza, którą steruje. Semafor samoczynny: poprzedni semafor. \"none\": bez powiązania (p7 szablonu)" msgid "Distance of the mast from the axis of the track. 2.25 m is what the signals of the sceneries stand at" -msgstr "Odległość masztu od osi toru. Na 2,25 m stoją sygnalizatory w sceneriach" +msgstr "Odległość masztu od osi toru. W sceneriach sygnalizatory stoją 2.25 m od osi" msgid "Over the rails (m)" msgstr "Nad szynami (m)" @@ -4612,9 +3774,6 @@ msgstr "Grupa" msgid "Double click: fly to it" msgstr "Dwuklik: kamera leci do niego" -msgid "By the track" -msgstr "Przy torze" - msgid "Checks" msgstr "Kontrola" @@ -4628,19 +3787,19 @@ msgid "What the check finds is listed in its window" msgstr "Wyniki kontroli są w jej oknie" msgid "Unpin: the dock hides at the edge of the window, its tabs show it over the view (Ctrl+Space: all docks)" -msgstr "Odepnij: dok chowa się przy krawędzi okna, jego zakładki wysuwają go nad widok (Ctrl+Spacja: wszystkie doki)" +msgstr "Odepnij: panel chowa się przy krawędzi okna, a jego zakładka wysuwa go nad widok (Ctrl+Spacja: wszystkie panele)" msgid "Pin: the dock goes back to its place" -msgstr "Przypnij: dok wraca na swoje miejsce" +msgstr "Przypnij: panel wraca na swoje miejsce" msgid "Unpin (hide at the edge)" msgstr "Odepnij (schowaj przy krawędzi)" msgid "Unpin all docks" -msgstr "Odepnij wszystkie doki" +msgstr "Odepnij wszystkie panele" msgid "Pin the docks back" -msgstr "Przypnij doki z powrotem" +msgstr "Przypnij panele z powrotem" msgid "Previews" msgstr "Podglądy" @@ -4651,9 +3810,6 @@ msgstr "%d px" msgid "No previews yet: they're made by eu07 -generate-nodebank-previews" msgstr "Brak podglądów: tworzy je eu07 -generate-nodebank-previews" -msgid "Scenery templates (.inc) are listed with the previews off" -msgstr "Szablony scenerii (.inc) są na liście przy wyłączonych podglądach" - msgid "not a model" msgstr "to nie model" @@ -4667,7 +3823,7 @@ msgid "Green, as the rest of the simulator" msgstr "Zielone, jak reszta symulatora" msgid "Amber, of the editor" -msgstr "Bursztynowe, edytora" +msgstr "Bursztynowe, własne edytora" msgid "Surroundings" msgstr "Otoczenie" @@ -4693,11 +3849,8 @@ msgstr "Drogi i skrzyżowania, przejazdy, punkty pojawiania się i znikania ruch msgid "Heightmap terrain: chunks, sculpting, painting, water, orthophoto" msgstr "Teren z mapy wysokości: kafle, rzeźbienie, malowanie, woda, ortofotomapa" -msgid "Terrain patches and chunks, sculpting, streaming, orthophoto" -msgstr "Płaty i kafle terenu, rzeźbienie, strumieniowanie, ortofotomapa" - msgid "LMB picks the models, as in the surroundings" -msgstr "LPM zaznacza modele, jak w otoczeniu" +msgstr "LPM zaznacza modele, jak w dziale Otoczenie" msgid "LMB raises the terrain under the brush, Shift+LMB lowers it" msgstr "LPM podnosi teren pod pędzlem, Shift+LPM go obniża" @@ -4709,7 +3862,7 @@ msgid "LMB adds a chunk next to the clicked one, Shift+LMB deletes it" msgstr "LPM dodaje kafel obok klikniętego, Shift+LPM go usuwa" msgid "A trainset on the selected track: written to the scenery on save, and to drive at once" -msgstr "Skład na zaznaczonym torze: zapisywany do scenerii przy zapisie, od razu do jazdy" +msgstr "Stawia skład na zaznaczonym torze. Możesz nim od razu jechać, a przy zapisie trafia do scenerii" msgid "%zu groups taken apart" msgstr "Rozgrupowano grup: %zu" @@ -4718,7 +3871,7 @@ msgid "%zu models copied; Ctrl+V puts them at the cursor" msgstr "Skopiowano modeli: %zu; Ctrl+V wstawia je przy kursorze" msgid "%zu models grouped: a click on one selects them all; Ctrl+Shift+G takes the group apart" -msgstr "Zgrupowano modeli: %zu; klik na jednym zaznacza wszystkie, Ctrl+Shift+G rozgrupowuje" +msgstr "Zgrupowano modeli: %zu; LPM na jednym zaznacza wszystkie, Ctrl+Shift+G rozgrupowuje" msgid "%zu models pasted; Ctrl+Z takes them back" msgstr "Wklejono modeli: %zu; Ctrl+Z cofa" @@ -4730,7 +3883,7 @@ msgid "%zu models selected; Shift+LMB adds or takes one out" msgstr "Zaznaczono modeli: %zu; Shift+LPM dodaje lub odejmuje model" msgid "Add a model to the selection, or take it out" -msgstr "Dodaje model do zaznaczenia albo go odejmuje" +msgstr "Dodaj model do zaznaczenia albo go z niego zdejmij" msgid "Add a track to the selection, drag: a box" msgstr "Dodaje tor do zaznaczenia, przeciąganie: ramka" @@ -4778,7 +3931,7 @@ msgid "Deletes the selected models" msgstr "Usuwa zaznaczone modele" msgid "Deletes what's selected: a model, tracks, a road piece or point, a signal, an include of a scenery template" -msgstr "Usuwa to, co zaznaczone: model, tory, kawałek lub punkt drogi, sygnalizator, include szablonu scenerii" +msgstr "Usuwa to, co zaznaczone: model, tory, odcinek lub punkt drogi, sygnalizator, include szablonu scenerii" msgid "Drop the selected onto the ground, each model of a group by itself" msgstr "Opuść zaznaczone na ziemię, każdy model grupy osobno" @@ -4790,7 +3943,7 @@ msgid "Enter / Backspace" msgstr "Enter / Backspace" msgid "Fields of work" -msgstr "Tryby pracy" +msgstr "Działy" msgid "Find a track by its name" msgstr "Znajdź tor po nazwie" @@ -4817,10 +3970,10 @@ msgid "Keyboard shortcuts" msgstr "Skróty klawiszowe" msgid "Lay track / switch / turntable" -msgstr "Układanie toru / rozjazd / obrotnica" +msgstr "Układanie / Rozjazd / Obrotnica" msgid "Laying track: lay up to the cursor / take the last piece back" -msgstr "Układanie toru: połóż do kursora / cofnij ostatni odcinek" +msgstr "Układanie toru: ułóż do kursora / cofnij ostatni odcinek" msgid "Leave the editor" msgstr "Wyjście z edytora" @@ -4852,8 +4005,8 @@ msgstr "Wklej" msgid "Paste here" msgstr "Wklej tutaj" -msgid "Profile / speed / joints / infrastructure" -msgstr "Profil / prędkość / styki / infrastruktura" +msgid "Profile / speed / joints" +msgstr "Profil / Prędkość / Styki" msgid "Puts each selected model, also each model of their groups, down onto the terrain or the object under it" msgstr "Opuszcza każdy zaznaczony model, także każdy model ich grup, na teren albo obiekt pod nim" @@ -4865,7 +4018,7 @@ msgid "Puts the copied models here, around this point as they were around the se msgstr "Wstawia skopiowane modele tutaj, rozłożone wokół tego punktu tak jak wokół zaznaczonego" msgid "Quit" -msgstr "Wyjście" +msgstr "Wyjdź" msgid "RMB held, E / Q" msgstr "PPM wciśnięty, E / Q" @@ -4895,19 +4048,19 @@ msgid "Select a model to copy" msgstr "Zaznacz model do skopiowania" msgid "Select a model, or place one in the insert and brush modes" -msgstr "Zaznacz model albo postaw go w trybach wstawiania i pędzla" +msgstr "Zaznacz model albo postaw go (narzędzia Wstawianie i Pędzel)" msgid "Select tool; cancels what's going on first" -msgstr "Narzędzie zaznaczania; najpierw przerywa to, co trwa" +msgstr "Narzędzie Zaznaczanie; najpierw przerywa to, co trwa" msgid "Select two models or more to group them: Shift+LMB adds one to the selection" msgstr "Zaznacz co najmniej dwa modele, by je zgrupować: Shift+LPM dodaje model do zaznaczenia" msgid "Select, insert, brush, area fill, copy to bank" -msgstr "Zaznaczanie, wstawianie, pędzel, wypełnianie obszaru, kopiowanie do banku" +msgstr "Zaznaczanie, Wstawianie, Pędzel, Wypełnianie obszaru, Kopiuj do banku" msgid "Select: add a point, or take it out" -msgstr "Zaznaczanie: dodaj punkt albo go odejmij" +msgstr "Zaznaczanie: dodaj punkt do zaznaczenia albo go z niego zdejmij" msgid "Signals / objects along the track" msgstr "Sygnalizatory / obiekty wzdłuż toru" @@ -4921,9 +4074,6 @@ msgstr "Podziel drogę przy kursorze albo zrób tam punkt" msgid "Split the selected track at the cursor" msgstr "Podziel zaznaczony tor przy kursorze" -msgid "Straight / curve" -msgstr "Prosta / łuk" - msgid "Takes the last change back" msgstr "Cofa ostatnią zmianę" @@ -4934,7 +4084,7 @@ msgid "The handles along the axes of the selected model instead of the axes of t msgstr "Uchwyty wzdłuż osi zaznaczonego modelu zamiast osi świata" msgid "The keys of the editor, for each field of work" -msgstr "Klawisze edytora w każdym trybie pracy" +msgstr "Klawisze edytora w każdym dziale" msgid "The models of the group move each by itself again" msgstr "Modele grupy znów poruszają się osobno" @@ -4943,22 +4093,22 @@ msgid "The selected model isn't in a group" msgstr "Zaznaczony model nie jest w grupie" msgid "The selected models move together from now on; a click on one selects them all" -msgstr "Zaznaczone modele poruszają się odtąd razem; klik na jednym zaznacza wszystkie" +msgstr "Zaznaczone modele poruszają się odtąd razem; LPM na jednym zaznacza wszystkie" msgid "The selected models move together from now on; a click on one selects them all. Shift+LMB adds a model to the selection" msgstr "Zaznaczone modele poruszają się odtąd razem; klik na jednym zaznacza wszystkie. Shift+LPM dodaje model do zaznaczenia" msgid "The view straight down without perspective, as on a map; again for the camera as it was" -msgstr "Widok z góry bez perspektywy, jak na mapie; ponownie: kamera jak była" +msgstr "Widok prosto z góry bez perspektywy, jak na mapie. Drugi raz wraca do poprzedniej kamery" msgid "This list of shortcuts" msgstr "Ta lista skrótów" msgid "Top view, orthographic" -msgstr "Widok z góry, ortogonalny" +msgstr "Widok z góry" msgid "Translate / rotate / scale" -msgstr "Przesuwanie / obrót / skala" +msgstr "Przesuń / Obróć / Skaluj" msgid "Undo" msgstr "Cofnij" @@ -4970,7 +4120,7 @@ msgid "Ungroup" msgstr "Rozgrupuj" msgid "Unpin all docks for the most of the view, or pin them back" -msgstr "Odepnij wszystkie doki dla większego widoku albo przypnij je z powrotem" +msgstr "Odepnij wszystkie panele, żeby widok był większy, albo przypnij je z powrotem" msgid "What can be done with it" msgstr "Co można z nim zrobić" @@ -5016,7 +4166,7 @@ msgstr "Widok z góry, ortogonalny (O)" # scenery editor: tools over the 3d view msgid "Translate" -msgstr "Przesuwanie" +msgstr "Przesunięcie" msgid "Rotate" msgstr "Obrót" @@ -5034,10 +4184,10 @@ msgid "The lanes of the roads drawn as the paths the vehicles take" msgstr "Pasy ruchu dróg rysowane jako tory jazdy pojazdów" msgid "LMB: a road piece, a junction, a point where pieces meet, a level crossing or a traffic point" -msgstr "LPM: kawałek drogi, skrzyżowanie, punkt łączenia kawałków, przejazd kolejowy albo punkt ruchu" +msgstr "LPM zaznacza odcinek drogi, skrzyżowanie, punkt łączenia odcinków, przejazd kolejowy albo punkt ruchu" msgid "LMB: start a road, then each next point; on a loose end, the side of a road or a junction it's joined to them" -msgstr "LPM: początek drogi, potem każdy kolejny punkt; na wolnym końcu, boku drogi albo skrzyżowaniu droga zostaje do nich dołączona" +msgstr "LPM zaczyna drogę, potem stawia każdy kolejny punkt. Na wolnym końcu, boku drogi albo skrzyżowaniu dołącza do nich drogę" msgid "Level crossings, and the points where vehicles appear on the roads or are taken off them" msgstr "Przejazdy kolejowe oraz punkty, w których pojazdy pojawiają się na drogach albo są z nich zdejmowane" @@ -5053,7 +4203,7 @@ msgstr "albo przeciągnij tu modele z banku obiektów" # scenery editor: the toolbar with icons, the rows captioned msgid "Work modes" -msgstr "Tryby pracy" +msgstr "Działy" msgid "Tools" msgstr "Narzędzia" @@ -5062,7 +4212,7 @@ msgid "Level crossing" msgstr "Przejazd kolejowy" msgid "Spawn point" -msgstr "Punkt pojawiania" +msgstr "Punkt pojawiania się" msgid "Removal point" msgstr "Punkt znikania" @@ -5086,11 +4236,8 @@ msgstr "Łuk przez kursor" msgid "Shape from a free end" msgstr "Kształt z wolnego końca" -msgid "What a drag or a click from a free end lays towards the cursor; Shift+wheel changes it, Ctrl+wheel the radius" -msgstr "Co przeciągnięcie lub kliknięcie z wolnego końca kładzie w stronę kursora; Shift+kółko zmienia kształt, Ctrl+kółko promień" - msgid "Speed limit of the new paths, and the design speed of the transition curves and of the cant; the speed of the track joined comes first, and none higher than the radius allows" -msgstr "Prędkość nowych torów i prędkość projektowa krzywych przejściowych oraz przechyłki; pierwszeństwo ma prędkość toru, do którego się dołącza, i nigdy nie wyższa, niż pozwala promień" +msgstr "Ograniczenie prędkości nowych torów i prędkość projektowa krzywych przejściowych oraz przechyłki. Pierwszeństwo ma prędkość toru, do którego się dołącza; nigdy nie jest wyższa, niż pozwala promień" msgid "Ctrl+wheel: radius, Shift+wheel: arc / straight" msgstr "Ctrl+kółko: promień, Shift+kółko: łuk / prosta" @@ -5125,17 +4272,14 @@ msgstr "Ctrl+kółko" msgid "Shift+wheel" msgstr "Shift+kółko" -msgid "type the radius" -msgstr "wpisz promień" - msgid "moves the control point, the joined track keeps the tangent" -msgstr "przesuwa punkt kontrolny, dołączony tor zachowuje styczność" +msgstr "przesuwa punkt sterujący, dołączony tor zachowuje styczność" msgid "turns the control vector sideways or up, cant at its end, the joined track keeps the tangent" -msgstr "obraca wektor kontrolny w bok lub w górę, przechyłka na jego końcu, dołączony tor zachowuje styczność" +msgstr "obraca wektor sterujący w bok lub w górę, przechyłka na jego końcu; dołączony tor zachowuje styczność" msgid "length of the control vector" -msgstr "długość wektora kontrolnego" +msgstr "długość wektora sterującego" msgid "rings about the point: direction, grade, cant; the joined tracks follow" msgstr "pierścienie wokół punktu: kierunek, pochylenie, przechyłka; dołączone tory idą za nim" @@ -5180,7 +4324,7 @@ msgid "\nCurves recognized from the paths, the scenery keeps no design for them" msgstr "\nŁuki rozpoznane z torów, sceneria nie ma dla nich zapisanego projektu" msgid "Km of the start" -msgstr "Km początku" +msgstr "Kilometraż początku (km)" 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 torów liczy się od niego" @@ -5189,7 +4333,7 @@ msgid "The design kept in %s for this fragment departs from its paths by up to % msgstr "Projekt zapisany w %s dla tego fragmentu odbiega od jego torów o maks. %.2f m" msgid "Take the kept design" -msgstr "Weź zapisany projekt" +msgstr "Wczytaj zapisany projekt" msgid "The kept design is loaded, applying it lays the paths by it again" msgstr "Wczytano zapisany projekt, zastosowanie ułoży tory ponownie według niego" @@ -5198,7 +4342,7 @@ msgid "Loads the design kept in the scenery in place of the one read from the pa msgstr "Wczytuje projekt zapisany w scenerii w miejsce odczytanego z torów; nic się nie zmienia do zastosowania" msgid "Forget the kept design" -msgstr "Zapomnij zapisany projekt" +msgstr "Usuń zapisany projekt" msgid "The kept design will be removed from " msgstr "Zapisany projekt zostanie usunięty z " @@ -5207,7 +4351,7 @@ msgid "The paths stay as they are, the scenery no longer keeps a design for them msgstr "Tory zostają bez zmian, sceneria przestaje przechowywać dla nich projekt" msgid "Digitize all the tracks" -msgstr "Zdigitalizuj wszystkie tory" +msgstr "Zdigitalizuj tylko linie" msgid "Every line of the scenery not digitized yet is kept in it with its straights, arcs and transition curves, the way the drawn tracks are kept; the paths stay as they are" msgstr "Każda jeszcze niezdigitalizowana linia scenerii zostaje w niej zapisana z prostymi, łukami i krzywymi przejściowymi, tak jak rysowane tory; tory zostają tak, jak leżą" @@ -5216,7 +4360,7 @@ msgid "Lines digitized: %d, paths left out: %d" msgstr "Zdigitalizowane linie: %d, pominięte tory: %d" msgid ", the first: " -msgstr ", pierwsza: " +msgstr ", pierwsza przyczyna: " msgid "\nThe line is digitized now, a straight" msgstr "\nLinia zdigitalizowana: prosta" @@ -5249,10 +4393,7 @@ msgid "Type kept in the scenery: %s" msgstr "Typ zapisany w scenerii: %s" msgid "Diverging track R %.0f m: the R %.0f m of the type bent with the main track" -msgstr "Tor zwrotny R %.0f m: R %.0f m typu wygięte razem z torem zasadniczym" - -msgid "Copied from a switch of the scenery: the size is measured from its shape" -msgstr "Skopiowany z rozjazdu scenerii: wymiary zmierzone z jego kształtu" +msgstr "Tor zwrotny R %.0f m: promień typu R %.0f m wygięty razem z torem zasadniczym" msgid "Number##switchnumber" msgstr "Numer##switchnumber" @@ -5267,20 +4408,14 @@ msgid "Rail of the switch: 60E1, 49E1..." msgstr "Szyna rozjazdu: 60E1, 49E1..." msgid "Km of the start##switchkm" -msgstr "Km początku##switchkm" +msgstr "Kilometraż początku (km)##switchkm" msgid "Chainage of the start of the switch; taken from the horizontal alignment kept in the scenery when the switch lies on one" msgstr "Kilometraż początku rozjazdu; brany z planu sytuacyjnego zapisanego w scenerii, gdy rozjazd na nim leży" -msgid "Slip switches and crossings" -msgstr "Rozjazdy krzyżowe i skrzyżowania" - msgid "The kept design of the line went along with its paths" msgstr "Zapisany projekt linii przesunął się razem z jej torami" -msgid "%d path(s) left the design kept in %s, by up to %.2f m: Ctrl+Z takes it back, the route design lays them by the design again or forgets it" -msgstr "Tory (%d) zeszły z projektu zapisanego w %s, maks. o %.2f m: Ctrl+Z cofa, projekt trasy ułoży je ponownie według projektu albo go zapomni" - msgid "The cant differs from the kept design by up to %.0f mm" msgstr "Przechyłka różni się od zapisanego projektu maks. o %.0f mm" @@ -5303,10 +4438,10 @@ msgid "Room on the straight: %.2f m back, %.2f m ahead" msgstr "Miejsce na prostej: %.2f m do tyłu, %.2f m do przodu" msgid "Click: edit this switch, drag: slide it along the straight" -msgstr "Klik: edytuj ten rozjazd, przeciągnij: przesuń go wzdłuż prostej" +msgstr "LPM: edytuj ten rozjazd, przeciągnij: przesuń go wzdłuż prostej" msgid "Click: select this switch, drag: slide it along the straight" -msgstr "Klik: zaznacz ten rozjazd, przeciągnij: przesuń go wzdłuż prostej" +msgstr "LPM: zaznacz ten rozjazd, przeciągnij: przesuń go wzdłuż prostej" msgid "Drive##standing" msgstr "Napęd##standing" @@ -5314,17 +4449,11 @@ msgstr "Napęd##standing" msgid "Puts another drive in place of the drive of this switch, on the same side" msgstr "Stawia inny napęd w miejsce napędu tej zwrotnicy, po tej samej stronie" -msgid "diverging side##standing" -msgstr "od toru zwrotnego##standing" - -msgid "other side##standing" -msgstr "z drugiej strony##standing" - msgid "Puts the drive of the switch on the other side of the track: the same drive, its variant for the other side" msgstr "Stawia napęd zwrotnicy po drugiej stronie toru: ten sam napęd, jego wariant na drugą stronę" msgid "No drive of the switch %s among the described drives" -msgstr "Wśród opisanych napędów nie ma napędu zwrotnicy %s" +msgstr "Zwrotnica %s nie ma napędu z listy modeli napędów" msgid "The drive %s has no variant with the drive on the other side" msgstr "Napęd %s nie ma wariantu z napędem po drugiej stronie" @@ -5375,7 +4504,7 @@ msgid ", the switch moved %.2f m" msgstr ", rozjazd przesunięty o %.2f m" msgid "take back the last piece" -msgstr "cofnij ostatni odcinek" +msgstr "cofa ostatni odcinek" msgid "The last piece was taken back" msgstr "Ostatni odcinek cofnięty" @@ -5383,42 +4512,6 @@ msgstr "Ostatni odcinek cofnięty" msgid "R %.0f m doesn't reach the cursor, the arc goes through it with R %.0f m" msgstr "R %.0f m nie sięga kursora, łuk przechodzi przez niego z R %.0f m" -msgid "First steps" -msgstr "Pierwsze kroki" - -msgid "Laying the first track, step by step" -msgstr "Pierwszy tor krok po kroku" - -msgid "The ticks fill in by themselves, the window doesn't stop anything" -msgstr "Kroki odhaczają się same, okno niczego nie blokuje" - -msgid "F3: the Tracks field of work" -msgstr "F3: dział Tory" - -msgid "L: lay track" -msgstr "L: układanie toru" - -msgid "Click on the ground, then once more: a straight. Each next click lays a piece and goes on from its end, Esc ends" -msgstr "Kliknij na terenie, potem jeszcze raz: prosta. Każdy kolejny klik kładzie odcinek i ciągnie tor dalej z jego końca, Esc kończy" - -msgid "While laying on: Ctrl+wheel changes the radius, Shift+wheel the shape (arc then straight, straight then arc, arc, straight). The plain wheel zooms" -msgstr "W trakcie układania: Ctrl+kółko zmienia promień, Shift+kółko kształt (łuk i prosta, prosta i łuk, łuk, prosta). Samo kółko przybliża widok" - -msgid "Joining: press on a free end (magenta ring) and drag onto another free end" -msgstr "Łączenie: wciśnij na wolnym końcu (różowy pierścień) i przeciągnij na inny wolny koniec" - -msgid "T: switch. Click a straight, then press on it and drag: along sets the direction, sideways the side" -msgstr "T: rozjazd. Kliknij prostą, potem wciśnij na niej i przeciągnij: wzdłuż ustala kierunek, w bok stronę" - -msgid "Ctrl+Z takes a step back, Backspace while laying takes back the last piece" -msgstr "Ctrl+Z cofa krok, Backspace w trakcie układania cofa ostatni odcinek" - -msgid "Ctrl+S saves the scenery" -msgstr "Ctrl+S zapisuje scenerię" - -msgid "RMB held + W S A D: fly the camera, F: fly to the selected, F1: all the keys" -msgstr "Wciśnięty PPM + W S A D: lot kamerą, F: leć do zaznaczonego, F1: wszystkie klawisze" - msgid "No ground under the cursor: track goes onto the level 0 plane, drawn as a grid. Terrain: F5" msgstr "Pod kursorem nie ma terenu: tor trafia na płaszczyznę na wysokości 0, widoczną jako siatka. Teren: F5" @@ -5429,7 +4522,7 @@ msgid "L, LMB, LMB..." msgstr "L, LPM, LPM..." msgid "Lay track: each click lays a piece and goes on from its end" -msgstr "Układanie toru: każdy klik kładzie odcinek i ciągnie tor dalej z jego końca" +msgstr "Układanie toru: każdy klik układa odcinek i ciągnie tor dalej z jego końca" msgid "Lead a track out of it; onto another free end: join the two" msgstr "Wyprowadza z niego tor; na inny wolny koniec: łączy oba" @@ -5444,7 +4537,7 @@ msgid "0-9 while laying" msgstr "0-9 w trakcie układania" msgid "Type the length (or the radius) of the next piece, Enter lays it" -msgstr "Wpisz długość (lub promień) następnego odcinka, Enter go kładzie" +msgstr "Wpisz długość (lub promień) następnego odcinka, Enter go układa" msgid "canyon" msgstr "jar" @@ -5465,7 +4558,7 @@ msgid "No current, coasting" msgstr "Bez napięcia, jazda z rozpędu" msgid "Pantographs lowered, speed limit" -msgstr "Opuszczone pantografy, ograniczenie prędkości" +msgstr "Opuszczone pantografy" msgid "Overhead line" msgstr "Sieć trakcyjna" @@ -5488,23 +4581,14 @@ msgstr "PKP PLK" msgid "Norms" msgstr "Normy" -msgid "Free: plain arcs with no transition curves and no cant, no warnings about the norms, the track goes where it's clicked.\nPKP PLK: transition curves and cant for the design speed, warnings when the radius, the cant or the transitions don't meet the norms" -msgstr "Swobodne: zwykłe łuki bez krzywych przejściowych i przechyłki, bez ostrzeżeń o normach, tor idzie tam, gdzie klikniesz.\nPKP PLK: krzywe przejściowe i przechyłka dla prędkości projektowej, ostrzeżenia, gdy promień, przechyłka lub krzywe przejściowe nie spełniają norm" - -msgid "Click: choose this track, the parallel track goes beside it" -msgstr "Klik: wybierz ten tor, obok niego pójdzie tor równoległy" - msgid "Click: start on the ground beside this track. Shift+click: a parallel track to it, T: a switch into it" -msgstr "Klik: początek na terenie obok tego toru. Shift+klik: tor równoległy do niego, T: rozjazd w nim" +msgstr "LPM: początek na terenie obok tego toru. Shift+LPM: tor równoległy do niego, T: rozjazd w nim" msgid "K: split here" msgstr "K: podziel tutaj" msgid "Shift+click a straight" -msgstr "Shift+klik na prostej" - -msgid "a parallel track beside it, in the panel" -msgstr "tor równoległy obok niej, w panelu" +msgstr "Shift+LPM na prostej" msgid "the direction in steps of 15 degrees" msgstr "kierunek co 15 stopni" @@ -5516,19 +4600,13 @@ msgid "The direction in steps of 15 degrees" msgstr "Kierunek co 15 stopni" msgid "Shift+LMB on a track in L" -msgstr "Shift+LPM na torze w L" - -msgid "Choose it for a parallel track" -msgstr "Wybiera go pod tor równoległy" - -msgid "A second track beside a straight: LMB on the straight, then the side and the spacing" -msgstr "Drugi tor obok prostej: LPM na prostej, potem strona i rozstaw" +msgstr "Shift+LPM na torze przy układaniu (L)" msgid "LMB on a straight" msgstr "LPM na prostej" msgid "Click: the straight the new track goes beside" -msgstr "Klik: prosta, obok której pójdzie nowy tor" +msgstr "LPM: prosta, obok której pójdzie nowy tor" msgid "More" msgstr "Więcej" @@ -5536,35 +4614,23 @@ msgstr "Więcej" msgid "Less" msgstr "Mniej" -msgid "The tools that correct a line already laid: straight, curve, profile, speed, joints, infrastructure, structure gauge" -msgstr "Narzędzia do poprawiania ułożonej linii: prosta, łuk, profil, prędkość, styki, infrastruktura, skrajnia" - -msgid "Norms:" -msgstr "Normy:" - msgid "Plain arcs with no transition curves and no cant, no warnings about the norms: the track goes where it's clicked" msgstr "Zwykłe łuki bez krzywych przejściowych i przechyłki, bez ostrzeżeń o normach: tor idzie tam, gdzie klikniesz" msgid "Transition curves and cant for the design speed, warnings when the radius, the cant or the transitions don't meet the norms" -msgstr "Krzywe przejściowe i przechyłka dla prędkości projektowej, ostrzeżenia, gdy promień, przechyłka albo przejściówki nie spełniają norm" +msgstr "Krzywe przejściowe i przechyłka dla prędkości projektowej; ostrzeżenia, gdy promień, przechyłka albo krzywe przejściowe nie spełniają norm" msgid "Drag to change, Ctrl+click to type the value. While laying: Ctrl+wheel" -msgstr "Przeciągnij, by zmienić, Ctrl+klik, by wpisać. W trakcie układania: Ctrl+kółko" +msgstr "Przeciągnij, by zmienić, Ctrl+LPM, by wpisać. W trakcie układania: Ctrl+kółko" msgid "Speed limit of the new paths; the speed of the track joined comes first" msgstr "Ograniczenie prędkości nowych torów; pierwszeństwo ma prędkość dołączanego toru" -msgid "Drive it: Trainset in the toolbar, pick a locomotive from the list, LMB on the track, then Place and drive" -msgstr "Przejedź się: Skład na pasku narzędzi, wybierz lokomotywę z listy, LPM na torze, potem Postaw i jedź" - -msgid "Parallel track: LMB on a straight, then the side and the spacing" -msgstr "Tor równoległy: LPM na prostej, potem strona i rozstaw" - msgid "The Parallel tool (N) with it" msgstr "Narzędzie Równoległy (N) z tym torem" msgid "Click: the Parallel tool (N) with this track" -msgstr "Klik: narzędzie Równoległy (N) z tym torem" +msgstr "LPM: narzędzie Równoległy (N) z tym torem" msgid "Trainset" msgstr "Skład" @@ -5573,13 +4639,13 @@ msgid "The track has no name, the trainset can't be written to the scenery" msgstr "Tor nie ma nazwy, składu nie da się zapisać do scenerii" msgid "Written to the scenery as a trainset on save (Ctrl+S)" -msgstr "Trafi do scenerii jako trainset przy zapisie (Ctrl+S)" +msgstr "Skład trafi do scenerii przy zapisie (Ctrl+S)" msgid "Only to drive, it isn't written to the scenery" msgstr "Tylko do jazdy, nie trafia do scenerii" msgid "Write to the scenery as a trainset" -msgstr "Zapisz do scenerii jako trainset" +msgstr "Zapisz skład w scenerii" msgid "On: the trainset is written to the scenery on save (Ctrl+S). Off: only to drive now, gone when the scenery is loaded again" msgstr "Włączone: skład trafia do scenerii przy zapisie (Ctrl+S). Wyłączone: tylko do jazdy teraz, znika po ponownym wczytaniu scenerii" @@ -5593,14 +4659,11 @@ msgstr "Rozkład jazdy pociągu; none: bez rozkładu, czeka na polecenia" msgid "Speed at the start (km/h)" msgstr "Prędkość na starcie (km/h)" -msgid "%d trainset(s) to write on save:" -msgstr "%d skład(ów) do zapisania:" - msgid "Not written to the scenery; the vehicles stay until the scenery is loaded again" msgstr "Nie trafi do scenerii; pojazdy zostają do ponownego wczytania scenerii" msgid "%d trainset(s) written to the scenery" -msgstr "Zapisano %d skład(ów) do scenerii" +msgstr "Składy zapisane w scenerii: %d" msgid "Plan" msgstr "Plan" @@ -5608,9 +4671,6 @@ msgstr "Plan" msgid "The plan of the line laid: LMB on a straight drags its ends or the middle, on a curve its vertex and radius. C works too" msgstr "Plan ułożonej linii: LPM na prostej – ciągniesz jej końce lub środek, na łuku – wierzchołek i promień. Działa też C" -msgid "The checks of a line laid: profile, speed, joints, infrastructure, structure gauge" -msgstr "Kontrole ułożonej linii: profil, prędkość, styki, infrastruktura, skrajnia" - msgid "its ends and the middle to drag" msgstr "jej końce i środek do ciągnięcia" @@ -5660,10 +4720,10 @@ msgid "a parallel track (N)" msgstr "tor równoległy (N)" msgid "lay the shape up to here" -msgstr "połóż kształt do tego miejsca" +msgstr "układa kształt do tego miejsca" msgid "lay the straight up to here" -msgstr "połóż prostą do tego miejsca" +msgstr "układa prostą do tego miejsca" msgid "another shape" msgstr "inny kształt" @@ -5716,15 +4776,6 @@ msgstr "łamie ją tam i obraca resztę" msgid "point 1 of 4: leave the axis" msgstr "punkt 1 z 4: zejście z osi" -msgid "point 2 of 4: shifted" -msgstr "punkt 2 z 4: przesunięcie" - -msgid "point 3 of 4: start back" -msgstr "punkt 3 z 4: początek powrotu" - -msgid "point 4 of 4: back on the axis" -msgstr "punkt 4 z 4: powrót na oś" - msgid "Close to another straight" msgstr "Blisko innej prostej" @@ -5771,7 +4822,7 @@ msgid "places a new turntable" msgstr "stawia nową obrotnicę" msgid "Shifts a piece of the straight sideways around an obstacle and back: four clicks on the straight (Shift+click in the view does the same)" -msgstr "Przesuwa kawałek prostej w bok, wokół przeszkody, i z powrotem: cztery kliknięcia na prostej (Shift+klik w widoku robi to samo)" +msgstr "Przesuwa kawałek prostej w bok, wokół przeszkody, i z powrotem: cztery kliknięcia na prostej (to samo robi Shift+LPM w widoku)" msgid "Frog and check rails made from the rails" msgstr "Krzyżownica i kierownice z szyn" @@ -5795,16 +4846,16 @@ msgid "a##designa" msgstr "a##designa" msgid "A double switch is laid as two switches: the first one with its diverging track and the main track up to the second one, which starts there with the other diverging track" -msgstr "Rozjazd podwójny kładzie się jako dwie zwrotnice: pierwszą z jej torem zwrotnym i torem zasadniczym aż do drugiej, która zaczyna się tam z drugim torem zwrotnym" +msgstr "Rozjazd podwójny układa się jako dwie zwrotnice: pierwszą z jej torem zwrotnym i torem zasadniczym aż do drugiej, która zaczyna się tam z drugim torem zwrotnym" msgid "A new own template, an Rz 1:9 R190 to start from" -msgstr "Nowy własny szablon, na początek Rz 1:9 R190" +msgstr "Nowy własny szablon, na początek rozjazd zwyczajny Rz 1:9 R190" msgid "Angle 1:%.3f (%.4f deg), sharpest R %.1f m, a %.3f m, b %.3f m" msgstr "Skos 1:%.3f (%.4f°), najmniejszy R %.1f m, a %.3f m, b %.3f m" msgid "Angle from l and z" -msgstr "Kąt z l i z" +msgstr "Oblicz kąt z l i z" msgid "+ Arc" msgstr "+ Łuk" @@ -5816,13 +4867,10 @@ msgid "b##designb" msgstr "b##designb" msgid "Blade angle, deg##designentry" -msgstr "Kąt iglicy, °##designentry" +msgstr "Kąt iglicy (°)##designentry" msgid "Blade angle, deg##designentry2" -msgstr "Kąt iglicy, °##designentry2" - -msgid "Choose a template on the left. The catalogue ones can be copied to own templates and changed there. A template is a straight main track and the diverging track from the start of the switch as a chain of straights, arcs and transition curves; a double switch is two switches, the second one starting on the main track of the first." -msgstr "Wybierz szablon po lewej. Katalogowe można skopiować do własnych i tam zmieniać. Szablon to prosty tor zasadniczy i tor zwrotny od początku rozjazdu jako ciąg prostych, łuków i krzywych przejściowych; rozjazd podwójny to dwie zwrotnice, druga zaczyna się na torze zasadniczym pierwszej." +msgstr "Kąt iglicy (°)##designentry2" msgid " (copy)" msgstr " (kopia)" @@ -5837,10 +4885,10 @@ msgid "Double switch %s + %s: length %.3f m, the second switch starts %.3f m on" msgstr "Rozjazd podwójny %s + %s: długość %.3f m, druga zwrotnica zaczyna się po %.3f m" msgid "Double switch %s + %s with drives: events %s+, %s- and %s+, %s- switch them, also in the map" -msgstr "Rozjazd podwójny %s + %s z napędami: zdarzenia %s+, %s- i %s+, %s- je przestawiają, także w mapie" +msgstr "Rozjazd podwójny %s + %s z napędami: eventy %s+, %s- i %s+, %s- je przestawiają, także na mapie" msgid "Slip with drives at its switches %s: the events of the name of a switch with + and - switch it, also in the map" -msgstr "Rozjazd krzyżowy z napędami zwrotnic %s: zdarzenia z nazwą zwrotnicy i + albo - ją przestawiają, także w mapie" +msgstr "Rozjazd krzyżowy z napędami zwrotnic %s: eventy z nazwą zwrotnicy i + albo - przestawiają ją, także na mapie" msgid "Double switch %s with its second switch %s inserted" msgstr "Wstawiono rozjazd podwójny %s z drugą zwrotnicą %s" @@ -5852,7 +4900,7 @@ msgid "Fill in" msgstr "Wypełnij" msgid "First switch: diverging %s, end offset %.3f m" -msgstr "Pierwsza zwrotnica: zwrotny w %s, odsunięcie końca %.3f m" +msgstr "Pierwsza zwrotnica: tor zwrotny %s, odsunięcie końca %.3f m" msgid "From the point where the tangents meet to the end, as the catalogue gives it; 0: the main track less a" msgstr "Od punktu przecięcia stycznych do końca, jak podaje katalog; 0: tor zasadniczy minus a" @@ -5867,7 +4915,7 @@ msgid "Keeps the own templates in eu07_switch_templates.txt in the configuration msgstr "Zapisuje własne szablony w eu07_switch_templates.txt w katalogu konfiguracji symulatora i dodaje je do listy narzędzia rozjazdów" msgid "Length %.3f m, the diverging track ends %.3f m beside the main one, in %zu pieces" -msgstr "Długość %.3f m, tor zwrotny kończy się %.3f m obok zasadniczego, w %zu kawałkach" +msgstr "Długość %.3f m, tor zwrotny kończy się %.3f m obok zasadniczego, kawałków: %zu" msgid "Length of the main track, 0: as far as the diverging tracks reach" msgstr "Długość toru zasadniczego, 0: tak daleko, jak sięgają tory zwrotne" @@ -5876,10 +4924,10 @@ msgid "Length of the part, measured along it" msgstr "Długość części, mierzona wzdłuż niej" msgid "l, m##bladelength" -msgstr "l, m##bladelength" +msgstr "l (m)##bladelength" msgid "Main track, m##designmain" -msgstr "Tor zasadniczy, m##designmain" +msgstr "Tor zasadniczy (m)##designmain" msgid "m##length" msgstr "m##length" @@ -5894,16 +4942,16 @@ msgid "Own: " msgstr "Własny: " msgid "Own switch templates: diverging tracks of straights, arcs and transition curves, double switches" -msgstr "Własne szablony rozjazdów: tory zwrotne z prostych, łuków i krzywych przejściowych, rozjazdy podwójne" +msgstr "Otwiera edytor własnych szablonów rozjazdów: tory zwrotne z prostych, łuków i krzywych przejściowych, rozjazdy podwójne" msgid "Place this switch" msgstr "Postaw ten rozjazd" msgid "Pre-blade stretch, m##designlead" -msgstr "Odcinek przediglicowy, m##designlead" +msgstr "Odcinek przediglicowy (m)##designlead" msgid "Pre-blade stretch, m##designlead2" -msgstr "Odcinek przediglicowy, m##designlead2" +msgstr "Odcinek przediglicowy (m)##designlead2" msgid "Radius at the end of the transition curve, 0: into a straight; the curvature changes evenly along it" msgstr "Promień na końcu krzywej przejściowej, 0: w prostą; krzywizna zmienia się wzdłuż niej równomiernie" @@ -5918,7 +4966,7 @@ msgid "Rail##designrail" msgstr "Szyna##designrail" msgid "Rail of the switch kept in its record: 60E1, 49E1..." -msgstr "Szyna rozjazdu zapisywana w jego rekordzie: 60E1, 49E1..." +msgstr "Szyna rozjazdu, np. 60E1, 49E1; zapisuje się przy rozjeździe w scenerii" msgid "Removes the template; Save keeps the change" msgstr "Usuwa szablon; Zapisz utrwala zmianę" @@ -5960,10 +5008,10 @@ msgid "Saves the own templates and chooses this one in the switch tool: then cli msgstr "Zapisuje własne szablony i wybiera ten w narzędziu rozjazdów: potem kliknij prostą i przeciągnij w stronę, w którą idzie rozjazd" msgid "Second switch starts at, m##stagger" -msgstr "Druga zwrotnica zaczyna się po, m##stagger" +msgstr "Początek drugiej zwrotnicy (m)##stagger" msgid "sin angle = z / l: l the length of a straight blade, z the gap between the running edges of the stock rail and the blade at its heel" -msgstr "sin kąta = z / l: l długość iglicy prostej, z odstęp krawędzi tocznych opornicy i iglicy w osadzie" +msgstr "sin kąta = z / l: l długość iglicy prostej, z odstęp krawędzi tocznych opornicy i iglicy przy piętce" msgid "A catalogue template can't be changed" msgstr "Szablonu katalogowego nie można zmieniać" @@ -5987,16 +5035,16 @@ msgid "+ Straight" msgstr "+ Prosta" msgid "straight after, m##quickstraight" -msgstr "prosta za łukiem, m##quickstraight" +msgstr "Prosta za łukiem (m)##quickstraight" msgid "Stretch across" -msgstr "Rozciągnij w poprzek" +msgstr "Rozciągnięcie w poprzek" msgid "Switch templates" msgstr "Szablony rozjazdów" msgid "Template editor..." -msgstr "Edytor szablonów..." +msgstr "Edytuj szablony..." msgid "The angle at which the blade meets the stock rail at its tip: the diverging track starts there at this angle instead of tangentially. Curved blades a fraction of a degree, straight blades 1 to 2 degrees; 0: tangential" msgstr "Kąt oparcia iglicy o opornicę przy ostrzu: tor zwrotny zaczyna się tam pod tym kątem zamiast stycznie. Iglice krzywe ułamek stopnia, proste 1 do 2 stopni; 0: stycznie" @@ -6008,7 +5056,7 @@ msgid "The double switch is replaced by its main track, Shift+Del removes all of msgstr "Rozjazd podwójny zastąpiono jego torem zasadniczym, Shift+Del usuwa całość" msgid "The second switch has a diverging track of its own" -msgstr "Druga zwrotnica ma własny tor zwrotny" +msgstr "Własny tor zwrotny drugiej zwrotnicy" msgid "Tip of the blade %.3f m from the start, the diverging track leaves there at %.4f deg" msgstr "Ostrze iglicy %.3f m od początku, tor zwrotny odchodzi tam pod kątem %.4f°" @@ -6017,10 +5065,10 @@ msgid "Tip of the blade %.3f m from the start, the diverging track leaves tangen msgstr "Ostrze iglicy %.3f m od początku, tor zwrotny odchodzi stycznie" msgid "Tip of a tangent blade, m##designtip" -msgstr "Ostrze iglicy stycznej, m##designtip" +msgstr "Ostrze iglicy stycznej (m)##designtip" msgid "Tip of a tangent blade, m##designtip2" -msgstr "Ostrze iglicy stycznej, m##designtip2" +msgstr "Ostrze iglicy stycznej (m)##designtip2" msgid "With the blade angle 0: the tip of the blade from the start of the switch as its scheme gives it, the diverging track is broken there into pieces; 0: where the blade gets 5 mm thick, less the 125 mm its nose is planed over" msgstr "Przy kącie iglicy 0: ostrze iglicy od początku rozjazdu według jego schematu, tor zwrotny jest tam podzielony na kawałki; 0: tam, gdzie iglica ma 5 mm grubości, minus 125 mm zestruganego dzioba" @@ -6035,7 +5083,7 @@ msgid "Type##designtype" msgstr "Typ##designtype" msgid "z, m##bladegap" -msgstr "z, m##bladegap" +msgstr "z (m)##bladegap" msgid "Unknown type of switch" msgstr "Nieznany typ rozjazdu" @@ -6074,10 +5122,10 @@ msgid "A radius of the diverging track is under 10 m" msgstr "Promień toru zwrotnego jest mniejszy niż 10 m" msgid "%s laid, switches %s" -msgstr "Ułożono: %s, zwrotnice %s" +msgstr "Ułożono: %s, rozjazdy %s" msgid "%s: %d switches, spacing %.2f m" -msgstr "%s: zwrotnic %d, rozstaw %.2f m" +msgstr "%s: rozjazdów %d, rozstaw torów %.2f m" msgid ", aligning curve R %.0f m" msgstr ", łuk wyrównujący R %.0f m" @@ -6094,36 +5142,27 @@ msgstr ", wstawki proste" msgid ", tracks at" msgstr ", tory w odległości" -msgid "A connection is laid between straights" -msgstr "Połączenie układa się między prostymi" - msgid "A connection is made of ordinary switches, not of slips, crossings or double switches" msgstr "Połączenie składa się z rozjazdów zwyczajnych, nie z krzyżowych, skrzyżowań ani podwójnych" -msgid "A connection of tracks laid whole: press on a straight and drag onto the other track. Its switches are cut into the tracks with the inserts between them and get their drives; Shift+wheel while dragging: another kind, Ctrl+wheel: R of the aligning curve" -msgstr "Połączenie torów układane w całości: wciśnij na prostej i przeciągnij na drugi tor. Zwrotnice są wcinane w tory razem ze wstawkami między nimi i dostają napędy; Shift+kółko podczas przeciągania: inny rodzaj, Ctrl+kółko: R łuku wyrównującego" - msgid "A scissors crossover needs parallel tracks and switches of the same angle" msgstr "Nożyce wymagają torów równoległych i rozjazdów o tym samym skosie" msgid "A switch of the connection doesn't fit where it was planned, its straight is too short" -msgstr "Zwrotnica połączenia nie mieści się tam, gdzie ją zaplanowano, jej prosta jest za krótka" +msgstr "Rozjazd połączenia nie mieści się tam, gdzie go zaplanowano, jego prosta jest za krótka" msgid "A switch of the connection goes where its track isn't a plain straight" -msgstr "Zwrotnica połączenia wypada tam, gdzie tor nie jest zwykłą prostą" +msgstr "Rozjazd połączenia wypada tam, gdzie tor nie jest zwykłą prostą" msgid "A switch of the connection runs off the end of its straight" -msgstr "Zwrotnica połączenia wychodzi poza koniec swojej prostej" +msgstr "Rozjazd połączenia wychodzi poza koniec swojej prostej" msgid "Aligning curve R, m##connectionradius" -msgstr "R łuku wyrównującego, m##connectionradius" +msgstr "R łuku wyrównującego (m)##connectionradius" msgid "Alt+wheel" msgstr "Alt+kółko" -msgid "Connection##switchconnection" -msgstr "Połączenie##switchconnection" - msgid "Crossover" msgstr "Przejście rozjazdowe" @@ -6137,7 +5176,7 @@ msgid "Drag onto the other track" msgstr "Przeciągnij na drugi tor" msgid "Gap between the switches, m##connectiongap" -msgstr "Odstęp między zwrotnicami, m##connectiongap" +msgstr "Odstęp między rozjazdami (m)##connectiongap" msgid "Give the spacings of the tracks of the ladder" msgstr "Podaj rozstawy torów ulicy zwrotnicowej" @@ -6146,16 +5185,31 @@ msgid "Length of the new tracks of the fan where there are none to lead onto" msgstr "Długość nowych torów wachlarza tam, gdzie nie ma torów do połączenia" msgid "New tracks, m##connectionstub" -msgstr "Nowe tory, m##connectionstub" +msgstr "Długość nowych torów (m)##connectionstub" msgid "Of the curve between switches of different angles or on tracks that aren't parallel, and at the end of a ladder; 0: the radius of the sharper switch" -msgstr "Łuku między rozjazdami o różnych skosach lub na torach nierównoległych oraz na końcu ulicy zwrotnicowej; 0: promień ostrzejszego rozjazdu" +msgstr "Promień łuku między rozjazdami o różnych skosach lub na torach nierównoległych oraz na końcu ulicy zwrotnicowej; 0: promień ostrzejszego rozjazdu" msgid "Other switches##connectionsecond" -msgstr "Pozostałe zwrotnice##connectionsecond" +msgstr "Pozostałe rozjazdy##connectionsecond" -msgid "Otherwise the two crossovers meet on the other track; Alt+wheel while dragging switches it" -msgstr "W przeciwnym razie oba przejścia spotykają się na drugim torze; Alt+kółko podczas przeciągania to przełącza" +msgid "Where the two crossovers meet" +msgstr "Miejsce spotkania obu przejść" + +msgid "On this track" +msgstr "Na tym torze" + +msgid "On the other track" +msgstr "Na drugim torze" + +msgid "The two crossovers meet on the track the drag starts on; Alt+wheel while dragging switches it" +msgstr "Oba przejścia spotykają się na torze, od którego zaczyna się przeciąganie; Alt+kółko podczas przeciągania to przełącza" + +msgid "The two crossovers meet on the other track; Alt+wheel while dragging switches it" +msgstr "Oba przejścia spotykają się na drugim torze; Alt+kółko podczas przeciągania to przełącza" + +msgid "0: the radius of the sharper switch" +msgstr "0: promień ostrzejszego rozjazdu" msgid "R of the aligning curve" msgstr "R łuku wyrównującego" @@ -6163,23 +5217,20 @@ msgstr "R łuku wyrównującego" msgid "Scissors crossover" msgstr "Nożyce" -msgid "Single switch" -msgstr "Pojedynczy rozjazd" - msgid "Spacings, m##connectionspacings" -msgstr "Rozstawy, m##connectionspacings" +msgstr "Rozstawy torów (m)##connectionspacings" msgid "Straight between the starts of the two switches meeting on one track" -msgstr "Prosta między początkami dwóch zwrotnic spotykających się na jednym torze" +msgstr "Prosta między początkami dwóch rozjazdów spotykających się na jednym torze" msgid "Switch ladder" msgstr "Ulica zwrotnicowa" msgid "Template of the switches on the other track and along a ladder; the first switch is the one chosen on the list above" -msgstr "Szablon zwrotnic na drugim torze i wzdłuż ulicy zwrotnicowej; pierwsza zwrotnica to ta wybrana na liście powyżej" +msgstr "Szablon rozjazdów na drugim torze i wzdłuż ulicy zwrotnicowej; pierwszy rozjazd to ten wybrany na liście powyżej" msgid "The aligning curve R %.0f m with %.0f m straights doesn't fit between the switches" -msgstr "Łuk wyrównujący R %.0f m z prostymi %.0f m nie mieści się między zwrotnicami" +msgstr "Łuk wyrównujący R %.0f m z prostymi %.0f m nie mieści się między rozjazdami" msgid "The diverging track runs along the other track" msgstr "Tor zwrotny biegnie wzdłuż drugiego toru" @@ -6188,34 +5239,31 @@ msgid "The diverging tracks meet at %.2f deg, a straight insert can't join them" msgstr "Tory zwrotne spotykają się pod kątem %.2f°, wstawka prosta ich nie połączy" msgid "The same as the first one" -msgstr "Taka sama jak pierwsza" +msgstr "Taki sam jak pierwszy" msgid "The spacings of the tracks of a ladder have to be at least 1 m" msgstr "Rozstawy torów ulicy zwrotnicowej muszą mieć co najmniej 1 m" msgid "The straight between the curves of the switches is %.2f m, under %.0f m" -msgstr "Prosta między łukami zwrotnic ma %.2f m, mniej niż %.0f m" +msgstr "Prosta między łukami rozjazdów ma %.2f m, mniej niż %.0f m" msgid "The switch on the other track runs off the end of its straight" -msgstr "Zwrotnica na drugim torze wychodzi poza koniec swojej prostej" +msgstr "Rozjazd na drugim torze wychodzi poza koniec swojej prostej" msgid "The switch template has no diverging track" msgstr "Szablon rozjazdu nie ma toru zwrotnego" msgid "The switches can't face each other across these tracks" -msgstr "Zwrotnice nie mogą stanąć naprzeciw siebie na tych torach" - -msgid "The switches meet on this track##connectiontogether" -msgstr "Zwrotnice spotykają się na tym torze##connectiontogether" +msgstr "Rozjazdy nie mogą stanąć naprzeciw siebie na tych torach" msgid "The switches of the two crossovers overlap, the gap between them has to be longer" -msgstr "Zwrotnice obu przejść nachodzą na siebie, odstęp między nimi musi być dłuższy" +msgstr "Rozjazdy obu przejść nachodzą na siebie, odstęp między nimi musi być dłuższy" msgid "The tracks are %.2f m too close for these switches" msgstr "Tory są o %.2f m za blisko dla tych rozjazdów" msgid "The tracks are too close for a scissors crossover with these switches, its switches overlap" -msgstr "Tory są za blisko dla nożyc z tymi rozjazdami, ich zwrotnice nachodzą na siebie" +msgstr "Tory są za blisko dla nożyc z tymi rozjazdami, rozjazdy nachodzą na siebie" msgid "The tracks are too close for the crossing of a scissors crossover: %.2f m more spacing is needed" msgstr "Tory są za blisko dla skrzyżowania nożyc: potrzeba o %.2f m większego rozstawu" @@ -6241,14 +5289,11 @@ msgstr "gdzie spotyka się podwójne przejście" msgid "Tutorial" msgstr "Samouczek" -msgid "Seven short lessons with clips: the camera, laying track, curves, joining, switches, fixing, saving" -msgstr "Siedem krótkich lekcji z filmikami: kamera, układanie toru, łuki, łączenie, rozjazdy, poprawki, zapis" - msgid "Lesson %d of %d" msgstr "Lekcja %d z %d" msgid "Click: play it from the start" -msgstr "Klik: od początku" +msgstr "LPM: od początku" msgid "Mouse and keys" msgstr "Mysz i klawisze" @@ -6275,10 +5320,7 @@ msgid "With the right button held: fly forwards, back and to the sides; E and Q: msgstr "Z wciśniętym prawym przyciskiem: lot do przodu, do tyłu i na boki; E i Q: w górę i w dół" msgid "The top view, and back to 3D" -msgstr "Widok z góry i z powrotem 3D" - -msgid "In the top view: zoom in and out where the cursor points" -msgstr "W widoku z góry: przybliża i oddala tam, gdzie wskazuje kursor" +msgstr "Widok z góry i z powrotem do 3D" msgid "Zoom in and out where the cursor points" msgstr "Przybliża i oddala tam, gdzie wskazuje kursor" @@ -6353,7 +5395,7 @@ msgid "Joining tracks" msgstr "Łączenie torów" msgid "Press on a free end (a magenta ring) and drag: a new track goes out of it" -msgstr "Wciśnij na wolnym końcu (różowe kółko) i przeciągnij: wychodzi z niego nowy tor" +msgstr "Wciśnij na wolnym końcu (różowy okrąg) i przeciągnij: wychodzi z niego nowy tor" msgid "Let go on another free end: a curve joins the two tracks" msgstr "Puść na innym wolnym końcu: łuk połączy oba tory" @@ -6367,15 +5409,6 @@ msgstr "Nad drugim końcem: inny sposób połączenia" msgid "Join two free ends" msgstr "Połącz dwa wolne końce" -msgid "The switch tool; the type of the switch is chosen in its list" -msgstr "Narzędzie Rozjazd; typ rozjazdu wybierasz z jego listy" - -msgid "Click a straight track: the switch goes into it" -msgstr "Kliknij prosty tor: w niego wejdzie rozjazd" - -msgid "Press on it and drag: along sets the direction, sideways the side" -msgstr "Wciśnij na nim i przeciągnij: wzdłuż ustala kierunek, w bok stronę" - msgid "Back to selecting" msgstr "Powrót do zaznaczania" @@ -6386,7 +5419,7 @@ msgid "Selecting and fixing" msgstr "Zaznaczanie i poprawki" msgid "The select tool" -msgstr "Narzędzie Zaznacz" +msgstr "Narzędzie Zaznaczanie" msgid "Click a track: it is selected, with its points" msgstr "Kliknij tor: zostaje zaznaczony, widać jego punkty" @@ -6418,20 +5451,17 @@ msgstr "Zapis i jazda" msgid "Saves the scenery" msgstr "Zapisuje scenerię" -msgid "Trainset in the toolbar: pick a locomotive from the list, click the track, then Place and drive" -msgstr "Skład na pasku narzędzi: wybierz lokomotywę z listy, kliknij tor, potem Postaw i jedź" - msgid "Drive on the new track" msgstr "Przejedź się po nowym torze" msgid "Lay track (L)" -msgstr "Układaj tor (L)" +msgstr "Narzędzie Układanie (L)" msgid "The switch tool (T)" msgstr "Narzędzie Rozjazd (T)" msgid "The select tool (Esc)" -msgstr "Zaznaczanie (Esc)" +msgstr "Narzędzie Zaznaczanie (Esc)" msgid "no track fits to the end of %s" msgstr "żaden tor nie pasuje do końca %s" @@ -6445,10 +5475,6 @@ msgstr "zaczyna od końca %s" msgid "moved %.3f m" msgstr "przesunięto o %.3f m" -msgid "Tool options, Gizmo, Enabled unticked: the track can't be moved, turned or stretched by chance; its points still drag" -msgstr "Opcje narzędzia, Gizmo, bez Włączone: toru nie przesuniesz, nie obrócisz ani nie rozciągniesz przypadkiem; punkty dalej się ciągną" - - # scenery editor: properties of the selected tracks, copied and pasted msgid "no circuits" @@ -6470,7 +5496,7 @@ msgid "moves them together" msgstr "przesuwa je razem" msgid "adds or takes out a track" -msgstr "dodaje albo usuwa tor" +msgstr "dodaje albo odznacza tor" msgid "pastes the copied properties" msgstr "wkleja skopiowane właściwości" @@ -6497,7 +5523,7 @@ msgid "no limit" msgstr "bez ograniczenia" msgid "Surroundings of the track, they affect the sound of the passing vehicles" -msgstr "Otoczenie toru, wpływa na dźwięk przejeżdżających pojazdów" +msgstr "Co jest wokół toru; wpływa na dźwięk przejeżdżających pojazdów" msgid "State of the overhead line over the track: the trains coast through a section with no current,\nor lower the pantographs and keep to the speed limit given" msgstr "Stan sieci trakcyjnej nad torem: pociągi przejeżdżają miejsce bez napięcia z rozpędu\nalbo opuszczają pantografy i trzymają się podanego ograniczenia prędkości" @@ -6509,7 +5535,7 @@ msgid "Names of the track circuits (isolated) the track belongs to, separated by msgstr "Nazwy odcinków izolowanych, do których należy tor, rozdzielone spacjami.\nNazwa, której jeszcze nie ma, tworzy nowy odcinek izolowany. Tory z pojazdami zachowują swoje" msgid "Texture of the rails; of both paths of a switch.\nA material dragged from the texture browser can be dropped here" -msgstr "Tekstura szyn; na rozjeździe obu jego odcinków.\nMożna tu upuścić materiał przeciągnięty z przeglądarki tekstur" +msgstr "Tekstura szyn; na rozjeździe obu jego torów.\nMożna tu upuścić materiał przeciągnięty z przeglądarki tekstur" msgid "Texture of the trackbed (ballast) under the rails.\nA material dragged from the texture browser can be dropped here" msgstr "Tekstura podsypki (tłucznia) pod szynami.\nMożna tu upuścić materiał przeciągnięty z przeglądarki tekstur" @@ -6578,10 +5604,10 @@ msgid "0: track in order; any value adds jolts, 128: the vehicles derail on the msgstr "0: tor sprawny; każda inna wartość dodaje szarpnięcia, 128: pojazdy wykolejają się na torze" msgid "Gauge (m)" -msgstr "Rozstaw szyn (m)" +msgstr "Szerokość toru (m)" msgid "Gauge of the track, 1.435 m for the standard gauge" -msgstr "Rozstaw szyn toru, 1,435 m dla toru normalnego" +msgstr "Szerokość toru, 1.435 m dla toru normalnego" msgid "yes" msgstr "tak" @@ -6643,9 +5669,6 @@ msgstr "Wklejono właściwości %s, Ctrl+Z cofa" msgid "The track has these properties already" msgstr "Tor ma już te właściwości" -msgid "Copy properties" -msgstr "Kopiuj właściwości" - msgid "Copies the properties of this track, to paste them on others (Ctrl+Shift+C)" msgstr "Kopiuje właściwości tego toru, żeby wkleić je na inne (Ctrl+Shift+C)" @@ -6661,9 +5684,6 @@ msgstr "Najpierw skopiuj właściwości jakiegoś toru" msgid "Properties of %s: choose what goes on the selected tracks (Ctrl+Shift+V pastes the same choice at once)" msgstr "Właściwości %s: wybierz, co trafi na zaznaczone tory (Ctrl+Shift+V od razu wkleja ten sam wybór)" -msgid "Click a track... (Esc)" -msgstr "Kliknij tor... (Esc)" - msgid "Copying properties" msgstr "Kopiowanie właściwości" @@ -6677,7 +5697,7 @@ msgid "Click a track in the view to take its properties; Esc or this button give msgstr "Kliknij tor w widoku, żeby pobrać jego właściwości; Esc albo ten przycisk rezygnuje" msgid "Selection" -msgstr "Zaznaczanie" +msgstr "Zaznaczenie" msgid "Alike..." msgstr "Podobne..." @@ -6706,9 +5726,6 @@ msgstr "W folderze textures:" msgid "and %d more: type more of the name" msgstr "i jeszcze %d: wpisz więcej z nazwy" -msgid "From a track..." -msgstr "Z toru..." - msgid "Pipette: the next click on a track in the view takes its properties, to paste them on the selection" msgstr "Pipeta: następne kliknięcie toru w widoku pobiera jego właściwości, do wklejenia na zaznaczenie" @@ -6731,16 +5748,13 @@ msgid "Takes the track out of the selection" msgstr "Usuwa tor z zaznaczenia" msgid "Click: look at it, double click: select only this one" -msgstr "Klik: pokaż go, dwuklik: zaznacz tylko ten" +msgstr "LPM: pokaż go, dwuklik: zaznacz tylko ten" msgid "+ Joined" msgstr "+ Połączone" msgid "Adds the tracks joined to the selection, by the rules of the spread in the tool options" -msgstr "Dodaje tory połączone z zaznaczonymi, według zasad rozszerzania z opcji narzędzia" - -msgid "Select alike..." -msgstr "Zaznacz podobne..." +msgstr "Dodaje do zaznaczenia połączone tory, według ustawień „Zaznaczanie połączonych” w Opcjach narzędzia" msgid "Selects the tracks of the whole scenery which have the same rails, trackbed, speed... as the selected ones" msgstr "Zaznacza w całej scenerii tory, które mają te same szyny, podsypkę, prędkość... co zaznaczone" @@ -6755,7 +5769,7 @@ msgid "The same speed" msgstr "Ta sama prędkość" msgid "The same environment" -msgstr "To samo otoczenie" +msgstr "Ta sama okolica" msgid "The same track circuits" msgstr "Te same odcinki izolowane" @@ -6767,7 +5781,7 @@ msgid "%s: %s selected in the scenery" msgstr "%s: zaznaczono w scenerii %s" msgid "Shift+click: add or take out a track\nCtrl+click: the same\nDrag on the ground: select the tracks in a box, with Shift: add them\nCtrl+Shift+click: the whole run of joined tracks\nEsc: clear the selection, Del: delete the selected tracks\nCtrl+Shift+C / Ctrl+Shift+V: copy and paste the properties" -msgstr "Shift+klik: dodaj albo usuń tor\nCtrl+klik: to samo\nPrzeciągnięcie po terenie: zaznacz tory w ramce, z Shift: dodaj je\nCtrl+Shift+klik: cały ciąg połączonych torów\nEsc: wyczyść zaznaczenie, Del: usuń zaznaczone tory\nCtrl+Shift+C / Ctrl+Shift+V: kopiuj i wklej właściwości" +msgstr "Shift+LPM: dodaj tor do zaznaczenia albo go odznacz\nCtrl+LPM: to samo\nPrzeciągnij po terenie: zaznacz tory w ramce, z Shift: dodaj je\nCtrl+Shift+LPM: cały ciąg połączonych torów\nEsc: wyczyść zaznaczenie, Del: usuń zaznaczone tory\nCtrl+Shift+C / Ctrl+Shift+V: kopiuj i wklej właściwości" msgid "Select one track, or point at one, to copy its properties" msgstr "Zaznacz jeden tor albo wskaż go kursorem, żeby skopiować jego właściwości" @@ -6778,29 +5792,26 @@ msgstr "Najpierw skopiuj właściwości toru: Ctrl+Shift+C" msgid "Select the tracks to paste the properties on" msgstr "Zaznacz tory, na które mają trafić właściwości" -msgid "Click a track to see its properties. Shift+click adds more tracks to the selection, a drag on the ground selects those in a box" -msgstr "Kliknij tor, żeby zobaczyć jego właściwości. Shift+klik dodaje kolejne tory do zaznaczenia, przeciągnięcie po terenie zaznacza tory w ramce" - msgid "Deletes the selected tracks (Del); Ctrl+Z brings them back" msgstr "Usuwa zaznaczone tory (Del); Ctrl+Z je przywraca" msgid "The textures, the speed and the other properties of the switch are in the inspector" -msgstr "Tekstury, prędkość i inne właściwości rozjazdu są w inspektorze" +msgstr "Tekstury, prędkość i inne właściwości rozjazdu są w Inspektorze" msgid "What the track sets off as the vehicles run on it" msgstr "Co tor wyzwala, gdy jadą po nim pojazdy" msgid "Click: take the properties of this track" -msgstr "Klik: pobierz właściwości tego toru" +msgstr "LPM: pobierz właściwości tego toru" msgid "Click a track to take its properties, Esc: cancel" msgstr "Kliknij tor, żeby pobrać jego właściwości, Esc: anuluj" msgid "Click: take out of the selection" -msgstr "Klik: usuń z zaznaczenia" +msgstr "LPM: usuń z zaznaczenia" msgid "Click: add to the selection" -msgstr "Klik: dodaj do zaznaczenia" +msgstr "LPM: dodaj do zaznaczenia" msgid "Copy its properties" msgstr "Kopiuj jego właściwości" @@ -6815,7 +5826,7 @@ msgid "Select alike" msgstr "Zaznacz podobne" msgid "Their properties are in the inspector, the gizmo moves them together" -msgstr "Ich właściwości są w inspektorze, gizmo przesuwa je razem" +msgstr "Ich właściwości są w Inspektorze, gizmo przesuwa je razem" msgid "%d track" msgid_plural "%d tracks" @@ -6877,7 +5888,6 @@ msgstr[0] "Zaznaczony %d tor" msgstr[1] "Zaznaczone %d tory" msgstr[2] "Zaznaczonych %d torów" - msgid "The curve past the end of the switch can't keep to it, %s follows the switch on its own" msgstr "Nie da się przełożyć łuku za końcem rozjazdu, tor %s tylko podąża za rozjazdem" @@ -6885,16 +5895,16 @@ msgid "The curve past the end of the switch keeps to it with the radius of %.1f msgstr "Łuk za końcem rozjazdu nadal do niego przylega, ale ma promień %.1f m zamiast %.1f m" msgid "Blade heel, m##designheel" -msgstr "Piętka iglicy, m##designheel" +msgstr "Piętka iglicy (m)##designheel" msgid "Where the moving blades end, from the start of the switch, as its scheme gives it; the switch keeps it in the scenery. 0: as long as the flangeway of 58 mm behind them needs" msgstr "Gdzie kończą się ruchome iglice, od początku rozjazdu, jak podaje schemat; rozjazd zapisuje to w scenerii. 0: tak długie, jak wymaga żłobek 58 mm za nimi" msgid "Blade throw, m##designthrow" -msgstr "Przełożenie iglic, m##designthrow" +msgstr "Skok iglic (m)##designthrow" msgid "How far the tips of the blades move over between the two paths; 0: the usual 0.160 m" -msgstr "O ile przesuwają się ostrza iglic między torami; 0: zwykłe 0,160 m" +msgstr "O ile przesuwają się ostrza iglic przy przestawieniu zwrotnicy; 0: zwykłe 0.160 m" msgid "Blade heel (m)" msgstr "Piętka iglicy (m)" @@ -6906,13 +5916,13 @@ msgid "Blade throw (m)" msgstr "Przełożenie iglic (m)" msgid "How far the tips of the blades move over between the two paths.\n0: the usual 0.160 m" -msgstr "O ile przesuwają się ostrza iglic między torami.\n0: zwykłe 0,160 m" +msgstr "O ile przesuwają się ostrza iglic między torami.\n0: zwykłe 0.160 m" msgid "Sleeper model from the pack" msgstr "Model podkładów z paczki" msgid "Model of the sleepers of the track running alone, of the models of sleepers in the scenery data, laid as 3D sleepers\nin place of the one set with sleepers3dplain in eu07.ini. (as in eu07.ini): the one set there" -msgstr "Model podkładów toru biegnącego samodzielnie, spośród modeli podkładów w danych scenerii, układanych jako podkłady 3D\nzamiast modelu ustawionego wpisem sleepers3dplain w eu07.ini. (jak w eu07.ini): model ustawiony tam" +msgstr "Model podkładów zwykłego toru (nie rozjazdu), spośród modeli podkładów w danych scenerii, układanych jako podkłady 3D\nzamiast modelu ustawionego wpisem sleepers3dplain w eu07.ini. (jak w eu07.ini): model ustawiony tam" msgid "(as in eu07.ini)" msgstr "(jak w eu07.ini)" @@ -6929,9 +5939,6 @@ msgstr "Skóra podrozjazdnic" msgid "Replaceable skin of the timber model, empty: the one of the model" msgstr "Wymienna skóra modelu podrozjazdnic, puste: skóra samego modelu" -msgid "What to place" -msgstr "Co stawiasz" - msgid "Slip switch" msgstr "Rozjazd krzyżowy" @@ -6941,28 +5948,18 @@ msgstr "Skrzyżowanie" msgid "Connection" msgstr "Połączenie" - - msgid "A crossing of two straights" msgstr "Skrzyżowanie dwóch prostych" - - - - - - msgid "The first switch" msgstr "Pierwszy rozjazd" msgid "Template" -msgstr "Wzorzec" +msgstr "Szablon" msgid "Details##switchdetails" msgstr "Szczegóły##switchdetails" - - msgid "Press on a straight" msgstr "Wciśnij na prostej" @@ -6976,22 +5973,10 @@ msgid "A switch in a straight or a curve" msgstr "Rozjazd w prostej albo łuku" msgid "Rkp or Rkpd at a crossing of two straights" -msgstr "Rkp albo Rkpd na skrzyżowaniu dwóch prostych" +msgstr "Rozjazd krzyżowy pojedynczy (Rkp) albo podwójny (Rkpd) na skrzyżowaniu dwóch prostych" msgid "Crossover, double, scissors, ladder" -msgstr "Przejście, podwójne, nożyce, ulica zwrotnicowa" - -msgid "Click a crossing of two straights" -msgstr "Kliknij skrzyżowanie dwóch prostych" - -msgid "Drag from a straight onto the other track. Shift+wheel: kind, Ctrl+wheel: R, Alt+wheel: where they meet" -msgstr "Przeciągnij z prostej na drugi tor. Shift+kółko: rodzaj, Ctrl+kółko: R, Alt+kółko: miejsce spotkania" - -msgid "Drag from a straight onto the other track. Shift+wheel: kind, Ctrl+wheel: R" -msgstr "Przeciągnij z prostej na drugi tor. Shift+kółko: rodzaj, Ctrl+kółko: R" - -msgid "Click a track and drag: along for the direction, sideways for the side" -msgstr "Kliknij tor i przeciągnij: wzdłuż kierunek, w bok strona" +msgstr "Przejście rozjazdowe, podwójne przejście rozjazdowe, nożyce, ulica zwrotnicowa" msgid "Turntable, signals, objects, trainset, profile, speed, joints, gauge" msgstr "Obrotnica, sygnalizatory, obiekty, skład, profil, prędkość, styki, skrajnia" @@ -7006,16 +5991,37 @@ msgid "movable nose" msgstr "ruchomy dziób" msgid "gauge" -msgstr "rozstaw" +msgstr "szerokość toru" msgid "long" msgstr "wydłużony" -msgid "Rail of the track: %s##railonly" -msgstr "Szyna toru: %s##railonly" +msgid "Rail of the track: %s" +msgstr "Szyna toru: %s" + +msgid "LMB on a crossing" +msgstr "LPM na skrzyżowaniu" + +msgid "of two straights: it goes there" +msgstr "dwóch prostych: tam stanie" + +msgid "from a straight onto the other track" +msgstr "z prostej na drugi tor" + +msgid "the kind" +msgstr "rodzaj" + +msgid "where they meet" +msgstr "miejsce spotkania" + +msgid "Drag on a track" +msgstr "Przeciągnij po torze" + +msgid "along: the direction, sideways: the side" +msgstr "wzdłuż: kierunek, w bok: strona" msgid "Only the templates of the rail the selected track is laid with" -msgstr "Tylko wzorce na szynę, z której jest zaznaczony tor" +msgstr "Tylko szablony na szynę zaznaczonego toru" msgid "The switch tool; at the top: what to place" msgstr "Narzędzie Rozjazd; na górze: co stawiasz" @@ -7023,20 +6029,17 @@ msgstr "Narzędzie Rozjazd; na górze: co stawiasz" msgid "Click a track: the switch goes into it" msgstr "Kliknij tor: rozjazd wejdzie w niego" -msgid "Tool options, Gizmo, Enabled off: the track won't move by chance" -msgstr "Opcje narzędzia, Gizmo, Włączone wyłączone: tor nie przesunie się przypadkiem" - -msgid "More, Trainset: pick a locomotive, click the track, Place and drive" -msgstr "Więcej, Skład: wybierz lokomotywę, kliknij tor, Postaw i jedź" +msgid "Tool options, Gizmo, Off: the track won't move by chance" +msgstr "Opcje narzędzia, Gizmo, Wył.: tor nie przesunie się przypadkiem" msgid "Press on it and drag: along the direction, sideways the side" -msgstr "Wciśnij na nim i przeciągnij: wzdłuż kierunek, w bok strona" +msgstr "Wciśnij na nim i przeciągnij: wzdłuż toru zmienia kierunek, w bok stronę" msgid "Connection: drag from a straight onto the other track" msgstr "Połączenie: przeciągnij z prostej na drugi tor" msgid "Click the green or red point in the view: that one only" -msgstr "Kliknij zielony albo czerwony punkt w widoku: tylko ten" +msgstr "Kliknij zielony albo czerwony punkt w widoku: postawi albo poprawi tylko ten wskaźnik" msgid "%s: no W17" msgstr "%s: brak W17" @@ -7044,14 +6047,11 @@ msgstr "%s: brak W17" msgid "%s: W17 %.1f m off, %+.2f m high" msgstr "%s: W17 %.1f m od miejsca, %+.2f m w pionie" -msgid "Settings##fouling" -msgstr "Ustawienia##fouling" - msgid "Click: move this W17 to its place" -msgstr "Klik: przesuń ten W17 na miejsce" +msgstr "LPM: przesuń ten wskaźnik W17 na miejsce" msgid "Click: put the W17 here" -msgstr "Klik: postaw tu W17" +msgstr "LPM: postaw tu wskaźnik W17" msgid "Drop the signal by a track" msgstr "Upuść sygnalizator przy torze" @@ -7065,11 +6065,8 @@ msgstr "%zu kafli do zrobienia z NMT" msgid "; the formation of %zu paths shaped" msgstr "; torowisko %zu torów ukształtowane" -msgid "Along the tracks" -msgstr "Wzdłuż torów" - msgid "Band along the tracks (m)" -msgstr "Pas wzdłuż torów (m)" +msgstr "Zasięg od torów (m)" msgid "Chunks made from the terrain model: %zu, waiting for it: %zu" msgstr "Kafle z NMT: zrobione %zu, czeka %zu" @@ -7078,34 +6075,25 @@ msgid "Compares the formation of the tracks with the terrain model and moves the msgstr "Porównuje torowisko z NMT i przesuwa NMT w pionie, żeby się spotkały, np. dla scenerii zbudowanej na wysokości 0" msgid "Each track gets its formation with slopes 1:1.5 down or up to the terrain, as a modifier of the terrain; bridges and tunnels keep the model under them" -msgstr "Każdy tor dostaje torowisko ze skarpami 1:1,5 w dół albo w górę do terenu, jako modyfikator terenu; pod mostami i w tunelach zostaje NMT" +msgstr "Każdy tor dostaje torowisko ze skarpami 1:1.5 w dół albo w górę do terenu, jako modyfikator terenu; pod mostami i w tunelach zostaje NMT" msgid "Fit to the tracks" msgstr "Dopasuj do torów" msgid "Formation and slopes along the tracks" -msgstr "Torowisko i skarpy wzdłuż torów" +msgstr "Torowisko i skarpy" msgid "Height offset (m)" msgstr "Przesunięcie wysokości (m)" -msgid "Place: N %.0f, E %.0f (PUWG 1992), as the orthophoto" -msgstr "Położenie: N %.0f, E %.0f (PUWG 1992), jak ortofotomapa" - -msgid "Squares of the model being fetched: %zu" -msgstr "Pobierane kafle NMT: %zu" - msgid "Terrain model: GUGiK, geoportal.gov.pl" msgstr "NMT: GUGiK, geoportal.gov.pl" -msgid "The scenery isn't placed anywhere: give its place in the settings of the orthophoto first" -msgstr "Sceneria nie ma położenia: najpierw podaj je w ustawieniach ortofotomapy" - msgid "The tracks lie %+.2f m against the terrain model (%zu places, they differ by %.2f m around it)" msgstr "Tory leżą %+.2f m względem NMT (%zu miejsc, rozrzut %.2f m)" msgid "Terrain model (NMT)" -msgstr "Teren z NMT" +msgstr "Numeryczny model terenu (NMT)" msgid "No tracks to fit the terrain model to: its heights are taken as they are" msgstr "Brak torów do dopasowania: wysokości NMT bez przesunięcia" @@ -7126,7 +6114,7 @@ msgid "Terrain for the new scenery###nmtoffer" msgstr "Teren dla nowej scenerii###nmtoffer" msgid "The scenery has no terrain yet. Load the terrain model and the aerial photos around here? The tracks laid then go along the real ground." -msgstr "Sceneria nie ma jeszcze terenu. Wczytać teren z NMT i zdjęcia lotnicze wokół tego miejsca? Tory układane potem pójdą po prawdziwym terenie." +msgstr "Sceneria nie ma jeszcze terenu. Wczytać prawdziwy teren i zdjęcia lotnicze wokół tego miejsca? Tory układane potem pójdą po nim" msgid "Where is it? PUWG 1992 of this point, X to the north and Y to the east, as geoportal.gov.pl gives them:" msgstr "Gdzie to jest? Współrzędne PUWG 1992 tego miejsca, X na północ i Y na wschód, jak podaje geoportal.gov.pl:" @@ -7139,3 +6127,3738 @@ msgstr "Bez terenu" msgid "Ask with every new scenery" msgstr "Pytaj przy każdej nowej scenerii" + +msgid "Fetching the terrain model under the tracks: %zu squares left" +msgstr "Pobieranie NMT pod torami: zostało %zu kwadratów" + +msgid "Formation" +msgstr "Torowisko" + +msgid "Formation %.3f m, %.2f m under the rail top" +msgstr "Torowisko %.3f m, %.2f m pod główką szyny" + +msgid "Grade line from the switch %s (%s): %.2f m, %+.2f per mille" +msgstr "Niweleta dziedziczona z rozjazdu %s (%s): %.2f m, %+.2f ‰" + +msgid "No tracks to put onto the terrain" +msgstr "Brak torów do położenia na terenie" + +msgid "\n%zu lines part from the terrain model by over 0.5 m: the model or the track is off there, the terrain follows the tracks (the log names them)" +msgstr "\n%zu linii odchodzi od NMT o ponad 0.5 m: tam NMT albo tor się nie zgadza, teren idzie za torami (log je wymienia)" + +msgid "Other profiles: the properties of the track" +msgstr "Inne profile ustawisz we właściwościach toru" + +msgid "Rail profile %s set on %d tracks" +msgstr "Profil szyny %s ustawiony na torach: %d" + +msgid "Scenery moved %+.2f m; grade lines fitted to the terrain: %zu, refused by the checks: %zu, tracks left: %zu" +msgstr "Sceneria przesunięta o %+.2f m; niwelet dopasowanych do terenu: %zu, odrzuconych przez kontrole: %zu, zostało torów: %zu" + +msgid "Set...##railprofile" +msgstr "Ustaw...##railprofile" + +msgid "The track doesn't say its rail, so the templates of all the rails are offered.\nSet the profile and the list keeps to the templates of the rail; the rails of the track take its shape" +msgstr "Tor nie ma profilu szyny, więc lista pokazuje szablony wszystkich szyn.\nUstaw profil, a lista zawęzi się do szablonów tej szyny; szyny toru przyjmą jego kształt" + +msgid "Tracks onto the terrain" +msgstr "Tory na teren" + +msgid "Tracks with the rails %s and no profile: %d" +msgstr "Tory z szynami %s bez profilu: %d" + +msgid "Track with no rail profile" +msgstr "Tor bez profilu szyny" + +msgid "Rail profile" +msgstr "Profil szyny" + +msgid "Shape of the rails, the railprofile of the track. It also picks the switch templates of the rail: S60 60E1, S49 49E1.\nNo profile: the default shape, and the templates of all the rails are offered" +msgstr "Kształt szyn (wpis railprofile toru). Wybiera też szablony rozjazdów dla szyny: S60 60E1, S49 49E1.\nBez profilu: kształt domyślny, a lista pokazuje szablony wszystkich szyn" + +msgid "No profile" +msgstr "Bez profilu" + +msgid "shared timbers" +msgstr "wspólne podrozjazdnice" + +msgid "Shared timbers" +msgstr "Wspólne podrozjazdnice" + +msgid "Design put together from %zu designs kept for the routes along this one, largest departure from the track %.3f m" +msgstr "Niweleta złożona z %zu niwelet zapisanych dla tras wzdłuż tej, największa odchyłka od toru %.3f m" + +msgid "Signs" +msgstr "Wskaźniki i znaki" + +msgid "Poles and gantries" +msgstr "Słupy i bramki" + +msgid "Catenary" +msgstr "Sieć trakcyjna" + +msgid "Logic" +msgstr "Logika" + +msgid "Other" +msgstr "Inne" + +msgid "Track equipment" +msgstr "Wyposażenie toru" + +msgid "Along a curve only a crossover can be laid" +msgstr "Na łuku można ułożyć tylko zwykłe przejście rozjazdowe" + +msgid "The other track isn't a curve running along this one" +msgstr "Drugi tor nie jest łukiem biegnącym wzdłuż tego" + +msgid "The tracks are %.2f m apart, too close or too far for a crossover" +msgstr "Rozstaw torów %.2f m jest za mały albo za duży na przejście rozjazdowe" + +msgid "The crossover doesn't fit on the curve" +msgstr "Przejście rozjazdowe nie mieści się na łuku" + +msgid "The outer track can't be followed" +msgstr "Nie da się prześledzić toru zewnętrznego" + +msgid "The outer track can't be followed after it went up" +msgstr "Po podniesieniu nie da się prześledzić toru zewnętrznego" + +msgid "A switch of the crossover couldn't be made" +msgstr "Nie udało się utworzyć rozjazdu przejścia rozjazdowego" + +msgid "The crossover couldn't be laid" +msgstr "Nie udało się ułożyć przejścia rozjazdowego" + +msgid "Crossover laid along the curve, switches %s, the tracks %.2f m apart" +msgstr "Przejście rozjazdowe ułożone na łuku, rozjazdy %s, rozstaw torów %.2f m" + +msgid "\nThe outer track went up %.3f m onto the plane of the cant of the inner one, with ramps of %.1f per mille" +msgstr "\nTor zewnętrzny podniesiony o %.3f m do płaszczyzny przechyłki toru wewnętrznego, pochylenie ramp %.1f ‰" + +msgid "The template the crossover was laid of isn't among the templates" +msgstr "Szablonu, z którego ułożono przejście rozjazdowe, nie ma na liście" + +msgid "The crossover doesn't fit there" +msgstr "Przejście rozjazdowe się tam nie mieści" + +msgid "per mille" +msgstr "‰" + +msgid "as the line" +msgstr "jak linia" + +msgid "0: the limit of the line, a switch may lie on its grade.\nA lower one for the tracks of a station where the vehicles stand, e.g. 2.5" +msgstr "0: limit linii, rozjazd może leżeć na jej pochyleniu.\nNiższy dla torów stacyjnych, na których stoją pojazdy, np. 2.5" + +msgid "\nThe tracks across %zu crossover(s) followed the change" +msgstr "\nTory po drugiej stronie przejść rozjazdowych (%zu) poszły za zmianą" + +msgid "Crossover moved %.2f m" +msgstr "Przejście rozjazdowe przesunięte o %.2f m" + +msgid "Crossovers laid again on the new line: %s" +msgstr "Przejścia rozjazdowe ułożone ponownie na nowej linii: %s" + +msgid "Crossovers taken out which don't fit the new line, lay them again: %s" +msgstr "Usunięte przejścia rozjazdowe nie mieszczą się na nowej linii, ułóż je ponownie: %s" + +msgid "The other track doesn't run along at the same distance there" +msgstr "Drugi tor nie biegnie tam w tym samym odstępie" + +msgid "The crossover wouldn't join the tracks there" +msgstr "Przejście rozjazdowe nie połączyłoby się tam z torami" + +msgid "The line ends on the switch %s of a crossover: move the crossover off the curve or take it out, change both lines, then lay it again" +msgstr "Linia kończy się na rozjeździe %s przejścia rozjazdowego: przesuń przejście poza łuk albo je usuń, zmień obie linie i ułóż je ponownie" + +msgid "Make heightmap terrain for this scenery" +msgstr "Utwórz pusty teren dla tej scenerii" + +msgid "Digitize the scenery" +msgstr "Zdigitalizuj wszystko" + +msgid "All at once: the lines, the switches with their types out of the catalogue (or the size measured from their shape), the crossovers as connections of switches and the signals, signs, poles and catenary bound to the tracks. The paths and the objects stay as they are, the scenery keeps it on save" +msgstr "Wszystko naraz: linie, rozjazdy z typem z katalogu szablonów (albo z wymiarami zmierzonymi z kształtu), przejścia rozjazdowe jako połączenia rozjazdów oraz sygnalizatory, wskaźniki, słupy i sieć trakcyjna powiązane z torami. Tory i obiekty zostają, jak są; sceneria zapamiętuje to przy zapisie" + +msgid "the tracks of the switch don't start at the same point" +msgstr "tory rozjazdu nie zaczynają się w jednym punkcie" + +msgid "the diverging tracks meet without an insert between them" +msgstr "tory zwrotne stykają się bez wstawki między nimi" + +msgid "the insert is made of %zu paths" +msgstr "wstawka składa się z %zu torów" + +msgid "a switch of it has no type kept" +msgstr "jeden z rozjazdów nie ma zapisanego typu" + +msgid "the switches differ: %s and %s" +msgstr "rozjazdy są różne: %s i %s" + +msgid "Switches: %d, kept already %d, of the catalogue %d, measured %d, parts of slips %d, without a name %d, odd %d" +msgstr "Rozjazdy: %d, już zapisane %d, z katalogu %d, zmierzone %d, części rozjazdów krzyżowych %d, bez nazwy %d, nietypowe %d" + +msgid "Crossovers: %d, kept already %d, recorded %d, left out %d" +msgstr "Przejścia rozjazdowe: %d, już zapisane %d, zapisane teraz %d, pominięte %d" + +msgid "Objects by the tracks: %d found, %d bound" +msgstr "Obiekty przy torach: znalezione %d, powiązane %d" + +msgid "The size is measured from the shape of the switch, it's of no template of the catalogue" +msgstr "Rozjazd nie pasuje do żadnego szablonu z katalogu, wymiary zmierzone z jego kształtu" + +msgid "Switches: %d, kept already %d, of the catalogue %d, measured %d, parts of slips %d, odd %d" +msgstr "Rozjazdy: %d, już zapisane %d, z katalogu %d, zmierzone %d, części rozjazdów krzyżowych %d, nietypowe %d" + +msgid ", without a name, left out %d" +msgstr ", bez nazwy, pominięte %d" + +msgid "The scenery is there" +msgstr "Ustaw położenie" + +msgid "The terrain model is loaded along the tracks and the tracks go onto it, wherever the scenery was built: the top of the ballast at the height of the model, the grade line of every line fitted to it within the limits of the line, switches and the tracks beside them on one grade line, the formation and the slopes made along them. Where the model is off the tracks keep their line and it's reported" +msgstr "NMT wczytuje się wzdłuż torów, a tory idą na niego, niezależnie od tego, na jakiej wysokości zbudowano scenerię: korona podsypki na wysokości NMT, niweleta każdej linii dopasowana do terenu w granicach norm linii, rozjazdy i tory obok na jednej niwelecie, torowisko i skarpy wzdłuż nich. Gdzie NMT się myli, tory zostają na swojej linii i jest to zgłaszane" + +msgid "Load the terrain under the tracks, they stay" +msgstr "Wczytaj teren pod tory (tory zostają)" + +msgid "The tracks aren't touched: the terrain model is loaded along them and drawn to them, at each place by as much as the tracks lie above or below it there, fading out away from them, so its relief stays and the tracks don't end up in deep cuttings; then the formation and the slopes along them. For a scenery built level or at its own heights" +msgstr "Tory się nie ruszają: NMT wczytuje się wzdłuż nich i jest do nich dociągany, w każdym miejscu o tyle, o ile tor leży tam nad nim albo pod nim, z wygaszaniem wraz z odległością. Rzeźba terenu zostaje, a tory nie lądują w głębokich wykopach. Potem torowisko i skarpy wzdłuż torów. Dla scenerii zbudowanej płasko albo na własnych wysokościach" + +msgid "No tracks to make the terrain under" +msgstr "Brak torów, pod którymi można zrobić teren" + +msgid "Stopped: the grade lines split off %zu paths from %zu tracks, more than they should; Ctrl+Z takes the changes back" +msgstr "Zatrzymane: niwelety odcięły %zu torów z %zu, więcej niż powinny; Ctrl+Z cofa zmiany" + +msgid "Textures" +msgstr "Tekstury" + +msgid "terrain" +msgstr "teren" + +msgid "Folder: %s" +msgstr "Folder: %s" + +msgid "Chunks in memory: %zu%s" +msgstr "Kafle w pamięci: %zu%s" + +msgid ", changes not saved yet" +msgstr ", zmiany jeszcze niezapisane" + +msgid "LMB selects the clicked chunk, Ctrl+LMB adds it to the selection or takes it away;\nthe point spacing of the selected chunks is converted in \"Chunk resolution\" below" +msgstr "LPM zaznacza kliknięty kafel, Ctrl+LPM dodaje go do zaznaczenia albo z niego zdejmuje;\nodstęp punktów zaznaczonych kafli zmienia się w \"Rozdzielczość kafli\" poniżej" + +msgid "Water" +msgstr "Woda" + +msgid "LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back" +msgstr "LPM dodaje punkt obrysu zbiornika wody, Shift+LPM cofa ostatni" + +msgid "Fill" +msgstr "Wypełnienie" + +msgid "LMB adds a point of the outline of an area to fill with a material, Shift+LMB takes the last one back" +msgstr "LPM dodaje punkt obrysu obszaru do wypełnienia materiałem, Shift+LPM cofa ostatni" + +msgid "Level" +msgstr "Poziomowanie" + +msgid "LMB leads the terrain under the brush to the target height, Ctrl+LMB takes the target height from what's under the cursor" +msgstr "LPM podnosi albo obniża teren pod pędzlem do wysokości docelowej, Ctrl+LPM pobiera tę wysokość spod kursora" + +msgid "Paint" +msgstr "Malowanie" + +msgid "LMB paints the material chosen in the palette, Shift+LMB the first material of the palette" +msgstr "LPM maluje materiałem wybranym w palecie, Shift+LPM pierwszym materiałem palety" + +msgid "LMB takes back the touch-ups made over the tracks under the brush:\nthe terrain returns to the shape the modifiers (tracks) give it" +msgstr "LPM cofa poprawki zrobione nad torami pod pędzlem:\nteren wraca do kształtu, jaki nadają mu modyfikatory (tory)" + +msgid "Point spacing" +msgstr "Odstęp punktów" + +msgid "Spacing of the grid points of the chunks made.\nA chunk is 250 x 250 m; finer grids take more memory and time to draw." +msgstr "Odstęp punktów siatki tworzonych kafli.\nKafel ma 250 x 250 m; gęstsza siatka zajmuje więcej pamięci i dłużej się rysuje." + +msgid "Height of the chunks made where there's no ground under them" +msgstr "Wysokość kafli tworzonych tam, gdzie nie ma pod nimi terenu" + +msgid "Chunks along a side" +msgstr "Kafli na bok" + +msgid "Add around the camera" +msgstr "Dodaj wokół kamery" + +msgid "Makes the chunks missing around the camera, shaped as the ground under them" +msgstr "Tworzy brakujące kafle wokół kamery, o kształcie terenu pod nimi" + +msgid "Capture the selected model as terrain" +msgstr "Zamień zaznaczony model w teren" + +msgid "The chunks under the model take its shape, and the model is removed" +msgstr "Kafle pod modelem przyjmują jego kształt, a model znika" + +msgid "Chunk resolution" +msgstr "Rozdzielczość kafli" + +msgid "%d,%d: not loaded" +msgstr "%d,%d: niewczytany" + +msgid ", paint every %.2f m" +msgstr ", malowanie co %.2f m" + +msgid ", one material" +msgstr ", jeden materiał" + +msgid ", touch-ups" +msgstr ", poprawki" + +msgid "Convert to" +msgstr "Zamień na" + +msgid "Clear the selection" +msgstr "Wyczyść zaznaczenie" + +msgid "Paint every 0.5 m with 0.5 m spacing" +msgstr "Malowanie co 0.5 m przy odstępie 0.5 m" + +msgid "Chunks converted to 0.5 m get their paint samples every 0.5 m as well (every metre otherwise),\nso the materials can be painted as finely as the ground is shaped" +msgstr "Kafle zamienione na 0.5 m dostają próbki malowania też co 0.5 m (inaczej co metr),\nwięc materiały da się malować tak dokładnie, jak ukształtowany jest teren" + +msgid "Grid points: %zu -> %zu" +msgstr "Punkty siatki: %zu -> %zu" + +msgid "A coarser grid can't hold the shapes smaller than its spacing: they're averaged out. The edges of the chunks keep their heights, so the neighbours still meet them. Ctrl+Z takes it back." +msgstr "Rzadsza siatka nie utrzyma kształtów mniejszych niż jej odstęp: zostaną uśrednione. Krawędzie kafli zachowują wysokości, więc sąsiednie kafle dalej się z nimi stykają. Ctrl+Z cofa zmianę." + +msgid "Radius (m)" +msgstr "Promień (m)" + +msgid "Strength" +msgstr "Siła" + +msgid "Sculpting: metres per second at the middle of the brush; smoothing, levelling, painting and restoring: how fast they work" +msgstr "Rzeźbienie: metry na sekundę w środku pędzla; wygładzanie, poziomowanie, malowanie i przywracanie: jak szybko działają" + +msgid "Touch up over the tracks" +msgstr "Poprawki nad torami" + +msgid "On: the brush shapes the terrain as it's seen, also where the tracks (modifiers) shape it;\nthe changes there are kept as touch-ups over the modifiers, and stay when the track changes.\nThe Restore tool takes them back.\nOff: the brush shapes only the ground under the modifiers, the tracks keep their formation and slopes." +msgstr "Włączone: pędzel kształtuje teren tak, jak go widać, także tam, gdzie kształtują go tory (modyfikatory);\nzmiany zostają tam jako poprawki nad modyfikatorami i nie znikają, gdy tor się zmieni.\nNarzędzie Przywracanie je cofa.\nWyłączone: pędzel kształtuje tylko teren pod modyfikatorami, tory zachowują torowisko i skarpy." + +msgid "Target height (m)" +msgstr "Wysokość docelowa (m)" + +msgid "Click in the view..." +msgstr "Kliknij w widoku..." + +msgid "Pick" +msgstr "Pobierz" + +msgid "The next click takes the height of whatever is under the cursor: terrain, a track, a model.\nCtrl+LMB does the same at any time" +msgstr "Następne kliknięcie pobiera wysokość tego, co jest pod kursorem: terenu, toru, modelu.\nCtrl+LPM robi to samo w każdej chwili" + +msgid "Raise and lower" +msgstr "Podnoś i obniżaj" + +msgid "Only raise" +msgstr "Tylko podnoś" + +msgid "Only lower" +msgstr "Tylko obniżaj" + +msgid "Target where the stroke starts" +msgstr "Cel tam, gdzie zaczyna się pociągnięcie" + +msgid "Each stroke takes the height of the terrain under the cursor where it starts as its target,\nand drags it along: the ground stroked over is levelled with that point" +msgstr "Każde pociągnięcie bierze za cel wysokość terenu pod kursorem w miejscu, gdzie się zaczyna,\ni ciągnie ją dalej: przeciągnięty teren wyrównuje się do tego punktu" + +msgid "Materials" +msgstr "Materiały" + +msgid "Repeats every (m)" +msgstr "Rozmiar tekstury (m)" + +msgid "Metres the textures of the chosen material repeat at; 0: as the material gives it, or 8 m" +msgstr "Co ile metrów powtarzają się tekstury wybranego materiału; 0: tak jak podaje materiał, albo 8 m" + +msgid "Angle the textures of the chosen material are turned by, on every chunk painted with it.\nCtrl+click to type it. To have the material turned in one place only, fill it there with an angle of its own." +msgstr "Kąt obrotu tekstur wybranego materiału na każdym kaflu nim malowanym.\nCtrl+klik, żeby wpisać. Żeby obrócić materiał tylko w jednym miejscu, wypełnij je materiałem z własnym kątem." + +msgid "Material" +msgstr "Materiał" + +msgid "Add to the palette" +msgstr "Dodaj do palety" + +msgid "New chunks are covered with the first material. A chunk holds up to 8 of them. A texture dragged from the browser onto an entry of the palette takes its place." +msgstr "Nowe kafle pokrywa pierwszy materiał. Kafel mieści do 8 materiałów. Tekstura przeciągnięta z przeglądarki na pozycję palety zajmuje jej miejsce." + +msgid "Material: %s (chosen in the palette below)" +msgstr "Materiał: %s (wybrany w palecie poniżej)" + +msgid "Outline: %zu points" +msgstr "Punkty obrysu: %zu" + +msgid "Edge blur (m)" +msgstr "Rozmycie krawędzi (m)" + +msgid "sharp" +msgstr "ostra" + +msgid "Metres across the outline the material blends in over, half of them inside the area.\n0: a sharp edge, as sharp as the paint samples go: every metre, or every 0.5 m in chunks converted to 0.5 m with fine paint" +msgstr "Na ilu metrach w poprzek obrysu materiał się przenika, połowa z nich w środku obszaru.\n0: ostra krawędź, tak ostra, jak gęste są próbki malowania: co metr albo co 0.5 m w kaflach zamienionych na 0.5 m z dokładnym malowaniem" + +msgid "Texture turned by (deg)" +msgstr "Obrót tekstury (°)" + +msgid "The area gets the material turned by this angle: an entry of the palette with the material at that angle\nis used, or added. Ctrl+click to type it" +msgstr "Obszar dostaje materiał obrócony o ten kąt: używana jest pozycja palety z tym materiałem pod tym kątem\nalbo dodawana nowa. Ctrl+klik, żeby wpisać" + +msgid "Fill the area" +msgstr "Wypełnij obszar" + +msgid "Clear the outline##fill" +msgstr "Wyczyść obrys##fill" + +msgid "Level (m)" +msgstr "Poziom (m)" + +msgid "Material##water" +msgstr "Materiał##water" + +msgid "Empty: %s, the water material of the terrain" +msgstr "Puste: %s, materiał wody terenu" + +msgid "Make the water" +msgstr "Utwórz wodę" + +msgid "Clear the outline" +msgstr "Wyczyść obrys" + +msgid "default material" +msgstr "materiał domyślny" + +msgid "Level of the chosen (m)" +msgstr "Poziom wybranej (m)" + +msgid "Material of the chosen" +msgstr "Materiał wybranej" + +msgid "A material or a texture (drag one from the texture browser).\nEmpty: %s, the water material of the terrain" +msgstr "Materiał albo tekstura (przeciągnij z przeglądarki tekstur).\nPuste: %s, materiał wody terenu" + +msgid "Texture repeats every (m)##water" +msgstr "Rozmiar tekstury (m)##water" + +msgid "Metres the texture of the water repeats at; 0: as the material gives it, or 8 m" +msgstr "Co ile metrów powtarza się tekstura wody; 0: tak jak podaje materiał, albo 8 m" + +msgid "Remove the chosen water" +msgstr "Usuń wybraną wodę" + +msgid "Default water material" +msgstr "Domyślny materiał wody" + +msgid "Material of the bodies of water which don't name their own" +msgstr "Materiał zbiorników wody, które nie podają własnego" + +msgid "Modifiers" +msgstr "Modyfikatory" + +msgid "None. The vertical profile of the tracks makes them: the terrain follows the formation and the slopes of the route." +msgstr "Brak. Tworzy je profil podłużny torów: teren idzie za torowiskiem i skarpami trasy." + +msgid "Bake" +msgstr "Utrwal" + +msgid "The ground keeps the shape the modifier gives it, and the modifier is removed:\nfrom then on the terrain there is shaped by hand like anywhere else, and no longer follows the track" +msgstr "Teren zachowuje kształt nadany przez modyfikator, a modyfikator znika:\nod tej chwili teren kształtuje się tam ręcznie jak wszędzie indziej i nie idzie już za torem" + +msgid "Touch-ups over the modifiers: %zu points (of the chunks in memory)" +msgstr "Poprawki nad modyfikatorami, punkty: %zu (w kaflach w pamięci)" + +msgid "Drawing and files" +msgstr "Rysowanie i pliki" + +msgid "Detail" +msgstr "Szczegółowość" + +msgid "How far the full grid of the chunks reaches: metres of distance per metre of the drawn grid. Further away the grid is coarser" +msgstr "Jak daleko sięga pełna siatka kafli: metry odległości na metr rysowanej siatki. Dalej siatka jest rzadsza" + +msgid "Compress the files (zstd)" +msgstr "Kompresuj pliki (zstd)" + +msgid "The terrain is written with the scenery (Ctrl+S)." +msgstr "Teren zapisuje się razem ze scenerią (Ctrl+S)." + +msgid " (selected)" +msgstr " (zaznaczony)" + +msgid "Lays the imagery onto the ground geometry (terrain shapes, terrain tile models, heightmap terrain).\nPlaces without any ground use the height below." +msgstr "Kładzie zdjęcia na geometrii terenu (kształty terenu, modele kafli terenu, teren z mapy wysokości).\nMiejsca bez żadnego terenu używają wysokości poniżej." + +msgid "%d chunks made" +msgstr "Utworzono kafli: %d" + +msgid "Chunk %d,%d removed" +msgstr "Usunięto kafel %d,%d" + +msgid "Chunk %d,%d made" +msgstr "Utworzono kafel %d,%d" + +msgid "There's a chunk there already" +msgstr "Tu już jest kafel" + +msgid "Chunks selected: %zu" +msgstr "Zaznaczone kafle: %zu" + +msgid "The heightmap terrain can't be opened" +msgstr "Nie da się otworzyć terenu z mapy wysokości" + +msgid "The chosen files and materials have no triangles" +msgstr "Wybrane pliki i materiały nie mają trójkątów" + +msgid "Ground converted: %zu chunks of heightmap terrain made, %zu shapes taken out of the scene, %zu triangle nodes go from the terrain files on save" +msgstr "Teren zamieniony. Utworzone kafle terenu z mapy wysokości: %zu, kształty zdjęte ze sceny: %zu, obiekty z trójkątów usuwane z plików terenu przy zapisie: %zu" + +msgid ", %zu chunks kept as there was terrain already" +msgstr ", kafle pozostawione, bo był tam już teren: %zu" + +msgid ". %zu of the files are binary (.btf) without their text: their triangles come back with the next load, until the files are taken out of the terrain directive" +msgstr ". Pliki binarne (.btf) bez wersji tekstowej: %zu; ich trójkąty wrócą przy następnym wczytaniu, dopóki pliki nie zostaną usunięte z dyrektywy terrain" + +msgid "The terrain couldn't be saved: %s" +msgstr "Nie udało się zapisać terenu: %s" + +msgid "Area filled with %s" +msgstr "Obszar wypełniony materiałem %s" + +msgid "Nothing filled: no chunk under the outline, or no room for another material" +msgstr "Nic nie wypełniono: pod obrysem nie ma kafla albo brak miejsca na kolejny materiał" + +msgid "Chunks converted to %s: %zu of %zu" +msgstr "Kafle zamienione na %s: %zu z %zu" + +msgid "Target height: %.2f m" +msgstr "Wysokość docelowa: %.2f m" + +msgid "Del" +msgstr "Del" + +msgid "none" +msgstr "brak" + +msgid "Location" +msgstr "Położenie" + +msgid "N %.0f, E %.0f" +msgstr "X %.0f, Y %.0f" + +msgid "PUWG 1992" +msgstr "PUWG 1992" + +msgid "Change..." +msgstr "Zmień..." + +msgid "The place is shared with the orthophoto" +msgstr "Położenie jest wspólne z ortofotomapą" + +msgid "What gives way" +msgstr "Co dopasować" + +msgid "Terrain under the tracks" +msgstr "Teren pod tory" + +msgid "The tracks go down or up onto the real ground: grade lines fitted to it within the limits, switches level with their tracks" +msgstr "Tory schodzą lub wznoszą się na prawdziwy teren" + +msgid "The tracks stay where they are; the real ground is drawn to them and keeps its relief farther away" +msgstr "Tory zostają, a teren dopasowuje się do nich" + +msgid "No tracks yet: the terrain is made around the camera, the tracks laid then go along it" +msgstr "Nie ma jeszcze torów: teren powstanie wokół kamery, a tory układane potem pójdą po nim" + +msgid "Load" +msgstr "Wczytaj" + +msgid "Load the terrain, put the tracks onto it" +msgstr "Wczytaj teren i posadź na nim tory" + +msgid "Load the terrain under the tracks" +msgstr "Wczytaj teren pod tory" + +msgid "Load the terrain around the camera" +msgstr "Wczytaj teren wokół kamery" + +msgid "Terrain model: %zu squares left" +msgstr "NMT: zostało %zu kwadratów" + +msgid "Grade lines: %zu fitted, %zu tracks left" +msgstr "Niwelety: dopasowano %zu, zostało %zu torów" + +msgid "Chunks: %zu of %zu" +msgstr "Kafle: %zu z %zu" + +msgid "By hand: the model as it is, the tracks stay" +msgstr "Ręcznie: NMT bez zmian, tory zostają" + +msgid "Terrain shaped by the modifier \"%s\" (%zu points): embankment up to %.2f m, cutting up to %.2f m" +msgstr "Teren ukształtowany modyfikatorem \"%s\" (%zu punktów): nasyp do %.2f m, wykop do %.2f m" + +msgid "The shaping of the terrain is gone already" +msgstr "Ukształtowanie terenu już zostało cofnięte" + +msgid "Convert the terrain files to heightmap terrain..." +msgstr "Zamień pliki terenu na teren z mapy wysokości..." + +msgid "Replaces the triangles of the terrain files of the scenery (terrain directive, .txtf/.btf) with heightmap terrain\nof the same shape, painted with their materials; the files lose these triangles on save" +msgstr "Zastępuje trójkąty plików terenu scenerii (dyrektywa terrain, .txtf/.btf) terenem z mapy wysokości\no tym samym kształcie, pomalowanym ich materiałami; przy zapisie pliki tracą te trójkąty" + +msgid "The scenery has no terrain files: no terrain directive names a .txtf or .btf file" +msgstr "Sceneria nie ma plików terenu: żadna dyrektywa terrain nie wskazuje pliku .txtf ani .btf" + +msgid "Terrain files of the scenery:" +msgstr "Pliki terenu scenerii:" + +msgid "(text, %zu triangles)" +msgstr "(tekstowy, %zu trójkątów)" + +msgid "(binary, %zu triangles)" +msgstr "(binarny, %zu trójkątów)" + +msgid "Materials: tick the ones which are the ground" +msgstr "Materiały: zaznacz te, które są gruntem" + +msgid "The chunks are painted with the materials of the triangles, up to 8 of them per chunk, added to the palette of the terrain. Until saved, loading the scenery again takes it all back." +msgstr "Kafle malowane są materiałami trójkątów, do 8 na kafel, dodanymi do palety terenu. Do zapisu ponowne wczytanie scenerii cofa wszystko." + +msgid "Only the heightmap terrain changes: a formation under the track and slopes to the ground, kept as a modifier of the terrain which follows the grade line when it's shaped again; the triangles and models of the scenery stay as they are." +msgstr "Zmienia się tylko teren z mapy wysokości: powstaje torowisko i skarpy, modele scenerii zostają bez zmian" + +msgid "Chunks of heightmap terrain are made along the route where there's none, from the ground around,\nthen shaped. Where the scenery has its own ground triangles the chunk is left out: convert them instead" +msgstr "Wzdłuż trasy, tam gdzie nie ma terenu z mapy wysokości, powstają kafle z terenu dookoła,\npotem są kształtowane. Tam, gdzie sceneria ma własne trójkąty gruntu, kafel jest pomijany: zamień je najpierw" + +msgid "Removes the modifier of the route, and takes away the terrain the shaping made" +msgstr "Usuwa modyfikator trasy i zabiera teren utworzony przy kształtowaniu" + +msgid "Route" +msgstr "Trasa" + +msgid "%zu paths, %.2f m: %s ... %s" +msgstr "%zu torów, %.2f m: %s ... %s" + +msgid " %s > %s, %zu paths, %.2f m" +msgstr " %s > %s, %zu torów, %.2f m" + +msgid "From the track" +msgstr "Odczytaj z toru" + +msgid "The points of the grade line read from the track as it lies now" +msgstr "Odczytuje punkty niwelety z toru tak, jak teraz leży" + +msgid "To the ground" +msgstr "Dopasuj do terenu" + +msgid "+-%.2f m" +msgstr "±%.2f m" + +msgid "%d errors, %d warnings" +msgstr "błędy: %d, ostrzeżenia: %d" + +msgid "%d errors" +msgstr "błędy: %d" + +msgid "%d warnings" +msgstr "ostrzeżenia: %d" + +msgid "Within the limits" +msgstr "Zgodna z normami" + +msgid "Correct the errors first: the red remarks, a click on the state shows them" +msgstr "Najpierw popraw błędy (czerwone uwagi). Kliknij licznik błędów obok, żeby je zobaczyć" + +msgid "Sets the heights of the track along the route to the grade line, the track moves by up to %.3f m; Ctrl+Z takes it back" +msgstr "Ustawia wysokości toru wzdłuż trasy według niwelety, tor przesunie się najwyżej o %.3f m; Ctrl+Z cofa" + +msgid "The track lies on the grade line already" +msgstr "Tor już leży na niwelecie" + +msgid "Click a track in the view" +msgstr "Kliknij tor w widoku" + +msgid "Along the line of %s" +msgstr "Pokaż niweletę toru %s" + +msgid "Designs kept in the scenery (%zu)..." +msgstr "Otwórz zapisany projekt (%zu)..." + +msgid "%s %.1f m, %zu points" +msgstr "%s %.1f m, %zu punktów" + +msgid "From the track, To the ground and Apply to the scenery are over the plot of the strip" +msgstr "Odczytaj z toru, Dopasuj do terenu i Zastosuj w scenerii są nad wykresem w oknie profilu" + +msgid "km %s H %.3f m R %.0f m" +msgstr "km %s H %.3f m R %.0f m" + +msgid "km %s%s" +msgstr "km %s%s" + +msgid "Show:" +msgstr "Pokaż:" + +msgid "Legend" +msgstr "Legenda" + +msgid "km %s H %.3f m" +msgstr "km %s H %.3f m" + +msgid "km %s" +msgstr "km %s" + +msgid "Name" +msgstr "Nazwa" + +msgid "%.0f m / %.0f mm" +msgstr "%.0f m / %.0f mm" + +msgid "Up" +msgstr "W górę" + +msgid "Filter" +msgstr "Filtr" + +msgid "No textures here" +msgstr "Brak tekstur" + +msgid "Drag a texture onto a field of a material; double click: add it to the palette of the terrain" +msgstr "Przeciągnij teksturę na pole materiału; dwuklik: dodaj ją do palety terenu" + +msgid "material" +msgstr "materiał" + +msgid "texture" +msgstr "tekstura" + +msgid "LMB adds a chunk next to the clicked one, Shift+LMB removes it" +msgstr "LPM dodaje kafel obok klikniętego, Shift+LPM go usuwa" + +msgid "LMB selects the clicked chunk, Ctrl+LMB adds one or takes it away; the tool options convert the point spacing of the selected" +msgstr "LPM zaznacza kliknięty kafel, Ctrl+LPM dodaje albo zdejmuje; w opcjach narzędzia zmienia się odstęp punktów zaznaczonych" + +msgid "LMB takes back the touch-ups made over the tracks: the terrain returns to the shape the tracks give it" +msgstr "LPM cofa poprawki zrobione nad torami: teren wraca do kształtu, jaki nadają mu tory" + +msgid "zoom" +msgstr "przybliżenie" + +msgid "heights" +msgstr "wysokości" + +msgid "pan" +msgstr "przesuwanie" + +msgid "on the line: add a point" +msgstr "na linii: dodaj punkt" + +msgid "only the height" +msgstr "tylko wysokość" + +msgid "keep the grade before" +msgstr "zachowaj pochylenie przed" + +msgid "up or down: radius of the vertical curve" +msgstr "w górę albo w dół: promień łuku pionowego" + +msgid "move the point" +msgstr "przesuń punkt" + +msgid "remove" +msgstr "usuń" + +msgid "add a point here" +msgstr "dodaj tu punkt" + +msgid "Errors" +msgstr "Błędy" + +msgid "Warnings" +msgstr "Ostrzeżenia" + +msgid "The scenery has terrain already: along the tracks it's made anew from the model, its painting too" +msgstr "Sceneria ma już teren: wzdłuż torów powstanie od nowa z NMT, a jego malowanie zniknie" + +msgid "Building" +msgstr "Budowa" + +msgid "Equipment" +msgstr "Wyposażenie" + +msgid "F1: all the shortcuts" +msgstr "F1: wszystkie skróty" + +msgid "Free laying" +msgstr "Swobodnie" + +msgid "Plain arcs, no transition curves nor cant" +msgstr "Same łuki, bez krzywych przejściowych i przechyłki" + +msgid "To the PKP PLK norms" +msgstr "Według norm PKP PLK" + +msgid "Transition curves and cant for the speed, checked against the norms" +msgstr "Krzywe przejściowe i przechyłka dobrane do prędkości" + +msgid "its properties show here" +msgstr "jego właściwości pokazują się tutaj" + +msgid "Shift+click" +msgstr "Shift+LPM" + +msgid "Ctrl+F" +msgstr "Ctrl+F" + +msgid "finds a track by its name" +msgstr "szuka toru po nazwie" + +msgid "Or start with:" +msgstr "Albo zacznij od:" + +msgid "Lay a track (L)" +msgstr "Ułóż tor (L)" + +msgid "embankment and cutting" +msgstr "nasyp i wykop" + +msgid "the terrain model" +msgstr "model terenu" + +msgid "The profile tool (P)" +msgstr "Narzędzie Profil (P)" + +msgid "Terrain (F5)" +msgstr "Teren (F5)" + +msgid "; %zu stretches changed on the track since read from it again" +msgstr "; %zu odcinków zmienionych na torze odczytano z niego ponownie" + +msgid "The grade line" +msgstr "Niweleta" + +msgid "The profile tool: a click on a track opens the grade line of its whole line in the strip below" +msgstr "Narzędzie Profil: klik na torze otwiera niweletę całej jego linii w pasku na dole" + +msgid "Drag a point of the grade line: the grades on both sides of it follow" +msgstr "Przeciągnij punkt niwelety: pochylenia po obu jego stronach idą za nim" + +msgid "A double click on the line adds a point" +msgstr "Podwójny klik na linii dodaje punkt" + +msgid "Removes the chosen point" +msgstr "Usuwa wybrany punkt" + +msgid "Over the plot: the heights stretched, the grades seen better" +msgstr "Nad wykresem: wysokości rozciągnięte, pochylenia lepiej widać" + +msgid "Over the plot: the view moves along the line" +msgstr "Nad wykresem: widok przesuwa się wzdłuż linii" + +msgid "Open the grade line of a track" +msgstr "Otwórz niweletę toru" + +msgid "Move or add a point" +msgstr "Przesuń albo dodaj punkt" + +msgid "Apply it: the tracks take its heights" +msgstr "Zastosuj ją: tory przyjmą jej wysokości" + +msgid "Terrain from the terrain model" +msgstr "Teren z modelu terenu" + +msgid "The Terrain field of work: the terrain model (NMT) is in its tool options" +msgstr "Dział Teren: numeryczny model terenu (NMT) jest w jego opcjach narzędzia" + +msgid "Location: where the scenery lies in PUWG 1992; a scenery begun from nothing asks for it with its first track" +msgstr "Położenie: gdzie na mapie leży sceneria; nowa sceneria pyta o nie przy pierwszym torze" + +msgid "What gives way: the tracks go onto the terrain, or the terrain is drawn to the tracks" +msgstr "Co dopasować: tory kładą się na teren albo teren dociąga się do torów" + +msgid "Load: the model comes from geoportal.gov.pl, the terrain is made along the tracks with the formation and the slopes" +msgstr "Wczytaj: model przychodzi z geoportal.gov.pl, teren powstaje wzdłuż torów razem z torowiskiem i skarpami" + +msgid "Takes the changes of the tracks back" +msgstr "Cofa zmiany torów" + +msgid "Open the Terrain field of work" +msgstr "Otwórz dział Teren" + +msgid "Load terrain from the model" +msgstr "Wczytaj teren z modelu" + +msgid "Sceneries made before" +msgstr "Starsze scenerie" + +msgid "A track of an include or of a file that isn't saved: the inspector says why it can't be changed" +msgstr "Tor z pliku dołączanego (include) albo z pliku, który nie jest zapisywany: Inspektor mówi, czemu nie da się go zmienić" + +msgid "Switches laid before keep their shape; Replace the geometry with the template, under More settings of the switch, gives them the one of the catalogue" +msgstr "Rozjazdy ułożone wcześniej zachowują kształt; „Zastąp geometrię szablonem” w „Więcej ustawień” rozjazdu daje im kształt z katalogu szablonów" + +msgid "Grade lines kept by an older editor are read as they were kept" +msgstr "Niwelety zapisane przez starszy edytor są czytane tak, jak je zapisano" + +msgid "Only what changed is written: the other lines stay as they were, the comment lines of other tools too" +msgstr "Zapisuje się tylko to, co się zmieniło: reszta pliku zostaje jak była, także komentarze innych narzędzi" + +msgid "P, LMB on a track" +msgstr "P, LPM na torze" + +msgid "The grade line of its whole line, in the strip under the view" +msgstr "Niweleta całej jego linii, w pasku pod widokiem" + +msgid "LMB + move on a point" +msgstr "LPM + ruch na punkcie" + +msgid "Change the grade before and after it" +msgstr "Zmienia pochylenie przed nim i za nim" + +msgid "LMB LMB on the line" +msgstr "LPM LPM na linii" + +msgid "Add a point" +msgstr "Dodaje punkt" + +msgid "Remove the chosen point" +msgstr "Usuwa wybrany punkt" + +msgid "Wheel" +msgstr "Kółko" + +msgid "Zoom along the line" +msgstr "Przybliża wzdłuż linii" + +msgid "Stretch the heights" +msgstr "Rozciąga wysokości" + +msgid "RMB + move" +msgstr "PPM + ruch" + +msgid "Move the view along the line" +msgstr "Przesuwa widok wzdłuż linii" + +msgid "Apply in the scenery" +msgstr "Zastosuj w scenerii" + +msgid "Write the heights into the tracks; Ctrl+Z takes them back" +msgstr "Zapisuje wysokości w torach; Ctrl+Z je cofa" + +msgid "Find: a key or what it does" +msgstr "Szukaj: klawisz albo co robi" + +msgid "Digitize the whole scenery" +msgstr "Digitalizacja całej scenerii" + +msgid "Keeps the plan of the lines in the scenery as straights, arcs and transitions, so the tools can edit them; the paths stay as they are" +msgstr "Zapisuje w scenerii plan linii jako proste, łuki i przejściowe, żeby narzędzia mogły je edytować; tory zostają, jakie są" + +msgid "R1 %.0f m" +msgstr "R1 %.0f m" + +msgid "R %.0f m" +msgstr "R %.0f m" + +msgid "cant %.0f mm" +msgstr "przechyłka %.0f mm" + +msgid "entry %.0f m" +msgstr "wejście %.0f m" + +msgid "exit %.0f m" +msgstr "wyjście %.0f m" + +msgid "Radius of the arc: drag sideways, or a double click to type it" +msgstr "Promień łuku: przeciągnij w bok albo kliknij dwukrotnie, żeby go wpisać" + +msgid "Length of the transition curve leading into the arc" +msgstr "Długość krzywej przejściowej na wejściu w łuk" + +msgid "Length of the transition curve leading out of the arc" +msgstr "Długość krzywej przejściowej na wyjściu z łuku" + +msgid "Curve %d: R %.0f m, cant %.0f mm" +msgstr "Łuk %d: R %.0f m, przechyłka %.0f mm" + +msgid "Transition into curve %d: %.0f m" +msgstr "Przejściowa na wejściu w łuk %d: %.0f m" + +msgid "Transition out of curve %d: %.0f m" +msgstr "Przejściowa na wyjściu z łuku %d: %.0f m" + +msgid "up or down: the radius" +msgstr "w górę albo w dół: promień" + +msgid "sideways: the length" +msgstr "w bok: długość" + +msgid "the radius by 5%" +msgstr "promień o 5%" + +msgid "the length by 5 m" +msgstr "długość o 5 m" + +msgid "Plan of a curve" +msgstr "Plan łuku" + +msgid "G, LMB on a curve" +msgstr "G, LPM na łuku" + +msgid "Its design: the vertex and the radius to drag in the view, the curvature in the panel" +msgstr "Jego projekt: wierzchołek i promień do przeciągania w widoku, krzywizna w panelu" + +msgid "Drag a plateau of the curvature band" +msgstr "Przeciągnij półkę na pasku krzywizny" + +msgid "The radius of that arc" +msgstr "Promień tego łuku" + +msgid "Drag a ramp sideways" +msgstr "Przeciągnij rampę w bok" + +msgid "The length of the transition curve" +msgstr "Długość krzywej przejściowej" + +msgid "Wheel over the band" +msgstr "Kółko nad paskiem" + +msgid "The radius by 5%, a transition by 5 m" +msgstr "Promień o 5%, przejściowa o 5 m" + +msgid "Lay the paths by the design; Ctrl+Z takes it back" +msgstr "Układa tory według projektu; Ctrl+Z to cofa" + +msgid "Drag in the curvature band" +msgstr "Przeciągnij na pasku krzywizny" + +msgid "R up and down, transitions sideways" +msgstr "R w górę i w dół, przejściowe w bok" + +msgid "Correcting a curve" +msgstr "Poprawianie łuku" + +msgid "The plan tool: a click on a curve shows its design, the vertex and the radius to drag in the view" +msgstr "Narzędzie Plan: klik na łuku pokazuje jego projekt, wierzchołek i promień do przeciągania w widoku" + +msgid "In the panel, the band of the curvature: drag the plateau of the arc up or down for its radius" +msgstr "W panelu pasek krzywizny: półka łuku przeciągnięta w górę albo w dół zmienia jego promień" + +msgid "Drag a ramp sideways: the transition curve gets longer or shorter" +msgstr "Rampa przeciągnięta w bok: krzywa przejściowa wydłuża się albo skraca" + +msgid "Over the band: the radius by 5%, a transition by 5 m" +msgstr "Nad paskiem: promień o 5%, przejściowa o 5 m" + +msgid "Apply to the scenery: the tracks are laid by the design" +msgstr "Zastosuj w scenerii: tory układają się według projektu" + +msgid "Open the plan of a curve" +msgstr "Otwórz plan łuku" + +msgid "Change the curve in the band" +msgstr "Zmień łuk na pasku krzywizny" + +msgid "Apply it to the scenery" +msgstr "Zastosuj go w scenerii" + +msgid "The plan tool (G)" +msgstr "Narzędzie Plan (G)" + +msgid "A click: the new track on this side" +msgstr "LPM: nowy tor po tej stronie" + +msgid "Only beside the selected path" +msgstr "Tylko obok zaznaczonego toru" + +msgid "Beside the whole line of the selected path, up to the switches: its curves go along with R minus or plus the spacing" +msgstr "Nowy tor idzie obok całej linii zaznaczonego toru, aż do rozjazdów; jego łuki mają R mniejszy albo większy o rozstaw" + +msgid "Spacing of the axes of the tracks; the usual ones beside it" +msgstr "Rozstaw osi torów; obok najczęstsze" + +msgid "Click a track in the view: the new one goes beside it." +msgstr "Kliknij tor w widoku: nowy pójdzie obok niego." + +msgid "A new track beside the one selected, as in the preview; the selected stays as it is. Ctrl+Z takes it back" +msgstr "Nowy tor obok zaznaczonego, jak na podglądzie; zaznaczony zostaje, jaki był. Ctrl+Z cofa" + +msgid "A second track beside a track: LMB on it, then the side and the spacing" +msgstr "Drugi tor obok toru: LPM na nim, potem strona i rozstaw" + +msgid "Click on a side in the panel" +msgstr "LPM na Lewa/Prawa w panelu" + +msgid "the side of the new track" +msgstr "strona nowego toru" + +msgid "Parallel track: LMB on a track, then the side and the spacing" +msgstr "Tor równoległy: LPM na torze, potem strona i rozstaw" + +msgid "cant change rate" +msgstr "przyrost przechyłki" + +msgid "cant ramp" +msgstr "rampa przechyłki" + +msgid "no transition" +msgstr "brak przejściowej" + +msgid "switch branch" +msgstr "tor zwrotny rozjazdu" + +msgid "Limit" +msgstr "Limit" + +msgid "Safe" +msgstr "Bezpieczna" + +msgid "Suggested" +msgstr "Sugerowana" + +msgid "Cause" +msgstr "Przyczyna" + +msgid "R / cant" +msgstr "R / przechyłka" + +msgid "All the paths" +msgstr "Wszystkie tory" + +msgid "With a speed limit set" +msgstr "Z ograniczeniem" + +msgid "Without a speed limit" +msgstr "Bez ograniczenia" + +msgid "selected" +msgstr "zaznaczony" + +msgid "Left and right as seen along the line, the way the arrow points" +msgstr "Lewa i prawa strona, gdy patrzysz wzdłuż linii w kierunku strzałki" + +msgid "%.0f m, up to %d positions set by the tracks" +msgstr "%.0f m, do %d położeń według torów" + +msgid "%.0f m, %d fixed positions" +msgstr "%.0f m, stałe położenia: %d" + +msgid "While laying from a free end: Shift+wheel" +msgstr "Przy układaniu od wolnego końca: Shift+kółko" + +msgid "(Shift+wheel)" +msgstr "(Shift+kółko)" + +msgid "length" +msgstr "długość" + +msgid "plan" +msgstr "plan" + +msgid "grade" +msgstr "pochylenie" + +msgid "cant" +msgstr "przechyłka" + +msgid "The terrain model and the aerial photos of geoportal.gov.pl around here; the tracks go along the real ground" +msgstr "Model terenu i zdjęcia lotnicze z geoportal.gov.pl wokół tego miejsca; tory pójdą po prawdziwym terenie" + +msgid "Type where the scenery lies first" +msgstr "Najpierw wpisz, gdzie leży sceneria" + +msgid "The tracks lie on the flat plane at the height 0; the terrain can be loaded later in Terrain (F5)" +msgstr "Tory leżą na płaszczyźnie na wysokości 0; teren możesz wczytać później w dziale Teren (F5)" + +msgid "The scenery has no tracks yet" +msgstr "Sceneria nie ma jeszcze torów" + +msgid "the lay tool" +msgstr "narzędzie układania" + +msgid "on the ground: the first point, then each next one lays a piece" +msgstr "na terenie: pierwszy punkt, każdy następny układa kawałek" + +msgid "the radius of the curve while laying" +msgstr "promień łuku przy układaniu" + +msgid "all the shortcuts, and the tutorial" +msgstr "wszystkie skróty i samouczek" + +msgid "Redo (Ctrl+Y): %zu steps" +msgstr "Ponów (Ctrl+Y): kroków %zu" + +msgid "Undo (Ctrl+Z): %zu steps" +msgstr "Cofnij (Ctrl+Z): kroków %zu" + +msgid "azimuth" +msgstr "azymut" + +msgid "paths" +msgstr "tory" + +msgid "green: a track out of the position" +msgstr "zielony: tor wychodzi z położenia" + +msgid "Look of the new track" +msgstr "Wygląd nowego toru" + +msgid "These textures" +msgstr "Te tekstury" + +msgid "Like the selected one" +msgstr "Jak zaznaczony" + +msgid "The rails and the trackbed chosen below.\nA track started or finished at a free end always takes them from the track it joins" +msgstr "Szyny i podsypka wybrane niżej.\nTor zaczęty albo skończony na wolnym końcu zawsze bierze je od toru, do którego dochodzi" + +msgid "Texture of the rails.\nA material dragged from the texture browser can be dropped here" +msgstr "Tekstura szyn.\nMożna tu upuścić materiał przeciągnięty z przeglądarki tekstur" + +msgid "Off" +msgstr "Wył." + +msgid "The gizmo hidden: the selected things won't move by chance" +msgstr "Gizmo ukryte: zaznaczone rzeczy nie przesuną się przypadkiem" + +msgid "Arrows which move the selected thing" +msgstr "Strzałki, które przesuwają zaznaczoną rzecz" + +msgid "Rings which turn the selected thing" +msgstr "Pierścienie, które obracają zaznaczoną rzecz" + +msgid "Handles which scale the selected thing; a track is lengthened" +msgstr "Uchwyty, które skalują zaznaczoną rzecz; tor jest wydłużany" + +msgid "Add %s to the train" +msgstr "Dodaj %s do składu" + +msgid "A double click on the list adds it too" +msgstr "Podwójny klik na liście też go dodaje" + +msgid "Train" +msgstr "Skład" + +msgid "No vehicles yet: pick one from the list and add it" +msgstr "Jeszcze bez pojazdów: wybierz któryś z listy i dodaj go" + +msgid "Forward" +msgstr "Do przodu" + +msgid "Moves it one place towards the head" +msgstr "Przesuwa go o jedno miejsce w stronę czoła" + +msgid "Back" +msgstr "Do tyłu" + +msgid "Moves it one place towards the end" +msgstr "Przesuwa go o jedno miejsce w stronę końca" + +msgid "Turn around" +msgstr "Obróć" + +msgid "On the track" +msgstr "Na torze" + +msgid "The train can be driven or run by the AI at once" +msgstr "Składem można od razu jechać albo puścić go z AI" + +msgid "The vehicles stand braked, without a crew" +msgstr "Pojazdy stoją zahamowane, bez obsady" + +msgid "Takes it out of the train" +msgstr "Wyjmuje go ze składu" + +msgid "Railway" +msgstr "Kolej" + +msgid "Road" +msgstr "Droga" + +msgid "From the scenery" +msgstr "Ze scenerii" + +msgid "errors" +msgstr "błędy" + +msgid "warnings" +msgstr "ostrzeżenia" + +msgid "off the track" +msgstr "odchyłka od toru" + +msgid "The list of them: Remarks in the strip under the view" +msgstr "Lista: zakładka Uwagi w oknie profilu pod widokiem" + +msgid "Kind of the connection" +msgstr "Rodzaj połączenia" + +msgid "B, Trainset, LMB on a track" +msgstr "B, Skład, LPM na torze" + +msgid "Where the train stands" +msgstr "Gdzie stoi pociąg" + +msgid "LMB LMB on the list" +msgstr "LPM LPM na liście" + +msgid "Adds the vehicle to the train" +msgstr "Dodaje pojazd do pociągu" + +msgid "LMB on a vehicle of the train" +msgstr "LPM na pojeździe pociągu" + +msgid "Moves it, turns it around or takes it out" +msgstr "Przesuwa go, obraca albo wyjmuje" + +msgid "Puts the train on the track and gives the cab at once" +msgstr "Stawia pociąg na torze i od razu daje kabinę" + +msgid "Tool options, Gizmo, Off" +msgstr "Opcje narzędzia, Gizmo, Wył." + +msgid "Hides the gizmo, so nothing moves by chance" +msgstr "Ukrywa gizmo, żeby nic nie przesunęło się przypadkiem" + +msgid "the grade line of its whole line" +msgstr "niweleta całej jego linii" + +msgid "%d of the selected tracks are off the route from %s to %s" +msgstr "%d zaznaczonych torów leży poza trasą od %s do %s" + +msgid "Click: take it out of the route, drag: a box" +msgstr "LPM: wyjmij go z trasy, przeciągnij: ramka" + +msgid "Click: add it to the route, drag: a box" +msgstr "LPM: dodaj go do trasy, przeciągnij: ramka" + +msgid "Click: the grade line of the whole line through this track; Shift: the route between the tracks" +msgstr "LPM: niweleta całej linii przez ten tor; Shift: trasa między torami" + +msgid "its whole line" +msgstr "cała jego linia" + +msgid "The grade line of its whole line shows here; Shift+LMB on more tracks or Shift+drag a box: the route from one end of them to the other. Drag the points of the line up and down, then apply it to the scenery." +msgstr "Tu pokaże się niweleta całej jego linii; przeciągaj jej punkty w górę i w dół, potem kliknij Zastosuj w scenerii" + +msgid "the route between the outermost ones" +msgstr "trasa między skrajnymi" + +msgid "Drag: the route over the tracks in a box" +msgstr "Przeciągnij: trasa po torach w ramce" + +msgid "The route between the outermost of the tracks, through the switches" +msgstr "Trasa między skrajnymi torami, przez rozjazdy" + +msgid "Shift + a box: between the outermost" +msgstr "Shift+ramka: między skrajnymi" + +msgid "LMB on a track: its whole line" +msgstr "LPM na torze: cała jego linia" + +msgid "A click adds a track, a box from the empty ground takes the tracks in it: the route runs between the outermost of them" +msgstr "Klik dodaje tor, ramka z pustego terenu bierze tory w niej: trasa biegnie między skrajnymi z nich" + +msgid "Trainset tool: a click on the track where the train stands" +msgstr "Narzędzie Skład: klik na torze, gdzie stoi pociąg" + +msgid "A double click on a vehicle of the list puts it into the train; a click on a vehicle of the train moves it, turns it or takes it out" +msgstr "Podwójny klik na pojeździe z listy wstawia go do pociągu; klik na pojeździe pociągu przesuwa go, obraca albo wyjmuje" + +msgid "Place and drive: the train goes onto the track and the cab is yours" +msgstr "Postaw i jedź: pociąg staje na torze, a kabina jest twoja" + +msgid "The trainset tool" +msgstr "Narzędzie Skład" + +msgid "Twelve short lessons with clips and drawings: the camera, laying track, curves, correcting a curve, joining, switches, signals and posts, fixing, the grade line, terrain, older sceneries, saving and driving" +msgstr "Dwanaście krótkich lekcji z filmikami i rysunkami: kamera, układanie toru, łuki, poprawianie łuku, łączenie, rozjazdy, sygnalizatory i słupki, poprawki, niweleta, teren, starsze scenerie, zapis i jazda" + +msgid "H: by the track, drag along: the way the trains run" +msgstr "H: przy torze, przeciągnij wzdłuż: kierunek pociągów" + +msgid "B: the hectometre posts of the whole line at once" +msgstr "B: słupki hektometrowe całej linii naraz" + +msgid "Signals and posts" +msgstr "Sygnalizatory i słupki" + +msgid "The signals tool: the type of the signal in the panel, from the templates of the scenery" +msgstr "Narzędzie Sygnalizatory: typ sygnalizatora w panelu, z szablonów scenerii" + +msgid "A click by a track places the signal on that side" +msgstr "Klik przy torze stawia sygnalizator po tej stronie" + +msgid "Press by the track and drag along it: the trains running that way see the signal" +msgstr "Wciśnij przy torze i przeciągnij wzdłuż: sygnalizator widzą pociągi jadące w tę stronę" + +msgid "Drag a standing signal: it moves along its track" +msgstr "Przeciągnij stojący sygnalizator: przesuwa się wzdłuż swojego toru" + +msgid "Objects along the track: the hectometre posts and the W17 markers of a whole line at once, and trainsets" +msgstr "Obiekty przy torze: słupki hektometrowe i wskaźniki W17 całej linii naraz oraz składy" + +msgid "Place a signal" +msgstr "Postaw sygnalizator" + +msgid "Place the hectometre posts of a line" +msgstr "Postaw słupki hektometrowe linii" + +msgid "The signals tool (H)" +msgstr "Narzędzie Sygnalizatory (H)" + +msgid "\nPaths off the route brought to its height:" +msgstr "\nTory obok trasy doprowadzone do jej wysokości:" + +msgid "spawn point" +msgstr "punkt pojawiania się" + +msgid "removal point" +msgstr "punkt znikania" + +msgid "pedestrian crossing" +msgstr "przejście dla pieszych" + +msgid "level crossing" +msgstr "przejazd kolejowy" + +msgid "\"%s\" can't be changed: %s" +msgstr "Nie da się zmienić \"%s\": %s" + +msgid "The cursor isn't over anything near enough" +msgstr "Pod kursorem nie ma niczego wystarczająco blisko" + +msgid "Selected: %s" +msgstr "Zaznaczone: %s" + +msgid "(unnamed road)" +msgstr "(droga bez nazwy)" + +msgid "It can't be changed: %s" +msgstr "Nie da się tego zmienić: %s" + +msgid "Point at a road" +msgstr "Wskaż drogę" + +msgid "Point at the place a road crosses the rails" +msgstr "Wskaż miejsce, w którym droga przecina tory" + +msgid "The road gains or loses lanes on this piece; a level crossing goes on a piece which keeps its lanes" +msgstr "Na tym odcinku droga zyskuje albo traci pasy; przejazd stawiasz na odcinku ze stałą liczbą pasów" + +msgid "No rails cross the road here; a crossing put here won't close until some do" +msgstr "Tu drogi nie przecinają żadne tory; postawiony tu przejazd nie zamknie się, dopóki ich nie będzie" + +msgid "Point at a lane of a road" +msgstr "Wskaż pas ruchu na drodze" + +msgid "No scenery is loaded" +msgstr "Nie wczytano żadnej scenerii" + +msgid "Level crossing made, Ctrl+Z takes it back. Lanes stopping %.1f m from its middle: %d, tracks watched for trains: %d" +msgstr "Przejazd kolejowy postawiony, Ctrl+Z cofa. Pojazdy stają %.1f m od środka; pasów: %d, obserwowanych torów: %d" + +msgid "Spawn point made, with no vehicles: nothing appears there until it's given some (Select, click the point)" +msgstr "Punkt pojawiania się postawiony, ale bez pojazdów: nic się tu nie pojawi, dopóki ich nie dodasz (narzędzie Zaznaczanie, kliknij punkt)" + +msgid "Spawn point made, Ctrl+Z takes it back. Vehicles on the roads at a time: up to %d. They appear while the simulation runs" +msgstr "Punkt pojawiania się postawiony, Ctrl+Z cofa. Pojazdów naraz na drogach: do %d. Pojawiają się w trakcie symulacji" + +msgid "Removal point made, Ctrl+Z takes it back. Lanes it takes vehicles from: %d" +msgstr "Punkt znikania postawiony, Ctrl+Z cofa. Pasów, z których zdejmuje pojazdy: %d" + +msgid "Pedestrian crossing painted, Ctrl+Z takes it back" +msgstr "Przejście dla pieszych namalowane, Ctrl+Z cofa" + +msgid "Changed, Ctrl+Z takes it back" +msgstr "Zmienione, Ctrl+Z cofa" + +msgid "Lanes stopping here: %d, tracks watched for trains: %d" +msgstr "Pasów, które tu stają: %d, obserwowanych torów: %d" + +msgid "No rails pass through it, so it never closes" +msgstr "Nie przechodzą przez niego żadne tory, więc nigdy się nie zamyka" + +msgid "Look for the rails again" +msgstr "Wyszukaj tory ponownie" + +msgid "For when the tracks were laid or changed after the crossing was made" +msgstr "Na nowo szuka torów, które przecinają drogę. Użyj, gdy tory ułożono albo zmieniono już po postawieniu przejazdu" + +msgid "Stop distance from the middle [m]" +msgstr "Zatrzymanie od środka (m)" + +msgid "Where the vehicles stop, measured along the road from the middle of the crossing.\nThe crossing closes for the tracks passing within that distance from the middle" +msgstr "Gdzie stają pojazdy, mierzone wzdłuż drogi od środka przejazdu.\nPrzejazd zamyka się dla torów, które przechodzą w tej odległości od środka" + +msgid "Stop distance from the track [m]" +msgstr "Zatrzymanie od toru (m)" + +msgid "Where the vehicles stop, measured along the road from the axis of the first track they'd get to" +msgstr "Gdzie stają pojazdy, mierzone wzdłuż drogi od osi pierwszego toru, do którego dojeżdżają" + +msgid "Closing distance [m]" +msgstr "Odległość zamykania (m)" + +msgid "How far a train is when the crossing closes. Measured along the tracks, both ways, with both ways of each switch counted.\nA rail vehicle within that distance which gets nearer closes the crossing, and so does one right at it.\nIt opens a few seconds after the last one is gone" +msgstr "Jak daleko jest pociąg, gdy przejazd się zamyka. Mierzone wzdłuż torów w obie strony, licząc obie drogi przez każdy rozjazd.\nPojazd szynowy w tej odległości, który się zbliża, zamyka przejazd, tak samo jak taki, który stoi na przejeździe.\nPrzejazd otwiera się kilka sekund po odjeździe ostatniego" + +msgid "Paint the stop lines" +msgstr "Linie zatrzymania" + +msgid "It isn't on a lane, so nothing appears here" +msgstr "Nie stoi na pasie ruchu, więc nic się tu nie pojawi" + +msgid "The vehicles are made when the simulation runs" +msgstr "Pojazdy powstają w trakcie symulacji" + +msgid "Vehicles made: %d, waiting for their turn: %d" +msgstr "Utworzonych pojazdów: %d, czekających na swoją kolej: %d" + +msgid "A vehicle every [s]" +msgstr "Pojazd co (s)" + +msgid "Randomness [0-1]" +msgstr "Losowość (0-1)" + +msgid "Part of the time between vehicles which is drawn at random each time.\n0: they come like clockwork, 1: anything from one right after another to twice the time" +msgstr "Część odstępu między pojazdami losowana za każdym razem.\n0: przyjeżdżają jak w zegarku, 1: od jednego tuż za drugim do dwa razy dłuższego odstępu" + +msgid "Speed [km/h]" +msgstr "Prędkość (km/h)" + +msgid "The speed the vehicles appear with. From there on the drivers go by the speed limits of the lanes" +msgstr "Prędkość, z jaką pojawiają się pojazdy. Dalej kierowcy jadą według ograniczeń prędkości na pasach" + +msgid "Vehicles at a time" +msgstr "Pojazdów naraz" + +msgid "How many vehicles of this point can be on the scenery at once. That many are made along with the scenery,\ndrawn from the listed ones in turn; one of those which wait is put on the road each time its turn comes,\nuntil all of them are out. A vehicle taken by a removal point goes back to wait for its next turn,\nand so does one which stood still for 15 seconds, other than at a closed level crossing, or went backwards" +msgstr "Ile pojazdów z tego punktu może być naraz w scenerii. Tyle powstaje razem ze scenerią,\nkolejno z listy; za każdym razem, gdy przychodzi kolej, jeden z czekających wjeżdża na drogę,\naż wyjadą wszystkie. Pojazd zdjęty przez punkt znikania wraca czekać na swoją kolej,\ntak samo jak taki, który stał 15 sekund (poza zamkniętym przejazdem kolejowym) albo jechał do tyłu" + +msgid "Vehicles of the set: %d" +msgstr "Pojazdów w zestawie: %d" + +msgid "Remove from the set" +msgstr "Usuwa pojazd z zestawu" + +msgid "No vehicles, so nothing appears here" +msgstr "Brak pojazdów, więc nic się tu nie pojawi" + +msgid "node -1 0 none dynamic \n enddynamic\nor , one vehicle a line.\nThe path, the offset, the driver and the velocity don't matter" +msgstr "node -1 0 none dynamic \n <ładunek> enddynamic\nalbo , jeden pojazd w wierszu.\nTor, przesunięcie, kierowca i prędkość nie mają znaczenia" + +msgid "Add to the set" +msgstr "Dodaj do zestawu" + +msgid "No vehicle found in the text" +msgstr "W tekście nie ma żadnego pojazdu" + +msgid "Empty the set" +msgstr "Opróżnij zestaw" + +msgid "Paste the vehicles above the way a scenery lists them" +msgstr "Wklej pojazdy powyżej tak, jak zapisuje je sceneria" + +msgid "Lanes it takes vehicles from: %d" +msgstr "Pasów, z których zdejmuje pojazdy: %d" + +msgid "Reach [m]" +msgstr "Zasięg (m)" + +msgid "Road vehicles on the lanes passing within this distance from the point are taken off the road when they get here,\nexcept for the one the user drives. Vehicles which came from a spawn point go back to wait for their next turn.\nThe distance it starts with takes in a single lane" +msgstr "Pojazdy drogowe na pasach, które przechodzą w tej odległości od punktu, znikają z drogi, gdy tu dojadą,\npoza tym, którym jedziesz ty. Pojazdy z punktu pojawiania się wracają czekać na swoją kolej.\nOdległość początkowa obejmuje jeden pas" + +msgid "Length along the road [m]" +msgstr "Długość wzdłuż drogi (m)" + +msgid "Stripes across the whole road" +msgstr "Zebra na całą szerokość drogi" + +msgid "The drivers don't stop for it: there's nobody to cross" +msgstr "Kierowcy przed nim nie stają: nikt tu nie przechodzi" + +msgid "Speed limit [km/h]" +msgstr "Dozwolona prędkość (km/h)" + +msgid "0: no limit" +msgstr "0: bez ograniczenia" + +msgid "Height of the middle [m]" +msgstr "Wysokość środka (m)" + +msgid "The surface is spanned between the ends of the roads and this point.\nThe ends of the roads are raised or lowered as points: select the white dot at the end of a road" +msgstr "Nawierzchnia rozpina się między końcami dróg a tym punktem.\nKońce dróg podnosisz albo obniżasz jak punkty: zaznacz białą kropkę na końcu drogi" + +msgid "no markings" +msgstr "bez linii" + +msgid "white" +msgstr "białe" + +msgid "orange" +msgstr "pomarańczowe" + +msgid "Markings" +msgstr "Linie na jezdni" + +msgid "Surface" +msgstr "Nawierzchnia" + +msgid "Texture length [m]" +msgstr "Długość tekstury (m)" + +msgid "How many metres of the surface one copy of the texture covers" +msgstr "Ile metrów nawierzchni pokrywa jedno powtórzenie tekstury" + +msgid "Kerb around the corners" +msgstr "Krawężnik na narożnikach" + +msgid "Kerb width [m]" +msgstr "Szerokość krawężnika (m)" + +msgid "Kerb material" +msgstr "Materiał krawężnika" + +msgid "Kerb height [m]" +msgstr "Wysokość krawężnika (m)" + +msgid "no side" +msgstr "brak" + +msgid "shoulder" +msgstr "pobocze" + +msgid "sidewalk" +msgstr "chodnik" + +msgid "Beside the corners" +msgstr "Przy narożnikach" + +msgid "Width [m]" +msgstr "Szerokość (m)" + +msgid "Pick a material to have the sides drawn" +msgstr "Wybierz materiał, żeby boki były widoczne" + +msgid "Bank width [m]" +msgstr "Szerokość skarpy (m)" + +msgid "Bank height [m]" +msgstr "Wysokość skarpy (m)" + +msgid "Banks to the ground##banks" +msgstr "Skarpy do terenu##banks" + +msgid "Bank material" +msgstr "Materiał skarp" + +msgid "What the banks are covered with next to a sidewalk, a kerb or the bare edge.\nA bank of a shoulder is covered with the material of the shoulder" +msgstr "Czym pokryte są skarpy obok chodnika, krawężnika albo gołej krawędzi.\nSkarpa pobocza ma materiał pobocza" + +msgid "Bank slope 1:n" +msgstr "Pochylenie skarpy 1:n" + +msgid "n: metres across for a metre of height; 1.5 is a usual embankment" +msgstr "n: ile metrów w poziomie na metr wysokości; 1.5 to zwykły nasyp" + +msgid "Lead the banks to the ground" +msgstr "Doprowadź skarpy do terenu" + +msgid "Looks for the ground past each corner, at both roads the corner runs between,\nand sets the bank of the corner to reach it. Done for the roads too, the banks meet at the ends of the roads" +msgstr "Szuka terenu za każdym narożnikiem, przy obu drogach, między którymi biegnie narożnik,\ni ustawia skarpę narożnika tak, żeby do niego sięgała. To samo robi z drogami, skarpy spotykają się na końcach dróg" + +msgid "Remove the banks" +msgstr "Usuń skarpy" + +msgid "nothing, the ground shows" +msgstr "nic, widać teren" + +msgid "surface closed to the traffic" +msgstr "nawierzchnia wyłączona z ruchu" + +msgid "island" +msgstr "wysepka" + +msgid "Between the directions" +msgstr "Między kierunkami" + +msgid "Shows where a road which keeps its two directions apart meets this stretch:\nwhat's between the directions is led on to the other road, where it closes or carries on" +msgstr "Pokazuje, gdzie droga z rozdzielonymi kierunkami styka się z tym miejscem:\nto, co jest między kierunkami, przechodzi na drugą drogę i tam się kończy albo biegnie dalej" + +msgid "Island material" +msgstr "Materiał wysepki" + +msgid "Roads of the junction##arms" +msgstr "Drogi skrzyżowania##arms" + +msgid "Lanes come from the roads: change them on the road" +msgstr "Pasy pochodzą z dróg: zmieniaj je na drodze" + +msgid "The junction follows the roads. The roads are numbered in the view. Lanes are counted from the middle of the road;\nautomatic: the inner lane turns left, the outer one right, all go straight on" +msgstr "Skrzyżowanie dopasowuje się do dróg. Drogi są ponumerowane w widoku. Pasy liczy się od środka drogi;\nautomatycznie: wewnętrzny pas skręca w lewo, zewnętrzny w prawo, wszystkie jadą prosto" + +msgid "%d: %d in, %d out, lanes %.2f m wide" +msgstr "%d: wjazd %d, wyjazd %d, pasy po %.2f m" + +msgid "Stop line" +msgstr "Linia zatrzymania" + +msgid "Length of the pedestrian crossing along the road" +msgstr "Długość przejścia dla pieszych wzdłuż drogi" + +msgid "no sign: gives way to the right" +msgstr "bez znaku: ustępuje jadącym z prawej" + +msgid "has the right of way" +msgstr "ma pierwszeństwo" + +msgid "gives way" +msgstr "ustępuje pierwszeństwa" + +msgid "stops, then gives way" +msgstr "stop, potem ustępuje" + +msgid "Right of way of the traffic coming in from this road" +msgstr "Pierwszeństwo ruchu, który wjeżdża z tej drogi" + +msgid "automatic" +msgstr "automatycznie" + +msgid "straight on" +msgstr "prosto" + +msgid "left, straight on" +msgstr "w lewo, prosto" + +msgid "left, right" +msgstr "w lewo, w prawo" + +msgid "straight on, right" +msgstr "prosto, w prawo" + +msgid "any way" +msgstr "w każdą stronę" + +msgid "Lane %d goes" +msgstr "Kierunki z pasa %d" + +msgid "Lanes of the road being built differ in width, and a junction takes roads with lanes of the same width" +msgstr "Pasy budowanej drogi mają różne szerokości, a skrzyżowanie przyjmuje tylko drogi z pasami równej szerokości" + +msgid "This arm of the junction takes a road with lanes: %d leading to it and %d away from it, %.2f m wide each" +msgstr "Ten wlot skrzyżowania przyjmuje drogę z pasami: %d w jego stronę i %d od niego, każdy po %.2f m" + +msgid "This roadway takes a road with lanes: %d leading to it and %d away from it, %.2f m wide each" +msgstr "Ta jezdnia przyjmuje drogę z pasami: %d w jej stronę i %d od niej, każdy po %.2f m" + +msgid "Move the cursor to where the road is to go" +msgstr "Przesuń kursor tam, dokąd ma biec droga" + +msgid "The junctions at both ends would take the same piece of road; lead the road somewhere else first" +msgstr "Skrzyżowania na obu końcach zajęłyby ten sam odcinek drogi; najpierw poprowadź drogę gdzie indziej" + +msgid "Too far from the last point" +msgstr "Za daleko od ostatniego punktu" + +msgid "The click didn't land on anything near enough" +msgstr "Kliknięcie nie trafiło w nic wystarczająco bliskiego" + +msgid "Points selected: %d" +msgstr "Zaznaczonych punktów: %d" + +msgid "No road here" +msgstr "Tu nie ma drogi" + +msgid "Can't build from here: %s" +msgstr "Stąd nie da się budować: %s" + +msgid "Carrying on \"%s\" with its layout" +msgstr "Przedłużasz drogę \"%s\" z jej układem pasów" + +msgid "Leading a road out of \"%s\", with the lanes the junction has there" +msgstr "Wyprowadzasz drogę ze skrzyżowania \"%s\", z pasami, jakie ma tam skrzyżowanie" + +msgid "Carrying on one of the roadways of \"%s\" as a one-way road" +msgstr "Przedłużasz jedną z jezdni drogi \"%s\" jako drogę jednokierunkową" + +msgid "Leading a road out of the side of \"%s\": click where it goes" +msgstr "Wyprowadzasz drogę z boku drogi \"%s\": kliknij, dokąd ma biec" + +msgid "\"%s\" has four roads already" +msgstr "Skrzyżowanie \"%s\" ma już cztery drogi" + +msgid "Leading another road out of \"%s\": click where it goes" +msgstr "Wyprowadzasz kolejną drogę ze skrzyżowania \"%s\": kliknij, dokąd ma biec" + +msgid "Start set: click the next points, Esc finishes" +msgstr "Początek ustawiony: klikaj kolejne punkty, Esc kończy" + +msgid "Can't build this: %s" +msgstr "Nie da się tego zbudować: %s" + +msgid "Can't join here: %s" +msgstr "Tu nie da się połączyć: %s" + +msgid "The road wasn't created" +msgstr "Nie udało się utworzyć drogi" + +msgid "Joined with \"%s\", Ctrl+Z takes it back" +msgstr "Połączono z \"%s\", Ctrl+Z cofa" + +msgid "Led into \"%s\" as another of its roads, Ctrl+Z takes it back" +msgstr "Doprowadzono do \"%s\" jako kolejną drogę skrzyżowania, Ctrl+Z cofa" + +msgid "Led into the side of the road, with a junction made there, Ctrl+Z takes it back" +msgstr "Doprowadzono do boku drogi i zrobiono tam skrzyżowanie, Ctrl+Z cofa" + +msgid "Pieces added: %d" +msgstr "Dodanych odcinków: %d" + +msgid ", as another road of the junction" +msgstr ", jako kolejna droga skrzyżowania" + +msgid ", with a junction where the road leaves" +msgstr ", ze skrzyżowaniem w miejscu odejścia drogi" + +msgid ", Ctrl+Z takes them back" +msgstr ", Ctrl+Z cofa" + +msgid "Pieces changed: %d, Ctrl+Z takes it back" +msgstr "Zmienionych odcinków: %d, Ctrl+Z cofa" + +msgid "What's next to it was fitted to it" +msgstr "Sąsiednie odcinki i skrzyżowania dopasowano" + +msgid "Can't delete: %s" +msgstr "Nie da się usunąć: %s" + +msgid "\"%s\" deleted, Ctrl+Z brings it back" +msgstr "Usunięto \"%s\", Ctrl+Z przywraca" + +msgid "The roundabout is too small for its lanes: give it a larger radius" +msgstr "Rondo jest za małe na swoje pasy: daj mu większy promień" + +msgid "There's a road in the way. Make the roundabout next to it, then lead the road into its side" +msgstr "Rondo zachodzi na istniejącą drogę. Zrób rondo obok niej, potem doprowadź tę drogę do boku ronda" + +msgid "The roundabout wasn't created" +msgstr "Nie udało się utworzyć ronda" + +msgid "Roundabout made, Ctrl+Z takes it back. Untick Roundabout and lead roads out of its side, or into it: the traffic joining it gives way to the traffic going around" +msgstr "Rondo zrobione, Ctrl+Z cofa. Odznacz Rondo i wyprowadzaj drogi z jego boku albo do niego: wjeżdżający ustępują jadącym po rondzie" + +msgid "Banks of the junction led to the ground, Ctrl+Z takes it back" +msgstr "Skarpy skrzyżowania doprowadzone do terenu, Ctrl+Z cofa" + +msgid "Pick a bank material to have them drawn" +msgstr "Wybierz materiał skarp, żeby były widoczne" + +msgid "Banks led to the ground, pieces: %d, Ctrl+Z takes it back" +msgstr "Skarpy doprowadzone do terenu, odcinków: %d, Ctrl+Z cofa" + +msgid "Pick a bank material to have them drawn where the road has no shoulder" +msgstr "Wybierz materiał skarp, żeby były widoczne tam, gdzie droga nie ma pobocza" + +msgid "A point was left: %s" +msgstr "Punkt nie został usunięty: %s" + +msgid "Nothing deleted" +msgstr "Nic nie usunięto" + +msgid "Point deleted, Ctrl+Z brings it back" +msgstr "Punkt usunięty, Ctrl+Z przywraca" + +msgid "Points deleted: %d, Ctrl+Z brings them back" +msgstr "Usuniętych punktów: %d, Ctrl+Z przywraca" + +msgid "Can't split: %s" +msgstr "Nie da się podzielić: %s" + +msgid "Can't split a piece which gains or loses lanes; give it the same lanes at both ends first" +msgstr "Nie da się podzielić odcinka, który zyskuje albo traci pasy; najpierw daj mu te same pasy na obu końcach" + +msgid "Can't split this close to the end" +msgstr "Nie da się podzielić tak blisko końca" + +msgid "Split in two: \"%s\" and \"%s\", Ctrl+Z takes it back. Click the point between them to set the width or the height of the road there" +msgstr "Podzielono na dwa: \"%s\" i \"%s\", Ctrl+Z cofa. Kliknij punkt między nimi, żeby ustawić tam szerokość albo wysokość drogi" + +msgid "There's nothing to change at the selected points" +msgstr "W zaznaczonych punktach nie ma czego zmienić" + +msgid "Pick from the materials in the texture folder" +msgstr "Pokazuje materiały z katalogu tekstur do wyboru" + +msgid "Materials and images found in the texture folder" +msgstr "Materiały i obrazy z katalogu tekstur" + +msgid "Lanes along the axis, at the start" +msgstr "Pasy zgodnie z osią, na początku" + +msgid "Lanes along the axis" +msgstr "Pasy zgodnie z osią" + +msgid "Lanes going the way the road was built, from its start to its end; drawn green with Show lanes" +msgstr "Pasy jadące tak, jak budowano drogę, od jej początku do końca; z „Pokaż pasy ruchu” rysowane na zielono" + +msgid "Lanes against the axis, at the start" +msgstr "Pasy przeciw osi, na początku" + +msgid "Lanes against the axis" +msgstr "Pasy przeciw osi" + +msgid "Lanes going back, from the end of the road to its start; drawn orange with Show lanes" +msgstr "Pasy jadące z powrotem, od końca drogi do jej początku; z „Pokaż pasy ruchu” rysowane na pomarańczowo" + +msgid "Other lanes at the end of the piece" +msgstr "Inne pasy na końcu odcinka" + +msgid "The road gains or loses lanes along this piece: it gets wider or narrower, a lane it gains branches off the outermost lane\nof its direction (two at most), a lane it loses joins the outermost one left.\nWhat's next to the piece is fitted to the lanes the piece has at that end.\nThe axis goes the way the lanes drawn in green do: the start of the piece is where these come from.\nSplit the piece (K) first to have the lanes change over a part of it" +msgstr "Na tym odcinku droga zyskuje albo traci pasy: poszerza się albo zwęża, nowy pas odgałęzia się od skrajnego pasa\nswojego kierunku (najwyżej dwa), znikający pas łączy się ze skrajnym, który zostaje.\nTo, co sąsiaduje z odcinkiem, dopasowuje się do pasów, jakie odcinek ma na tym końcu.\nOś biegnie tak jak pasy rysowane na zielono: początek odcinka jest tam, skąd one wychodzą.\nNajpierw podziel odcinek (K), żeby pasy zmieniały się tylko na jego części" + +msgid "Lanes along the axis, at the end" +msgstr "Pasy zgodnie z osią, na końcu" + +msgid "Lanes against the axis, at the end" +msgstr "Pasy przeciw osi, na końcu" + +msgid "A lane forks into three at most: lanes past the second one gained can't be got on" +msgstr "Pas rozwidla się najwyżej na trzy: na pasy dodane ponad dwa nie da się wjechać" + +msgid "Lane width [m]" +msgstr "Szerokość pasa (m)" + +msgid "Left side" +msgstr "Lewa strona" + +msgid "Right side" +msgstr "Prawa strona" + +msgid "Pick a material to have the side drawn" +msgstr "Wybierz materiał, żeby ten bok był widoczny" + +msgid "Kerb on the left" +msgstr "Krawężnik z lewej" + +msgid "on the right##kerb" +msgstr "z prawej##kerb" + +msgid "Pick a material to have the kerbs drawn" +msgstr "Wybierz materiał, żeby krawężniki były widoczne" + +msgid "The bank every shoulder of the road is closed with, unless the side has a bank of its own set below" +msgstr "Skarpa, którą kończy się każde pobocze drogi, chyba że bok ma niżej ustawioną własną skarpę" + +msgid "A slope from the edge of the road to the ground" +msgstr "Skarpa od krawędzi drogi do terenu" + +msgid "It goes down, or up, to the ground. Set here it goes to every piece the changes go to;\nthe button below works out each piece on its own" +msgstr "Biegnie w dół albo w górę, do terenu. Ustawiona tutaj trafia do każdego odcinka, którego dotyczą zmiany;\nprzycisk niżej wylicza każdy odcinek osobno" + +msgid "Bank on the left" +msgstr "Skarpa z lewej" + +msgid "Bank on the right" +msgstr "Skarpa z prawej" + +msgid "Width at the start [m]" +msgstr "Szerokość na początku (m)" + +msgid "Height at the start [m]" +msgstr "Wysokość na początku (m)" + +msgid "Width at the end [m]" +msgstr "Szerokość na końcu (m)" + +msgid "Height at the end [m]" +msgstr "Wysokość na końcu (m)" + +msgid "What a bank is covered with next to a sidewalk, a kerb or the bare edge of the road.\nA bank of a shoulder is covered with the material of the shoulder" +msgstr "Czym pokryta jest skarpa obok chodnika, krawężnika albo gołej krawędzi drogi.\nSkarpa pobocza ma materiał pobocza" + +msgid "Looks for the ground on both sides of the road, at the start and at the end of each piece,\nand sets the banks to reach it with the slope given above: down where the road is over the ground, up where it's cut into it.\nDone for the selected piece, or for every piece of the road if the changes go to the whole road" +msgstr "Szuka terenu po obu stronach drogi, na początku i na końcu każdego odcinka,\ni ustawia skarpy tak, żeby do niego sięgały z pochyleniem podanym wyżej: w dół, gdzie droga biegnie nad terenem, w górę, gdzie jest w niego wcięta.\nDziała na zaznaczony odcinek albo na każdy odcinek drogi, jeśli zmiany dotyczą całej drogi" + +msgid "Between the directions##median" +msgstr "Między kierunkami##median" + +msgid "How far apart the two directions are led at each end of the piece; 0 at one end and more at the other makes them part along it.\nSet for each piece on its own. The pieces next to it follow it where they meet, and so do the junctions.\nA road with its directions apart can be carried on as two one-way roads: start the Build tool\non the middle of one of its roadways, at an end of the road with nothing attached" +msgstr "Jak daleko od siebie biegną oba kierunki na każdym końcu odcinka; 0 na jednym końcu i więcej na drugim rozdziela je wzdłuż odcinka.\nUstawiane dla każdego odcinka osobno. Sąsiednie odcinki i skrzyżowania dopasowują się w miejscu styku.\nDrogę z rozdzielonymi kierunkami można przedłużyć jako dwie drogi jednokierunkowe: zacznij narzędziem Budowa\nna środku jednej z jej jezdni, na końcu drogi, do którego nic nie jest dołączone" + +msgid "Apart at the start [m]" +msgstr "Odstęp na początku (m)" + +msgid "Apart at the end [m]" +msgstr "Odstęp na końcu (m)" + +msgid "The island is as high as the kerbs; with a kerb material set it gets kerbs of its own" +msgstr "Wysepka jest tak wysoka jak krawężniki; z ustawionym materiałem krawężnika dostaje własne krawężniki" + +msgid "Length of the rounded end of the island at that end of the piece; half of the width of the island makes it a half circle.\n0: the island is cut straight, with a face. Use it where the next piece has no island" +msgstr "Długość zaokrąglonego końca wysepki na tym końcu odcinka; połowa szerokości wysepki daje półkole.\n0: wysepka jest ucięta prosto, ze ścianką. Użyj tego tam, gdzie następny odcinek nie ma wysepki" + +msgid "Rounded at the start [m]" +msgstr "Zaokrąglenie na początku (m)" + +msgid "Rounded at the end [m]" +msgstr "Zaokrąglenie na końcu (m)" + +msgid "Traffic share" +msgstr "Udział w ruchu" + +msgid "How willing the drivers are to take this road when they leave a junction, next to the other roads they can take there.\n100: like any other. With 100 for the main road and 20 for a dirt road leaving it, one vehicle in six turns into the dirt road.\n0: nobody takes it, unless there's no other way" +msgstr "Jak chętnie kierowcy wybierają tę drogę, gdy wyjeżdżają ze skrzyżowania, w porównaniu z innymi drogami, którymi mogą tam pojechać.\n100: jak każda inna. Przy 100 dla głównej drogi i 20 dla odchodzącej od niej drogi gruntowej co szósty pojazd skręca w gruntową.\n0: nikt nią nie jedzie, chyba że nie ma innej drogi" + +msgid "Part of a roundabout" +msgstr "Część ronda" + +msgid "A junction made on this road gives the right of way to the traffic going along it,\nand tells the traffic of the road joining it to give way" +msgstr "Skrzyżowanie zrobione na tej drodze daje pierwszeństwo jadącym wzdłuż niej,\na ruch z dołączającej drogi musi ustąpić pierwszeństwa" + +msgid "Lanes and lane changes##lanes" +msgstr "Pasy i zmiana pasa##lanes" + +msgid "Left to right, facing along the axis" +msgstr "Od lewej do prawej, patrząc wzdłuż osi" + +msgid "f: a lane along the axis, b: against it" +msgstr "f: pas zgodnie z osią, b: przeciw osi" + +msgid "Width of the lane" +msgstr "Szerokość tego pasa w metrach" + +msgid "Speed limit of the lane, 0: no limit" +msgstr "Dozwolona prędkość na tym pasie, 0: bez ograniczenia" + +msgid "only %s to %s" +msgstr "tylko z %s na %s" + +msgid "no lane change" +msgstr "bez zmiany pasa" + +msgid "lane change both ways" +msgstr "zmiana pasa w obie strony" + +msgid "Which way a vehicle may change between these two lanes" +msgstr "W którą stronę pojazd może zmienić pas między tymi dwoma pasami" + +msgid "Roundabout" +msgstr "Rondo" + +msgid "A one-way road going around, with the lanes set below as the lanes along the axis.\nThen untick this and lead the roads out of its side, or into it" +msgstr "Droga jednokierunkowa dookoła, z pasami ustawionymi niżej jako pasami zgodnymi z osią.\nPotem odznacz to i wyprowadzaj drogi z boku ronda albo do niego" + +msgid "a roundabout around the cursor" +msgstr "stawia rondo wokół kursora" + +msgid "Radius [m]##ring" +msgstr "Promień (m)##ring" + +msgid "Radius of the middle of the road going around" +msgstr "Promień środka jezdni ronda" + +msgid "Island in the middle" +msgstr "Wyspa na środku" + +msgid "What the middle is covered with; it's raised on a kerb as high as the kerbs of the road.\nnone: the middle is left as it is" +msgstr "Czym pokryty jest środek ronda; leży na krawężniku tak wysokim jak krawężniki drogi.\nnone: środek zostaje taki, jaki jest" + +msgid "the start, then each next point" +msgstr "stawia początek, potem każdy następny punkt" + +msgid "straight ahead" +msgstr "prowadzi drogę prosto przed siebie" + +msgid "finish the road" +msgstr "kończy drogę" + +msgid "LMB on a road or a junction" +msgstr "LPM na drodze albo skrzyżowaniu" + +msgid "the new road is joined to it there" +msgstr "nowa droga dołącza się w tym miejscu" + +msgid "Finish (Esc)" +msgstr "Zakończ (Esc)" + +msgid "Height above the ground [m]" +msgstr "Wysokość nad terenem (m)" + +msgid "How far over the ground under the cursor the points are put.\nA few centimetres keep the surface from flickering where it lies on the ground" +msgstr "Jak wysoko nad terenem pod kursorem stawiane są punkty.\nKilka centymetrów chroni nawierzchnię przed migotaniem tam, gdzie leży na terenie" + +msgid "On: the road is led over the ground between the clicked points, in pieces as short as the ground calls for,\nwith the profile rounded so the slope doesn't change abruptly.\nOff: the road goes straight from a point to the next one, like a ramp or a viaduct" +msgstr "Włączone: droga biegnie po terenie między klikniętymi punktami, odcinkami tak krótkimi, jak wymaga teren,\nz zaokrąglonym profilem, żeby pochylenie nie zmieniało się gwałtownie.\nWyłączone: droga biegnie prosto od punktu do punktu, jak rampa albo wiadukt" + +msgid "Junction speed limit [km/h]" +msgstr "Prędkość na skrzyżowaniach (km/h)" + +msgid "The speed limit the junctions made by the new roads get" +msgstr "Dozwolona prędkość na skrzyżowaniach, które tworzą nowe drogi" + +msgid "LMB on a road" +msgstr "LPM na drodze" + +msgid "where the rails cross it; the vehicles stop ahead of the rails while a train is coming" +msgstr "stawia przejazd tam, gdzie przecinają ją tory; pojazdy stają przed torami, gdy nadjeżdża pociąg" + +msgid "LMB on a lane" +msgstr "LPM na pasie ruchu" + +msgid "the vehicles of the set appear there one by one and drive the way the lane goes" +msgstr "stawia punkt: pojazdy z zestawu pojawiają się tam po kolei i jadą w kierunku pasa" + +msgid "stripes are painted across it there" +msgstr "maluje zebrę w poprzek drogi" + +msgid "the vehicles getting there are taken off the road; put one where the road leaves the scenery" +msgstr "stawia punkt: pojazdy, które tu dojadą, znikają z drogi; stawiaj go tam, gdzie droga opuszcza scenerię" + +msgid "The selected road is set up in the inspector" +msgstr "Zaznaczoną drogę ustawiasz w inspektorze" + +msgid "The Select tool shows the road picked in the view here" +msgstr "Narzędzie Zaznaczanie pokazuje tu drogę wybraną w widoku" + +msgid "Delete (Del)" +msgstr "Usuń (Del)" + +msgid "adds a point, or takes it out" +msgstr "dodaje punkt albo go odznacza" + +msgid "on a selected piece: a point at the cursor" +msgstr "na zaznaczonym odcinku robi punkt przy kursorze" + +msgid "Road width here [m]" +msgstr "Szerokość drogi tutaj (m)" + +msgid "Combined width of the lanes at this point; each lane takes its share of it.\nThe pieces on both sides get wider or narrower along their length to meet it.\nSelect several points to keep the width over the pieces between them" +msgstr "Łączna szerokość pasów w tym punkcie; każdy pas bierze swoją część.\nOdcinki po obu stronach poszerzają się albo zwężają na swojej długości, żeby do niej dojść.\nZaznacz kilka punktów, żeby utrzymać szerokość na odcinkach między nimi" + +msgid "Height [m]" +msgstr "Wysokość (m)" + +msgid "Height of the axis of the road at the point; with several points selected all are put at this height.\nThe slopes of the pieces at the point are kept, so the road stays smooth" +msgstr "Wysokość osi drogi w punkcie; przy kilku zaznaczonych punktach wszystkie dostają tę wysokość.\nPochylenia odcinków w punkcie zostają, więc droga pozostaje gładka" + +msgid "Lower" +msgstr "Obniż" + +msgid "Raise" +msgstr "Podnieś" + +msgid "by [m]##heightstep" +msgstr "o (m)##heightstep" + +msgid "Put on the ground" +msgstr "Połóż na terenie" + +msgid "Puts each point %.2f m over the ground under it; the height above the ground is set in the Build tool" +msgstr "Stawia każdy punkt %.2f m nad terenem pod nim; wysokość nad terenem ustawiasz w narzędziu Budowa" + +msgid "Delete the point (Del)" +msgstr "Usuń punkt (Del)" + +msgid "Delete the points (Del)" +msgstr "Usuń punkty (Del)" + +msgid "Takes the point out of the road: the pieces on its two sides become one piece,\nled from the far end of one to the far end of the other the way the road goes there.\nA point the road ends at takes the last piece of the road with it" +msgstr "Wyjmuje punkt z drogi: odcinki po jego obu stronach stają się jednym odcinkiem,\nprowadzonym od dalszego końca jednego do dalszego końca drugiego tak, jak biegnie tam droga.\nPunkt, na którym droga się kończy, zabiera ze sobą ostatni odcinek drogi" + +msgid "%s, a stretch where the road changes its lanes" +msgstr "%s, miejsce zmiany liczby pasów" + +msgid "A part of the road, leading the lanes of one side to the other" +msgstr "Część drogi, łączy pasy z jednej strony z pasami z drugiej" + +msgid "%s, roads: %d, ways through: %d" +msgstr "%s, dróg: %d, kierunków jazdy: %d" + +msgid "The ways through follow the roads attached now" +msgstr "Kierunki jazdy wynikają z dróg, które są teraz dołączone" + +msgid "Build tool, LMB on the junction: lead another road out of it" +msgstr "Narzędzie Budowa, LPM na skrzyżowaniu: wyprowadzasz z niego kolejną drogę" + +msgid "%s, %.1f m, lanes %d + %d to %d + %d" +msgstr "%s, %.1f m, pasy %d + %d na początku, %d + %d na końcu" + +msgid "%s, %.1f m, lanes %d + %d" +msgstr "%s, %.1f m, pasy %d + %d" + +msgid "K: split at the cursor" +msgstr "K: podziel przy kursorze" + +msgid "This piece has lanes of its own at its end, so it's changed alone" +msgstr "Ten odcinek ma na końcu własne pasy, więc zmienia się go osobno" + +msgid "Changes go to the whole road" +msgstr "Zmiany dla całej drogi" + +msgid "A road is a string of pieces. With this on a change of the layout is made to all of them,\nexcept for the lane change rules, which are always set for the selected piece alone.\nWith this off the selected piece alone is changed, and the road next to it is fitted to it:\nmade wider or narrower towards it, or given a stretch where the lanes change if their number differs.\nSplit a piece (K) to get a piece of the length you need" +msgstr "Droga to ciąg odcinków. Gdy to jest włączone, zmiana układu dotyczy ich wszystkich,\npoza zasadami zmiany pasa, które zawsze ustawia się dla samego zaznaczonego odcinka.\nGdy wyłączone, zmienia się tylko zaznaczony odcinek, a sąsiednia część drogi dopasowuje się do niego:\nposzerza się albo zwęża w jego stronę albo dostaje miejsce zmiany liczby pasów, jeśli ich liczba jest inna.\nPodziel odcinek (K), żeby uzyskać odcinek potrzebnej długości" + +msgid "a road piece, a junction, a point where pieces meet, a level crossing or a traffic point" +msgstr "zaznacza odcinek drogi, skrzyżowanie, punkt łączenia odcinków, przejazd kolejowy albo punkt ruchu" + +msgid "crossing, closed" +msgstr "przejazd, zamknięty" + +msgid "crossing" +msgstr "przejazd" + +msgid "spawn" +msgstr "pojawianie się" + +msgid "removal" +msgstr "znikanie" + +msgid "pedestrians" +msgstr "przejście dla pieszych" + +msgid "LMB: put a roundabout here" +msgstr "LPM: postaw tu rondo" + +msgid "LMB: lead the road out to the cursor, with a junction where it leaves Esc: give up" +msgstr "LPM: wyprowadź drogę do kursora, ze skrzyżowaniem w miejscu odejścia Esc: rezygnuj" + +msgid "LMB: lead the road out to the cursor, as another road of the junction Esc: give up" +msgstr "LPM: wyprowadź drogę do kursora jako kolejną drogę skrzyżowania Esc: rezygnuj" + +msgid "LMB: build up to the cursor Ctrl: straight ahead Esc: finish Ctrl+Z: undo the last piece" +msgstr "LPM: buduj do kursora Ctrl: prosto przed siebie Esc: zakończ Ctrl+Z: cofnij ostatni odcinek" + +msgid "LMB: carry on from this end" +msgstr "LPM: przedłuż drogę od tego końca" + +msgid "LMB: lead a road out of here, with a junction" +msgstr "LPM: wyprowadź stąd drogę, ze skrzyżowaniem" + +msgid "LMB: set where the road starts; on a loose end of a road to carry it on, on the side of a road or on a junction to lead a road out of it" +msgstr "LPM: ustaw początek drogi; na wolnym końcu drogi, żeby ją przedłużyć, na boku drogi albo na skrzyżowaniu, żeby wyprowadzić z nich drogę" + +msgid "LMB: put a %s here" +msgstr "LPM: postaw tu %s" + +msgid "LMB: put a %s on a road" +msgstr "LPM: postaw %s na drodze" + +msgid "LMB: select a piece, a junction, a point or a marker Del: delete Ctrl+Z: undo" +msgstr "LPM: zaznacz odcinek, skrzyżowanie, punkt albo znacznik Del: usuń Ctrl+Z: cofnij" + +msgid "LMB: select a point Shift+LMB: add a point or take it out Del: delete the points Ctrl+Z: undo" +msgstr "LPM: zaznacz punkt Shift+LPM: dodaj punkt albo go odznacz Del: usuń punkty Ctrl+Z: cofnij" + +msgid "LMB: select a piece, a junction or a point (white dot) K: split at the cursor Del: delete Ctrl+Z: undo" +msgstr "LPM: zaznacz odcinek, skrzyżowanie albo punkt (biała kropka) K: podziel przy kursorze Del: usuń Ctrl+Z: cofnij" + +msgid "LMB: select a piece, a junction or a point (white dot) Del: delete Ctrl+Z: undo" +msgstr "LPM: zaznacz odcinek, skrzyżowanie albo punkt (biała kropka) Del: usuń Ctrl+Z: cofnij" + +msgid "LMB: select a piece, a junction, a point (white dot) or a marker" +msgstr "LPM: zaznacz odcinek, skrzyżowanie, punkt (biała kropka) albo znacznik" + +msgid "Too close to a place where the road changes its lanes" +msgstr "Za blisko miejsca, w którym droga zmienia liczbę pasów" + +msgid "Too close to a junction" +msgstr "Za blisko skrzyżowania" + +msgid "Too close to the end of the road" +msgstr "Za blisko końca drogi" + +msgid "Too close to a place where roads meet" +msgstr "Za blisko miejsca, w którym łączą się drogi" + +msgid "The road is too short for a junction" +msgstr "Droga jest za krótka na skrzyżowanie" + +msgid "The road is cut into too many pieces here" +msgstr "Droga jest tu podzielona na zbyt wiele odcinków" + +msgid "Geometry of the roads can't be rebuilt with the experimental renderer" +msgstr "Z eksperymentalnym rendererem nie da się przebudować geometrii dróg" + +msgid "the scenery wasn't opened for editing (-edit), geometry of the roads it came with can't be taken apart" +msgstr "sceneria nie jest otwarta do edycji (-edit), więc geometrii jej dróg nie da się rozebrać" + +msgid "there are vehicles on it" +msgstr "są tam pojazdy" + +msgid "there are vehicles turning back next to it" +msgstr "obok zawracają pojazdy" + +msgid "there are vehicles on the junction it leads to" +msgstr "na skrzyżowaniu, do którego prowadzi, są pojazdy" + +msgid "the scenery wasn't opened for editing (-edit), geometry of the junctions it came with can't be taken apart" +msgstr "sceneria nie jest otwarta do edycji (-edit), więc geometrii jej skrzyżowań nie da się rozebrać" + +msgid "there are vehicles turning back on the roads it leads to" +msgstr "na drogach, do których prowadzi, zawracają pojazdy" + +msgid "there are vehicles turning back on the road" +msgstr "na drodze zawracają pojazdy" + +msgid "there are vehicles on the junction" +msgstr "na skrzyżowaniu są pojazdy" + +msgid "there are vehicles turning back on the roads the junction leads to" +msgstr "na drogach wychodzących ze skrzyżowania zawracają pojazdy" + +msgid "the point is where a road meets a junction. Delete the junction or the road instead" +msgstr "w tym punkcie droga styka się ze skrzyżowaniem. Usuń zamiast tego skrzyżowanie albo drogę" + +msgid "there's no point of a road there" +msgstr "nie ma tu punktu drogi" + +msgid "more than two ends of roads meet at the point" +msgstr "w punkcie spotykają się więcej niż dwa końce dróg" + +msgid "the road changes its lanes next to the point. Give that piece the same lanes at both its ends first" +msgstr "obok punktu droga zmienia liczbę pasów. Najpierw daj temu odcinkowi te same pasy na obu końcach" + +msgid "the road has other lanes on each side of the point. Give both sides the same lanes first" +msgstr "po obu stronach punktu droga ma inne pasy. Najpierw daj obu stronom te same pasy" + +msgid "the pieces at the point are too short" +msgstr "odcinki przy punkcie są za krótkie" + +msgid "Too short" +msgstr "Za krótki odcinek" + +msgid "The road would have to turn back to meet the end" +msgstr "Droga musiałaby zawrócić, żeby dojść do końca" + +msgid "The bend is too tight" +msgstr "Łuk jest za ciasny" + +msgid "The target is behind the start" +msgstr "Cel jest za początkiem" + +msgid "The road would have to turn back to reach the target" +msgstr "Droga musiałaby zawrócić, żeby dojść do celu" + +msgid "The road would leave at too sharp an angle" +msgstr "Droga odchodziłaby pod zbyt ostrym kątem" + +msgid "The junction has four roads already" +msgstr "Skrzyżowanie ma już cztery drogi" + +msgid "The road would leave too close to another road of the junction" +msgstr "Droga odchodziłaby za blisko innej drogi skrzyżowania" + +msgid "There's no room for the road between the roads the junction has; lead it out at a wider angle to them" +msgstr "Między drogami skrzyżowania nie ma miejsca na nową drogę; wyprowadź ją pod większym kątem do nich" + +msgid "Group together" +msgstr "Zgrupuj" + +msgid "\"%s\" can't be grouped: %s" +msgstr "\"%s\" nie da się zgrupować: %s" + +msgid "it's a part of a scenery template" +msgstr "jest częścią szablonu scenerii" + +msgid "unknown type" +msgstr "nieznany typ" + +msgid "L %.2f m azimuth %.4f deg grade %.2f per mille %zu paths" +msgstr "długość %.2f m azymut %.4f° pochylenie %.2f‰ torów: %zu" + +msgid "azimuth %.0f deg" +msgstr "azymut %.0f°" + +msgid "turn %+.3f deg R %.0f m" +msgstr "obrót %+.3f° R %.0f m" + +msgid "shift %+.3f m over %.2f m" +msgstr "przesunięcie %+.3f m na długości %.2f m" + +msgid "no curve fits" +msgstr "żaden łuk tu nie pasuje" + +msgid "curve L %.2f m" +msgstr "łuk L %.2f m" + +msgid " deflection %.3f deg" +msgstr " kąt zwrotu %.3f°" + +msgid "shift %+.3f m" +msgstr "przesunięcie %+.3f m" + +msgid "L %.3f m (%+.3f) azimuth %.4f deg" +msgstr "L %.3f m (%+.3f) azymut %.4f°" + +msgid "Drag: the curve of the switch goes on; Ctrl+drag: move the end" +msgstr "Przeciągnij: łuk rozjazdu biegnie dalej; Ctrl+przeciągnij: przesuń koniec" + +msgid "Drag: a new track out of this end, as in laying; Ctrl+drag: move the end" +msgstr "Przeciągnij: nowy tor z tego końca, jak przy układaniu; Ctrl+przeciągnij: przesuń koniec" + +msgid "Shift+LMB on more tracks" +msgstr "Shift+LPM na kolejnych torach" + +msgid "Shift+drag a box" +msgstr "Shift+przeciągnij ramkę" + +msgid "the route over the tracks in it" +msgstr "trasa przez tory w ramce" + +msgid "Cuts the rails of the switch at its frog and lays wing rails and check rails.\nOff for a switch whose include has its own model of them" +msgstr "Tnie szyny rozjazdu na krzyżownicy i układa szyny skrzydłowe oraz kierownice.\nWyłącz dla rozjazdu, którego include (plik dołączany) ma własny model krzyżownicy" + +msgid "The events listed below work until the scenery is loaded again; then they need the new name in their files" +msgstr "Eventy wymienione poniżej działają do ponownego wczytania scenerii; potem trzeba im wpisać nową nazwę w plikach" + +msgid "What is set below goes to all of them" +msgstr "Ustawienia poniżej trafią do wszystkich" + +msgid "Spread along the joins" +msgstr "Zaznaczanie połączonych" + +msgid "Ctrl+Shift+LMB on a track adds the tracks joined to it to the selection.\nHolding Ctrl+Shift shows what the click takes; on a track of the selection it takes them out" +msgstr "Ctrl+Shift+LPM na torze dodaje do zaznaczenia tory z nim połączone.\nPrzytrzymany Ctrl+Shift pokazuje, co zabierze kliknięcie; na zaznaczonym torze kliknięcie je wyjmuje" + +msgid "Reach along the track (m)" +msgstr "Zasięg wzdłuż toru (m)" + +msgid "How far along the tracks the spread goes from the clicked one; 0: no limit" +msgstr "Jak daleko wzdłuż torów sięga zaznaczanie od klikniętego; 0: bez limitu" + +msgid "%zu track deleted, Ctrl+Z brings it back" +msgid_plural "%zu tracks deleted, Ctrl+Z brings them back" +msgstr[0] "Usunięty %zu tor, Ctrl+Z go przywraca" +msgstr[1] "Usunięte %zu tory, Ctrl+Z je przywraca" +msgstr[2] "Usuniętych %zu torów, Ctrl+Z je przywraca" + +msgid "Laid %zu track, %.1f m, Ctrl+Z takes it back" +msgid_plural "Laid %zu tracks, %.1f m, Ctrl+Z takes them back" +msgstr[0] "Ułożony %zu tor, %.1f m, Ctrl+Z cofa" +msgstr[1] "Ułożone %zu tory, razem %.1f m, Ctrl+Z cofa" +msgstr[2] "Ułożonych %zu torów, razem %.1f m, Ctrl+Z cofa" + +msgid "The geometry allows any speed up to %.0f km/h" +msgstr "Geometria pozwala na dowolną prędkość do %.0f km/h" + +msgid "a straight" +msgstr "prostej" + +msgid " the other way" +msgstr " w przeciwną stronę" + +msgid "Warning only, the limit of a switch branch: " +msgstr "Tylko ostrzeżenie, ograniczenie toru zwrotnego: " + +msgid "No speed limit is set, so the trains run at the line speed, while the geometry allows %.0f km/h.\n" +msgstr "Nie ustawiono ograniczenia prędkości, więc pociągi jadą z prędkością linii, a geometria pozwala na %.0f km/h.\n" + +msgid "The speed set, %.0f km/h, is above the %.0f km/h the geometry allows.\n" +msgstr "Ustawiona prędkość %.0f km/h jest większa niż %.0f km/h, na które pozwala geometria.\n" + +msgid "The speed set, %.0f km/h, is within the %.0f km/h the geometry allows.\n" +msgstr "Ustawiona prędkość %.0f km/h mieści się w %.0f km/h, na które pozwala geometria.\n" + +msgid "Without the switch branch limit the geometry allows %.0f km/h (%s).\n" +msgstr "Bez ograniczenia toru zwrotnego geometria pozwala na %.0f km/h (%s).\n" + +msgid "Without the switch branch limit the geometry allows any speed.\n" +msgstr "Bez ograniczenia toru zwrotnego geometria pozwala na dowolną prędkość.\n" + +msgid "%s, %s, cant %.0f mm.\n" +msgstr "%s, %s, przechyłka %.0f mm.\n" + +msgid "Diverging track of the switch" +msgstr "Tor zwrotny rozjazdu" + +msgid "Unbalanced acceleration V²/(12.96 R) - g h/s at %.0f km/h: %.2f m/s², the limit is %.2f m/s²%s.\n" +msgstr "Przyspieszenie niezrównoważone V²/(12.96 R) - g h/s przy %.0f km/h: %.2f m/s², dopuszczalne %.2f m/s²%s.\n" + +msgid " for the switch branches, which have no cant" +msgstr " na torach zwrotnych, które nie mają przechyłki" + +msgid "A switch of a larger radius would allow more" +msgstr "Rozjazd o większym promieniu pozwoliłby na więcej" + +msgid "More cant or a larger radius would allow more" +msgstr "Większa przechyłka albo większy promień pozwoliłyby na więcej" + +msgid "The cant changes from %.0f to %.0f mm over %.0f m, %.2f mm per m.\n" +msgstr "Przechyłka zmienia się z %.0f na %.0f mm na długości %.0f m, o %.2f mm na metr.\n" + +msgid "A train at %.0f km/h feels it change by %.0f mm/s, the limit is %.0f mm/s.\n" +msgstr "Pociąg jadący %.0f km/h odczuwa jej zmianę o %.0f mm/s, dopuszczalne %.0f mm/s.\n" + +msgid "A longer cant ramp, along a longer transition curve, would allow more" +msgstr "Dłuższa rampa przechyłkowa, na dłuższej krzywej przejściowej, pozwoliłaby na więcej" + +msgid "The cant changes from %.0f to %.0f mm over %.0f m, a ramp of 1:%.0f.\n" +msgstr "Przechyłka zmienia się z %.0f na %.0f mm na długości %.0f m, rampa 1:%.0f.\n" + +msgid "At %.0f km/h the ramp can't be steeper than 1:(k V) = 1:%.0f, k = %.1f.\n" +msgstr "Przy %.0f km/h rampa nie może być bardziej stroma niż 1:(k V) = 1:%.0f, k = %.1f.\n" + +msgid "A longer cant ramp would allow more" +msgstr "Dłuższa rampa przechyłkowa pozwoliłaby na więcej" + +msgid "Within %.0f m the track goes from %s, cant %.0f mm, to %s, cant %.0f mm.\n" +msgstr "Na długości %.0f m tor przechodzi od %s (przechyłka %.0f mm) do %s (przechyłka %.0f mm).\n" + +msgid "At %.0f km/h the unbalanced acceleration changes from %.2f to %.2f m/s² in %.1f s: %.2f m/s³, the limit is %.2f m/s³.\n" +msgstr "Przy %.0f km/h przyspieszenie niezrównoważone zmienia się z %.2f na %.2f m/s² w %.1f s: %.2f m/s³, dopuszczalne %.2f m/s³.\n" + +msgid "The curve lacks a transition, or it is too short" +msgstr "Łuk nie ma krzywej przejściowej albo jest ona za krótka" + +msgid "Checks that the speed on the tracks isn't above what their geometry allows" +msgstr "Sprawdza, czy prędkość na torach nie jest większa, niż pozwala ich geometria" + +msgid "Looks at the radius and the cant, the cant ramps, the curves without the transitions and the switch branches" +msgstr "Bierze pod uwagę promień i przechyłkę, rampy przechyłkowe, łuki bez krzywych przejściowych i tory zwrotne" + +msgid "Thresholds" +msgstr "Progi" + +msgid "Change the thresholds, then Check again" +msgstr "Zmień progi i znów kliknij Sprawdź" + +msgid "Set the safe speed on %zu track" +msgid_plural "Set the safe speed on %zu tracks" +msgstr[0] "Ustaw bezpieczną prędkość na %zu torze" +msgstr[1] "Ustaw bezpieczną prędkość na %zu torach" +msgstr[2] "Ustaw bezpieczną prędkość na %zu torach" + +msgid "Looks for faults at the joints of the tracks" +msgstr "Szuka błędów na stykach torów" + +msgid "Finds the gaps between the ends of the tracks, the steps, the kinks and the jumps of the cant and of the grade" +msgstr "Znajduje szczeliny między końcami torów, uskoki, załamania oraz skoki przechyłki i pochylenia" + +msgid "Done: click a curve to adjust it; Ctrl+Z takes it back" +msgstr "Gotowe: kliknij łuk, by go poprawić; Ctrl+Z cofa" + +msgid "Lay %zu track, %.1f m" +msgid_plural "Lay %zu tracks, %.1f m" +msgstr[0] "Ułóż %zu tor, %.1f m" +msgstr[1] "Ułóż %zu tory, %.1f m" +msgstr[2] "Ułóż %zu torów, %.1f m" + +msgid "Two tracks selected: set the spacing of the other in the Select tool" +msgstr "Zaznaczone są dwa tory: rozstaw drugiego ustawisz w narzędziu Zaznaczanie" + +msgid "The first selected track must be a straight" +msgstr "Pierwszy zaznaczony tor musi być prostą" + +msgid "%s stays, %s moves to this spacing with its curves" +msgstr "%s zostaje, %s przesuwa się na ten rozstaw razem ze swoimi łukami" + +msgid "Scanning the scenery %.0f%%, %zu found" +msgstr "Skanowanie scenerii %.0f%%, znalezione: %zu" + +msgid "Find the violations in the whole scenery" +msgstr "Znajdź naruszenia w całej scenerii" + +msgid "No model in the scenery enters the gauge" +msgstr "Żaden model w scenerii nie wchodzi w skrajnię" + +msgid " in the whole scenery" +msgstr " (cała sceneria)" + +msgid "PKP PLK standard, volume II; below 1170 mm the limit installation gauge.\nMetres from the track axis and above the rail top, bottom up." +msgstr "Standard PKP PLK, tom II; poniżej 1170 mm graniczna skrajnia instalacyjna.\nMetry od osi toru i nad główką szyny, od dołu." + +msgid "nearby, no rule matches the file name" +msgstr "w pobliżu, nazwa pliku nie pasuje do żadnej reguły" + +msgid "the file name matches a rule" +msgstr "nazwa pliku pasuje do reguły" + +msgid "nearby, no rule matches the template or its description" +msgstr "w pobliżu, szablon ani jego opis nie pasują do żadnej reguły" + +msgid "the template or its description matches a rule" +msgstr "szablon lub jego opis pasuje do reguły" + +msgid "the description of the template says what it is" +msgstr "opis szablonu mówi, czym jest" + +msgid "wire over the track" +msgstr "przewód nad torem" + +msgid "names the path %s" +msgstr "wskazuje tor %s" + +msgid "nearby" +msgstr "w pobliżu" + +msgid "on the right" +msgstr "po prawej" + +msgid "on the left" +msgstr "po lewej" + +msgid "Texture (skin)" +msgstr "Tekstura (skin)" + +msgid "Along the track" +msgstr "Wzdłuż toru" + +msgid "Across" +msgstr "W poprzek" + +msgid "Point (x, y, z of the model)" +msgstr "Punkt (x, y, z modelu)" + +msgid "From the track axis (m, + right)" +msgstr "Od osi toru (m, + w prawo)" + +msgid "Over the rail head (m)" +msgstr "Nad główką szyny (m)" + +msgid "Along the track (m)" +msgstr "Wzdłuż toru (m)" + +msgid "Copy every (m, 0: its length)" +msgstr "Odstęp kopii (m, 0: długość modelu)" + +msgid "The model stands %.1f° off the track, bent it'll follow the track" +msgstr "Model stoi pod kątem %.1f° do toru, po wygięciu pójdzie wzdłuż toru" + +msgid "Single slip (Rkp)" +msgstr "Rozjazd krzyżowy pojedynczy (Rkp)" + +msgid "Double slip (Rkpd)" +msgstr "Rozjazd krzyżowy podwójny (Rkpd)" + +msgid "Crossing (Skrz)" +msgstr "Skrzyżowanie torów (Skrz)" + +msgid "a %.3f m from the middle of the crossing to the joint" +msgstr "a %.3f m od środka skrzyżowania do styku" + +msgid "Click at a crossing of two straights, on the side of the tracks to join" +msgstr "Kliknij skrzyżowanie dwóch prostych po stronie torów do połączenia" + +msgid "Click at a crossing of two straights" +msgstr "Kliknij skrzyżowanie dwóch prostych" + +msgid "a %.3f m to the centre of the switch, b %.3f m on to its end" +msgstr "a %.3f m do środka rozjazdu, b %.3f m dalej do końca" + +msgid "(approx.)" +msgstr "(w przybliżeniu)" + +msgid "(designed)" +msgstr "(zaprojektowany, spoza Id-4)" + +msgid "(from the geometry)" +msgstr "(wyliczone z geometrii)" + +msgid "industrial" +msgstr "przemysłowy" + +msgid "gauge %d" +msgstr "szerokość toru %d mm" + +msgid "Transition" +msgstr "Przejściowa" + +msgid "Move the part up" +msgstr "Przesuń część wyżej" + +msgid "Remove the part" +msgstr "Usuń część" + +msgid "Choose a template on the left; copy a catalogue one to own templates to change it" +msgstr "Wybierz szablon po lewej; katalogowy skopiuj do własnych, żeby go zmienić" + +msgid "A template is a straight main track and the diverging track from the start of the switch as a chain of straights, arcs and transition curves; a double switch is two switches, the second one starting on the main track of the first" +msgstr "Szablon to prosty tor zasadniczy i tor zwrotny od początku rozjazdu jako ciąg prostych, łuków i krzywych przejściowych; rozjazd podwójny to dwie zwrotnice, druga zaczyna się na torze zasadniczym pierwszej" + +msgid "Can't write %s" +msgstr "Nie da się zapisać %s" + +msgid "The drive %s has no variant for a right-hand switch with the drive %s" +msgstr "Napęd %s nie ma wariantu dla rozjazdu prawego z napędem %s" + +msgid "The drive %s has no variant for a left-hand switch with the drive %s" +msgstr "Napęd %s nie ma wariantu dla rozjazdu lewego z napędem %s" + +msgid "Switch %s inserted, Ctrl+Z takes it back" +msgstr "Rozjazd %s wstawiony, Ctrl+Z cofa" + +msgid "Switch %s inserted into the curve, Ctrl+Z takes it back" +msgstr "Rozjazd %s wstawiony w łuk, Ctrl+Z cofa" + +msgid "R reduced to %d m, the straights end %d m from the crossing" +msgstr "R zmniejszony do %d m, proste kończą się %d m od skrzyżowania" + +msgid "R limited to %d m, a neighbouring track isn't straight" +msgstr "R ograniczony do %d m, sąsiedni tor nie jest prosty" + +msgid "R reduced to %d m to fit the straights" +msgstr "R zmniejszony do %d m, żeby zmieścić się na prostych" + +msgid "Turntable %s placed; it turns after the scenery is saved and loaded again" +msgstr "Obrotnica %s postawiona; obraca się dopiero po zapisaniu i ponownym wczytaniu scenerii" + +msgid "The grade line needs at least the two ends" +msgstr "Niweleta musi mieć co najmniej dwa punkty końcowe" + +msgid "Points %zu and %zu are closer than 0.5 m" +msgstr "Punkty %zu i %zu są bliżej niż 0.5 m" + +msgid "Grade %.2f per mille between points %zu and %zu exceeds %.2f" +msgstr "Pochylenie %.2f ‰ między punktami %zu i %zu przekracza %.2f ‰" + +msgid "Point %zu: change of grade %.2f per mille needs a vertical curve" +msgstr "Punkt %zu: zmiana pochylenia o %.2f ‰ wymaga łuku pionowego" + +msgid "Point %zu: R %.0f m is below the required %.0f m for %.0f km/h" +msgstr "Punkt %zu: R %.0f m jest mniejszy od wymaganego %.0f m dla %.0f km/h" + +msgid "Point %zu: the grade changes inside switch %s" +msgstr "Punkt %zu: pochylenie zmienia się na rozjeździe %s" + +msgid "Point %zu: the vertical curve reaches past the end of the route" +msgstr "Punkt %zu: łuk pionowy wychodzi poza koniec trasy" + +msgid "Vertical curves of points %zu and %zu overlap by %.2f m" +msgstr "Łuki pionowe punktów %zu i %zu nachodzą na siebie o %.2f m" + +msgid "Point %zu: the vertical curve lies on switch %s" +msgstr "Punkt %zu: łuk pionowy leży na rozjeździe %s" + +msgid "Constant grade between points %zu and %zu is %.1f m long, less than %.0f m" +msgstr "Stałe pochylenie między punktami %zu i %zu ma %.1f m, mniej niż %.0f m" + +msgid "Grade %.2f per mille over switch %s exceeds %.2f" +msgstr "Pochylenie %.2f ‰ na rozjeździe %s przekracza %.2f ‰" + +msgid "At %s the grade line departs %.3f m from a fixed point" +msgstr "W km %s niweleta odchodzi o %.3f m od punktu stałego" + +msgid "The start of the grade line is %.3f m off the adjoining track" +msgstr "Początek niwelety odchodzi o %.3f m od przylegającego toru" + +msgid "The end of the grade line is %.3f m off the adjoining track" +msgstr "Koniec niwelety odchodzi o %.3f m od przylegającego toru" + +msgid "Change of grade %.2f per mille against the adjoining track at the start" +msgstr "Zmiana pochylenia o %.2f ‰ względem przylegającego toru na początku" + +msgid "Change of grade %.2f per mille against the adjoining track at the end" +msgstr "Zmiana pochylenia o %.2f ‰ względem przylegającego toru na końcu" + +msgid "The direction of an end of the fragment is undefined, its end path has zero length" +msgstr "Kierunek końca fragmentu jest nieokreślony, jego skrajny tor ma zerową długość" + +msgid "The ends don't lie on a common straight line, add a vertex" +msgstr "Końce nie leżą na wspólnej prostej, dodaj wierzchołek" + +msgid "Tangents of the ends are parallel, use two vertices" +msgstr "Styczne końców są równoległe, użyj dwóch wierzchołków" + +msgid "Tangents of the ends intersect outside of the fragment, turn the longer way or use two vertices" +msgstr "Styczne końców przecinają się poza fragmentem, skręć dłuższą drogą albo użyj dwóch wierzchołków" + +msgid "Tangent %zu has zero length" +msgstr "Styczna %zu ma zerową długość" + +msgid "Vertex 1: the curve starts %.2f m before the adjoining straight" +msgstr "Wierzchołek 1: łuk zaczyna się %.2f m przed przyległą prostą" + +msgid "Vertex 1: the curve starts %.2f m before the fixed start" +msgstr "Wierzchołek 1: łuk zaczyna się %.2f m przed stałym początkiem" + +msgid "Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex" +msgstr "Wierzchołek %zu: zwrot o około 180° albo 360° wymaga jeszcze jednego wierzchołka" + +msgid "Vertex %zu: transition curves shortened to %.1f / %.1f m to fit the deflection angle" +msgstr "Wierzchołek %zu: krzywe przejściowe skrócone do %.1f / %.1f m, żeby zmieściły się w kącie zwrotu" + +msgid "Vertex %zu: R reduced from %.1f to %.1f m, transitions to %.1f / %.1f m, to fit between the ends" +msgstr "Wierzchołek %zu: R zmniejszony z %.1f do %.1f m, przejściowe do %.1f / %.1f m, żeby zmieścić się między końcami" + +msgid "Vertex %zu: transition curves shortened to %.1f / %.1f m, the circular arc keeps %.0f m" +msgstr "Wierzchołek %zu: krzywe przejściowe skrócone do %.1f / %.1f m, łuk kołowy zachowuje %.0f m" + +msgid "Vertex %zu: cant lowered to %.0f mm, the shortened transition curves can't ramp more" +msgstr "Wierzchołek %zu: przechyłka obniżona do %.0f mm, na skróconych krzywych przejściowych więcej nie narośnie" + +msgid "Vertex %zu lies inside the curve of the previous vertex" +msgstr "Wierzchołek %zu leży wewnątrz łuku poprzedniego wierzchołka" + +msgid "Vertex %zu: transition curves shortened to fit the deflection angle" +msgstr "Wierzchołek %zu: krzywe przejściowe skrócone, żeby zmieściły się w kącie zwrotu" + +msgid "Curves of vertices %zu and %zu overlap by %.2f m" +msgstr "Łuki wierzchołków %zu i %zu nachodzą na siebie o %.2f m" + +msgid "Vertex %zu: R %.0f m is below the minimum %.0f m for %.0f km/h" +msgstr "Wierzchołek %zu: R %.0f m jest mniejszy od najmniejszego dopuszczalnego %.0f m dla %.0f km/h" + +msgid "Vertex %zu: unbalanced acceleration %.2f m/s2 exceeds %.2f m/s2" +msgstr "Wierzchołek %zu: przyspieszenie niezrównoważone %.2f m/s² przekracza %.2f m/s²" + +msgid "Vertex %zu: transition curves are too long for the deflection angle" +msgstr "Wierzchołek %zu: krzywe przejściowe są za długie na ten kąt zwrotu" + +msgid "Vertex %zu: cant %.0f mm exceeds %.0f mm" +msgstr "Wierzchołek %zu: przechyłka %.0f mm przekracza %.0f mm" + +msgid "Vertex %zu: unbalanced acceleration %.2f m/s2 exceeds %.2f m/s2 (cant at least %.0f mm)" +msgstr "Wierzchołek %zu: przyspieszenie niezrównoważone %.2f m/s² przekracza %.2f m/s² (przechyłka co najmniej %.0f mm)" + +msgid "Vertex %zu: transition curves shorter than the required %.0f m" +msgstr "Wierzchołek %zu: krzywe przejściowe krótsze niż wymagane %.0f m" + +msgid "Vertex %zu: cant without a transition curve to ramp it" +msgstr "Wierzchołek %zu: przechyłka bez krzywej przejściowej, na której mogłaby narastać" + +msgid "Vertex %zu: the curve ends %.2f m past the adjoining straight" +msgstr "Wierzchołek %zu: łuk kończy się %.2f m za przyległą prostą" + +msgid "Vertex %zu: the curve ends %.2f m past the fixed end" +msgstr "Wierzchołek %zu: łuk kończy się %.2f m za stałym końcem" + +msgid "Straight between curves is %.1f m long, less than %.0f m" +msgstr "Prosta między łukami ma %.1f m, mniej niż %.0f m" + +msgid "The adjoining track at the start is curved (R %.0f m), the curvature changes abruptly" +msgstr "Przylegający tor na początku leży w łuku (R %.0f m), krzywizna zmienia się skokowo" + +msgid "The adjoining track at the end is curved (R %.0f m), the curvature changes abruptly" +msgstr "Przylegający tor na końcu leży w łuku (R %.0f m), krzywizna zmienia się skokowo" + +msgid "This build can't fetch from the internet" +msgstr "Ta wersja symulatora nie pobiera danych z internetu" + +msgid "The service gave no terrain model" +msgstr "Serwis nie zwrócił modelu terenu" + +msgid "\nThe paths and the design (//$p lines) go to the scenery files on save" +msgstr "\nTory i projekt (wiersze //$p) trafią do plików scenerii przy zapisie" + +msgid "\nPaths off the route brought to the height of the switches:" +msgstr "\nTory spoza trasy dociągnięte do wysokości rozjazdów:" + +msgid "\n %s at %s: %+.3f m" +msgstr "\n %s przy %s: %+.3f m" + +msgid "\nSwitch ends off the adjoining paths, which can't be changed here:" +msgstr "\nKońce rozjazdów odchodzą od przylegających torów, których nie da się tu zmienić:" + +msgid "The grade line meets the limits of the line" +msgstr "Niweleta mieści się w normach linii" + +msgid "Click: the list of the remarks; a click on a remark shows its place" +msgstr "Kliknij, żeby zobaczyć listę uwag; kliknięcie uwagi pokazuje jej miejsce" + +msgid "Placed automatically, moves with the neighbouring point; untick the joining in the tool options to edit the end freely" +msgstr "Ustawiany automatycznie, porusza się z sąsiednim punktem; odznacz dowiązanie w Opcjach narzędzia, aby swobodnie edytować koniec" + +msgid "The required radius of a vertical curve grows with the square of the design speed: R = k * V^2, V in km/h" +msgstr "Wymagany promień łuku pionowego rośnie z kwadratem prędkości projektowej: R = k * V², V w km/h" + +msgid "A vertical curve is never smaller than this, even at a low speed" +msgstr "Łuk pionowy nigdy nie jest mniejszy, nawet przy małej prędkości" + +msgid "A smaller change of grade may stay a sharp break, without a curve" +msgstr "Mniejsza zmiana pochylenia może zostać ostrym załomem, bez łuku" + +msgid "The shortest stretch of one grade between two vertical curves; 0: not checked" +msgstr "Najkrótszy odcinek jednego pochylenia między dwoma łukami pionowymi; 0: nie sprawdzane" + +msgid "How far the grade line may pass from a fixed point, e.g. the joint with the adjoining track, before it's reported" +msgstr "O ile niweleta może minąć punkt stały, np. styk z przylegającym torem, zanim pojawi się uwaga" + +msgid "The radius of the vertical curve which rounds off the break of the grade here" +msgstr "Promień łuku pionowego, który łagodzi tu załom pochylenia" + +msgid "Only the heightmap terrain changes: a formation and slopes are made, the models of the scenery stay as they are" +msgstr "Zmienia się tylko teren z mapy wysokości: powstaje torowisko i skarpy, modele scenerii zostają bez zmian" + +msgid "Makes the formation and the slopes along the route in the heightmap terrain.\nThey're kept as a modifier of the terrain, which follows the grade line when you shape it again; the triangles and models of the scenery stay as they are" +msgstr "Robi torowisko i skarpy wzdłuż trasy w terenie z mapy wysokości.\nZostają jako modyfikator terenu, który idzie za niweletą przy kolejnym kształtowaniu; trójkąty i modele scenerii zostają bez zmian" + +msgid "Design read from the scenery file %s, largest departure from the track %.3f m" +msgstr "Projekt odczytany z pliku scenerii %s, największa odchyłka od toru %.3f m" + +msgid "The design will be removed from %s on save" +msgstr "Projekt zostanie usunięty z %s przy zapisie" + +msgid "Grade line recognized from the track: %zu point, largest departure %.3f m" +msgid_plural "Grade line recognized from the track: %zu points, largest departure %.3f m" +msgstr[0] "Niweleta rozpoznana z toru: %zu punkt, największa odchyłka %.3f m" +msgstr[1] "Niweleta rozpoznana z toru: %zu punkty, największa odchyłka %.3f m" +msgstr[2] "Niweleta rozpoznana z toru: %zu punktów, największa odchyłka %.3f m" + +msgid "Grade line fitted to the ground: %zu point, embankment up to %.2f m, cutting up to %.2f m" +msgid_plural "Grade line fitted to the ground: %zu points, embankment up to %.2f m, cutting up to %.2f m" +msgstr[0] "Niweleta dopasowana do terenu: %zu punkt, nasyp do %.2f m, wykop do %.2f m" +msgstr[1] "Niweleta dopasowana do terenu: %zu punkty, nasyp do %.2f m, wykop do %.2f m" +msgstr[2] "Niweleta dopasowana do terenu: %zu punktów, nasyp do %.2f m, wykop do %.2f m" + +msgid "Terrain shaped by the modifier \"%s\" (%zu point): embankment up to %.2f m, cutting up to %.2f m" +msgid_plural "Terrain shaped by the modifier \"%s\" (%zu points): embankment up to %.2f m, cutting up to %.2f m" +msgstr[0] "Teren ukształtowany modyfikatorem \"%s\" (%zu punkt): nasyp do %.2f m, wykop do %.2f m" +msgstr[1] "Teren ukształtowany modyfikatorem \"%s\" (%zu punkty): nasyp do %.2f m, wykop do %.2f m" +msgstr[2] "Teren ukształtowany modyfikatorem \"%s\" (%zu punktów): nasyp do %.2f m, wykop do %.2f m" + +msgid "; %d chunk of terrain made under the route" +msgid_plural "; %d chunks of terrain made under the route" +msgstr[0] "; utworzono %d kafel terenu pod trasą" +msgstr[1] "; utworzono %d kafle terenu pod trasą" +msgstr[2] "; utworzono %d kafli terenu pod trasą" + +msgid "; %d chunk left out: the scenery has its own ground there, convert it to shape it" +msgid_plural "; %d chunks left out: the scenery has its own ground there, convert it to shape it" +msgstr[0] "; pominięto %d kafel: sceneria ma tam własny teren, zamień go, żeby go kształtować" +msgstr[1] "; pominięto %d kafle: sceneria ma tam własny teren, zamień go, żeby go kształtować" +msgstr[2] "; pominięto %d kafli: sceneria ma tam własny teren, zamień go, żeby go kształtować" + +msgid "Grade line applied: %zu path changed, %zu added, largest departure %.3f m" +msgid_plural "Grade line applied: %zu paths changed, %zu added, largest departure %.3f m" +msgstr[0] "Niweleta zastosowana: zmieniono %zu tor, dodano %zu, największa odchyłka %.3f m" +msgstr[1] "Niweleta zastosowana: zmieniono %zu tory, dodano %zu, największa odchyłka %.3f m" +msgstr[2] "Niweleta zastosowana: zmieniono %zu torów, dodano %zu, największa odchyłka %.3f m" + +msgid "%zu path, %.2f m: %s ... %s" +msgid_plural "%zu paths, %.2f m: %s ... %s" +msgstr[0] "%zu tor, %.2f m: %s ... %s" +msgstr[1] "%zu tory, %.2f m: %s ... %s" +msgstr[2] "%zu torów, %.2f m: %s ... %s" + +msgid " %s > %s, %zu path, %.2f m" +msgid_plural " %s > %s, %zu paths, %.2f m" +msgstr[0] " %s > %s, %zu tor, %.2f m" +msgstr[1] " %s > %s, %zu tory, %.2f m" +msgstr[2] " %s > %s, %zu torów, %.2f m" + +msgid "%s %.1f m, %zu point" +msgid_plural "%s %.1f m, %zu points" +msgstr[0] "%s %.1f m, %zu punkt" +msgstr[1] "%s %.1f m, %zu punkty" +msgstr[2] "%s %.1f m, %zu punktów" + +msgid "%s %.1f m, %zu point, from (%.1f, %.1f) to (%.1f, %.1f)" +msgid_plural "%s %.1f m, %zu points, from (%.1f, %.1f) to (%.1f, %.1f)" +msgstr[0] "%s %.1f m, %zu punkt, od (%.1f, %.1f) do (%.1f, %.1f)" +msgstr[1] "%s %.1f m, %zu punkty, od (%.1f, %.1f) do (%.1f, %.1f)" +msgstr[2] "%s %.1f m, %zu punktów, od (%.1f, %.1f) do (%.1f, %.1f)" + +msgid "Curve of %zu path, turning %d deg" +msgid_plural "Curve of %zu paths, turning %d deg" +msgstr[0] "Łuk z %zu toru, kąt zwrotu %d°" +msgstr[1] "Łuk z %zu torów, kąt zwrotu %d°" +msgstr[2] "Łuk z %zu torów, kąt zwrotu %d°" + +msgid "Line of %zu path, %.0f m, %zu vertices" +msgid_plural "Line of %zu paths, %.0f m, %zu vertices" +msgstr[0] "Linia z %zu toru, %.0f m, wierzchołki: %zu" +msgstr[1] "Linia z %zu torów, %.0f m, wierzchołki: %zu" +msgstr[2] "Linia z %zu torów, %.0f m, wierzchołki: %zu" + +msgid "Re-laid %zu path, added %zu" +msgid_plural "Re-laid %zu paths, added %zu" +msgstr[0] "Ułożono na nowo %zu tor, dodano %zu" +msgstr[1] "Ułożono na nowo %zu tory, dodano %zu" +msgstr[2] "Ułożono na nowo %zu torów, dodano %zu" + +msgid "Re-laid %zu path" +msgid_plural "Re-laid %zu paths" +msgstr[0] "Ułożono na nowo %zu tor" +msgstr[1] "Ułożono na nowo %zu tory" +msgstr[2] "Ułożono na nowo %zu torów" + +msgid ", the curve took over %.2f / %.2f m of the adjoining straights" +msgstr ", łuk zajął %.2f / %.2f m przyległych prostych" + +msgid "The kept design will be removed from %s on save" +msgstr "Zapisany projekt zostanie usunięty z %s przy zapisie" + +msgid "The switch %s doesn't fit on the designed line" +msgstr "Rozjazd %s nie mieści się na projektowanej linii" + +msgid "%zu path, %.2f m" +msgid_plural "%zu paths, %.2f m" +msgstr[0] "%zu tor, %.2f m" +msgstr[1] "%zu tory, %.2f m" +msgstr[2] "%zu torów, %.2f m" + +msgid "Length %.2f m (now %.2f m), %zu element" +msgid_plural "Length %.2f m (now %.2f m), %zu elements" +msgstr[0] "Długość %.2f m (teraz %.2f m), %zu element" +msgstr[1] "Długość %.2f m (teraz %.2f m), %zu elementy" +msgstr[2] "Długość %.2f m (teraz %.2f m), %zu elementów" + +msgid "transition between arcs" +msgstr "przejściowa między łukami" + +msgid "Where the point 0,0 of the scenery lies on the map:" +msgstr "Gdzie na mapie leży punkt 0,0 scenerii:" + +msgid "X of the point 0,0 in the PUWG 1992 coordinates of the Polish maps, metres to the north, as geoportal.gov.pl gives it" +msgstr "X punktu 0,0 w układzie współrzędnych PUWG 1992 polskich map, w metrach na północ, jak podaje geoportal.gov.pl" + +msgid "Y of the point 0,0 in the PUWG 1992 coordinates of the Polish maps, metres to the east, as geoportal.gov.pl gives it" +msgstr "Y punktu 0,0 w układzie współrzędnych PUWG 1992 polskich map, w metrach na wschód, jak podaje geoportal.gov.pl" + +msgid "How far from the tracks, on both sides, the terrain model is loaded" +msgstr "Jak daleko od torów, po obu stronach, wczytuje się NMT" + +msgid "Loads the terrain model, the real relief of the ground, around the camera" +msgstr "Wczytuje wokół kamery numeryczny model terenu (NMT), czyli prawdziwą rzeźbę terenu" + +msgid "Load along the tracks" +msgstr "Wczytaj wzdłuż torów" + +msgid "Load around the camera" +msgstr "Wczytaj wokół kamery" + +msgid "Loads the terrain model as it is along the tracks, with the height offset above; the tracks stay where they are" +msgstr "Wczytuje NMT bez zmian wzdłuż torów, z przesunięciem wysokości z pola wyżej; tory zostają na miejscu" + +msgid "Loads the terrain model as it is around the camera, with the height offset above; the tracks stay where they are" +msgstr "Wczytuje NMT bez zmian wokół kamery, z przesunięciem wysokości z pola wyżej; tory zostają na miejscu" + +msgid "The tracks lie %+.2f m against the terrain model (%zu place, they differ by %.2f m around it)" +msgid_plural "The tracks lie %+.2f m against the terrain model (%zu places, they differ by %.2f m around it)" +msgstr[0] "Tory leżą %+.2f m względem NMT (%zu miejsce, rozrzut %.2f m)" +msgstr[1] "Tory leżą %+.2f m względem NMT (%zu miejsca, rozrzut %.2f m)" +msgstr[2] "Tory leżą %+.2f m względem NMT (%zu miejsc, rozrzut %.2f m)" + +msgid "%zu chunk to make from the terrain model" +msgid_plural "%zu chunks to make from the terrain model" +msgstr[0] "%zu kafel do zrobienia z NMT" +msgstr[1] "%zu kafle do zrobienia z NMT" +msgstr[2] "%zu kafli do zrobienia z NMT" + +msgid "; the formation of %zu path shaped" +msgid_plural "; the formation of %zu paths shaped" +msgstr[0] "; torowisko %zu toru ukształtowane" +msgstr[1] "; torowisko %zu torów ukształtowane" +msgstr[2] "; torowisko %zu torów ukształtowane" + +msgid "Terrain model: %zu square left" +msgid_plural "Terrain model: %zu squares left" +msgstr[0] "NMT: został %zu kwadrat" +msgstr[1] "NMT: zostały %zu kwadraty" +msgstr[2] "NMT: zostało %zu kwadratów" + +msgid "Fetching the terrain model under the tracks: %zu square left" +msgid_plural "Fetching the terrain model under the tracks: %zu squares left" +msgstr[0] "Pobieranie NMT pod torami: został %zu kwadrat" +msgstr[1] "Pobieranie NMT pod torami: zostały %zu kwadraty" +msgstr[2] "Pobieranie NMT pod torami: zostało %zu kwadratów" + +msgid "\n%zu line parts from the terrain model by over 0.5 m: the model or the track is off there, the terrain follows the tracks (the log names it)" +msgid_plural "\n%zu lines part from the terrain model by over 0.5 m: the model or the track is off there, the terrain follows the tracks (the log names them)" +msgstr[0] "\n%zu linia odchodzi od NMT o ponad 0.5 m: tam NMT albo tor się nie zgadza, teren idzie za torami (log ją wymienia)" +msgstr[1] "\n%zu linie odchodzą od NMT o ponad 0.5 m: tam NMT albo tor się nie zgadza, teren idzie za torami (log je wymienia)" +msgstr[2] "\n%zu linii odchodzi od NMT o ponad 0.5 m: tam NMT albo tor się nie zgadza, teren idzie za torami (log je wymienia)" + +msgid "Stopped: the grade lines split off %zu path from %zu tracks, more than they should; Ctrl+Z takes the changes back" +msgid_plural "Stopped: the grade lines split off %zu paths from %zu tracks, more than they should; Ctrl+Z takes the changes back" +msgstr[0] "Zatrzymane: niwelety odcięły %zu tor z %zu, więcej niż powinny; Ctrl+Z cofa zmiany" +msgstr[1] "Zatrzymane: niwelety odcięły %zu tory z %zu, więcej niż powinny; Ctrl+Z cofa zmiany" +msgstr[2] "Zatrzymane: niwelety odcięły %zu torów z %zu, więcej niż powinny; Ctrl+Z cofa zmiany" + +msgid "Grade lines: %zu fitted, %zu track left" +msgid_plural "Grade lines: %zu fitted, %zu tracks left" +msgstr[0] "Niwelety: dopasowano %zu, został %zu tor" +msgstr[1] "Niwelety: dopasowano %zu, zostały %zu tory" +msgstr[2] "Niwelety: dopasowano %zu, zostało %zu torów" + +msgid "new" +msgstr "nowe" + +msgid "old" +msgstr "stare" + +msgid "rotten" +msgstr "zgniłe" + +msgid "mixed" +msgstr "mieszane" + +msgid "New, old and rotten ones at random, mostly new" +msgstr "Losowo nowe, stare i zgniłe, najczęściej nowe" + +msgid "Tolerance (m)" +msgstr "Tolerancja (m)" + +msgid "A post standing farther than this from its place counts as wrong" +msgstr "Słupek stojący dalej niż tyle od swojego miejsca liczy się jako stojący źle" + +msgid "A marker standing farther than this from its place counts as wrong" +msgstr "Wskaźnik stojący dalej niż tyle od swojego miejsca liczy się jako stojący źle" + +msgid "Height tolerance (m)" +msgstr "Tolerancja wysokości (m)" + +msgid "A marker higher or lower than its place by more than this counts as wrong" +msgstr "Wskaźnik wyżej lub niżej od swojego miejsca o więcej niż tyle liczy się jako stojący źle" + +msgid "To place: %d" +msgstr "Do postawienia: %d" + +msgid "Line %.0f m, km %s - %s. Standing: %zu, %d of them wrong" +msgstr "Linia %.0f m, km %s - %s. Stojących: %zu, w tym źle: %d" + +msgid "I-beam" +msgstr "dwuteownik" + +msgid "Round post (ip/pkp/w17.inc)" +msgstr "Okrągły słupek (ip/pkp/w17.inc)" + +msgid "Round post, taller than w17 (ip/pkp/w17_new.inc)" +msgstr "Okrągły słupek, wyższy niż w17 (ip/pkp/w17_new.inc)" + +msgid "Square post (ip/pkp/w17_old.inc)" +msgstr "Kwadratowy słupek (ip/pkp/w17_old.inc)" + +msgid "Post of an HEB I-beam (ip/pkp/w17_dwuteo.inc)" +msgstr "Słupek z dwuteownika HEB (ip/pkp/w17_dwuteo.inc)" + +msgid "Signal %s moved, Ctrl+Z takes it back" +msgstr "Sygnalizator %s przesunięty, Ctrl+Z cofa" + +msgid "Signal %s moved, read by %s %s of %s, Ctrl+Z takes it back" +msgstr "Sygnalizator %s przesunięty, pociąg odczytuje go przez %s %s toru %s, Ctrl+Z cofa" + +msgid "Signal %s placed (%s), the train doesn't read it, Ctrl+Z takes it back" +msgstr "Sygnalizator %s postawiony (%s), pociąg go nie odczytuje, Ctrl+Z cofa" + +msgid "Signal %s placed (%s), but vehicles stand on the path: add %s to its %s by hand, Ctrl+Z takes it back" +msgstr "Sygnalizator %s postawiony (%s), ale na torze stoją pojazdy: dopisz %s do jego %s ręcznie, Ctrl+Z cofa" + +msgid "Signal %s placed (%s), but the path can't take its event: %s, Ctrl+Z takes it back" +msgstr "Sygnalizator %s postawiony (%s), ale tor nie przyjmie jego eventu: %s, Ctrl+Z cofa" + +msgid "Signal %s placed (%s), read by %s %s of %s, Ctrl+Z takes it back. Its logic works after the scenery is saved and opened again" +msgstr "Sygnalizator %s postawiony (%s), pociąg odczytuje go przez %s %s toru %s, Ctrl+Z cofa. Jego logika zadziała po zapisaniu i ponownym otwarciu scenerii" + +msgid "The head goes where the arrow at the marker points" +msgstr "Czoło jedzie tam, gdzie wskazuje strzałka przy znaczniku" + +msgid "Timetable##trainset" +msgstr "Rozkład jazdy##trainset" + +msgid "%s stands on %s" +msgstr "%s stoi na torze %s" + +msgid "%zu duplicate hectometre post removed, Ctrl+Z brings it back" +msgid_plural "%zu duplicate hectometre posts removed, Ctrl+Z brings them back" +msgstr[0] "Usunięty %zu zdublowany słupek hektometrowy, Ctrl+Z go przywraca" +msgstr[1] "Usunięte %zu zdublowane słupki hektometrowe, Ctrl+Z je przywraca" +msgstr[2] "Usuniętych %zu zdublowanych słupków hektometrowych, Ctrl+Z je przywraca" + +msgid "%zu hectometre post placed, Ctrl+Z takes it back" +msgid_plural "%zu hectometre posts placed, Ctrl+Z takes them back" +msgstr[0] "Postawiony %zu słupek hektometrowy, Ctrl+Z go usuwa" +msgstr[1] "Postawione %zu słupki hektometrowe, Ctrl+Z je usuwa" +msgstr[2] "Postawionych %zu słupków hektometrowych, Ctrl+Z je usuwa" + +msgid "%zu hectometre post moved to its place, Ctrl+Z takes it back" +msgid_plural "%zu hectometre posts moved to their places, Ctrl+Z takes it back" +msgstr[0] "Przestawiony %zu słupek hektometrowy, Ctrl+Z cofa" +msgstr[1] "Przestawione %zu słupki hektometrowe, Ctrl+Z cofa" +msgstr[2] "Przestawionych %zu słupków hektometrowych, Ctrl+Z cofa" + +msgid "Place %d post" +msgid_plural "Place %d posts" +msgstr[0] "Postaw %d słupek" +msgstr[1] "Postaw %d słupki" +msgstr[2] "Postaw %d słupków" + +msgid "Remove %d duplicate post" +msgid_plural "Remove %d duplicate posts" +msgstr[0] "Usuń %d zdublowany" +msgstr[1] "Usuń %d zdublowane" +msgstr[2] "Usuń %d zdublowanych" + +msgid "Move %d wrong post to its place" +msgid_plural "Move %d wrong posts to their places" +msgstr[0] "Przestaw %d źle stojący" +msgstr[1] "Przestaw %d źle stojące" +msgstr[2] "Przestaw %d źle stojących" + +msgid "%zu fouling point marker placed, Ctrl+Z takes it back" +msgid_plural "%zu fouling point markers placed, Ctrl+Z takes them back" +msgstr[0] "Postawiony %zu wskaźnik W17, Ctrl+Z go usuwa" +msgstr[1] "Postawione %zu wskaźniki W17, Ctrl+Z je usuwa" +msgstr[2] "Postawionych %zu wskaźników W17, Ctrl+Z je usuwa" + +msgid "%zu fouling point marker moved to its place, Ctrl+Z takes it back" +msgid_plural "%zu fouling point markers moved to their places, Ctrl+Z takes it back" +msgstr[0] "Przestawiony %zu wskaźnik W17, Ctrl+Z cofa" +msgstr[1] "Przestawione %zu wskaźniki W17, Ctrl+Z cofa" +msgstr[2] "Przestawionych %zu wskaźników W17, Ctrl+Z cofa" + +msgid "Place %d marker" +msgid_plural "Place %d markers" +msgstr[0] "Postaw %d wskaźnik W17" +msgstr[1] "Postaw %d wskaźniki W17" +msgstr[2] "Postaw %d wskaźników W17" + +msgid "Move %d wrong marker to its place" +msgid_plural "Move %d wrong markers to their places" +msgstr[0] "Przestaw %d źle stojący wskaźnik" +msgstr[1] "Przestaw %d źle stojące wskaźniki" +msgstr[2] "Przestaw %d źle stojących wskaźników" + +msgid "%s and %d vehicle behind it stand on %s" +msgid_plural "%s and %d vehicles behind it stand on %s" +msgstr[0] "%s i %d pojazd za nim stoją na torze %s" +msgstr[1] "%s i %d pojazdy za nim stoją na torze %s" +msgstr[2] "%s i %d pojazdów za nim stoją na torze %s" + +msgid "%d vehicle, the head on the left: click it to move it, turn it or remove it" +msgid_plural "%d vehicles, the head on the left: click one to move it, turn it or remove it" +msgstr[0] "%d pojazd, czoło po lewej: kliknij go, żeby go przesunąć, obrócić albo usunąć" +msgstr[1] "%d pojazdy, czoło po lewej: kliknij któryś, żeby go przesunąć, obrócić albo usunąć" +msgstr[2] "%d pojazdów, czoło po lewej: kliknij któryś, żeby go przesunąć, obrócić albo usunąć" + +msgid "%d trainset to write on save:" +msgid_plural "%d trainsets to write on save:" +msgstr[0] "%d skład do zapisania w scenerii:" +msgstr[1] "%d składy do zapisania w scenerii:" +msgstr[2] "%d składów do zapisania w scenerii:" + +msgid "%d path left the design kept in %s, by up to %.2f m: Ctrl+Z takes it back, the Plan tool (G) lays it by the design again or forgets the design" +msgid_plural "%d paths left the design kept in %s, by up to %.2f m: Ctrl+Z takes it back, the Plan tool (G) lays them by the design again or forgets it" +msgstr[0] "%d tor zszedł z projektu zapisanego w %s, maks. o %.2f m: Ctrl+Z cofa, a w narzędziu Plan (G) ułożysz go znów według projektu albo usuniesz projekt" +msgstr[1] "%d tory zeszły z projektu zapisanego w %s, maks. o %.2f m: Ctrl+Z cofa, a w narzędziu Plan (G) ułożysz je znów według projektu albo go usuniesz" +msgstr[2] "%d torów zeszło z projektu zapisanego w %s, maks. o %.2f m: Ctrl+Z cofa, a w narzędziu Plan (G) ułożysz je znów według projektu albo go usuniesz" + +msgid "\"%s\" can't be edited: %s" +msgstr "\"%s\" nie da się edytować: %s" + +msgid "The scenery has no terrain yet. Make an empty one, or load it from the terrain model below" +msgstr "Sceneria nie ma jeszcze terenu: utwórz pusty albo wczytaj go z modelu terenu (NMT) poniżej" + +msgid "Kept in the folder terrain// and written there with the scenery (Ctrl+S)" +msgstr "Trzymany w folderze terrain// i zapisywany tam razem ze scenerią (Ctrl+S)" + +msgid "No chunk selected. With the Point spacing tool LMB selects the clicked chunk, Ctrl+LMB adds one or takes it away." +msgstr "Nie zaznaczono kafla. W narzędziu Odstęp punktów LPM zaznacza kliknięty kafel, Ctrl+LPM dodaje go albo odejmuje." + +msgid "New chunks are covered with the first material. A texture dragged from the browser onto an entry of the palette takes its place" +msgstr "Nowe kafle pokrywa pierwszy materiał. Tekstura przeciągnięta z przeglądarki na pozycję palety zajmuje jej miejsce" + +msgid "A chunk holds up to 8 materials" +msgstr "Kafel mieści do 8 materiałów" + +msgid "Material: %s (chosen in the palette above)" +msgstr "Materiał: %s (wybrany w palecie powyżej)" + +msgid "%s, %.2f m, %zu points, %s" +msgstr "%s, %.2f m, punkty: %zu, %s" + +msgid "%s, %zu points" +msgstr "%s, punkty: %zu" + +msgid "PUWG 1992 (EPSG:2180): the coordinate system of the maps of Poland, in metres; the numbers as on geoportal.gov.pl" +msgstr "PUWG 1992 (EPSG:2180): układ współrzędnych map Polski, w metrach; liczby jak na geoportal.gov.pl" + +msgid "Photo year" +msgstr "Rok zdjęć" + +msgid "Include of \"%s\" was removed from the saved scenery, this can't be taken back" +msgstr "Include \"%s\" został usunięty z zapisanej scenerii, tego nie da się cofnąć" + +msgid "Include of \"%s\" removed" +msgstr "Usunięto include \"%s\"" + +msgid "Include of \"%s\" restored" +msgstr "Przywrócono include \"%s\"" + +msgid "Include of \"%s\" is gone from the scenery, the change can't be taken back" +msgstr "Include \"%s\" zniknął ze scenerii, tej zmiany nie da się cofnąć" + +msgid "Includes gone from the saved scenery, they can't be taken back: %zu" +msgstr "Pliki include usunięte z zapisanej scenerii, nie do przywrócenia: %zu" + +msgid "Includes restored: %zu" +msgstr "Przywrócone pliki include: %zu" + +msgid "The directive of the include can't be read from the scenery file, its parameters can't be changed" +msgstr "Nie da się odczytać dyrektywy include z pliku scenerii, jej parametrów nie można zmienić" + +msgid "A parameter of an include can't be \"end\", or have a quotation mark in it" +msgstr "Parametr include nie może brzmieć \"end\" ani zawierać cudzysłowu" + +msgid "Scenery templates can be placed only in a scenery opened for editing (-edit)" +msgstr "Szablony scenerii można stawiać tylko w scenerii otwartej do edycji (-edit)" + +msgid "Template \"%s\" can't be placed: %s" +msgstr "Nie da się postawić szablonu \"%s\": %s" + +msgid "there's no active layer to put it in" +msgstr "nie ma aktywnej warstwy, do której mógłby trafić" + +msgid "the active layer can't take it, %s" +msgstr "aktywna warstwa go nie przyjmie, %s" + +msgid "Template \"%s\" can't be placed: the file of the active layer would receive it with a rotation or a scale in effect. Pick another layer" +msgstr "Nie da się postawić szablonu \"%s\": plik aktywnej warstwy przyjąłby go z obrotem albo skalą. Wybierz inną warstwę" + +msgid "Template \"%s\" can't be placed in the active layer" +msgstr "Szablonu \"%s\" nie da się postawić w aktywnej warstwie" + +msgid "\"%s\" placed in layer \"%s\", models shown: %d" +msgstr "\"%s\" postawiony w warstwie \"%s\", pokazane modele: %d" + +msgid ", other statements of the template, working once the scenery is saved and loaded again: %d" +msgstr ", pozostałe polecenia szablonu, działające po zapisaniu i ponownym wczytaniu scenerii: %d" + +msgid "\"%s\" is large enough to count as ground itself, so there's nothing to put it on" +msgstr "\"%s\" jest na tyle duży, że sam liczy się jako grunt, więc nie ma go na czym położyć" + +msgid "%d of %d copies of \"%s\" put on the ground" +msgstr "Opuszczono na ziemię %d z %d kopii \"%s\"" + +msgid "; Ctrl+Z takes them back one at a time" +msgstr "; Ctrl+Z cofa je po jednej" + +msgid ". Left alone, placed by a template or on a layer which can't be edited: %d" +msgstr ". Pominięte, postawione przez szablon albo na warstwie, której nie da się edytować: %d" + +msgid " (limited by min. spacing)" +msgstr " (ograniczone min. odstępem)" + +msgid ", without ground under them (height from the outline): %zu" +msgstr ", bez gruntu pod spodem (wysokość z obrysu): %zu" + +msgid "Include of \"%s\" removed, Ctrl+Z brings it back. Its directive is erased when the scenery is saved" +msgstr "Usunięto include \"%s\", Ctrl+Z go przywraca. Jego dyrektywa zniknie przy zapisie scenerii" + +msgid "Include of \"%s\" selected. Its parameters are in the inspector, Del removes it from the scenery" +msgstr "Zaznaczono include \"%s\". Jego parametry są w Inspektorze, Del usuwa go ze scenerii" + +msgid "Change History" +msgstr "Historia zmian" + +msgid "Undo back to the selected" +msgstr "Cofnij do wybranego" + +msgid ", turned %.0f°" +msgstr ", obrócony o %.0f°" + +msgid "Points: %zu Area: %.0f m² (%.2f ha)" +msgstr "Punkty: %zu Powierzchnia: %.0f m² (%.2f ha)" + +msgid "Quit the simulator" +msgstr "Zamknij symulator" + +msgid "Shift+LMB box" +msgstr "Shift+LPM ramka" + +msgid "Point spacing, restoring the terrain, orthophoto settings" +msgstr "Odstęp punktów, przywracanie terenu, ustawienia ortofotomapy" + +msgid "Restore terrain" +msgstr "Przywracanie terenu" + +msgid "Scene Hierarchy" +msgstr "Lista obiektów" + +msgid "Registered nodes: %zu" +msgstr "Obiektów: %zu" + +msgid "UUID: %s\nNode type: %s" +msgstr "UUID: %s\nTyp obiektu: %s" + +msgid "a name can't hold spaces, semicolons or quotes" +msgstr "nazwa nie może zawierać spacji, średników ani cudzysłowów" + +msgid "a name can't hold a comment mark" +msgstr "nazwa nie może zawierać znaku komentarza" + +msgid "this word has a meaning of its own in scenery files" +msgstr "to słowo ma własne znaczenie w plikach scenerii" + +msgid "another model instance goes by this name" +msgstr "inny model ma już tę nazwę" + +msgid "position (m)" +msgstr "położenie (m)" + +msgid "Location: %.2f, %.2f, %.2f (distance %.1f m)" +msgstr "Położenie: %.2f, %.2f, %.2f (odległość %.1f m)" + +msgid "Layer: %s" +msgstr "Warstwa: %s" + +msgid "Rotation (x, y, z): %.2f°, %.2f°, %.2f°" +msgstr "Obrót (x, y, z): %.2f°, %.2f°, %.2f°" + +msgid "Lights: %s" +msgstr "Światła: %s" + +msgid "Model: %s" +msgstr "Model: %s" + +msgid "Texture: %s" +msgstr "Tekstura: %s" + +msgid "Track circuits: %s" +msgstr "Odcinki izolowane: %s" + +msgid "Speed limit: %.0f km/h" +msgstr "Ograniczenie prędkości: %.0f km/h" + +msgid "Track gauge: %.0f mm" +msgstr "Szerokość toru: %.0f mm" + +msgid "Friction: %.2f" +msgstr "Tarcie: %.2f" + +msgid "Quality: %d" +msgstr "Jakość: %d" + +msgid "Textures: %s, %s" +msgstr "Tekstury: %s, %s" + +msgid "Geometry:" +msgstr "Geometria:" + +msgid " (missing)" +msgstr " (brak)" + +msgid "Data: [%s] [%.2f] [%.2f]" +msgstr "Dane: [%s] [%.2f] [%.2f]" + +msgid "Track: %s" +msgstr "Tor: %s" + +msgid "Objects: %zu, events: %zu" +msgstr "Obiekty: %zu, eventy: %zu" + +msgid "Common start of the names: %s" +msgstr "Wspólny początek nazw: %s" + +msgid ", the node bank selection is used" +msgstr ", używany jest wybór z Banku obiektów" + +msgid " (none yet, describe the templates in the Include database window)" +msgstr " (jeszcze żadnych, opisz szablony w oknie Baza include'ów)" + +msgid " (no match)" +msgstr " (brak wyników)" + +msgid "Close model sets" +msgstr "Zamknij zestawy modeli" + +msgid "Model sets..." +msgstr "Zestawy modeli..." + +msgid "Size of the cards with the previews" +msgstr "Rozmiar kart z podglądami" + +msgid "(ungrouped)" +msgstr "(bez grupy)" + +msgid "New set" +msgstr "Nowy zestaw" + +msgid "(deleted set)" +msgstr "(usunięty zestaw)" + +msgid "Node bank: %s" +msgstr "Bank obiektów: %s" + +msgid "(missing node bank group)" +msgstr "(brak grupy w Banku obiektów)" + +msgid "Makes an empty set. \"Save as set\" on a brush or area fill list makes one too.\nThe sets are chosen in Insert (random model), Brush and Area fill" +msgstr "Tworzy pusty zestaw. Robi go też „Zapisz jako zestaw” na liście Pędzla albo Wypełniania obszaru.\nZestawy wybiera się we Wstawianiu (losowy model), Pędzlu i Wypełnianiu obszaru" + +msgid "%s copy" +msgstr "%s (kopia)" + +msgid "(no node bank groups)" +msgstr "(brak grup w Banku obiektów)" + +msgid "models" +msgstr "modele" + +msgid "tracks" +msgstr "tory" + +msgid "traction" +msgstr "sieć trakcyjna" + +msgid "power sources" +msgstr "źródła zasilania" + +msgid "memory cells" +msgstr "komórki pamięci" + +msgid "event launchers" +msgstr "wyzwalacze eventów" + +msgid "events" +msgstr "eventy" + +msgid "vehicles" +msgstr "pojazdy" + +msgid "sounds" +msgstr "dźwięki" + +msgid "terrain shapes" +msgstr "kształty terenu" + +msgid "lines" +msgstr "linie" + +msgid "Active layer is always visible" +msgstr "Aktywna warstwa jest zawsze widoczna" + +msgid "Show or hide models, tracks and traction of the layer" +msgstr "Pokaż albo ukryj modele, tory i sieć trakcyjną warstwy" + +msgid "Active layer can't be locked" +msgstr "Aktywnej warstwy nie da się zablokować" + +msgid "Nodes of locked layer can't be selected in the viewport" +msgstr "Obiektów zablokowanej warstwy nie da się zaznaczyć w widoku" + +msgid "binary terrain" +msgstr "teren binarny" + +msgid "[new]" +msgstr "[nowa]" + +msgid "[removed]" +msgstr "[usunięta]" + +msgid "[read-only]" +msgstr "[tylko do odczytu]" + +msgid "[after FirstInit]" +msgstr "[po FirstInit]" + +msgid "included by: %s" +msgstr "dołączana przez: %s" + +msgid "read-only: %s" +msgstr "tylko do odczytu: %s" + +msgid "loaded after the scenario initialization (FirstInit): takes no new nodes, only vehicles belong there" +msgstr "wczytywana po inicjalizacji scenariusza (FirstInit): nie przyjmuje nowych obiektów, jest miejscem tylko dla pojazdów" + +msgid "Included by" +msgstr "Dołączana przez" + +msgid "The layer wasn't saved yet, so it leaves no file behind." +msgstr "Warstwa nie była jeszcze zapisana, więc nie zostawi pliku." + +msgid "Its include directive is erased on save. The file itself stays on the disk." +msgstr "Jej dyrektywa include zniknie przy zapisie. Sam plik zostaje na dysku." + +msgid "The template uses %d parameter." +msgid_plural "The template uses %d parameters." +msgstr[0] "Szablon używa %d parametru." +msgstr[1] "Szablon używa %d parametrów." +msgstr[2] "Szablon używa %d parametrów." + +msgid "It has no description yet; the roles are a suggestion based on how the template uses the parameters." +msgstr "Nie ma jeszcze opisu; role to podpowiedź na podstawie tego, jak szablon używa parametrów." + +msgid "[ready]" +msgstr "[gotowy]" + +msgid "[incomplete]" +msgstr "[niepełny]" + +msgid "(no .inc files in the scenery directory)" +msgstr "(brak plików .inc w katalogu scenerii)" + +msgid "(no match)" +msgstr "(brak wyników)" + +msgid "Category" +msgstr "Kategoria" + +msgid "Description" +msgstr "Opis" + +msgid "Description saved in \"%s\"" +msgstr "Opis zapisany w \"%s\"" + +msgid "Describe a template to add it to the node bank" +msgstr "Opisz szablon, żeby dodać go do Banku obiektów" + +msgid "the file is loaded after the scenario initialization (FirstInit), which only vehicles should follow" +msgstr "plik jest wczytywany po inicjalizacji scenariusza (FirstInit), po której powinny być tylko pojazdy" + +msgid "the file is loaded from binary terrain cache" +msgstr "plik jest wczytywany z binarnej pamięci podręcznej terenu" + +msgid "the file is included more than once" +msgstr "plik jest dołączany więcej niż raz" + +msgid "the file is included with parameters" +msgstr "plik jest dołączany z parametrami" + +msgid "it's a part of an include template (.inc)" +msgstr "jest częścią szablonu include (.inc)" + +msgid "its layer is locked" +msgstr "jego warstwa jest zablokowana" + +msgid "its layer is hidden" +msgstr "jego warstwa jest ukryta" + +msgid "its layer is no longer a part of the scenery" +msgstr "jego warstwa nie należy już do scenerii" + +msgid "the parent layer can't include other files" +msgstr "warstwa nadrzędna nie może dołączać innych plików" + +msgid "the parent layer is loaded after the scenario initialization (FirstInit), which only vehicles should follow" +msgstr "warstwa nadrzędna jest wczytywana po inicjalizacji scenariusza (FirstInit), po której powinny być tylko pojazdy" + +msgid "the layer needs a file name" +msgstr "warstwa potrzebuje nazwy pliku" + +msgid "the file name has to be a plain path inside the scenery directory, without spaces" +msgstr "nazwa pliku musi być zwykłą ścieżką w katalogu scenerii, bez spacji" + +msgid ".inc files are include templates, not layers" +msgstr "pliki .inc to szablony include, nie warstwy" + +msgid "too many layers" +msgstr "za dużo warstw" + +msgid "the file \"%s\" already exists" +msgstr "plik \"%s\" już istnieje" + +msgid "no such layer" +msgstr "nie ma takiej warstwy" + +msgid "the scenario file can't be removed" +msgstr "pliku scenariusza nie da się usunąć" + +msgid "the file is included by an include template (.inc)" +msgstr "plik jest dołączany przez szablon include (.inc)" + +msgid "pick another layer to merge into" +msgstr "wybierz inną warstwę, z którą scalić" + +msgid "the target layer is included by the merged one" +msgstr "warstwa docelowa jest dołączana przez scalaną" + +msgid "the target layer is loaded after the scenario initialization (FirstInit), which only vehicles should follow" +msgstr "warstwa docelowa jest wczytywana po inicjalizacji scenariusza (FirstInit), po której powinny być tylko pojazdy" + +msgid "the merged layer is loaded after the scenario initialization (FirstInit), its content has to stay there" +msgstr "scalana warstwa jest wczytywana po inicjalizacji scenariusza (FirstInit), jej zawartość musi tam zostać" + +msgid "the merged layer has includes which weren't saved yet, save the scenery first" +msgstr "scalana warstwa ma niezapisane pliki include, najpierw zapisz scenerię" + +msgid "the scenario file can't be merged into another layer" +msgstr "pliku scenariusza nie da się scalić z inną warstwą" + +msgid "the layers are loaded with different origin, rotation or scale" +msgstr "warstwy są wczytywane z innym początkiem układu, obrotem albo skalą" + +msgid "The scenery wasn't opened for editing, there are no layers to save" +msgstr "Sceneria nie została otwarta do edycji, nie ma warstw do zapisania" + +msgid "Scenery not saved: %s" +msgstr "Nie zapisano scenerii: %s" + +msgid "include \"%s\" was placed for different origin, rotation or scale than its layer file receives it with" +msgstr "include \"%s\" postawiono dla innego początku układu, obrotu albo skali niż te, z którymi przyjmuje go plik jego warstwy" + +msgid "can't write file \"%s\"" +msgstr "nie da się zapisać pliku \"%s\"" + +msgid "can't make a backup of file \"%s\": %s" +msgstr "nie da się zrobić kopii zapasowej pliku \"%s\": %s" + +msgid "can't replace file \"%s\": %s" +msgstr "nie da się zastąpić pliku \"%s\": %s" + +msgid "No changes to save" +msgstr "Brak zmian do zapisania" + +msgid "Saved: %s" +msgstr "Zapisano: %s" + +msgid ". Backup before the ground was taken out: %s" +msgstr ". Kopia zapasowa sprzed usunięcia gruntu: %s" + +msgid "Changed nodes left unsaved (their files are included with parameters or more than once): %zu, %s" +msgstr "Niezapisane zmienione obiekty (ich pliki są dołączane z parametrami albo więcej niż raz): %zu, %s" + +msgid "can't open file \"%s\"" +msgstr "nie da się otworzyć pliku \"%s\"" + +msgid "the description isn't a list of named values" +msgstr "opis nie jest listą nazwanych wartości" + +msgid "the description is malformed: %s" +msgstr "opis jest błędny: %s" + +msgid "the existing description isn't a list of named values" +msgstr "istniejący opis nie jest listą nazwanych wartości" + +msgid "the existing description is malformed, fix or remove it first: %s" +msgstr "istniejący opis jest błędny, najpierw go popraw albo usuń: %s" + +msgid "the template needs a name" +msgstr "szablon potrzebuje nazwy" + +msgid "(p%d) needs a default value, or a role filled in by the editor (name, pos.*, rot.*)" +msgstr "(p%d) potrzebuje wartości domyślnej albo roli wypełnianej przez edytor (name, pos.*, rot.*)" + +msgid "the template has no description" +msgstr "szablon nie ma opisu" + +msgid "The wheel zooms, dragging moves the map. A click sets the centre of the scenery, its point 0 0 0" +msgstr "Kółko przybliża, przeciąganie przesuwa mapę. Kliknięcie wyznacza środek scenerii, jej punkt 0 0 0" + +msgid "PUWG 1992: the coordinate system of the maps of Poland, in km" +msgstr "PUWG 1992: układ współrzędnych map Polski, w km" + +msgid "Lanes of \"%s\" differ in width, and a junction takes roads with lanes of the same width" +msgstr "Pasy drogi \"%s\" mają różne szerokości, a skrzyżowanie przyjmuje tylko drogi z pasami równej szerokości" + +msgid "\"%s\" wasn't fitted: %s" +msgstr "Nie dopasowano \"%s\": %s" + +msgid "Lanes of \"%s\" differ in width, and a road changes the number of its lanes only if they're all as wide" +msgstr "Pasy drogi \"%s\" mają różne szerokości, a droga zmienia liczbę pasów tylko wtedy, gdy wszystkie są równej szerokości" + +msgid "The lanes of \"%s\" can't be led on to the road next to it" +msgstr "Pasów drogi \"%s\" nie da się poprowadzić dalej na sąsiednią drogę" + +msgid "\"%s\" changes its lanes along the way, which leaves no place for a junction. Lead the road out of a piece next to it" +msgstr "\"%s\" zmienia pasy po drodze, więc nie ma tu miejsca na skrzyżowanie. Wyprowadź drogę z sąsiedniego odcinka" + +msgid "The lanes next to \"%s\" weren't fitted: %s" +msgstr "Nie dopasowano pasów obok \"%s\": %s" + +msgid "\"%s\" couldn't be cut for the lanes to change" +msgstr "Nie dało się przeciąć \"%s\", żeby zmienić pasy" diff --git a/launcher/scenery_wizard.cpp b/launcher/scenery_wizard.cpp index 59a5de87..f53b88f1 100644 --- a/launcher/scenery_wizard.cpp +++ b/launcher/scenery_wizard.cpp @@ -281,9 +281,6 @@ void ui::scenerywizard_panel::render_form() if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("No place on the map, no orthophoto: an empty plane seen from above")); ImGui::Spacing(); - if (m_realistic) - ImGui::TextWrapped("%s", STR_C("Find the place on the map: the wheel zooms, dragging moves the map. A click sets the centre of the scenery, its point 0 0 0.")); - ImGui::Spacing(); ImGui::SetNextItemWidth(-1.f); ImGui::InputTextWithHint("##name", STR_C("file name, e.g. my_line"), m_filename, sizeof(m_filename), ImGuiInputTextFlags_CharsNoBlank); @@ -305,61 +302,71 @@ void ui::scenerywizard_panel::render_form() ImGui::InputTextWithHint("##title", STR_C("title, written in the file"), m_title, sizeof(m_title)); ImGui::Spacing(); - ImGui::BeginDisabled(false == m_realistic); - ImGui::TextUnformatted(STR_C("Centre of the scenery, PUWG 1992")); - double east{m_point.x / 1000.0}; - double north{m_point.y / 1000.0}; - ImGui::SetNextItemWidth(130.f); - auto changed{ImGui::InputDouble(STR_C("E, km##east"), &east, 0.0, 0.0, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue)}; - ImGui::SetNextItemWidth(130.f); - changed |= ImGui::InputDouble(STR_C("N, km##north"), &north, 0.0, 0.0, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue); - if (changed) - { - pick({east * 1000.0, north * 1000.0}); - m_centre = m_point; - m_dirty = true; - } - ImGui::SetNextItemWidth(-90.f); - auto const entered{ImGui::InputTextWithHint("##latlon", STR_C("latitude, longitude"), m_latlon, sizeof(m_latlon), ImGuiInputTextFlags_EnterReturnsTrue)}; - ImGui::SameLine(); - if (ImGui::Button(STR_C("Show")) || entered) - { - std::string text{m_latlon}; - std::replace(text.begin(), text.end(), ',', ' '); - std::istringstream words(text); - double latitude{0.0}, longitude{0.0}; - if (words >> latitude >> longitude && latitude > 45.0 && latitude < 58.0 && longitude > 10.0 && longitude < 28.0) - { - pick(to_puwg(latitude, longitude)); - m_centre = m_point; - m_scale = std::min(m_scale, 4.0); - m_dirty = true; - m_status.clear(); - } - else - m_status = STR("Give the latitude and the longitude in degrees, e.g. 52.2297, 21.0122"); - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Degrees, as given by the online maps; the map moves there")); - if (m_picked) - { - auto const degrees{to_degrees(m_point)}; - ImGui::TextDisabled(STR_C("%.5f N, %.5f E"), degrees.x, degrees.y); - } - ImGui::Spacing(); - ImGui::TextWrapped("%s", STR_C("The scenery keeps the centre as its geographic reference (//$g PUWG1992): the orthophoto of the editor lies under it, in place.")); - ImGui::EndDisabled(); - ImGui::Spacing(); - auto const placed{m_picked || false == m_realistic}; auto const ready{named && false == exists && placed}; if (false == ready) + { ImGui::TextDisabled("%s", placed ? STR_C("Give the file a name") : STR_C("Click the centre on the map")); + if (m_realistic && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The wheel zooms, dragging moves the map. A click sets the centre of the scenery, its point 0 0 0")); + } if (ImGui::Button(STR_C("Create and open in the editor"), ImVec2(-1.f, 0.f)) && ready) create_scenery(); if (false == m_status.empty()) ImGui::TextWrapped("%s", m_status.c_str()); ImGui::Spacing(); + + if (ImGui::TreeNode(STR_C("More settings"))) + { + ImGui::BeginDisabled(false == m_realistic); + ImGui::TextUnformatted(STR_C("Centre of the scenery, PUWG 1992")); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("PUWG 1992: the coordinate system of the maps of Poland, in km")); + double east{m_point.x / 1000.0}; + double north{m_point.y / 1000.0}; + ImGui::SetNextItemWidth(130.f); + auto changed{ImGui::InputDouble(STR_C("E, km##east"), &east, 0.0, 0.0, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue)}; + ImGui::SetNextItemWidth(130.f); + changed |= ImGui::InputDouble(STR_C("N, km##north"), &north, 0.0, 0.0, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue); + if (changed) + { + pick({east * 1000.0, north * 1000.0}); + m_centre = m_point; + m_dirty = true; + } + ImGui::SetNextItemWidth(-90.f); + auto const entered{ImGui::InputTextWithHint("##latlon", STR_C("latitude, longitude"), m_latlon, sizeof(m_latlon), ImGuiInputTextFlags_EnterReturnsTrue)}; + ImGui::SameLine(); + if (ImGui::Button(STR_C("Show")) || entered) + { + std::string text{m_latlon}; + std::replace(text.begin(), text.end(), ',', ' '); + std::istringstream words(text); + double latitude{0.0}, longitude{0.0}; + if (words >> latitude >> longitude && latitude > 45.0 && latitude < 58.0 && longitude > 10.0 && longitude < 28.0) + { + pick(to_puwg(latitude, longitude)); + m_centre = m_point; + m_scale = std::min(m_scale, 4.0); + m_dirty = true; + m_status.clear(); + } + else + m_status = STR("Give the latitude and the longitude in degrees, e.g. 52.2297, 21.0122"); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Degrees, as given by the online maps; the map moves there")); + if (m_picked) + { + auto const degrees{to_degrees(m_point)}; + ImGui::TextDisabled(STR_C("%.5f N, %.5f E"), degrees.x, degrees.y); + } + ImGui::Spacing(); + ImGui::TextWrapped("%s", STR_C("The scenery keeps the centre as its geographic reference (//$g PUWG1992): the orthophoto of the editor lies under it, in place.")); + ImGui::EndDisabled(); + ImGui::TreePop(); + } + ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); ImGui::TextWrapped("%s", STR_C("Map: (c) OpenStreetMap contributors, served by geoportal.gov.pl")); ImGui::PopStyleColor(); diff --git a/scene/heightmapterrain.cpp b/scene/heightmapterrain.cpp index a2bc6a2f..e7b8f491 100644 --- a/scene/heightmapterrain.cpp +++ b/scene/heightmapterrain.cpp @@ -483,8 +483,17 @@ bool modifier_shape::reaches(glm::dvec2 const &Min, glm::dvec2 const &Max) const bool modifier_shape::apply(double const X, double const Z, float &Height) const { - if (m_grid.empty() || X < m_min.x || X > m_max.x || Z < m_min.y || Z > m_max.y) + auto const result{evaluate(X, Z, Height)}; + if (false == result.has_value() || std::abs(result->height - Height) < 0.001) return false; + Height = static_cast(result->height); + return true; +} + +std::optional modifier_shape::evaluate(double const X, double const Z, double const Ground) const +{ + if (m_grid.empty() || X < m_min.x || X > m_max.x || Z < m_min.y || Z > m_max.y) + return std::nullopt; auto const &points{m_modifier.points}; auto const reach{m_widest + m_modifier.reach}; auto distance{reach}; @@ -517,18 +526,17 @@ bool modifier_shape::apply(double const X, double const Z, float &Height) const } } if (false == found) - return false; + return std::nullopt; // the formation under the axis, slopes from its edges down or up to the ground, the bends rounded off auto const slope{std::max(0.1, m_modifier.slope)}; auto const rounding{std::max(0.0, m_modifier.rounding)}; - auto const ground{static_cast(Height)}; + auto const ground{Ground}; auto const beyond{soft_max(0.0, distance - halfwidth, rounding)}; - auto const bend{rounding / slope}; + // the bends of the slopes round off only away from the formation: under it the ground is the formation itself, a cut or a + // fill as shallow as the bend isn't let to sink or rise it + auto const bend{rounding / slope * std::clamp(beyond / std::max(rounding, 1e-6), 0.0, 1.0)}; auto const target{ground > formation ? -soft_max(-ground, -(formation + beyond / slope), bend) : soft_max(ground, formation - beyond / slope, bend)}; - if (std::abs(target - ground) < 0.001) - return false; - Height = static_cast(target); - return true; + return effect{target, distance - halfwidth, distance}; } // manifest diff --git a/scene/heightmapterrain.h b/scene/heightmapterrain.h index c578e78f..707af808 100644 --- a/scene/heightmapterrain.h +++ b/scene/heightmapterrain.h @@ -143,6 +143,15 @@ class modifier_shape explicit modifier_shape(modifier const &Modifier); // leads Height at specified point the way the modifier does; returns: true if the height changed bool apply(double const X, double const Z, float &Height) const; + // what the modifier makes of Ground at the point, and how far the point lies off the formation: 0 or less on it; none out of + // its reach + struct effect + { + double height{0.0}; + double outside{0.0}; + double distance{0.0}; // from the axis + }; + std::optional evaluate(double const X, double const Z, double const Ground) const; bool reaches(glm::dvec2 const &Min, glm::dvec2 const &Max) const; private: diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index 01d41014..4edc1033 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -16,11 +16,15 @@ http://mozilla.org/MPL/2.0/. #include "world/Traction.h" #include "utilities/Globals.h" #include "utilities/utilities.h" +#include "utilities/translation.h" +#include "editor/editorFormat.hpp" +#include #include #include #include #include +#include namespace scene { @@ -207,7 +211,7 @@ bool node_layers::accepts(layer_handle Layer, std::string *Reason) const { if (Reason != nullptr) { - *Reason = "the file is loaded after the scenario initialization (FirstInit), which only vehicles should follow"; + *Reason = STR_C("the file is loaded after the scenario initialization (FirstInit), which only vehicles should follow"); } return false; } @@ -355,16 +359,16 @@ bool node_layers::writable(layer_handle Layer, std::string *Reason) const char const *reason{nullptr}; if (target.binary) { - reason = "the file is loaded from binary terrain cache"; + reason = STR_C("the file is loaded from binary terrain cache"); } else if (target.sites.size() > 1) { // one definition yields several nodes, there's no telling which of them should be written back - reason = "the file is included more than once"; + reason = STR_C("the file is included more than once"); } else if (false == target.sites.empty() && target.sites.front().parameters) { - reason = "the file is included with parameters"; + reason = STR_C("the file is included with parameters"); } if (reason != nullptr && Reason != nullptr) { @@ -396,7 +400,7 @@ bool node_layers::editable(basic_node const *Node, std::string *Reason) const std::string reason; if (Node->from_template()) { - reason = "it's a part of an include template (.inc)"; + reason = STR_C("it's a part of an include template (.inc)"); } else if (false == writable(nodelayer, &reason)) { @@ -404,11 +408,11 @@ bool node_layers::editable(basic_node const *Node, std::string *Reason) const } else if (layer(nodelayer).locked) { - reason = "its layer is locked"; + reason = STR_C("its layer is locked"); } else if (false == layer(nodelayer).visible || layer(nodelayer).removed) { - reason = "its layer is hidden"; + reason = STR_C("its layer is hidden"); } if (false == reason.empty() && Reason != nullptr) { @@ -537,7 +541,9 @@ bool node_layers::is_descendant(layer_handle Layer, layer_handle const Ancestor) std::string node_layers::path(layer_handle const Layer) const { - return Global.asCurrentSceneryPath + layer(Layer).name; + auto const named{Global.asCurrentSceneryPath + layer(Layer).name}; + auto const cased{FileCased(named)}; + return cased.empty() ? named : cased; } // stores current state of the layer file, to detect later whether it was changed behind our back @@ -557,12 +563,12 @@ layer_handle node_layers::create(std::string Name, layer_handle Parent, std::str Parent = resolve(Parent); if (false == valid(Parent) || layer(Parent).removed || layer(Parent).binary) { - Reason = "the parent layer can't include other files"; + Reason = STR_C("the parent layer can't include other files"); return null_handle; } if (layer(Parent).late) { - Reason = "the parent layer is loaded after the scenario initialization (FirstInit), which only vehicles should follow"; + Reason = STR_C("the parent layer is loaded after the scenario initialization (FirstInit), which only vehicles should follow"); return null_handle; } // file names in the scenery are processed in lower case, with forward slashes @@ -572,12 +578,12 @@ layer_handle node_layers::create(std::string Name, layer_handle Parent, std::str Name.erase(Name.find_last_not_of(" \t") + 1); if (Name.empty()) { - Reason = "the layer needs a file name"; + Reason = STR_C("the layer needs a file name"); return null_handle; } if (Name.find_first_of(" \t\";:*?<>|") != std::string::npos || Name.front() == '/' || Name.back() == '/' || contains(Name, "..")) { - Reason = "the file name has to be a plain path inside the scenery directory, without spaces"; + Reason = STR_C("the file name has to be a plain path inside the scenery directory, without spaces"); return null_handle; } if (std::filesystem::path(Name).extension().empty()) @@ -586,18 +592,18 @@ layer_handle node_layers::create(std::string Name, layer_handle Parent, std::str } if (Name.ends_with(".inc")) { - Reason = ".inc files are include templates, not layers"; + Reason = STR_C(".inc files are include templates, not layers"); return null_handle; } if (m_layers.size() >= static_cast(std::numeric_limits::max())) { - Reason = "too many layers"; + Reason = STR_C("too many layers"); return null_handle; } auto const taken{std::any_of(std::begin(m_layers), std::end(m_layers), [&](basic_layer const &Layer) { return Layer.name == Name && false == Layer.dead; })}; if (taken || FileExists(Global.asCurrentSceneryPath + Name)) { - Reason = "the file \"" + Name + "\" already exists"; + Reason = format(STR_C("the file \"%s\" already exists"), Name.c_str()); return null_handle; } @@ -642,7 +648,7 @@ bool node_layers::can_remove(layer_handle Layer, std::string &Reason) const { if (false == listed(Layer) || layer(Layer).removed) { - Reason = "no such layer"; + Reason = STR_C("no such layer"); return false; } auto const &target{layer(Layer)}; @@ -652,22 +658,22 @@ bool node_layers::can_remove(layer_handle Layer, std::string &Reason) const } if (target.sites.empty()) { - Reason = "the scenario file can't be removed"; + Reason = STR_C("the scenario file can't be removed"); return false; } if (target.binary) { - Reason = "the file is loaded from binary terrain cache"; + Reason = STR_C("the file is loaded from binary terrain cache"); return false; } if (target.sites.size() > 1) { - Reason = "the file is included more than once"; + Reason = STR_C("the file is included more than once"); return false; } if (target.sites.front().fixed) { - Reason = "the file is included by an include template (.inc)"; + Reason = STR_C("the file is included by an include template (.inc)"); return false; } return true; @@ -733,12 +739,12 @@ bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::strin { if (false == listed(Source) || false == listed(Target) || layer(Source).removed || layer(Target).removed) { - Reason = "no such layer"; + Reason = STR_C("no such layer"); return false; } if (Source == Target) { - Reason = "pick another layer to merge into"; + Reason = STR_C("pick another layer to merge into"); return false; } auto const &source{layer(Source)}; @@ -750,14 +756,14 @@ bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::strin if (is_descendant(Target, Source)) { // the merged text would include the file it was put in - Reason = "the target layer is included by the merged one"; + Reason = STR_C("the target layer is included by the merged one"); return false; } if (target.late != source.late && (source.created || source.sites.empty() || resolve(source.sites.front().parent) != Target)) { // the content would change sides of the scenario initialization - Reason = (target.late ? "the target layer is loaded after the scenario initialization (FirstInit), which only vehicles should follow" : - "the merged layer is loaded after the scenario initialization (FirstInit), its content has to stay there"); + Reason = (target.late ? STR_C("the target layer is loaded after the scenario initialization (FirstInit), which only vehicles should follow") : + STR_C("the merged layer is loaded after the scenario initialization (FirstInit), its content has to stay there")); return false; } if (false == source.context_insert().matches(target.context_insert())) @@ -768,7 +774,7 @@ bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::strin })}; if (unsaved) { - Reason = "the merged layer has includes which weren't saved yet, save the scenery first"; + Reason = STR_C("the merged layer has includes which weren't saved yet, save the scenery first"); return false; } } @@ -779,7 +785,7 @@ bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::strin } if (source.sites.empty()) { - Reason = "the scenario file can't be merged into another layer"; + Reason = STR_C("the scenario file can't be merged into another layer"); return false; } if (false == writable(Source, &Reason)) @@ -788,7 +794,7 @@ bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::strin } if (source.sites.front().fixed) { - Reason = "the file is included by an include template (.inc)"; + Reason = STR_C("the file is included by an include template (.inc)"); return false; } if (resolve(source.sites.front().parent) != Target) @@ -797,7 +803,7 @@ bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::strin // and can't leave anything behind for what used to follow it if (false == source.context_begin.matches(target.context_insert()) || false == source.context_end.matches(source.context_begin)) { - Reason = "the layers are loaded with different origin, rotation or scale"; + Reason = STR_C("the layers are loaded with different origin, rotation or scale"); return false; } } @@ -838,6 +844,24 @@ namespace { // marks of the lines the editor keeps track of std::array const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c", "//$o"}; + +// other tools put lines of their own after some of these marks: Rainsted its includes after //$r, the launcher the descriptions +// of the trainsets after //$o. The editor's lines of them are told by their first word +bool editor_line(char const *Mark, std::string const &Text) +{ + static std::array>, 3> const words{{ + {"//$h", {"#", "alignment", "source", "ends", "norms", "pieces", "vertex", "element", "end"}}, + {"//$r", {"#", "switch", "slip", "crossing", "double", "connection"}}, + {"//$o", {"on"}}, + }}; + auto const lookup{std::find_if(words.begin(), words.end(), [&](auto const &Entry) { return Entry.first == Mark; })}; + if (lookup == words.end()) + { + return true; + } + auto const word{std::string_view{Text}.substr(0, Text.find_first_of(" \t"))}; + return std::any_of(lookup->second.begin(), lookup->second.end(), [&](std::string_view const Known) { return false == Known.empty() && Known == word; }); +} } // namespace void node_layers::scan(layer_handle const Layer) @@ -874,6 +898,10 @@ void node_layers::scan(layer_handle const Layer) text.pop_back(); } text.erase(0, text.find_first_not_of(" \t")); + if (false == editor_line(mark, text)) + { + break; + } m_marked[Layer].push_back({mark, {static_cast(begin), static_cast(end)}, std::move(text)}); break; } @@ -953,14 +981,14 @@ bool node_layers::removable(instance_handle Instance, std::string *Reason) const std::string reason; if (false == tracked(Instance) || instance(Instance).dead) { - reason = "it's a part of an include template (.inc)"; + reason = STR_C("it's a part of an include template (.inc)"); } else { auto const includelayer{resolve(instance(Instance).layer)}; if (false == valid(includelayer) || layer(includelayer).removed) { - reason = "its layer is no longer a part of the scenery"; + reason = STR_C("its layer is no longer a part of the scenery"); } else if (false == writable(includelayer, &reason)) { @@ -968,7 +996,7 @@ bool node_layers::removable(instance_handle Instance, std::string *Reason) const } else if (layer(includelayer).locked) { - reason = "its layer is locked"; + reason = STR_C("its layer is locked"); } } if (false == reason.empty() && Reason != nullptr) diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index abcb21f0..c869ff7d 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -24,6 +24,8 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Globals.h" #include "utilities/Logs.h" #include "utilities/utilities.h" +#include "utilities/translation.h" +#include "editor/editorFormat.hpp" #include #include @@ -636,13 +638,13 @@ save_result node_layers::save(std::vector const &Rootstatements, st save_result result; if (m_layers.empty()) { - result.message = "The scenery wasn't opened for editing, there are no layers to save."; + result.message = STR_C("The scenery wasn't opened for editing, there are no layers to save"); return result; } auto const root{layer_handle{1}}; save_state state; auto const fail = [&](std::string const &Message) { - result.message = "Scenery not saved: " + Message; + result.message = format(STR_C("Scenery not saved: %s"), Message.c_str()); ErrorLog(result.message); return result; }; @@ -1003,7 +1005,7 @@ save_result node_layers::save(std::vector const &Rootstatements, st } if (false == writable(source.layer)) { - unwritten.push_back(path->name().empty() ? std::string{"(noname)"} : path->name()); + unwritten.push_back(path->name().empty() ? std::string{STR_C("(no name)")} : path->name()); continue; } if (false == load(source.layer)) @@ -1354,7 +1356,7 @@ save_result node_layers::save(std::vector const &Rootstatements, st if (false == included.context.matches(layer(target).context_insert())) { // shouldn't happen, merge of layers is refused when it'd lead to this - return fail("include \"" + *included.file + "\" was placed for different origin, rotation or scale than its layer file receives it with"); + return fail(format(STR_C("include \"%s\" was placed for different origin, rotation or scale than its layer file receives it with"), included.file->c_str())); } placed[target].emplace_back(static_cast(idx + 1)); } @@ -1631,7 +1633,7 @@ save_result node_layers::save(std::vector const &Rootstatements, st std::filesystem::remove(path(leftover) + ".tmp", error); } std::filesystem::remove(path(output) + ".tmp", error); - return fail("can't write file \"" + layer(output).name + "\""); + return fail(format(STR_C("can't write file \"%s\""), layer(output).name.c_str())); } written.emplace_back(output); } @@ -1657,7 +1659,7 @@ save_result node_layers::save(std::vector const &Rootstatements, st { std::filesystem::remove(path(leftover) + ".tmp", error); } - return fail("can't make a backup of file \"" + layer(output).name + "\": " + copyerror.message()); + return fail(format(STR_C("can't make a backup of file \"%s\": %s"), layer(output).name.c_str(), copyerror.message().c_str())); } WriteLog("Scenery save: the triangles of the ground go from \"" + layer(output).name + "\", its previous content is kept in \"" + backup + "\""); result.backups.emplace_back(backup); @@ -1696,7 +1698,7 @@ save_result node_layers::save(std::vector const &Rootstatements, st std::filesystem::remove(path(leftover) + ".tmp", error); } result.files.clear(); - return fail("can't replace file \"" + layer(output).name + "\": " + cause); + return fail(format(STR_C("can't replace file \"%s\": %s"), layer(output).name.c_str(), cause.c_str())); } replaced.emplace_back(output); result.files.emplace_back(layer(output).name); @@ -1867,18 +1869,19 @@ save_result node_layers::save(std::vector const &Rootstatements, st result.success = true; if (result.files.empty()) { - result.message = "No changes to save."; + result.message = STR_C("No changes to save"); } else { - result.message = "Saved: "; + std::string files; for (auto const &file : result.files) { - result.message += file + (&file != &result.files.back() ? ", " : ""); + files += file + (&file != &result.files.back() ? ", " : ""); } + result.message = format(STR_C("Saved: %s"), files.c_str()); for (auto const &backup : result.backups) { - result.message += ". Backup before the ground was taken out: " + std::filesystem::path(backup).filename().string(); + result.message += format(STR_C(". Backup before the ground was taken out: %s"), std::filesystem::path(backup).filename().string().c_str()); } } if (false == unwritten.empty()) @@ -1892,8 +1895,8 @@ save_result node_layers::save(std::vector const &Rootstatements, st { names += ", ..."; } - auto const warning{std::to_string(unwritten.size()) + " changed nodes not saved, their files are included with parameters or more than once: " + names}; - WriteLog("Scenery save: " + warning, logtype::generic); + WriteLog("Scenery save: " + std::to_string(unwritten.size()) + " changed nodes not saved, their files are included with parameters or more than once: " + names, logtype::generic); + auto const warning{format(STR_C("Changed nodes left unsaved (their files are included with parameters or more than once): %zu, %s"), unwritten.size(), names.c_str())}; result.message = result.files.empty() ? warning : result.message + ". " + warning; } return result; diff --git a/tools/editor/usage_headers.py b/tools/editor/usage_headers.py new file mode 100644 index 00000000..d996317c --- /dev/null +++ b/tools/editor/usage_headers.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Writes descriptions of the scenery templates beside the game data (editor/templates), from how the sceneries use them: +what each template is (its kind, by its path, its name and the comment at its top), and how it typically stands by the +track (how far from the axis, how high over the rail top), out of the tables the editor writes with the self-test command +"infrascan ". The templates with a description of their own, or one beside the data already, are +left alone; so are the ones no rule knows. + +usage: usage_headers.py ... [--write] [--least ] +without --write it only reports what it would write +""" + +import os +import re +import statistics +import sys + +from editor_headers import decode, load + +# kind of a template by the words of its path, its name and the comment at its top, the first rule which matches +RULES = [ + ('signal', r'semafor|sem_|sem-|tarcz|/tz_|/tm_|powtarz|sygnaliz'), + ('equipment', r'rezonator|shp|przejazd|plyty|zwrotni|zwrl|zwrp|wzm|-wz_|2-5-13/|6-9-9/|1435mm/|podsypk|kozio|most|wiadukt|przepust|track\.inc|cbp-'), + ('environment', r'tree|grass|krzak|plants/|posers/|drzew|trawa|/las_|lawka|plot_|wiata|bud/|ludzi|nastawni|budyn'), + ('sign', r'wskaz|wsk_|wsk-|(^|/)\d?w\d|/we\d|\+we\d|kilometr|hektometr|hekt|slhm|slupek|znak|tablic|napis'), + ('pole', r'slup|słup|maszt|wysieg|wysięg|bramk|kotw|odciag|stb1|sts1|zwis|tra/sb|tr/-[pl]-'), + ('catenary', r'trakcj|siec_|siec-|przewod|lina_nosna'), +] +CATEGORY_WORDS = { + 'signal': 'sygnał', + 'environment': 'otoczenie', + 'equipment': 'wyposażenie toru', + 'sign': 'wskaźnik, znak', + 'pole': 'słup, wysięgnik sieci', + 'catenary': 'sieć trakcyjna', +} + + +def heading(text): + """the first line of the comment at the top of the template, as its name""" + for line in text.splitlines()[:12]: + line = line.strip() + if not line.startswith('//') or line.startswith('//$'): + continue + line = line.lstrip('/').strip(' -=') + if len(line) >= 6 and not re.match(r'^(p\d|param|autor|author|origin|rotate|node|ra \w+:)', line, re.I): + return line[:90] + return '' + + +def kind_of(path, text): + """by the path first, it says the most; then by the comment at the top too""" + for words in (path.lower(), (path + ' ' + '\n'.join(text.splitlines()[:6])).lower()): + for kind, pattern in RULES: + if re.search(pattern, words): + return kind + return None + + +def described(data, file): + template = os.path.join(data, 'scenery', file) + beside = os.path.join(data, 'editor', 'templates', file) + if os.path.exists(beside): + # a description this script wrote may be written again + with open(beside, encoding='utf-8', errors='replace') as source: + return 'usage: {' not in source.read() + try: + text, _ = decode(load(template)) + except OSError: + return True + return any(line.startswith('//$e') for line in text.splitlines()[:40]) + + +def read_tables(tables): + uses = {} + for table in tables: + scenery = os.path.basename(table).replace('use_', '').replace('.tsv', '') + with open(table, encoding='utf-8') as source: + next(source) + for row in source: + cells = row.rstrip('\n').split('\t') + if len(cells) < 7 or cells[1] != 'include': + continue + file = cells[0].replace('\\', '/') + entry = uses.setdefault(file.lower(), {'file': file, 'offsets': [], 'heights': [], 'sceneries': set()}) + entry['offsets'] += [float(v) for v in cells[4].split(',') if v] + entry['heights'] += [float(v) for v in cells[5].split(',') if v] + entry['sceneries'].add(scenery) + return uses + + +def header(entry, kind, name): + distances = [abs(v) for v in entry['offsets']] + offset = statistics.median(distances) + spread = statistics.pstdev(distances) if len(distances) > 1 else 0.0 + height = statistics.median(entry['heights']) + count = len(entry['offsets']) + where = 'na osi toru' if offset < 0.6 else '%.1f m od osi toru' % offset + sceneries = sorted(entry['sceneries']) + description = '%s; typowo %s, %.2f m względem główki szyny; użyty %d razy (%s)' % (CATEGORY_WORDS[kind], where, height, count, ', '.join(sceneries)) + lines = [ + 'version: 1', + 'name: "%s"' % name.replace('"', "'"), + 'category: %s' % kind, + 'description: "%s"' % description, + 'usage: {count: %d, offset: %.2f, spread: %.2f, height: %.2f, sceneries: [%s]}' % (count, offset, spread, height, ', '.join(sceneries)), + ] + return lines + + +def main(argv): + if len(argv) < 3: + print(__doc__) + return 1 + data = argv[1] + tables = [arg for arg in argv[2:] if arg.endswith('.tsv')] + write = '--write' in argv + least = int(argv[argv.index('--least') + 1]) if '--least' in argv else 3 + uses = read_tables(tables) + counts = {} + written = 0 + unknown = [] + for key, entry in sorted(uses.items(), key=lambda item: -len(item[1]['offsets'])): + file = entry['file'] + if len(entry['offsets']) < least or described(data, file): + continue + try: + text, _ = decode(load(os.path.join(data, 'scenery', file))) + except OSError: + continue + kind = kind_of(file, text) + if kind is None: + unknown.append((len(entry['offsets']), file)) + continue + name = heading(text) or os.path.splitext(os.path.basename(file))[0] + counts[kind] = counts.get(kind, 0) + 1 + lines = header(entry, kind, name) + if write: + target = os.path.join(data, 'editor', 'templates', file) + os.makedirs(os.path.dirname(target), exist_ok=True) # NOSONAR + with open(target, 'w', encoding='utf-8', newline='\n') as out: # NOSONAR + out.write('\n'.join(lines) + '\n') + written += 1 + else: + print('%-45s %-12s %s' % (file, kind, lines[3][13:120])) + print('by kind:', ', '.join('%s %d' % item for item in sorted(counts.items()))) + print('%d descriptions %s beside the data' % (sum(counts.values()), 'written' if write else 'would be written')) + print('not known to the rules, the most used: ' + ', '.join('%s (%d)' % (file, count) for count, file in sorted(unknown, reverse=True)[:25])) + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/utilities/parser.cpp b/utilities/parser.cpp index b64aee39..ee5a9ce5 100644 --- a/utilities/parser.cpp +++ b/utilities/parser.cpp @@ -63,6 +63,15 @@ cParser::cParser(std::string const &Stream, buffertype const Type, std::string P // the file is read in one go, tokens are then extracted straight from the memory std::ifstream file(Path, std::ios_base::binary); if (file.fail()) + { + // named in another case than it's on the disk + if (auto const cased{FileCased(Path)}; false == cased.empty()) + { + file.clear(); + file.open(cased, std::ios_base::binary); + } + } + if (file.fail()) { mFailBit = true; } diff --git a/utilities/translation.cpp b/utilities/translation.cpp index c2590e81..5de51b91 100644 --- a/utilities/translation.cpp +++ b/utilities/translation.cpp @@ -117,6 +117,18 @@ bool locale::parse_translation(std::istream &stream) std::vector forms; char last = 'x'; + auto const commit = [&]() { + if (!msgid.empty() && !forms.empty()) + plural_mapping.emplace(msgid, std::move(forms)); + else if (!msgid.empty() && !msgstr.empty()) { + // TODO: context support is incomplete + if (!msgctxt.empty()) + lang_mapping.emplace(msgctxt + "\x1d" + msgid, msgstr); + else + lang_mapping.emplace(msgid, msgstr); + } + }; + while (std::getline(stream, line)) { if (line.size() > 0 && line[0] == '#') continue; @@ -145,19 +157,15 @@ bool locale::parse_translation(std::istream &stream) msgctxt += parse_c_literal(line); } else { - if (!msgid.empty() && !forms.empty()) - plural_mapping.emplace(msgid, std::move(forms)); - else if (!msgid.empty() && !msgstr.empty()) { - // TODO: context support is incomplete - if (!msgctxt.empty()) - lang_mapping.emplace(msgctxt + "\x1d" + msgid, msgstr); - else - lang_mapping.emplace(msgid, msgstr); - } + commit(); return true; } } + if (!msgid.empty()) { + commit(); + return true; + } return false; } diff --git a/utilities/utilities.cpp b/utilities/utilities.cpp index 02608262..0c18e002 100644 --- a/utilities/utilities.cpp +++ b/utilities/utilities.cpp @@ -471,6 +471,20 @@ bool FileExists(std::string const &Filename) return std::filesystem::exists(Filename); } +std::string FileCased(std::string const &Filename) +{ + std::error_code error; + if (std::filesystem::exists(Filename, error)) + return Filename; + std::filesystem::path const path{Filename}; + auto const directory{path.has_parent_path() ? path.parent_path() : std::filesystem::path{"."}}; + auto const wanted{ToLower(path.filename().string())}; + for (std::filesystem::directory_iterator entry{directory, error}, end; entry != end; entry.increment(error)) + if (ToLower(entry->path().filename().string()) == wanted) + return path.has_parent_path() ? (directory / entry->path().filename()).string() : entry->path().filename().string(); + return {}; +} + std::pair FileExists(std::vector const &Names, std::vector const &Extensions) { diff --git a/utilities/utilities.h b/utilities/utilities.h index 5392ba6c..d98b7bc9 100644 --- a/utilities/utilities.h +++ b/utilities/utilities.h @@ -217,6 +217,9 @@ template bool extract_value(Type_ &Variable, std::string const template <> bool extract_value(bool &Variable, std::string const &Key, std::string const &Input, std::string const &Default); bool FileExists(std::string const &Filename); +// the file as it's named on the disk when only the case of its name differs, as in the sceneries made on Windows; empty when +// there's none +std::string FileCased(std::string const &Filename); std::pair FileExists(std::vector const &Names, std::vector const &Extensions); diff --git a/world/Track.cpp b/world/Track.cpp index 599873f7..11b42e45 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -759,7 +759,11 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) str = token; } + auto const velocity { fVelocity }; init_segments( true ); + if( SwitchExtension ) { + fVelocity = velocity; + } // alternatywny zapis nazwy odcinka izolowanego - po znaku "@" w nazwie toru if ((i = m_name.find("@")) != std::string::npos && i < m_name.length()) { @@ -2731,6 +2735,12 @@ std::string TTrack::tooltip() const // locates specified profile in the profile database, potentially loading it from a file // returns: pair +void +TTrack::rail_profile( std::string const &Name ) { + + m_profile1 = ( Name.empty() ? std::pair{} : fetch_track_rail_profile( Name ) ); +} + std::pair TTrack::fetch_track_rail_profile( std::string const &Profile ) { diff --git a/world/Track.h b/world/Track.h index b15eae19..ed1225c5 100644 --- a/world/Track.h +++ b/world/Track.h @@ -197,6 +197,8 @@ public: material_handle m_material1 = 0; // tekstura szyn albo nawierzchni material_handle m_material2 = 0; // tekstura automatycznej podsypki albo pobocza std::pair m_profile1 {}; // profile of geometry chunks textured with texture 1 + // sets the rail profile by its name, as railprofile does; empty: none, the default shape + void rail_profile( std::string const &Name ); using geometryhandle_sequence = std::vector; geometryhandle_sequence Geometry1; // geometry chunks textured with texture 1 geometryhandle_sequence Geometry2; // geometry chunks textured with texture 2