diff --git a/.gitignore b/.gitignore index 5186ceae..70a6d1c6 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ vcpkg.json # Local Python (setup.bat / CMake) for embedding tools/python314/ +__pycache__/ +*.pyc diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b8c94e5..25104d32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,20 @@ set(SOURCES "model/Texture.cpp" "utilities/Timer.cpp" "world/Track.cpp" +"world/TrackSleepers.cpp" +"world/SleeperPaths.cpp" +"world/SleeperFeatures.cpp" +"world/SleeperLines.cpp" +"world/SleeperLayout.cpp" +"world/SleeperModels.cpp" +"world/SleeperWorker.cpp" +"world/GaugeLine.cpp" +"world/RailCrossings.cpp" +"world/RailMaterials.cpp" +"world/RailProfile.cpp" +"world/RailPlan.cpp" +"world/RailCarve.cpp" +"world/RailLayer.cpp" "world/Road.cpp" "world/Sweep.cpp" "world/RoadPoint.cpp" @@ -175,17 +189,42 @@ set(SOURCES "editor/editorTrack.cpp" "editor/editorRoad.cpp" "editor/editorAlignment.cpp" +"editor/editorTrackShape.cpp" +"editor/editorGroundFollow.cpp" +"editor/editorAlignmentStore.cpp" +"editor/editorSwitchStore.cpp" +"editor/editorSwitchDesign.cpp" +"editor/editorConnections.cpp" +"application/editormodeconnection.cpp" +"application/editormodeconnectioncurve.cpp" +"application/editormodedigitize.cpp" "editor/editorSpeed.cpp" "editor/editorProfile.cpp" "editor/editorGauge.cpp" "editor/editorInfra.cpp" "editor/editorTerrainStreamer.cpp" "editor/editorOrthophoto.cpp" +"editor/editorHttp.cpp" +"editor/editorNmt.cpp" "editor/editorPreviews.cpp" "editor/editorIcons.cpp" +"editor/editorClips.cpp" "application/editormode.cpp" "application/editormodetrack.cpp" +"application/editormoderoute.cpp" +"application/editormodestraights.cpp" +"application/editormodelay.cpp" +"application/editormodeswitchplace.cpp" +"application/editormodetrackgizmo.cpp" +"application/editormoderecords.cpp" +"application/editormodeswitchdrive.cpp" +"application/editormodeswitchtool.cpp" +"application/editormodenmt.cpp" +"application/editormodeswitchslide.cpp" +"application/editormodeswitchdesign.cpp" +"application/editormodetrackbed.cpp" "application/editormodetrackux.cpp" +"application/editormodetrackprops.cpp" "application/editormoderoad.cpp" "application/editormodeprofile.cpp" "application/editormodetextures.cpp" @@ -202,6 +241,17 @@ set(SOURCES "application/editormodesweep.cpp" "application/editormodeturntable.cpp" "application/editormodeselftest.cpp" +"application/editormodeselftestscene.cpp" +"application/editormodeselftestsleepers.cpp" +"application/editormodeselftestsweep.cpp" +"application/editormodeselftesttrack.cpp" +"application/editormodeselftestswitch.cpp" +"application/editormodeselftestjoin.cpp" +"application/editormodeselftestsignal.cpp" +"application/editormodeselftestterrain.cpp" +"application/editormodeselftestrails.cpp" +"application/editormodeselftestmonkey.cpp" +"application/scriptedinput.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" "application/editoruipanels.cpp" diff --git a/McZapkie/MOVER.h b/McZapkie/MOVER.h index 29b3e75c..ccd0935b 100644 --- a/McZapkie/MOVER.h +++ b/McZapkie/MOVER.h @@ -169,6 +169,8 @@ enum DerailReason WRONG_TRACK_TYPE = 4, // Ra: powód wykolejenia: nieodpowiednia trajektoria /// Collision with another vehicle. COLLISION = 5, // powód wykolejenia: zderzenie z innym pojazdem + /// Trailing through a switch with a movable nose set the other way. + TRAILED_SWITCH = 6, // powód wykolejenia: rozprucie rozjazdu z ruchomym dziobem }; /*wagi prawdopodobienstwa dla funkcji FuzzyLogic*/ diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 3b5b6215..69d20723 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -1346,6 +1346,8 @@ void TMoverParameters::Derail(DerailReason const Reason) ErrorLog("Bad dynamic: " + Name + " derailed due to wrong track type"); // błąd w scenerii else if (Reason == COLLISION) WriteLog("Bad driving: " + Name + " derailed"); // This reason also generates its own message in `TMoverParameters::CollisionDetect()` + else if (Reason == TRAILED_SWITCH) + ErrorLog("Bad driving: " + Name + " derailed trailing through a switch with a movable nose set the other way"); } } diff --git a/application/application.cpp b/application/application.cpp index 64827c41..bc3afb6c 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -80,6 +80,8 @@ void focus_callback(GLFWwindow *window, int focus) void cursor_enter_callback(GLFWwindow *window, int entered) { + if (scripted_input::driving()) + return; ui_layer::cursor_enter_callback(entered); } @@ -96,6 +98,8 @@ void window_resize_callback(GLFWwindow *, int w, int h) void cursor_pos_callback(GLFWwindow *window, double x, double y) { + if (scripted_input::driving()) + return; ui_layer::cursor_pos_callback(x, y); Global.cursor_pos = glm::ivec2(x, y); Application.on_cursor_pos(x, y); @@ -103,22 +107,30 @@ void cursor_pos_callback(GLFWwindow *window, double x, double y) void mouse_button_callback(GLFWwindow *window, int button, int action, int mods) { + if (scripted_input::driving()) + return; Application.on_mouse_button(button, action, mods); } void scroll_callback(GLFWwindow *window, double xoffset, double yoffset) { + if (scripted_input::driving()) + return; Application.on_scroll(xoffset, yoffset); } void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods) { + if (scripted_input::driving()) + return; Application.on_key(key, scancode, action, mods); } void char_callback(GLFWwindow *window, unsigned int c) { + if (scripted_input::driving()) + return; Application.on_char(c); } @@ -270,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 4d4b41ba..96759802 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -8,11 +8,14 @@ http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" +#include "world/TrackSleepers.h" #include #include #include #include #include "application/editormode.h" +#include "application/editormodeshared.h" +#include "application/editorgestures.h" #include "scene/scenenodegroups.h" #include "application/editoruilayer.h" #ifndef _WIN32 @@ -28,6 +31,8 @@ extern char **environ; #include "editor/editorIncludeInfo.hpp" #include "editor/editorGroundMesh.hpp" #include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIcons.hpp" #include "utilities/translation.h" @@ -496,6 +501,8 @@ void editor_mode::editormode_input::poll() } editor_mode::editor_mode() { + alignment_store::reset(); + switch_store::reset(); m_userinterface = std::make_shared(); // the area fill settings live in the node bank window, in the tab of the fill mode ui()->set_fill_options([this]() { render_area_fill(); }); @@ -513,7 +520,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; @@ -549,7 +556,7 @@ editor_mode::~editor_mode() editor_track::observe(nullptr); } -std::vector editor_mode::ground_heights(std::vector const &Points, bool const Fresh, std::vector *Found) +std::vector editor_mode::ground_heights(std::vector const &Points, bool const Fresh, std::vector *Found) const { std::vector heights; heights.reserve(Points.size()); @@ -568,6 +575,8 @@ std::vector editor_mode::ground_heights(std::vector const &P for (std::size_t i = 0; i < Points.size(); ++i) { auto const &p = Points[i]; + if (false == std::isfinite(p.x) || false == std::isfinite(p.y) || false == std::isfinite(p.z)) + continue; std::pair const key{static_cast(std::floor(p.x / ground_tile_size)), static_cast(std::floor(p.z / ground_tile_size))}; auto &tile = ground_tiles[key]; bool const missing = (tile.grid == nullptr); @@ -628,6 +637,7 @@ editor_ui *editor_mode::ui() const bool editor_mode::init() { EditorSettings.load(); + Global.EditorOrthoBallast = EditorSettings.top_ballast(); EditorModelSets.load(); Camera.Init({0, 15, 0}, {glm::radians(-30.0), glm::radians(180.0), 0}, nullptr); return m_input.init(); @@ -669,23 +679,19 @@ void editor_mode::start_focus(scene::basic_node *node, double duration) double const len = glm::length(dir); dir = len > 1e-3 ? dir / len : glm::normalize(glm::dvec3(1.0, 0.5, 1.0)); + // looking from there straight at the object + fly_to(center, -dir, distance, duration); +} + +void editor_mode::fly_to(glm::dvec3 const &Target, glm::dvec3 const &Look, double const Distance, double const Duration) +{ m_focus_start_pos = Camera.Pos; m_focus_start_angle = Camera.Angle; - m_focus_target_pos = center + dir * distance; - - // target orientation looks from the target position straight at the object - glm::dvec3 look = center - m_focus_target_pos; - double const looklen = glm::length(look); - if (looklen > 1e-6) - look /= looklen; - m_focus_target_angle = glm::vec3( - static_cast(std::asin(glm::clamp(look.y, -1.0, 1.0))), // pitch - static_cast(std::atan2(-look.x, -look.z)), // yaw - 0.0f); // roll - + m_focus_target_pos = Target - Look * Distance; + m_focus_target_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); m_focus_active = true; m_focus_time = 0.0; - m_focus_duration = duration; + m_focus_duration = Duration; } void editor_mode::snap_to_ground(scene::basic_node *node, bool const Alone, bool const Joined) @@ -926,6 +932,7 @@ void editor_mode::restore_terrain(EditorSnapshot &Snapshot) void editor_mode::undo_last() { + first_step_done(first_step::undo); // a change of the terrain made a moment ago is a step of its own before it's taken back m_history_busy = true; terrain_history_step(); @@ -1281,6 +1288,12 @@ bool editor_mode::update() // bindings are matched with the objects by their locations, before anything gets moved if (false == m_bindings_loaded && simulation::is_ready) infra_load(); + // the templates are looked for in the background from the start, for the tools to have them at hand + if (simulation::is_ready) + { + EditorIncludes.start(); + EditorIncludes.poll(); + } auto const deltarealtime = Timer::GetDeltaRenderTime(); @@ -1393,6 +1406,7 @@ bool editor_mode::update() // --- geoportal orthophoto: streamed around the camera, drawn beneath the other overlays --- m_orthophoto.update(Camera.Pos); + nmt_update(); draw_orthophoto(); // --- ImGuizmo: in-viewport transform gizmo for the selected node --- @@ -1401,6 +1415,8 @@ bool editor_mode::update() render_viewport_overlays(); render_track_inspector(); + if (ui()->mode() == nodebank_panel::TRACK) + draw_track_lines(); if (selected_track()) draw_track_overlay(); if (route_active()) @@ -1413,6 +1429,8 @@ bool editor_mode::update() finish_track_box(); update_track_hover(); point_drag_update(); + switch_slide_update(); + draw_switch_slide(); if (m_track_tab == track_tab::turntable) draw_turntable_overlay(); if (m_track_tab == track_tab::signals) @@ -1439,6 +1457,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(); @@ -1471,7 +1490,10 @@ bool editor_mode::update() render_new_scenery_popup(); render_open_scenery_popup(); render_shortcuts_window(); + render_first_steps(); + render_nmt_prompt(); render_orthophoto_window(); + render_switch_designer(); render_environment_window(); draw_selection_overlay(); render_context_menu(); @@ -1552,82 +1574,24 @@ namespace return changed; } - // grid points without a height (or another value) take the one of the nearest point which has it (by steps along the grid) - template void fill_from_nearest(std::vector &Heights, std::vector &Found, std::size_t const Side) - { - std::deque queue; - for (std::size_t index = 0; index < Found.size(); ++index) - if (Found[index] != 0) - queue.push_back(index); - while (false == queue.empty()) - { - auto const index = queue.front(); - queue.pop_front(); - auto const ix = index % Side, iz = index / Side; - auto const visit = [&](std::size_t const Next) { - if (Found[Next] != 0) - return; - Found[Next] = 1; - Heights[Next] = Heights[index]; - queue.push_back(Next); - }; - if (ix > 0) - visit(index - 1); - if (ix + 1 < Side) - visit(index + 1); - if (iz > 0) - visit(index - Side); - if (iz + 1 < Side) - visit(index + Side); - } - } } // namespace 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); @@ -1656,13 +1620,16 @@ void editor_mode::render_terrain_ui() } } + 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)) { // chunks removed since are no longer selected 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; @@ -1753,9 +1720,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)) @@ -1791,8 +1760,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)) @@ -1801,7 +1769,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); @@ -1868,7 +1836,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; @@ -1959,7 +1927,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; @@ -1987,8 +1955,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")) @@ -2188,6 +2161,14 @@ void editor_mode::load_orthophoto_settings() config.north = m_georeference_origin.x; config.east = m_georeference_origin.y; } + auto enabled{m_orthophoto.enabled()}; + m_orthophoto_mark.clear(); + if (auto const marked{scene::Layers.empty() ? std::vector{} : scene::Layers.marked(scene::layer_handle{1}, "//$o")}; false == marked.empty() && editor_orthophoto::read_mark(marked.front(), config, enabled)) + { + m_orthophoto_mark = marked.front(); + m_orthophoto.enabled(enabled); + WriteLog(format("Editor: the map settings of the scenery, orthophoto %s", enabled ? "on" : "off")); + } m_orthophoto.settings(config); m_orthophoto_origin_edit = {config.north, config.east}; } @@ -2223,6 +2204,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); @@ -2235,7 +2218,8 @@ void editor_mode::render_orthophoto_ui() { config.north = m_orthophoto_origin_edit.x; config.east = m_orthophoto_origin_edit.y; - changed = persist = true; + changed = true; + persist = true; } if (m_georeference) { @@ -2246,9 +2230,12 @@ void editor_mode::render_orthophoto_ui() ImGui::SameLine(); if (ImGui::SmallButton(STR_C("Use it"))) { - config.north = m_orthophoto_origin_edit.x = m_georeference_origin.x; - config.east = m_orthophoto_origin_edit.y = m_georeference_origin.y; - changed = persist = true; + m_orthophoto_origin_edit.x = m_georeference_origin.x; + config.north = m_orthophoto_origin_edit.x; + m_orthophoto_origin_edit.y = m_georeference_origin.y; + config.east = m_orthophoto_origin_edit.y; + changed = true; + persist = true; } } } @@ -2279,104 +2266,123 @@ void editor_mode::render_orthophoto_ui() ImGui::PopItemFlag(); }; - if (ImGui::Checkbox(STR_C("Fit to terrain"), &config.drape)) - changed = 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"))) + if (ImGui::Button(STR_C("Retry"))) + m_orthophoto.retry_failed(); + } + + if (ImGui::TreeNode(STR_C("More settings"))) + { + if (ImGui::Checkbox(STR_C("Fit to terrain"), &config.drape)) { - // the ground was edited: what's kept of it has to go as well - ground_tiles.clear(); - forget_ground_shapes(); - m_orthophoto.refit(); + changed = true; + persist = true; } 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 = 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)) + 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) { - config.year = 0; - changed = persist = true; - } - for (int year = thisyear; year >= editor_orthophoto::oldest_year; --year) - { - if (ImGui::Selectable(std::to_string(year).c_str(), config.year == year)) + ImGui::SameLine(); + if (ImGui::Button(STR_C("Refit"))) { - config.year = year; - changed = persist = true; + // 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")); } - ImGui::EndCombo(); - } - if (ImGui::Checkbox(STR_C("4K tiles where available"), &config.hires)) - changed = 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.")); + 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(); + } + + 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 (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")); } @@ -2450,7 +2456,7 @@ editor_terrain *editor_mode::terrain_at(double X, double Z) return m_streamer.terrain_at(X, Z); } -std::vector editor_mode::active_terrains() +std::vector editor_mode::active_terrains() const { std::vector out; out.reserve(m_streamer.resident()); @@ -2884,6 +2890,7 @@ bool editor_mode::save() ui()->set_status(STR("The scenery wasn't opened for editing. Start the simulator with: -edit "), true); return false; } + first_step_done(first_step::save); // the heightmap terrain is kept in files of its own, the scenery only needs the directive which loads it std::vector rootstatements; @@ -2901,23 +2908,48 @@ bool editor_mode::save() if (m_profile.changed) profile_store(); + switch_store::tidy(); + switch_store::settle(); infra_store(); if (false == m_saved_camera.has_value() || glm::distance(m_saved_camera->first, glm::dvec3{Camera.Pos}) > 0.5 || glm::distance(m_saved_camera->second, glm::vec3{Camera.Angle}) > 0.01f) { m_saved_camera = std::make_pair(glm::dvec3{Camera.Pos}, glm::vec3{Camera.Angle}); scene::Layers.mark(scene::layer_handle{1}, "//$c", {format("%.3f %.3f %.3f %.5f %.5f", Camera.Pos.x, Camera.Pos.y, Camera.Pos.z, Camera.Angle.x, Camera.Angle.y)}); } + if (auto const map{editor_orthophoto::mark(m_orthophoto.settings(), m_orthophoto.enabled(), m_orthophoto.settings().north != 0.0 || m_orthophoto.settings().east != 0.0)}; false == m_orthophoto_scenery.empty() && map != m_orthophoto_mark && (m_orthophoto.enabled() || false == m_orthophoto_mark.empty())) + { + m_orthophoto_mark = map; + scene::Layers.mark(scene::layer_handle{1}, "//$o", {map}); + } auto const terrainadded{false == rootstatements.empty()}; auto const controls{turntable_controls(rootstatements)}; + auto const noses{nose_controls(rootstatements)}; std::vector trailing; - starter_trainset(trailing); + std::erase_if(m_vehicle.trainsets, [](auto const &Trainset) { + auto const *track{simulation::Paths.find(Trainset.track)}; + return track == nullptr || track->m_editorremoved; + }); + auto const placed{m_vehicle.trainsets.size()}; + for (auto const &trainset : m_vehicle.trainsets) + trailing.insert(trailing.end(), trainset.lines.begin(), trainset.lines.end()); + if (placed == 0) + starter_trainset(trailing); auto const result = scene::Layers.save(rootstatements, trailing); if (result.success && controls) m_turntable_included = true; - if (result.success && trailing.size() == 3) + if (result.success && noses) + m_nose_included = true; + if (result.success && placed > 0) + { + m_vehicle.trainsets.clear(); + m_vehicle.status = format(STR_C("%d trainset(s) written to the scenery"), static_cast(placed)); + } + if (result.success && placed == 0 && trailing.size() == 3) { std::istringstream header{trailing.front()}; - std::string keyword, timetable, track; + std::string keyword; + std::string timetable; + std::string track; double offset{0.0}; header >> keyword >> timetable >> track >> offset; if (auto *path{simulation::Paths.find(track)}; path != nullptr && simulation::Vehicles.find("wmb10-6457") == nullptr) @@ -2940,11 +2972,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) @@ -2960,7 +2992,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); @@ -2992,7 +3024,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) @@ -3014,7 +3046,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() @@ -3042,7 +3074,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; } @@ -3051,7 +3083,8 @@ void editor_mode::apply_include() auto const directive = editor_includes::compose_directive(m_include.target, m_include.values); if (directive == before) return; - std::string read_before, read_after; + std::string read_before; + std::string read_after; if (false == m_include.info.signal_read.empty()) { std::string file; @@ -3085,7 +3118,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; } @@ -3094,7 +3127,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; } @@ -3102,7 +3135,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(); @@ -3110,7 +3143,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())); @@ -3124,7 +3157,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 @@ -3137,9 +3170,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() @@ -3319,6 +3351,8 @@ glm::dvec3 editor_mode::placement_on_ground(glm::dvec3 Location) double constexpr height_tolerance = 50.0; // with nothing there or lower, how far up the ground is looked for float constexpr terrain_model_radius = 50.0f; // the same models count as ground as for the area fill and the orthophoto + if (false == std::isfinite(Location.x) || false == std::isfinite(Location.y) || false == std::isfinite(Location.z)) + return Location; glm::dvec2 const bmin{Location.x - reach, Location.z - reach}; glm::dvec2 const bmax{Location.x + reach, Location.z + reach}; std::vector triangles; @@ -3362,7 +3396,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; @@ -3400,11 +3434,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); } @@ -3441,7 +3475,8 @@ void editor_mode::read_georeference() // Rainsted writes the origin of the scenery as: //$g , in kilometres std::istringstream words(line.substr(mark + 4)); std::string system; - double east{0.0}, north{0.0}; + double east{0.0}; + double north{0.0}; if (false == static_cast(words >> system >> east >> north) || system.find("1992") == std::string::npos) continue; auto const scale = (east < 10000.0 && north < 10000.0) ? 1000.0 : 1.0; @@ -3485,9 +3520,16 @@ void editor_mode::render_view_menu() toggle_ortho(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("The view straight down without perspective, as on a map; again for the camera as it was")); - ImGui::MenuItem("Show lanes", nullptr, &m_roadtool.lanes); + if (ImGui::MenuItem(STR_C("Ballast in the top view"), nullptr, &Global.EditorOrthoBallast)) + { + EditorSettings.top_ballast(Global.EditorOrthoBallast); + EditorSettings.save(); + } 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("Off: the top view draws the rails and sleepers of the tracks without the ballast, so the ground under them shows")); + ImGui::MenuItem(STR_C("Show lanes"), nullptr, &m_roadtool.lanes); + if (ImGui::IsItemHovered()) + 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()) @@ -3551,7 +3593,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); @@ -3925,9 +3967,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); } @@ -3983,7 +4025,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(); @@ -4005,15 +4047,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)); @@ -4323,33 +4371,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) { @@ -4520,7 +4572,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")); @@ -4529,7 +4581,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")); @@ -4543,7 +4595,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")); @@ -4557,9 +4609,765 @@ void editor_mode::render_help_menu() ImGui::MenuItem(STR_C("Keyboard shortcuts"), "F1", &m_shortcuts_open); if (ImGui::IsItemHovered()) 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("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(); } +void editor_mode::first_step_done(first_step const Step) +{ + m_first_steps_done[static_cast(Step)] = true; +} + +namespace +{ + +void tutorial_input(char const *Keys, char const *Text, float const Column) +{ + auto &list{*ImGui::GetWindowDrawList()}; + auto const at{ImGui::GetCursorScreenPos()}; + auto const height{std::round(ImGui::GetTextLineHeight() * 1.45f)}; + auto const gap{std::round(height * 0.2f)}; + float x{0.0f}; + std::istringstream words{Keys}; + for (std::string word; words >> word;) + { + ImVec2 const place{at.x + x, at.y}; + if (word == "+") + { + auto const size{ImGui::CalcTextSize("+")}; + list.AddText(ImVec2(place.x, std::round(at.y + (height - size.y) * 0.5f)), ImGui::GetColorU32(ImGuiCol_TextDisabled), "+"); + x += size.x + gap; + } + else if (word == "LMB" || word == "RMB" || word == "MMB" || word == "wheel" || word == "move") + x += gesture_mouse(list, place, height, word) + gap; + else + x += gesture_key(list, place, height, word) + gap; + } + ImGui::Dummy(ImVec2(std::max(x, Column), height)); + ImGui::SameLine(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + std::round((height - ImGui::GetTextLineHeight()) * 0.5f)); + 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() +{ + if (false == m_first_steps_open) + { + if (false == m_tutorial_clip.path().empty()) + m_tutorial_clip.close(); + return; + } + if (ui()->mode() == nodebank_panel::TRACK) + first_step_done(first_step::tracks); + if (m_lay.active) + 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); + 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; + char const *text; + }; + struct goal + { + first_step step; + char const *text; + }; + struct lesson + { + char const *title; + char const *clip; + std::vector inputs; + std::vector goals; + trial action; + void (*sketch)(ImDrawList &, ImVec2, ImVec2, double){nullptr}; + }; + static std::vector const lessons{ + {"Looking around", + "camera", + {{"RMB + move", "Hold the right button and move the mouse: look around"}, + {"RMB + W S A D", "With the right button held: fly forwards, back and to the sides; E and Q: up and down"}, + {"O", "The top view, and back to 3D"}, + {"wheel", "Zoom in and out where the cursor points"}, + {"MMB + move", "In the top view: hold the wheel and drag to move the map"}, + {"F", "Fly to what is selected"}}, + {{first_step::look, "Look around with the right button held"}, {first_step::top, "Switch to the top view"}}, + trial::top}, + {"Laying track", + "lay", + {{"F3", "The Tracks field of work"}, + {"L", "The lay tool"}, + {"LMB", "A click on the ground: the first point; the next click: a straight up to it"}, + {"LMB", "Each next click lays a piece on from the end of the last one"}, + {"Ctrl + wheel", "The radius of the curve, while laying on from an end"}, + {"Shift + wheel", "The shape: arc then straight, straight then arc, arc, straight"}, + {"Backspace", "Takes the last piece back"}, + {"Esc", "Ends laying"}}, + {{first_step::tracks, "Open the Tracks field of work"}, {first_step::lay, "Turn the lay tool on"}, {first_step::laid, "Lay a track"}}, + trial::lay}, + {"Curves", + "curves", + {{"Ctrl + wheel", "The radius of the curve, while laying on from an end"}, + {"Shift + wheel", "The shape: arc then straight, straight then arc, arc, straight"}, + {"Alt", "Held: the direction turns in steps of 15 degrees, or to 1:9, 1:6.6... to a straight track it starts by or crosses"}, + {"0 1 2 3", "Type the length of the next piece, Enter lays it"}, + {"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"}, + {"LMB", "Let go on another free end: a curve joins the two tracks"}, + {"Ctrl + wheel", "While over the other end: the radius of the joining curve"}, + {"Shift + wheel", "While over the other end: another way to join"}}, + {{first_step::join, "Join two free ends"}}, + trial::lay}, + {"Switches", + "switch", + {{"T", "The switch tool; at the top: what to place"}, + {"LMB", "Click a track: the switch goes into it"}, + {"LMB + move", "Press on it and drag: along the direction, sideways the side"}, + {"LMB + move", "Connection: drag from a straight onto the other track"}, + {"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, 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", "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::trainset, + tutorial_trainset_sketch}, + }; + auto const done = [&](lesson const &Lesson) { + 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]}; + if (lesson.clip == nullptr) + m_tutorial_clip.close(); + else if (auto const path{std::string{"lang/editor_tutorial/"} + lesson.clip + ".gif"}; path != m_tutorial_clip.path()) + { + m_tutorial_clip.open(path); + m_tutorial_since = ImGui::GetTime(); + } + auto const scale{Global.ui_scale}; + 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); + ImGui::TextDisabled(STR_C("Lesson %d of %d"), m_tutorial_page + 1, static_cast(lessons.size())); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.55f, 0.8f, 1.0f, 1.0f), "%s", STR_C(lesson.title)); + if (lesson.clip != nullptr && false == m_tutorial_clip.missing()) + { + auto const height{width * 320.0f / 520.0f}; + if (auto const texture{m_tutorial_clip.texture(ImGui::GetTime() - m_tutorial_since)}; texture != 0) + { + ImGui::Image(static_cast(texture), ImVec2(width, width * m_tutorial_clip.height() / std::max(1, m_tutorial_clip.width()))); + if (ImGui::IsItemClicked()) + m_tutorial_since = ImGui::GetTime(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Click: play it from the start")); + } + else + { + auto const at{ImGui::GetCursorScreenPos()}; + ImGui::GetWindowDrawList()->AddRectFilled(at, ImVec2(at.x + width, at.y + height), ImGui::GetColorU32(ImGuiCol_FrameBg)); + 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); + 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)]}; + ImGui::TextColored(reached ? ImVec4(0.4f, 0.9f, 0.4f, 1.0f) : ImGui::GetStyleColorVec4(ImGuiCol_Text), "%s", reached ? "[x]" : "[ ]"); + ImGui::SameLine(); + if (reached) + ImGui::TextDisabled("%s", STR_C(entry.text)); + else + ImGui::TextWrapped("%s", STR_C(entry.text)); + } + if (m_tutorial_page == 1 && ui()->mode() == nodebank_panel::TRACK && false == GfxRenderer->Mouse_Hit()) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", STR_C("No ground under the cursor: track goes onto the level 0 plane, drawn as a grid. Terrain: F5")); + switch (lesson.action) + { + case trial::top: + 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: + { + auto const tab{lesson.action == trial::lay ? track_tab::lay : lesson.action == trial::turnout ? track_tab::turnout : track_tab::path}; + auto const on{ui()->mode() == nodebank_panel::TRACK && m_track_tab == tab}; + ImGui::BeginDisabled(on); + if (ImGui::Button(lesson.action == trial::lay ? STR_C("Lay track (L)") : lesson.action == trial::turnout ? STR_C("The switch tool (T)") : STR_C("The select tool (Esc)"))) + { + choose_work_area(work_area::tracks); + show_track_tab(tab); + } + ImGui::EndDisabled(); + break; + } + default: break; + } + ImGui::PopTextWrapPos(); + ImGui::Separator(); + ImGui::BeginDisabled(m_tutorial_page == 0); + if (ImGui::Button(STR_C("< Back"))) + --m_tutorial_page; + ImGui::EndDisabled(); + ImGui::SameLine(); + auto const dot{ImGui::GetFrameHeight()}; + for (int index = 0; index < static_cast(lessons.size()); ++index) + { + ImGui::PushID(index); + auto const at{ImGui::GetCursorScreenPos()}; + if (ImGui::InvisibleButton("##lesson", ImVec2(dot * 0.8f, dot))) + m_tutorial_page = index; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C(lessons[index].title)); + 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, kGestureLit, 0, 2.0f); + ImGui::GetWindowDrawList()->AddCircleFilled(centre, dot * 0.17f, colour); + ImGui::PopID(); + ImGui::SameLine(0.0f, 0.0f); + } + ImGui::SameLine(); + if (m_tutorial_page + 1 < static_cast(lessons.size())) + { + if (ImGui::Button(STR_C("Next >"))) + ++m_tutorial_page; + } + else if (ImGui::Button(STR_C("Close"))) + m_first_steps_open = false; + bool show{EditorSettings.first_steps()}; + if (ImGui::Checkbox(STR_C("Show when the editor opens"), &show)) + { + EditorSettings.first_steps(show); + EditorSettings.save(); + } + } + ImGui::End(); +} + void editor_mode::render_shortcuts_window() { if (false == m_shortcuts_open) @@ -4584,7 +5392,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"}, @@ -4593,7 +5401,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"}, @@ -4608,15 +5419,44 @@ void editor_mode::render_shortcuts_window() {"Tracks", {{"Esc", "Select tool; cancels what's going on first"}, {"L / T / U", "Lay track / switch / turntable"}, - {"G / C", "Straight / curve"}, + {"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"}, {"K", "Split the selected track at the cursor"}, - {"Enter / Backspace", "Laying track: finish / take the last point back"}, + {"L, LMB, LMB...", "Lay track: each click lays a piece and goes on from its end"}, + {"Drag from a free end", "Lead a track out of it; onto another free end: join the two"}, + {"Ctrl+wheel", "Laying: radius"}, + {"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, or 1:9, 1:6.6... to a straight track; the first click with Alt goes onto the axis of the track"}, + {"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"}, @@ -4626,22 +5466,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(); @@ -4652,7 +5524,7 @@ void editor_mode::render_shortcuts_window() void editor_mode::render_gizmo_options() { - if (ImGui::Button(Global.EditorOrtho ? "3D view (O)" : "Top view, orthographic (O)")) + if (ImGui::Button(Global.EditorOrtho ? STR_C("3D view (O)") : STR_C("Top view, orthographic (O)"))) toggle_ortho(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Global.EditorOrtho ? STR_C("Wheel zooms. Hold the wheel and drag to pan.") : STR_C("Top-down orthographic view")); @@ -4660,11 +5532,32 @@ void editor_mode::render_gizmo_options() { ImGui::SameLine(); ImGui::SetNextItemWidth(120.0f); - ImGui::SliderFloat(STR_C("Extent (m), wheel / MMB pan"), &Global.EditorOrthoExtent, 5.0f, 5000.0f, "%.0f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat(STR_C("Extent (m), wheel / MMB pan"), &Global.EditorOrthoExtent, 5.0f, 50000.0f, "%.0f", ImGuiSliderFlags_Logarithmic); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Wheel zooms. Hold the wheel and drag to pan.")); + ImGui::SameLine(); + if (ImGui::Checkbox(STR_C("Ballast"), &Global.EditorOrthoBallast)) + { + EditorSettings.top_ballast(Global.EditorOrthoBallast); + EditorSettings.save(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Off: the top view draws the rails and sleepers of the tracks without the ballast, so the ground under them shows")); + } + 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; + 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); } - ImGui::Checkbox(STR_C("Enabled"), &m_gizmo_enabled); if (!m_gizmo_enabled) return; if (terrain_brush() || terrain_clicking()) @@ -4673,15 +5566,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) @@ -4695,16 +5579,28 @@ void editor_mode::render_gizmo_options() { auto const located = editor_includes::parameter_with_role(m_include.info, "pos.x") != 0 || editor_includes::parameter_with_role(m_include.info, "pos.y") != 0 || editor_includes::parameter_with_role(m_include.info, "pos.z") != 0; - ImGui::TextDisabled("%s", false == located ? "Include selected, no position parameters described" : + ImGui::TextDisabled("%s", false == located ? STR_C("Include selected, no position parameters described") : false == m_include.placement ? STR_C("Include selected, no gizmo under rotate or scale") : STR_C("Include selected, axes as its template allows")); } - else if (!m_node) + else if (!m_node && ui()->mode() != nodebank_panel::TRACK) ImGui::TextDisabled(STR_C("No node selected")); } void editor_mode::render_gizmo() { + if (straights_active()) + { + render_straight_gizmo(); + return; + } + + if (route_active()) + { + render_route_gizmo(); + return; + } + // the transform gizmo is suppressed while editing terrain, so the brush/chunk tool owns the mouse if (!m_gizmo_enabled || terrain_brush() || terrain_clicking()) { @@ -4718,18 +5614,6 @@ void editor_mode::render_gizmo() return; } - if (straights_active()) - { - render_straight_gizmo(); - return; - } - - if (route_active()) - { - render_route_gizmo(); - return; - } - if (!m_node && m_instance == 0) { m_gizmo_using = false; @@ -4912,7 +5796,12 @@ void editor_mode::render_include_gizmo(glm::mat4 const &View, glm::mat4 const &P glm::vec3 const snapvalue(translating ? m_gizmo_snap : 5.0f); float const *snap = Global.ctrlState && snapvalue.x > 0.0f ? glm::value_ptr(snapvalue) : nullptr; + // in the signal tool a press on the mast drags the signal along its track: the gizmo, whose up axis runs along the + // mast, gives way over a signal and while one is dragged + auto const yield{ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::signals && false == ImGuizmo::IsUsing() && (m_signal.moving || m_signal.placing || signal_hit() >= 0)}; + ImGuizmo::Enable(false == yield); ImGuizmo::Manipulate(glm::value_ptr(View), glm::value_ptr(Projection), static_cast(operation), mode, glm::value_ptr(matrix), nullptr, snap); + ImGuizmo::Enable(true); if (false == ImGuizmo::IsUsing()) return; @@ -4963,6 +5852,7 @@ void editor_mode::toggle_ortho() Global.EditorOrtho = !Global.EditorOrtho; if (Global.EditorOrtho) { + first_step_done(first_step::top); m_ortho_pitch = Camera.Angle.x; Camera.Angle.x = -M_PI_2; Camera.Angle.z = 0.0; @@ -4998,12 +5888,44 @@ void editor_mode::ortho_pan_stop() void editor_mode::on_scroll(double const Xoffset, double const Yoffset) { - if (false == Global.EditorOrtho || ImGui::GetIO().WantCaptureMouse) + if (false == ImGui::GetIO().WantCaptureMouse && track_wheel(Yoffset != 0.0 ? Yoffset : Xoffset)) return; + // a swipe on a touchpad goes on after the cursor left the window, with no place for the ray under it + if (ImGui::GetIO().WantCaptureMouse || Yoffset == 0.0 || false == ImGui::IsMousePosValid()) + return; + // a fast swipe on a touchpad can come as one big step + auto const notches{std::clamp(Yoffset, -2.0, 2.0)}; + if (false == Global.EditorOrtho) + { + // in 3D the camera flies along the ray under the cursor, by a share of the distance to what it points at; + // a touchpad sends many small steps, together they come to the same + screen_projection const projection; + glm::dvec3 origin; + glm::dvec3 direction; + projection.ray(ImGui::GetIO().MousePos, origin, direction); + // with nothing under the cursor, the ground under the camera stands for it: the level 0 can lie far below the terrain + std::vector found; + auto const below{ground_heights({Camera.Pos}, false, &found)}; + auto const ground{found[0] != 0 ? below[0] : 0.0}; + auto const height{std::max(2.0, std::abs(Camera.Pos.y - ground))}; + glm::dvec3 level; + auto distance{std::min(GfxRenderer->Mouse_Hit() ? glm::length(GfxRenderer->Mouse_Position()) : projection.on_level(ImGui::GetIO().MousePos, ground, level) ? glm::distance(level, origin) : 2.0 * height, 20000.0)}; + // backing away from a point on the horizon would throw the camera kilometres up + if (notches < 0.0) + distance = std::min(distance, std::max(100.0, 4.0 * height)); + auto step{distance * (1.0 - std::pow(0.85, notches))}; + // no nearer than 2 m to the point, not through it + if (notches > 0.0) + step = std::max(0.0, std::min(step, distance - 2.0)); + Camera.Pos += direction * step; + Global.pCamera = Camera; + return; + } auto const before{Global.EditorOrthoExtent}; - auto const after{std::clamp(before * static_cast(std::pow(0.85, Yoffset)), 5.0f, 5000.0f)}; + auto const after{std::clamp(before * static_cast(std::pow(0.85, notches)), 5.0f, 50000.0f)}; screen_projection const projection; - glm::dvec3 origin, direction; + glm::dvec3 origin; + glm::dvec3 direction; projection.ray(ImGui::GetIO().MousePos, origin, direction); auto const scale{static_cast(after) / static_cast(before)}; Camera.Pos.x = origin.x + (Camera.Pos.x - origin.x) * scale; @@ -5058,6 +5980,8 @@ void editor_mode::update_camera(double const Deltatime) void editor_mode::enter() { m_statebackup = {Global.pCamera, FreeFlyModeFlag, Global.ControlPicking}; + // the editor changes the tracks, their sleepers are worked out as they're needed + track_sleepers::lay_in_background(false); if (simulation::Region != nullptr) { @@ -5122,6 +6046,24 @@ void editor_mode::enter() WriteLog("Editor: camera at the first track, " + track->name()); break; } + if (false == m_saved_camera.has_value() && simulation::Paths.sequence().empty()) + { + std::vector found; + auto const ground{ground_heights({glm::dvec3{0.0, 5000.0, 0.0}}, true, &found).front()}; + Camera.Pos = glm::dvec3{0.0, (found.front() != 0 ? ground : 0.0) + 60.0, 0.0}; + Camera.Angle = glm::vec3(glm::radians(-45.0f), 0.0f, 0.0f); + Camera.m_owner = nullptr; + FreeFlyModeFlag = true; + if (false == Global.EditorOrtho) + { + Global.EditorOrthoExtent = 150.0f; + toggle_ortho(); + } + Global.pCamera = Camera; + choose_work_area(work_area::tracks); + WriteLog("Editor: camera over the origin of the empty scenery"); + } + m_first_steps_open = EditorSettings.first_steps(); } Global.ControlPicking = true; @@ -5134,6 +6076,7 @@ void editor_mode::enter() void editor_mode::exit() { + track_sleepers::lay_in_background(true); Global.editor_tracks = false; EditorModeFlag = false; Global.ControlPicking = m_statebackup.picking; @@ -5147,7 +6090,7 @@ void editor_mode::exit() m_track_snap = {}; m_track_point = {}; m_route = {}; - m_route_gizmo_using = false; + m_route_dragging = false; if (Global.EditorOrtho) toggle_ortho(); @@ -5213,7 +6156,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; } @@ -5293,11 +6236,11 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in case GLFW_KEY_F4: choose_work_area(work_area::roads); break; case GLFW_KEY_F5: choose_work_area(work_area::terrain); break; // the edit modes in the order of the toolbar; in the track mode the digits typed as a value were taken already - case GLFW_KEY_1: choose_edit_mode(nodebank_panel::MODIFY); break; - case GLFW_KEY_2: choose_edit_mode(nodebank_panel::ADD); break; - case GLFW_KEY_3: choose_edit_mode(nodebank_panel::BRUSH); break; - case GLFW_KEY_4: choose_edit_mode(nodebank_panel::FILL); break; - case GLFW_KEY_5: choose_edit_mode(nodebank_panel::COPY); break; + case GLFW_KEY_1: handled = ui()->mode() != nodebank_panel::TRACK; if (handled) choose_edit_mode(nodebank_panel::MODIFY); break; + case GLFW_KEY_2: handled = ui()->mode() != nodebank_panel::TRACK; if (handled) choose_edit_mode(nodebank_panel::ADD); break; + case GLFW_KEY_3: handled = ui()->mode() != nodebank_panel::TRACK; if (handled) choose_edit_mode(nodebank_panel::BRUSH); break; + case GLFW_KEY_4: handled = ui()->mode() != nodebank_panel::TRACK; if (handled) choose_edit_mode(nodebank_panel::FILL); break; + case GLFW_KEY_5: handled = ui()->mode() != nodebank_panel::TRACK; if (handled) choose_edit_mode(nodebank_panel::COPY); break; default: handled = false; break; } if (handled) @@ -5329,6 +6272,31 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in return; } + // the properties of a track copied and pasted on the selected ones + if (Global.ctrlState && Global.shiftState && (Key == GLFW_KEY_C || Key == GLFW_KEY_V) && ui()->mode() == nodebank_panel::TRACK) + { + if (is_press(Action) && false == ImGui::GetIO().WantTextInput) + { + auto const tracks{selected_tracks()}; + if (Key == GLFW_KEY_C) + { + // the selected track, or the one under the cursor when there are more of them + auto *from{tracks.size() == 1 ? tracks.front() : m_hover.track}; + if (from != nullptr) + copy_track_style(*from); + else + ui()->set_status(STR_C("Select one track, or point at one, to copy its properties"), true); + } + else if (m_track_style.values.empty()) + ui()->set_status(STR_C("Copy the properties of a track first: Ctrl+Shift+C"), true); + else if (tracks.empty()) + ui()->set_status(STR_C("Select the tracks to paste the properties on"), true); + else + paste_track_style(tracks); + } + return; + } + // save; checked ahead of the camera keys, which have S bound to moving back if (Global.ctrlState && false == Global.shiftState && Key == GLFW_KEY_S) { @@ -5398,6 +6366,9 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in break; case GLFW_KEY_ESCAPE: + // an open list or menu takes this Esc for itself, the selection behind it stays + if (ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) + break; if (is_press(Action)) { road_cancel(); @@ -5407,13 +6378,15 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in { if (m_lay.active && false == m_lay.points.empty()) lay_cancel(); + else if (m_track_style.picking) + m_track_style.picking = false; else if (track_busy()) { cancel_track_tools(); m_straights.tool = 0; } - else if (m_track_tab == track_tab::path && false == m_track_set.empty()) - m_track_set.clear(); + else if (m_track_tab == track_tab::path && m_track_set.size() >= 2) + select_tracks({}); else if (m_track_tab != track_tab::path) show_track_tab(track_tab::path); else if (selected_track() != nullptr) @@ -5430,11 +6403,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in if (is_press(Action) && ui()->mode() == nodebank_panel::FILL && !m_fill_points.empty()) m_fill_points.pop_back(); if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && m_lay.active && !m_lay.points.empty()) - { - m_lay.points.pop_back(); - if (m_lay.points.empty()) - m_lay.start = {}; - } + lay_back(); break; case GLFW_KEY_ENTER: @@ -5449,7 +6418,11 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in break; } if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && selected_track() != nullptr) - split_track_at(*selected_track(), Global.pCamera.Pos + GfxRenderer->Mouse_Position()); + { + auto const under{split_spot()}; + if (under.track != nullptr) + split_track_at(*under.track, under.point); + } break; case GLFW_KEY_F: @@ -5602,12 +6575,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; } } @@ -5694,6 +6667,8 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const turntable_finish_placement(); if (m_signal.placing || m_signal.moving) signal_release(); + if (m_switch.slide.active) + switch_slide_finish(); } m_dragging = false; @@ -5747,7 +6722,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)) { @@ -5787,7 +6762,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)) @@ -5805,7 +6780,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()) { @@ -5823,6 +6798,7 @@ void editor_mode::render_change_history(){ void editor_mode::on_event_poll() { + m_selftest.input.deliver(); // game-engine style camera: WSAD/EQ only fly the camera while the right mouse button is held. // when it's released the keyboard is free for gizmo shortcuts, and we flush a zero-movement // command once so the camera doesn't keep coasting on the last velocity it was given. @@ -5830,6 +6806,7 @@ void editor_mode::on_event_poll() if (flying) { m_input.poll(); + first_step_done(first_step::look); } else if (m_camera_flying) { diff --git a/application/editormode.h b/application/editormode.h index c7dbd391..4ef8dac9 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -19,13 +19,20 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorTerrain.hpp" #include "editor/editorTerrainStreamer.hpp" #include "editor/editorOrthophoto.hpp" +#include "editor/editorNmt.hpp" #include "editor/editorTrack.hpp" +#include "editor/editorConnections.hpp" #include "editor/editorAlignment.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" #include "editor/editorSpeed.hpp" #include "editor/editorRoad.hpp" #include "editor/editorProfile.hpp" #include "editor/editorInfra.hpp" #include "editor/editorGauge.hpp" +#include "editor/editorClips.hpp" +#include "editor/editorIcons.hpp" +#include "application/scriptedinput.h" #include "world/Sweep.h" #include @@ -34,6 +41,9 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include +#include +#include #include #include @@ -65,14 +75,14 @@ class editor_mode : public application_mode, private editor_track::observer // joint of the paths found wrong by the joints check struct joint_issue { - enum class kind { gap, step, kink, cant, grade, open }; + enum class kind { gap, step, kink, cant, grade, model, open }; kind type{kind::gap}; TTrack *track{nullptr}; editor_track::point_ref point; TTrack *other{nullptr}; editor_track::point_ref theirs; glm::dvec3 position{0.0}; - double value{0.0}; // m, deg, mm, per mille + double value{0.0}; // m, deg, mm, per mille; m of the track over the terrain model bool fixed{false}; }; // constructors @@ -168,6 +178,8 @@ class editor_mode : public application_mode, private editor_track::observer std::vector infra; // objects which followed the paths bool profiles{false}; // the stored grade line designs changed too, these were the ones before std::vector profile_library; + std::optional> alignments; // the kept horizontal alignments changed too, these were the ones before + std::optional> switch_records; // the kept switches changed too, these were the ones before std::vector copies; // models an array added to the one it was made of bool joined{false}; // undone and redone together with the change before it std::shared_ptr terrain; // the heightmap terrain from before the change (Action::Terrain) @@ -176,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); @@ -246,6 +259,14 @@ 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, 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}; + std::array(first_step::count)> m_first_steps_done{}; + int m_tutorial_page{0}; + double m_tutorial_since{0.0}; + editor_clips::clip m_tutorial_clip; // deletes what's selected: a road piece or point, a signal, an include of a scenery template, a model, tracks (Del) void delete_selected(); void delete_model(TAnimModel *Model, bool const Joined); @@ -302,6 +323,107 @@ class editor_mode : public application_mode, private editor_track::observer // adds a chunk of the terrain shaped as the ground under it; where there's no ground, carried on level with the nearest point // of it, or at the base height. returns: false if there's a chunk already bool add_chunk_from_ground(heightmap::chunk_key const &Key, float const Spacing, std::uint16_t const Layer = 0); + + // the terrain from the digital terrain model of GUGiK (editormodenmt.cpp) + 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, 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}; + bool shapes{true}; // not a bridge nor a tunnel + }; + struct nmt_state + { + double offset{0.0}; // added to the heights of the model, so they meet the tracks of the scenery + bool fitted{false}; + bool fitting{false}; // waiting for the model under the places of the tracks the offset is worked out from + std::vector fit; + double spread{0.0}; // how much the tracks differ from the model about the offset, m + std::size_t samples{0}; + double band{300.0}; // m from the tracks the chunks made along them reach + bool shape{true}; // the formation and the slopes of the tracks as modifiers of the terrain + bool grow{true}; // while a track is laid, the terrain is made around the cursor where there's none + bool grown{false}; // the chunks waiting were all asked for by growing: the formations aren't made anew after them + double grow_time{-10.0}; + double slope{1.5}; + double reach{60.0}; + std::vector pending; // chunks to make once their model is in + std::size_t made{0}; + 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; + // 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); + // Only: the paths of these tracks are shaped anew, the modifiers of those removed taken away; null: all of them. the number + // of the modifiers laid + std::size_t nmt_shape_tracks(std::vector const *Only = nullptr); + void nmt_update(); + void nmt_grow(); + void render_nmt_ui(); + // 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 { @@ -414,7 +536,7 @@ class editor_mode : public application_mode, private editor_track::observer // the chunk of the terrain (if any) covering the given world point editor_terrain *terrain_at(double X, double Z); // the chunks of the terrain held in memory - std::vector active_terrains(); + std::vector active_terrains() const; // samples the selected model instance's geometry into the chunks of the terrain it covers, then removes it void capture_terrain(); bool m_terrain_open{false}; // the terrain is the field of work, chosen in the toolbar @@ -470,6 +592,8 @@ class editor_mode : public application_mode, private editor_track::observer bool m_opened_at_track{false}; bool m_turntable_included{false}; bool turntable_controls(std::vector &Rootstatements); + bool m_nose_included{false}; + bool nose_controls(std::vector &Rootstatements); void starter_trainset(std::vector &Trailing); std::optional> m_saved_camera; char m_openscenery_search[64]{}; @@ -486,7 +610,6 @@ class editor_mode : public application_mode, private editor_track::observer // over the 3d view: the tools of the field of work along its left edge, the modes of the gizmo at its top, the axes of the // world and the top view in its top right corner void render_viewport_overlays(); - void render_viewport_tools(); // the tools of the field of work, in the order of the toolbar void work_area_tools(std::vector &Buttons); // the tools of the field of work in the toolbar, as icons with their names @@ -510,6 +633,7 @@ class editor_mode : public application_mode, private editor_track::observer void save_orthophoto_settings(); editor_orthophoto m_orthophoto; std::string m_orthophoto_scenery; // scenery whose origin is loaded + std::string m_orthophoto_mark; glm::dvec2 m_orthophoto_origin_edit{0.0}; // origin being typed in (northing, easting); applied when editing ends // hierarchy management @@ -630,6 +754,19 @@ class editor_mode : public application_mode, private editor_track::observer static bool tracks_editable(std::vector const &Tracks, std::string &Reason); void trim_history(); void restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo); + // changes of the kept alignments and switches go into the last step of the history, made just before them + void records_to_history(); + // the paths a drag of the gizmo takes, to tell after it what became of the kept alignments and switches + struct record_watch + { + bool active{false}; + std::vector moving; + std::vector> before; // ends of the first path of each moving track + std::map> on; // the kept alignments and the moving paths on them + std::set whole; // the kept alignments all of whose paths move + } m_record_watch; + void records_watch_begin(); + void records_watch_end(); editor_track::point_ref m_track_point; std::vector m_track_drag; std::vector> m_track_drag_points; @@ -638,20 +775,91 @@ class editor_mode : public application_mode, private editor_track::observer glm::dvec3 m_track_pivot{0.0}; glm::mat4 m_track_gizmo{1.0f}; bool m_track_gizmo_using{false}; + struct track_gizmo_drag + { + gizmo_operation operation{gizmo_operation::translate}; + bool control{false}; + double angle{0.0}; + int path{-1}; + bool atend{true}; + std::size_t pieces{1}; + std::vector paths; + editor_track::point_ref point; + glm::dvec3 origin{0.0}; + glm::dvec3 axis{0.0}; + double grabbed{0.0}; + double length{0.0}; + double stretch{0.0}; + std::vector> partners; + glm::dvec3 fixed{0.0}; + std::vector>> held; + std::vector extra; + int ring{-1}; + glm::dmat3 basis{1.0}; + glm::dvec3 pivot{0.0}; + double grade{0.0}; + double roll{0.0}; + double roll0{0.0}; + double pivot_chainage{0.0}; + std::vector chainages; + std::vector lengths; + struct switch_joint + { + TTrack *track{nullptr}; + editor_track::point_ref own; + editor_track::point_ref theirs; + glm::dvec3 point{0.0}; + glm::dvec3 tangent{0.0}; + double roll{0.0}; + bool moved{false}; + std::vector>> cluster; + std::vector>> joints; + }; + std::vector switches; + }; + track_gizmo_drag m_track_gizmo_drag; + gizmo_operation track_gizmo_operation(TTrack const &Track, bool const Pointmode) const; + glm::mat4 track_gizmo_frame(TTrack const &Track, bool const Pointmode, gizmo_operation const Operation) const; + void track_gizmo_hold(); + void track_gizmo_settle(); + void track_gizmo_stretch(TTrack &Track, double const Length); + void track_gizmo_control(TTrack &Track, glm::dvec3 Control); + void track_gizmo_point(TTrack &Track, glm::dvec3 const &Position); + void track_gizmo_switches(TTrack &Track); + void track_gizmo_conform(TTrack &Track) const; + bool track_gizmo_clustered(TTrack const *Track) const; + void track_gizmo_spill(TTrack &Track, std::size_t const Pieces); + void track_gizmo_carry(track_gizmo_drag::switch_joint &Link, glm::dvec3 const &Point, glm::dvec3 const &From, glm::dvec3 const &To, double const Grade); + void track_gizmo_roll_switch(track_gizmo_drag::switch_joint const &Link, double const Roll); + void track_gizmo_take_joints(TTrack &Track); + void track_gizmo_take_points(TTrack &Track); + void track_gizmo_take_partners(TTrack &Track); + void track_gizmo_take_switch_joints(TTrack &Track, bool const Controlmode); + track_gizmo_drag::switch_joint track_gizmo_link(TTrack &Track, editor_track::point_ref const &Own, std::pair const &Connection, std::function const &Movable); + void track_gizmo_take_switches(TTrack &Track); + // the other switch of a double switch and the track between them go with the moved or turned switch as a whole + void track_gizmo_take_double(TTrack &Track); + void track_gizmo_measure_rotation(TTrack &Track, bool const Pointmode); + void track_gizmo_measure(TTrack &Track, bool const Pointmode, gizmo_operation const Operation); + void track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_operation const Operation); + void track_gizmo_turn_direction(TTrack &Track, double const Angle); + void track_gizmo_turn_grade(TTrack &Track, double const Angle); + void track_gizmo_turn_cant(TTrack &Track, double const Angle); + void track_gizmo_turn(TTrack &Track, int const Axis, double const Angle); + void track_gizmo_rotated(TTrack &Track); + void track_gizmo_scaled(TTrack &Track); + void track_gizmo_moved_point(TTrack &Track, glm::dvec3 const &Position); + void track_gizmo_length(TTrack &Track, double Length); + void track_gizmo_end(TTrack &Track); bool m_track_dirty{false}; std::chrono::steady_clock::time_point m_track_last_commit; editor_track::state m_track_field_before; - std::array, 3> m_track_materials{}; - std::array m_track_material_edited{}; // path whose texture name is being typed - // typed names of the track circuits, the sleeper model and its skin, and the event to add, for the path - TTrack const *m_track_names_for{nullptr}; - std::array m_track_isolated{}; - std::array m_track_sleeper_model{}; - std::array m_track_sleeper_skin{}; + // the event to add to the path, typed + TTrack const *m_track_events_for{nullptr}; std::array m_track_event{}; int m_track_event_list{0}; std::string m_track_events_missing; - void render_path_circuits(TTrack &Track); + void render_path_events(TTrack &Track); std::string m_track_mode_notice; // structure gauge along the line of the selected path, shown in the 3d view struct gauge_state @@ -714,8 +922,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); @@ -725,10 +960,68 @@ class editor_mode : public application_mode, private editor_track::observer bool route_from_curve(TTrack &Track); bool route_from_line(TTrack &Track); alignment::vertex route_vertex_of(editor_track::curve const &Curve) const; - bool route_load(TTrack &Track) { return m_route.single ? route_from_curve(Track) : route_from_line(Track); } - void route_update() { m_route.result = alignment::compute(m_route.design); } + bool route_load(TTrack &Track); + void route_read_stored(TTrack &Track); + void route_take_stored(int const Index); + void record_switch(TTrack &Track, editor_track::switch_template const &Shape); + void record_double(TTrack &First, TTrack &Second, editor_track::switch_template const &Shape); + // the two switches of a double switch and the plain track between them, found from any of the three + struct double_switch + { + TTrack *first{nullptr}; + TTrack *link{nullptr}; + TTrack *second{nullptr}; + }; + std::optional double_of(TTrack const &Track) const; + // the paths of the set in the order editor_track::place_switch gives them + static std::vector double_paths(double_switch const &Set); + // the switch of the template, or the two switches of a double one with the plain track between them, committed and recorded + std::vector create_switch_set(editor_track::switch_template const &Shape, std::vector const &Paths, TTrack const &Style, std::string const &Name); + void record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base, double const Middle = 0.0); + void render_switch_record(TTrack const &Track) const; + void route_store(); + 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); + // 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() + { + m_route.design.norms.checked = false == m_norms_free; + m_route.result = alignment::compute(m_route.design); + } void route_apply(); - bool route_relay_through_switches(editor_track::chain const &Chain, std::vector> &States, std::vector &Created); + bool route_relay_through_switches(editor_track::chain const &Chain, std::vector> &States, std::vector &Created, TTrack *&Last); void route_recommend(alignment::vertex &Vertex) const; void draw_route_overlay() const; void render_route_gizmo(); @@ -760,7 +1053,6 @@ class editor_mode : public application_mode, private editor_track::observer std::vector> drag_states; glm::dvec3 preview_start{0.0}; glm::dvec3 preview_end{0.0}; - glm::mat4 gizmo{1.0f}; glm::dvec3 edit_start{0.0}; glm::dvec3 edit_end{0.0}; double edit_length{0.0}; @@ -770,17 +1062,16 @@ class editor_mode : public application_mode, private editor_track::observer bool auto_transitions{true}; double transition{0.0}; int tool{0}; + int tool_before{0}; bool tool_mouse{false}; std::vector detour; editor_track::straight detour_line; glm::dvec3 detour_mouse{0.0}; bool tool_placed{false}; - bool tool_dragging{false}; double tool_at{0.0}; glm::dvec3 tool_point{0.0}; glm::dvec3 tool_handle{0.0}; editor_track::straight tool_line; - glm::mat4 tool_gizmo{1.0f}; std::string status; }; void render_straights_ui(); @@ -792,7 +1083,6 @@ class editor_mode : public application_mode, private editor_track::observer void straights_apply(std::vector const &Lines, std::vector> const &Ends); bool in_straight_set(editor_track::straight const &Line) const; bool place_straight_tool(); - void render_straight_tool_gizmo(); void apply_straight_tool(); double straight_tool_radius(editor_track::straight const &Line) const; void straight_reshape(editor_track::straight const &Line, double const From, double const To, std::function const &Tail, double const Radius); @@ -827,8 +1117,25 @@ class editor_mode : public application_mode, private editor_track::observer float roll{0.f}; float radius{0.f}; }; + // the line leaving an end of a switch off its main straight, taken before the switch moves: its paths up to the end of + // the straight past the curve next to the switch. When the switch moves only the paths up to a joint on that straight + // are laid again, so the curve keeps to the end of the switch and the rest of the network stays where it is + struct switch_branch + { + std::size_t joint{0}; + editor_track::chain chain; + std::size_t straight{0}; // the first path of the straight past the curve + alignment::design design; // as the paths lie + alignment::result result; + // made for the new end of the switch + editor_track::chain part; // the paths laid again + std::vector pieces; + alignment::design laid; // of the whole chain + double factor{1.0}; // of the radius and the transitions of the curve + }; 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; @@ -847,16 +1154,96 @@ class editor_mode : public application_mode, private editor_track::observer { std::string name; std::array files; + std::array heights{}; + bool preferred{false}; }; std::vector drives; - bool drives_scanned{false}; + int drives_revision{-1}; // of the include bank the drives were read from int drive{-1}; int drive_side{0}; + struct slide_state + { + bool active{false}; + TTrack *track{nullptr}; + glm::vec2 pressed{0.f}; + double grip{0.0}; + double low{0.0}; + double high{0.0}; + double distance{0.0}; + double length{0.0}; + std::vector joints; + // the lines leaving the switch off its straight: the joint they leave at, the path of the switch in the shown ones + struct branch_line + { + editor_track::joint joint; + std::size_t path{0}; + switch_branch branch; + }; + std::vector branches; + }; + slide_state slide; + double slide_typed{0.0}; + int connection{-1}; + int connection_kind{0}; // the kind of connection chosen last, taken again when the connections are chosen + bool rail_only{true}; // the templates listed only for the rail of the selected track, where its rail profile tells it + int connection_second{-1}; + double connection_radius{0.0}; + double connection_gap{6.0}; + bool connection_together{false}; + char connection_spacings[64]{"4.75;4.75;5.0"}; + double connection_stub{25.0}; }; switch_tool m_switch; - void scan_switch_drives(); + // what the switch tool places: a switch into a straight or a curve, a slip switch or a crossing at a crossing of two + // straights, or a connection of tracks laid whole + enum class switch_kind + { + single, + slip, + crossing, + connection + }; + switch_kind current_switch_kind() const; + void choose_switch_kind(switch_kind const Kind); + bool switch_kind_offered(switch_kind const Kind) const; + bool plan_connection(connection::request &Request, connection::result &Result) const; + void insert_connection(); + void render_connection_options(); + // the name of a kind of connection of tracks, translated + static char const *connection_name(int const Kind); + // the name of a switch template for the lists: where it comes from and its name without the lengths + static std::string switch_template_label(editor_track::switch_template const &Template); + std::string connection_readout(connection::result const &Plan) const; + editor_track::straight straight_near(glm::dvec3 const &Point, std::vector const &Exclude, double const Reach) const; + editor_track::straight straight_through(glm::dvec3 const &Point, glm::dvec2 const &Direction) const; + void record_connection(connection::kind const Kind, std::vector const &Switches, connection::result const &Plan, editor_track::switch_template const &Shape); + bool switch_room(TTrack &Track, double &Low, double &High, std::string &Reason); + bool switch_vacate(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, double const Shift, editor_track::straight const &Line, editor_track::edit &Edit, std::string &Status); + bool move_switch_paths(TTrack &Track, std::vector const &Paths, std::string &Status); + std::optional take_switch_branch(TTrack const &Track, editor_track::joint const &Joint, std::size_t const Index) const; + // for the path of the switch the line leaves, as it'll lie + bool plan_switch_branch(segment_data const &Path, editor_track::joint const &Joint, switch_branch &Branch) const; + void lay_switch_branch(switch_branch const &Branch, editor_track::edit &Edit); + // of the joints off the main straight of the switch, the neighbours in Skip left out + std::vector switch_branches(TTrack const &Track, std::vector const &Joints, std::vector const &Skip = {}) const; + // for the paths the switch has now; the joints of the branches which can be laid are handled, the others are left out. Gives the notes for the user + std::string plan_switch_branches(TTrack const &Track, std::vector const &Joints, std::vector &Branches, std::vector &Handled) const; + // room on the straight of the whole double switch along the axis of its first switch, and the length of the set + bool double_room(double_switch const &Set, double &Low, double &High, double &Length, std::string &Reason); + bool move_double_paths(double_switch const &Set, std::vector const &Paths, std::string &Status); + bool slide_switch(TTrack &Track, double const Distance); + bool switch_slides(TTrack const &Track) const; + void switch_slide_start(TTrack &Track); + void switch_slide_update(); + void switch_slide_finish(); + void draw_switch_slide() const; + // reads the drives from the include bank once it changes; Wait: for the templates being looked for. true if it read them + bool scan_switch_drives(bool Wait = true); std::string switch_name_for_new() const; bool place_switch_drive(TTrack &Switch); + bool flip_switch_drive(TTrack &Switch); + void advance_switch_name(); + void join_last_steps(); bool render_switch_drive_choice(); int m_turnout_template{0}; struct turntable_template @@ -884,7 +1271,7 @@ class editor_mode : public application_mode, private editor_track::observer struct turntable_tool { std::vector templates; - bool scanned{false}; + int revision{-1}; // of the include bank the templates were read from int chosen{-1}; char name[64]{}; bool placing{false}; @@ -910,7 +1297,7 @@ class editor_mode : public application_mode, private editor_track::observer std::string error; }; turntable_tool m_turntable; - void scan_turntables(); + bool scan_turntables(bool Wait = true); std::string turntable_name_for_new() const; bool turntable_read(TTrack *Table); std::vector turntable_includes(TTrack const &Table); @@ -939,6 +1326,7 @@ class editor_mode : public application_mode, private editor_track::observer editor_track::snap_target origin; editor_track::snap_target snap; glm::vec2 pressed{0.f}; + std::vector line; }; extend_tool m_extend; // lays new track through the clicked points, with no path needed to start from @@ -953,23 +1341,39 @@ class editor_mode : public application_mode, private editor_track::observer std::string preview_error; double preview_length{0.0}; double radius{500.0}; - bool transitions{true}; + bool transitions{false}; double piece_length{100.0}; bool copy_selected{false}; editor_track::path_style style; std::string status; + double status_time{-100.0}; + std::string skew; // what Alt turned the straight to + struct step + { + editor_track::snap_target start; + glm::dvec3 point{0.0}; + }; + std::vector chain; }; lay_tool m_lay; + bool m_norms_free{true}; + alignment::limits laying_norms() const; + bool render_norms_choice(); + glm::dvec3 lay_angle_snap(glm::dvec3 const &Mouse, std::string &Label) const; + // the straight track Alt turns the laid straight to: the nearest one the straight from From to To crosses, or one along From + TTrack const *skew_reference(glm::dvec3 const &From, glm::dvec3 const &To, double const Reach) const; + void lay_after_history(bool const Undo); + void lay_back(); void render_lay_ui(); void render_lay_status(); void lay_click(); void lay_finish(editor_track::snap_target const &End); void lay_cancel(); void lay_enter(); - bool lay_heading(glm::dvec2 &Heading) const; 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; @@ -980,6 +1384,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 { @@ -992,10 +1420,48 @@ class editor_mode : public application_mode, private editor_track::observer std::vector paths; }; bool plan_crossover(crossover_plan &Plan) const; + struct switch_link + { + editor_track::snap_target target; + editor_track::branch_link link; + double along{0.0}; + int forward{1}; + int side{1}; + double moved{0.0}; + std::vector paths; + std::string reason; + }; + bool plan_switch_link(switch_link &Plan) const; + void insert_linked_switch(switch_link const &Plan); + bool plan_branch_link(editor_track::branch_link &Link, std::string &Reason) const; + void finish_branch_link(editor_track::branch_link const &Link); + std::string link_problem(editor_track::snap_target const &Target, editor_track::branch_link const &Link) const; + bool stretchable(editor_track::snap_target const &Target) const; + std::vector link_tracks(editor_track::snap_target const &Target, editor_track::branch_link const &Link, TTrack const &Style, std::vector> &States, std::vector &Created); bool cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack **Style); + // where a slip or a crossing goes for the cursor at Point on the straight Line: the crossing of Line with the rail track + // nearest the cursor, or the double slip there to be replaced + struct slip_plan + { + std::string problem; + std::string note; + TTrack *replaces{nullptr}; + TTrack *other{nullptr}; + glm::dvec2 crossing{0.0}; + glm::dvec2 first{0.0, 1.0}; + glm::dvec2 direction{0.0, 1.0}; + double angle{0.0}; + double alongfirst{0.0}; + double half{0.0}; + editor_track::slip_curve curve; + std::array points{}; + }; + bool plan_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape, slip_plan &Plan) const; + // the cursor where it points at the straight, at the height of the straight there + glm::dvec3 cursor_on_straight(editor_track::straight const &Line) const; bool insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape); bool replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape); - std::vector build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, double const Radius, TTrack &Style); + std::vector build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, editor_track::slip_curve const &Curve, TTrack &Style, std::string const &Type = "Rkpd"); bool start_extend(editor_track::snap_target const &End); void finish_extend(); std::vector extend_pieces() const; @@ -1006,33 +1472,83 @@ class editor_mode : public application_mode, private editor_track::observer // adds a straight (Side 0) or a curve to the left (1) or the right (-1) at the free end of the selected track void extend_add(int const Side); void render_extend_ui(); - bool m_extend_freehand{false}; // dragging the ring of a free end draws a curve through the cursor, not a straight + using track_layout = editor_track::layout; + track_layout m_track_layout{track_layout::arc_straight}; + double m_join_radius{0.0}; + int m_join_variant{0}; + TTrack const *m_join_track{nullptr}; + editor_track::point_ref m_join_point; + mutable alignment::join_result m_join_last; + bool track_wheel(double const Offset); + // a touchpad sends fractions of a notch: a choice moves a step for each whole notch gathered, and only one in a gesture when + // Gesture; a pause, another key or the other way starts over + struct wheel_state + { + double rest{0.0}; + double time{-10.0}; + int keys{0}; + bool spent{false}; + }; + wheel_state m_wheel; + int wheel_step(double const Offset, bool const Gesture); + std::string track_shaping(bool const Joined) const; + void render_layout_combo(); + double design_speed(TTrack const *From, TTrack const *To) const; + std::vector join_pieces(TTrack const *From, editor_track::point_ref const &Frompoint, glm::dvec3 const &Start, glm::dvec2 const &Direction, editor_track::snap_target const &End, double &Length, + std::string &Error) const; double m_extend_length{25.0}; double m_extend_radius{300.0}; bool extend_snap(editor_track::snap_target &Target) const; void update_build_tools(); void draw_build_overlay() const; // modes of the track editor, one at a time: path is the plain selection - enum class track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals }; + enum class track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals, parallel }; struct track_mode { char const *label; 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(); + static std::array const &track_modes(); + bool track_plan_tab() const { return m_track_tab == track_tab::straights || m_track_tab == track_tab::route; } + track_tab plan_tab_for(TTrack const *Track) const; // the objects along the track: a tab for each tool int m_lineside_tab{0}; void render_lineside_ui(); void arm_switch(); + // the window of the switch templates: the catalogue and the own ones, made of straights, arcs and transition curves + struct switch_designer + { + bool open{false}; + bool loaded{false}; + std::vector catalogue; + std::vector own; + bool from_catalogue{true}; + int selected{-1}; + bool dirty{false}; + bool stretched{true}; + double quick_radius{300.0}; + double quick_ratio{9.0}; + double quick_straight{0.0}; + double blade_length{5.0}; + double blade_gap{0.15}; + std::string status; + }; + switch_designer m_switch_designer; + void switch_designs_load(); + // the list of the switch tool built again with the own templates, keeping the armed one or arming the one whose label starts with Arm + void switch_templates_refresh(std::string const &Arm = {}); + void render_switch_designer(); bool m_track_window_open{false}; // the inspector is pinned to the right edge of the screen, the profile strip to the bottom one, both resized from the inner edge void render_track_inspector(); // the track tools in the tool options window, and the selected path in the inspector void render_track_tool_options(); void render_track_selection(); - void show_track_tab(track_tab const Tab); + void show_track_tab(track_tab const Wanted); bool track_analysis_tab() const { return m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || m_track_tab == track_tab::joints || m_track_tab == track_tab::infra; } bool track_shortcut(int const Key); bool track_busy() const; // something going on in the mode, which Esc drops before it leaves the mode @@ -1048,7 +1564,6 @@ class editor_mode : public application_mode, private editor_track::observer }; track_name_edit m_track_name; void render_track_name(TTrack &Track); - void render_path_parameters(TTrack &Track); void render_straight_ui(); track_tab m_track_tab{track_tab::path}; void delete_selected_track(); @@ -1059,11 +1574,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 { @@ -1073,12 +1591,14 @@ class editor_mode : public application_mode, private editor_track::observer double since{0.0}; }; track_hover m_hover; - track_hover track_under_cursor() const; + track_hover track_under_cursor(TTrack const *Only = nullptr, float const Pixels = 0.0f) const; + track_hover split_spot() const; void update_track_hover(); + void draw_track_lines() const; void draw_track_hover() const; struct track_intent { - enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move }; + enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling }; kind what{kind::none}; TTrack *track{nullptr}; int path{0}; @@ -1126,6 +1646,13 @@ class editor_mode : public application_mode, private editor_track::observer bool switch_reach(glm::dvec3 const &Ground, glm::dvec3 &Point); glm::dvec3 cursor_ground() const; glm::dvec3 cursor_level(double const Height) const; + glm::dvec3 cursor_terrain(double const Height); + double bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direction, TTrack const *Style) const; + double nearby_ground(glm::dvec3 const &Point) const; + static std::pair trackbed(TTrack const *Style); + std::vector bed_heights(std::vector const &Points, std::vector const &Directions, TTrack const *Style) const; + void follow_ground(std::vector &Pieces, double const Height0, double const Grade0, double const Height1, double const Grade1, TTrack const *Style) const; + glm::dvec3 lay_bed(glm::dvec3 const &Point); std::optional m_cursor_override; bool sweep_grabs(glm::dvec3 const &Point); // menu of the path a click of the right mouse button lands on @@ -1144,6 +1671,8 @@ class editor_mode : public application_mode, private editor_track::observer void render_track_context(); void split_track_at(TTrack &Track, glm::dvec3 const &Point); void focus_track(TTrack &Track, int const Path, glm::dvec3 const &Point); + // the camera flies over Duration seconds to the place Distance back from the target along Look, looking along it + void fly_to(glm::dvec3 const &Target, glm::dvec3 const &Look, double const Distance, double const Duration = 0.6); // paths found by their names struct track_search { @@ -1160,28 +1689,29 @@ class editor_mode : public application_mode, private editor_track::observer struct track_box { bool active{false}; + bool replace{false}; // the tracks in the box become the selection, else they join it glm::vec2 from{0.f}; glm::vec2 to{0.f}; TTrack *hovered{nullptr}; }; track_box m_track_box; - struct track_set_fields - { - double velocity{100.0}; - char rail[128]{}; - char trackbed[128]{}; - char circuits[256]{}; - int environment{0}; - }; - track_set_fields m_track_set_fields; glm::mat4 m_track_set_gizmo{1.0f}; bool m_track_set_using{false}; glm::dvec3 m_track_set_pivot{0.0}; std::vector>> m_track_set_joints; bool in_track_set(TTrack const *Track) const; + // the track is selected: alone or in the set + bool track_selected(TTrack const *Track) const; + // a click in the view: the track under the cursor becomes the selection, or nothing + void track_click_select(TTrack *Hovered); + bool track_set_begin(bool const Rotate); + void track_set_turn(double const Angle); + void track_set_move(glm::dvec3 const &Offset); + void track_set_end(); + glm::dvec3 track_set_centre() const; void track_set_toggle(TTrack &Track); void track_set_prune(); - void start_track_box(); + void start_track_box(bool const Replace = false); void finish_track_box(); void draw_track_set() const; void render_track_set_ui(); @@ -1206,8 +1736,82 @@ class editor_mode : public application_mode, private editor_track::observer void track_set_grow(); void render_track_spread_ui(); void draw_track_spread() const; - // changes each path of the set which can be changed; false from Change: the path stays as it was - void track_set_apply(std::function const &Change, bool const Rebuild, char const *What); + // the properties of the selected tracks in the inspector, one sheet for one track and for many; see editormodetrackprops.cpp + // the set when there's one, the selected track otherwise + std::vector selected_tracks(); + void select_tracks(std::vector const &Tracks); + // changes each track which can be changed, in one step of the undo; false from Change: the track stays as it was + std::size_t tracks_apply(std::vector const &Tracks, std::function const &Change, bool const Rebuild, char const *What); + int track_property_index(std::string const &Key) const; + std::size_t track_property_apply(int const Property, std::vector const &Tracks, std::string const &Text); + void track_field_settle(); + void track_textures_scan(); + // every texture and material under the folder of the textures, read on the first use of the picker + void track_textures_index(); + // the picker of a texture of the rails (Kind 0) or of the trackbed (1), in an open combo. returns: true when one is chosen, its name in Chosen, "none" for no texture + bool track_texture_picker(int const Kind, std::string const &Current, bool const Mixed, std::string &Chosen); + // image of the texture of the name for the picker, 0 while there's none + std::uint64_t track_texture_image(int const Kind, std::string const &Name); + void render_track_properties(std::vector const &Tracks); + void render_track_set_header(std::vector const &Tracks); + void render_track_select_tools(std::vector const &Tracks); + void render_track_style(std::vector const &Tracks); + void copy_track_style(TTrack const &Track); + std::size_t paste_track_style(std::vector const &Tracks); + std::vector similar_tracks(int const Property, std::vector const &Tracks) const; + // values of the properties over the selected tracks, each with the number of the tracks having it + struct track_values + { + std::size_t signature{0}; + int generation{0}; + int generation_seen{-1}; + std::size_t history{0}; + int frame{-100}; + std::vector>> values; + std::vector applies; + }; + track_values m_track_values; + // the field being typed in, with the tracks it was started on + struct track_field + { + int property{-1}; + std::vector tracks; + std::array text{}; + std::string initial; + int frame{0}; + }; + track_field m_track_field; + struct track_texture_use + { + material_handle material{null_handle}; + std::string name; + int count{0}; + }; + std::array, 2> m_track_textures; // of the rails, of the trackbed + std::array m_track_texture_filter{}; + struct texture_index_entry + { + std::string name; // as the materials are named: relative to the folder of the textures, without the extension + std::string lowered; + bool material{false}; // there's a .mat file of the name + }; + std::vector m_track_texture_index; // sorted by the name + bool m_track_texture_indexed{false}; + std::string m_track_texture_hovered; // the tile under the mouse in the picker, shown large above them + // properties copied from a track, to be pasted on others + struct track_style + { + std::string from; + std::vector> values; + // speed, environment, overhead line, track circuits, rails, trackbed, its shape, sleepers, ride and sound, frogs + std::array groups{false, false, false, false, true, true, true, true, false, true}; + bool open{false}; + bool picking{false}; // the next click on a track in the view copies its properties + }; + track_style m_track_style; + std::vector> m_track_similar; + TTrack const *m_track_list_hover{nullptr}; // in the list of the selected tracks + int m_track_list_hover_frame{-1}; // second track alongside the selected path or the line through it, at a spacing, curves included struct parallel_tool { @@ -1264,6 +1868,18 @@ class editor_mode : public application_mode, private editor_track::observer }; // includes of the templates whose file names the test accepts, with the location in p2..p4 and the rotation in p5 std::vector standing_templates(std::function const &Accept) const; + struct switch_drive + { + standing_template standing; + int family{-1}; + int index{0}; + }; + std::optional find_switch_drive(TTrack const &Switch, bool Wait = true); + std::string missing_drive_variant(int const Family, bool const Rightswitch, bool const Driveright) const; + bool replace_switch_drive(TTrack &Switch, int Family, bool Flip); + void apply_switch_drive_choice(TTrack &Switch); + void switch_drive_captured(TTrack const &Track); + void switch_drives_committed(std::vector const &Tracks); // places them in the active layer, as one step of the undo. returns: how many were placed std::size_t place_templates(std::vector const &Items, std::string &Error); // moves them, as one step of the undo @@ -1347,7 +1963,7 @@ class editor_mode : public application_mode, private editor_track::observer { bool open{false}; bool expand{false}; - int scope{0}; // 0: the selected switch, 1: around the camera, 2: the whole scenery + int scope{1}; // 0: the selected switch, 1: around the camera, 2: the whole scenery double reach{1500.0}; double spacing{3.5}; // m between the axes double tolerance{2.0}; // m, a marker farther than this from its point is wrong @@ -1366,12 +1982,17 @@ class editor_mode : public application_mode, private editor_track::observer }; fouling_tool m_fouling; void fouling_update(); - void fouling_place(); - void fouling_fix(); + // the markers put at the fouling points without one, or at the one only + void fouling_place(int const Only = -1); + // the markers standing wrong moved to their places, or the one found only + void fouling_fix(int const Only = -1); + // the fouling point without a marker or the marker standing wrong under the cursor: the index of the point or of the + // marker found, and whether it's a marker + std::optional> fouling_hit() const; void render_fouling_ui(); void draw_fouling_overlay() const; - // a vehicle put on the selected path, to be driven: it isn't written to the scenery files + // a trainset put on the selected path, written to the scenery on save, or only to be driven struct vehicle_tool { bool open{false}; @@ -1394,7 +2015,19 @@ 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"}; + double velocity{0.0}; + struct trainset + { + std::string head; + std::string track; + std::vector lines; + }; + std::vector trainsets; }; vehicle_tool m_vehicle; void vehicle_start(TTrack &Track, glm::dvec3 const &Point); @@ -1466,7 +2099,7 @@ class editor_mode : public application_mode, private editor_track::observer std::string status; }; signal_tool m_signal; - void signal_scan(); + bool signal_scan(bool Wait = true); signal_template const *signal_armed() const; signal_template const *signal_template_of(std::string const &File) const; bool signal_spot_at(TTrack &Track, int const Path, glm::dvec3 const &Point, glm::dvec3 const &Cursor, int const Facing, int const Side, signal_template const &Template, signal_spot &Spot) const; @@ -1499,6 +2132,7 @@ class editor_mode : public application_mode, private editor_track::observer char search[64]{}; std::vector models; bool scanned{false}; + int revision{-1}; // of the include bank the templates were taken from // parameters of a template laid along the track, with their names std::string parameters_for; std::vector> parameters; @@ -1635,8 +2269,27 @@ class editor_mode : public application_mode, private editor_track::observer std::size_t next{0}; int wait{0}; bool quitting{false}; + scripted_input input; }; selftest m_selftest; + enum class selftest_result + { + unhandled, + handled, + pause + }; + selftest_result selftest_scene(std::string const &command, std::istringstream &words); + selftest_result selftest_sleepers(std::string const &command, std::istringstream &words); + selftest_result selftest_sweep(std::string const &command, std::istringstream &words); + selftest_result selftest_track(std::string const &command, std::istringstream &words); + selftest_result selftest_switch(std::string const &command, std::istringstream &words); + selftest_result selftest_rails(std::string const &command, std::istringstream &words); + selftest_result selftest_join(std::string const &command, std::istringstream &words); + 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 std::string m_typed; @@ -1644,7 +2297,6 @@ class editor_mode : public application_mode, private editor_track::observer char const *typed_meaning() const; std::vector typed_values() const; bool typed_key(int const Key); - void typed_extend(); glm::dvec3 typed_lay(glm::dvec3 const &Mouse) const; void typed_turn(); bool m_route_tab{true}; @@ -1657,10 +2309,20 @@ 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 + std::vector model; // the ground of the terrain model (NMT) as measured; NaN where it's not known + bool model_waiting{false}; // squares of the model under the route still to come + double model_polled{0.0}; + // 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; @@ -1687,10 +2349,20 @@ class editor_mode : public application_mode, private editor_track::observer double view_centre{0.0}; float exaggeration{20.0f}; bool show_terrain{true}; + bool show_model{true}; bool show_track{true}; + bool camera_follows{true}; + // the part of the route the plot shows: as last seen and when, and as the camera last showed it + std::pair view_seen{0.0, 0.0}; + double view_seen_time{0.0}; + std::pair view_framed{0.0, 0.0}; 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}; @@ -1700,6 +2372,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; }; @@ -1712,6 +2387,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}; @@ -1719,12 +2395,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(); @@ -1733,18 +2414,32 @@ 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(); + // the ground of the terrain model under the route, as it was measured before any earthworks + void profile_sample_model(); 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(); void render_profile_earthworks(); void profile_check(); - void profile_apply(); + // Terrain: the heightmap terrain follows the tracks changed + void profile_apply(bool const Terrain = false); + // the terrain under the tracks changed by the last Edits steps of the history which changed tracks + void profile_follow_terrain(std::size_t const Edits); void profile_fit_view(); + // the camera shows the part of the route the plot shows, once the plot stops moving + void profile_follow_view(); + void profile_frame_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); @@ -1797,6 +2492,7 @@ class editor_mode : public application_mode, private editor_track::observer std::vector> m_bindings_unresolved; // lines kept as they are bool m_bindings_loaded{false}; std::vector m_infra_buffer; // states of the objects before the change in progress + std::vector> m_drive_buffer; bool m_infra_suspended{false}; void infra_load(); void infra_store(); @@ -1853,9 +2549,19 @@ class editor_mode : public application_mode, private editor_track::observer double angle{0.2}; // deg double cant{5.0}; // mm double grade{10.0}; // per mille + double model{0.5}; // m, the track over or under the terrain model (NMT) bool free_ends{false}; + bool model_check{true}; std::vector queue; std::size_t next{0}; + // the places of the tracks compared with the terrain model, in runs of one path each, once the model under them is in + std::vector places; + std::vector> runs; + std::size_t run_next{0}; + bool model_waiting{false}; + bool model_asked{false}; + double model_polled{0.0}; + bool model_unplaced{false}; std::vector issues; int filter{-1}; int selected{-1}; @@ -1866,6 +2572,8 @@ class editor_mode : public application_mode, private editor_track::observer void joints_start(); void joints_step(); void joints_check(TTrack &Track); + // the paths lying off the terrain model by more than the threshold. returns: false while the model under them is coming + bool joints_model(); void joints_focus(int const Index); bool joint_fix(joint_issue &Issue, std::vector> &States, std::vector &Changed); void joints_fix(std::vector const &Indices); @@ -1877,8 +2585,7 @@ class editor_mode : public application_mode, private editor_track::observer void render_infra_bound(); void draw_infra_overlay() const; - bool m_route_gizmo_using{false}; - glm::mat4 m_route_gizmo{1.0f}; + bool m_route_dragging{false}; float m_track_snap_radius{1.0f}; bool m_track_align_tangent{true}; bool m_track_drag_connected{true}; @@ -1997,7 +2704,7 @@ class editor_mode : public application_mode, private editor_track::observer void road_profile(std::vector &Pieces, double const *Firstgrade, double const *Lastgrade, bool const Fresh); // height of the ground under each of the points, with the roads not taken for the ground. a point with nothing under it keeps its height // Found: set for the points which have the ground under them - std::vector ground_heights(std::vector const &Points, bool const Fresh, std::vector *Found = nullptr); + std::vector ground_heights(std::vector const &Points, bool const Fresh, std::vector *Found = nullptr) const; void push_road_snapshot(editor_road::record Record); void restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo); road_tool m_roadtool; diff --git a/application/editormodeconnection.cpp b/application/editormodeconnection.cpp new file mode 100644 index 00000000..459d2480 --- /dev/null +++ b/application/editormodeconnection.cpp @@ -0,0 +1,369 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editormodeshared.h" +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "editor/editorGeometry.hpp" + +#include "scene/scene.h" +#include "simulation/simulation.h" +#include "world/Track.h" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include +#include +#include + +namespace +{ + +using geometry::cross; +using geometry::plan_of; + +std::vector spacings_of(char const *Text) +{ + std::string text{Text}; + for (auto &letter : text) + if (letter == ';') + letter = ' '; + else if (letter == ',') + letter = '.'; + std::istringstream words{text}; + std::vector result; + double value{0.0}; + while (words >> value) + result.push_back(value); + return result; +} + +bool usable_in_connection(editor_track::switch_template const &Template) +{ + return false == Template.double_slip && false == Template.twofold(); +} + +} // namespace + +char const *editor_mode::connection_name(int const Kind) +{ + switch (static_cast(Kind)) + { + case connection::kind::double_crossover: return STR_C("Double crossover"); + case connection::kind::scissors: return STR_C("Scissors crossover"); + case connection::kind::ladder: return STR_C("Switch ladder"); + default: return STR_C("Crossover"); + } +} + +editor_track::straight editor_mode::straight_near(glm::dvec3 const &Point, std::vector const &Exclude, double const Reach) const +{ + TTrack *found{nullptr}; + auto best{Reach}; + for (auto *section : simulation::Region->sections(Point, static_cast(std::max(40.0, Reach)))) + for (auto const &cell : section->m_cells) + for (auto *track : cell.m_paths) + { + if (track->eType != tt_Normal || track->m_editorremoved || track->m_paths.empty() || std::find(Exclude.begin(), Exclude.end(), track) != Exclude.end() || false == editor_track::is_straight(*track, m_straights.tolerance)) + continue; + auto const &path{track->m_paths.front()}; + auto const a{plan_of(path.points[segment_data::point::start])}; + auto const segment{plan_of(path.points[segment_data::point::end]) - a}; + auto const t{std::clamp(glm::dot(plan_of(Point) - a, segment) / std::max(1e-9, glm::dot(segment, segment)), 0.0, 1.0)}; + auto const distance{glm::distance(plan_of(Point), a + segment * t)}; + if (distance < best) + { + best = distance; + found = track; + } + } + return found != nullptr ? editor_track::find_straight(*found, m_straights.tolerance) : editor_track::straight{}; +} + +editor_track::straight editor_mode::straight_through(glm::dvec3 const &Point, glm::dvec2 const &Direction) const +{ + for (auto *section : simulation::Region->sections(Point, 40.f)) + for (auto const &cell : section->m_cells) + for (auto *track : cell.m_paths) + { + if (track->eType != tt_Normal || track->m_editorremoved || track->m_paths.empty() || false == editor_track::is_straight(*track, m_straights.tolerance)) + continue; + auto const &path{track->m_paths.front()}; + auto const a{plan_of(path.points[segment_data::point::start])}; + auto const segment{plan_of(path.points[segment_data::point::end]) - a}; + auto const length{glm::length(segment)}; + if (length < 1e-6 || std::abs(cross(segment / length, Direction)) > 1e-3 || std::abs(cross(segment / length, plan_of(Point) - a)) > 0.02) + continue; + auto const t{glm::dot(plan_of(Point) - a, segment) / (length * length)}; + if (t >= -1e-6 && t <= 1.0 + 1e-6) + return editor_track::find_straight(*track, m_straights.tolerance); + } + return {}; +} + +bool editor_mode::plan_connection(connection::request &Request, connection::result &Result) const +{ + auto const &tool{m_switch}; + Result = {}; + if (false == tool.placing || tool.curved || tool.connection < 0 || tool.armed < 0 || tool.armed >= static_cast(tool.templates.size()) || tool.line.tracks.empty()) + return false; + auto const &line{tool.line}; + Request = {}; + Request.type = static_cast(tool.connection); + Request.from = connection::of(line); + 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; + Request.gap = tool.connection_gap; + Request.together = tool.connection_together; + Request.stub = tool.connection_stub; + Request.spacings = spacings_of(tool.connection_spacings); + Request.along = tool.along; + glm::dvec2 const offset{plan_of(tool.mouse - tool.point)}; + Request.forward = glm::dot(offset, line.direction) >= 0.0 ? 1 : -1; + glm::dvec2 const travel{line.direction * static_cast(Request.forward)}; + Request.side = cross(travel, offset) >= 0.0 ? 1 : -1; + std::vector const exclude(line.tracks.begin(), line.tracks.end()); + auto const other{straight_near(tool.mouse, exclude, 8.0)}; + if (false == other.tracks.empty()) + { + auto const target{connection::of(other)}; + if (Request.type != connection::kind::ladder) + { + Request.to.push_back(target); + Request.target = target.along(tool.mouse); + } + else if (std::abs(cross(target.direction, line.direction)) < 1e-3) + { + auto const side{cross(travel, plan_of(target.point(target.along(tool.point)) - tool.point)) > 0.0 ? 1.0 : -1.0}; + auto const lateral = [&](glm::dvec3 const &Point) { return side * cross(travel, plan_of(Point - tool.point)); }; + auto const outermost{lateral(target.point(target.along(tool.point)))}; + std::map> fan; + auto const better = [](double const One, double const Two) { return (One < 0.0) != (Two < 0.0) ? One >= 0.0 : std::abs(One) < std::abs(Two); }; + auto const centre{(tool.point + tool.mouse) * 0.5}; + for (auto *section : simulation::Region->sections(centre, static_cast(glm::length(offset) * 0.5 + 200.0))) + for (auto const &cell : section->m_cells) + for (auto *track : cell.m_paths) + { + if (track->eType != tt_Normal || track->m_editorremoved || track->m_paths.empty() || std::find(exclude.begin(), exclude.end(), track) != exclude.end() || false == editor_track::is_straight(*track, m_straights.tolerance)) + continue; + 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 direction{plan_of(b - a)}; + if (glm::length(direction) < 1e-6 || std::abs(cross(glm::normalize(direction), line.direction)) > 1e-3) + continue; + auto const distance{lateral(a)}; + auto const along1{glm::dot(travel, plan_of(a - tool.point))}; + auto const along2{glm::dot(travel, plan_of(b - tool.point))}; + if (distance < 1.0 || distance > outermost + 0.05 || std::max(along1, along2) <= 0.0) + continue; + auto const nearer{std::min(along1, along2)}; + auto &entry{fan[std::lround(distance * 20.0)]}; + if (entry.second == nullptr || better(nearer, entry.first)) + entry = {nearer, track}; + } + for (auto const &entry : fan) + Request.to.push_back(connection::of(editor_track::find_straight(*entry.second.second, m_straights.tolerance))); + } + } + Result = connection::plan(Request); + return Result.valid; +} + +std::string editor_mode::connection_readout(connection::result const &Plan) const +{ + if (false == Plan.valid) + return Plan.error; + auto text{format(STR_C("%s: %d switches, spacing %.2f m"), connection_name(m_switch.connection), static_cast(Plan.switches.size()), Plan.spacing)}; + if (false == Plan.straights.empty()) + { + text += STR_C(", straight inserts"); + for (auto const length : Plan.straights) + text += format(" %.2f", length); + text += " m"; + } + if (Plan.points > 0.0) + text += format(STR_C(", mathematical points %.2f m apart"), Plan.points); + if (Plan.radius > 0.0) + text += format(STR_C(", aligning curve R %.0f m"), Plan.radius); + if (Plan.crossing && Plan.angle > 1e-6) + text += format(STR_C(", crossing 1:%.2f"), 1.0 / std::tan(Plan.angle)); + if (false == Plan.fan.empty()) + { + text += STR_C(", tracks at"); + for (auto const offset : Plan.fan) + text += format(" %.2f", offset); + text += " m"; + } + if (false == Plan.warning.empty()) + text += "\n" + Plan.warning; + return text; +} + +void editor_mode::insert_connection() +{ + auto &tool{m_switch}; + connection::request request; + connection::result plan; + auto const planned{plan_connection(request, plan)}; + tool.placing = false; + if (false == planned) + { + tool.status = plan.error.empty() ? std::string{STR_C("Drag onto the other track")} : plan.error; + ui()->set_status(tool.status, true); + return; + } + std::vector> states; + std::vector created; + std::vector removed; + auto *style{tool.line.tracks.front()}; + auto const fail = [&](std::string const &Reason) { + 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}); + } + if (auto before{switch_store::take_changes()}) + switch_store::restore(std::move(*before)); + if (auto before{alignment_store::take_changes()}) + alignment_store::restore(std::move(*before)); + straight_refresh(); + tool.status = Reason; + ui()->set_status(Reason, true); + }; + std::vector switches; + for (auto const &entry : plan.switches) + { + auto const &shape{entry.second ? request.second : request.first}; + auto const name{switch_name_for_new()}; + auto const before{created.size()}; + auto const start{entry.paths[0].points[segment_data::point::start]}; + auto const end{entry.paths[0].points[segment_data::point::end]}; + if (entry.line >= 0) + { + auto const &target{entry.line == 0 ? request.from : request.to[entry.line - 1]}; + auto const found{straight_through((start + end) * 0.5, target.direction)}; + if (found.tracks.empty()) + { + fail(STR_C("A switch of the connection goes where its track isn't a plain straight")); + return; + } + auto const forward{glm::dot(found.direction, plan_of(end - start)) > 0.0 ? 1 : -1}; + if (false == place_switch_on_straight(found, shape, nullptr, found.along(start), forward, entry.side, false, states, created, removed, name)) + { + fail(tool.status); + return; + } + } + else + { + auto const made{create_switch_set(shape, entry.paths, *style, name)}; + if (made.empty()) + { + fail(tool.status); + return; + } + created.insert(created.end(), made.begin(), made.end()); + } + auto const made{std::find_if(created.begin() + static_cast(before), created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; })}; + if (made == created.end() || glm::distance((*made)->m_paths.front().points[segment_data::point::start], start) > 0.01) + { + fail(STR_C("A switch of the connection doesn't fit where it was planned, its straight is too short")); + return; + } + switches.push_back(*made); + advance_switch_name(); + } + std::vector inserts; + for (auto const &insert : plan.inserts) + if (auto *track{editor_track::create_path(*style, insert)}) + inserts.push_back(track); + editor_track::commit(inserts); + digitize_drawn(inserts); + created.insert(created.end(), inserts.begin(), inserts.end()); + if (plan.crossing) + { + auto const first{glm::normalize(plan_of(plan.ends[1] - plan.ends[0]))}; + auto second{glm::normalize(plan_of(plan.ends[3] - plan.ends[2]))}; + if (glm::dot(first, second) < 0.0) + second = -second; + auto const parts{build_double_slip(plan.ends[0], plan.ends[1], plan.ends[2], plan.ends[3], plan_of(plan.centre), plan.centre.y, first, second, plan.angle, editor_track::slip_curve{}, *style, "Skrz")}; + editor_track::commit(parts); + created.insert(created.end(), parts.begin(), parts.end()); + } + record_connection(request.type, switches, plan, request.first); + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + first_step_done(first_step::turnout); + m_track_point = {}; + 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("%s laid, switches %s"), connection_name(tool.connection), names.c_str()) + "\n" + connection_readout(plan); + ui()->set_status(tool.status, false); +} + +void editor_mode::render_connection_options() +{ + auto &tool{m_switch}; + if (tool.connection < 0) + return; + 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]); }; + 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; + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + if (usable_in_connection(tool.templates[i]) && ImGui::Selectable((switch_template_label(tool.templates[i]) + "##connectionsecond" + std::to_string(i)).c_str(), tool.connection_second == i)) + tool.connection_second = i; + 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"); + 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)) + { + 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"); + } + if (tool.connection == static_cast(connection::kind::ladder)) + { + 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::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 298724e7..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 @@ -677,13 +731,7 @@ void editor_mode::gauge_focus(int const Index) look = glm::length(look) > 1e-6 ? glm::normalize(look) : glm::dvec3(0.0, 0.0, 1.0); if (glm::dot(Camera.Pos - hit.point, look) > 0.0) look = -look; - m_focus_start_pos = Camera.Pos; - m_focus_start_angle = Camera.Angle; - m_focus_target_pos = hit.point - look * gauge_view_distance; - m_focus_target_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); - m_focus_active = true; - m_focus_time = 0.0; - m_focus_duration = 0.6; + fly_to(hit.point, look, gauge_view_distance); } void editor_mode::gauge_spot(gauge_hit const &Hit) @@ -721,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"))) @@ -735,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")); @@ -766,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(); @@ -784,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); } @@ -794,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)) @@ -805,6 +859,7 @@ bool editor_mode::render_gauge_choice(char const *Label, int &Index, bool const } ImGui::EndCombo(); } + ImGui::PopID(); return changed; } @@ -816,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 e805c545..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; } @@ -658,15 +663,18 @@ std::optional read_binding(std::string const &Text, scene::layer { double const tolerance{0.05}; std::istringstream line{Text}; - std::string kindname, key; - int group{0}, turns{0}, count{0}; + std::string kindname; + std::string key; + int group{0}; + int turns{0}; + int count{0}; glm::dvec3 location; line >> kindname >> group >> location.x >> location.y >> location.z >> key >> turns >> count; if (line.fail() || count < 1 || count > 4) 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)) @@ -707,7 +715,8 @@ std::optional read_binding(std::string const &Text, scene::layer for (int i = 0; i < count && found; ++i) { int path{0}; - glm::dvec3 start, end; + glm::dvec3 start; + glm::dvec3 end; line >> path >> start.x >> start.y >> start.z >> end.x >> end.y >> end.z; start = Context.to_world(start); end = Context.to_world(end); @@ -865,7 +874,10 @@ void editor_mode::infra_recognize() state.error = (state.scope == 2 ? STR_C("Open the vertical profile of a route first") : state.scope == 3 ? STR_C("The scenery has no paths the editor can handle") : STR_C("Select a path first")); return; } - state.read = state.overruled = state.unread = state.elsewhere = 0; + state.read = 0; + state.overruled = 0; + state.unread = 0; + state.elsewhere = 0; track_index index; std::unordered_set names; std::unordered_set const scope(state.tracks.begin(), state.tracks.end()); @@ -956,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()) { @@ -974,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) { @@ -984,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()) { @@ -995,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()) { @@ -1006,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()) { @@ -1016,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())); @@ -1128,6 +1146,7 @@ void editor_mode::infra_buffer(infra::binding const &Binding, bool const Refresh void editor_mode::track_captured(TTrack const &Track) { sweeps_captured(Track); + switch_drive_captured(Track); if (m_infra_suspended || false == m_infra.follow) return; infra_load(); @@ -1166,6 +1185,7 @@ void editor_mode::track_captured(TTrack const &Track) void editor_mode::tracks_committed(std::vector const &Tracks) { sweeps_committed(Tracks); + switch_drives_committed(Tracks); if (m_infra_suspended || false == m_infra.follow) return; infra_load(); @@ -1408,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"))) @@ -1478,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); @@ -1516,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())) @@ -1547,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..856623cc 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" @@ -25,6 +26,8 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include +#include #include #include @@ -38,6 +41,10 @@ double const kLinked{0.02}; // m, ends this close are joined by the simulation double const kOverlayRange{3000.0}; double const kGauge{1.5}; // m between the rails, for the cant int const kRailCategory{1}; +double const kModelStep{5.0}; // m between the places of the tracks compared with the terrain model +std::size_t const kModelRun{200}; // places of a path whose model is asked for at once +double const kModelPoll{0.25}; // s between the looks whether the model is in +double const kUnfitted{30.0}; // m; a scenery whose tracks lie this far from the model as a whole isn't fitted to it using kind = editor_mode::joint_issue::kind; @@ -97,6 +104,7 @@ char const *kind_name(kind const Kind) case kind::kink: return "kink"; case kind::cant: return "cant jump"; case kind::grade: return "grade break"; + case kind::model: return "off the terrain model"; default: return "free end"; } } @@ -107,9 +115,10 @@ 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); + case kind::model: return format("%+.2f m", Issue.value); default: return "-"; } } @@ -123,6 +132,7 @@ ImU32 kind_colour(kind const Kind) case kind::kink: return IM_COL32(255, 190, 60, 225); case kind::cant: return IM_COL32(200, 120, 255, 225); case kind::grade: return IM_COL32(90, 200, 255, 220); + case kind::model: return IM_COL32(70, 200, 190, 235); default: return IM_COL32(200, 200, 200, 200); } } @@ -131,7 +141,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 @@ -155,6 +165,114 @@ void editor_mode::joints_start() state.queue.push_back(track); } state.history = m_history.size(); + state.places.clear(); + state.runs.clear(); + state.run_next = 0; + state.model_waiting = false; + state.model_asked = false; + state.model_polled = 0.0; + state.model_unplaced = false; + if (false == state.model_check || state.queue.empty()) + return; + nmt_locate(); + if (false == m_nmt.placed()) + { + state.model_unplaced = true; + return; + } + std::set const queued(state.queue.begin(), state.queue.end()); + for (auto const &place : nmt_track_places(kModelStep)) + if (place.shapes && queued.count(place.track) > 0) + { + if (state.places.empty() || state.places.back().track != place.track || state.places.back().path != place.path || state.places.size() - state.runs.back().first >= kModelRun) + state.runs.emplace_back(state.places.size(), state.places.size()); + state.places.push_back(place); + state.runs.back().second = state.places.size(); + } + state.model_waiting = false == state.places.empty(); +} + +bool editor_mode::joints_model() +{ + auto &state{m_joints}; + auto const now{ImGui::GetTime()}; + if (state.model_asked && now - state.model_polled < kModelPoll) + return false; + state.model_polled = now; + auto const box = [&](std::pair const &Run, glm::dvec2 &Min, glm::dvec2 &Max) { + Min = glm::dvec2{std::numeric_limits::max()}; + Max = glm::dvec2{std::numeric_limits::lowest()}; + for (auto i = Run.first; i < Run.second; ++i) + { + glm::dvec2 const at{state.places[i].position.x, state.places[i].position.z}; + Min = glm::min(Min, at - 2.0); + Max = glm::max(Max, at + 2.0); + } + }; + glm::dvec2 low, high; + // all of it asked for once, then the runs looked over in turn as their squares come + if (false == state.model_asked) + { + for (auto const &run : state.runs) + { + box(run, low, high); + m_nmt.request(low, high); + } + state.model_asked = true; + } + while (state.run_next < state.runs.size()) + { + box(state.runs[state.run_next], low, high); + if (false == m_nmt.request(low, high)) + return false; + ++state.run_next; + } + std::vector differences(state.places.size(), std::numeric_limits::quiet_NaN()); + std::vector known; + for (std::size_t i = 0; i < state.places.size(); ++i) + if (double height; m_nmt.height(state.places[i].position.x, state.places[i].position.z, height)) + { + differences[i] = state.places[i].crown - (height + m_nmt_state.offset); + known.push_back(differences[i]); + } + auto shift{0.0}; + if (false == m_nmt_state.fitted && false == known.empty()) + { + std::nth_element(known.begin(), known.begin() + known.size() / 2, known.end()); + if (std::abs(known[known.size() / 2]) > kUnfitted) + shift = known[known.size() / 2]; + } + // the place of each path the farthest from the model + std::map, std::size_t> worst; + for (std::size_t i = 0; i < state.places.size(); ++i) + { + if (false == std::isfinite(differences[i])) + continue; + differences[i] -= shift; + if (std::abs(differences[i]) <= state.model) + continue; + auto const key{std::make_pair(state.places[i].track, state.places[i].path)}; + auto const found{worst.find(key)}; + if (found == worst.end()) + worst.emplace(key, i); + else if (std::abs(differences[i]) > std::abs(differences[found->second])) + found->second = i; + } + for (auto const &entry : worst) + { + auto const &place{state.places[entry.second]}; + joint_issue issue; + issue.type = kind::model; + issue.track = place.track; + issue.point = {place.path, editor_track::point_kind::start}; + issue.position = place.position; + issue.value = differences[entry.second]; + state.issues.push_back(issue); + } + state.places.clear(); + state.runs.clear(); + state.model_waiting = false; + return true; } void editor_mode::joints_check(TTrack &Track) @@ -251,7 +369,7 @@ void editor_mode::joints_check(TTrack &Track) void editor_mode::joints_step() { auto &state{m_joints}; - if (state.next >= state.queue.size()) + if (state.next >= state.queue.size() && false == state.model_waiting) return; auto const deadline{std::chrono::steady_clock::now() + std::chrono::milliseconds(15)}; while (state.next < state.queue.size() && std::chrono::steady_clock::now() < deadline) @@ -263,17 +381,23 @@ void editor_mode::joints_step() } if (state.next < state.queue.size()) return; + if (state.model_waiting && false == joints_model()) + return; std::stable_sort(state.issues.begin(), state.issues.end(), [](joint_issue const &A, joint_issue const &B) { if (A.type != B.type) return A.type < B.type; - return A.value > B.value; + return std::abs(A.value) > std::abs(B.value); }); - std::array counts{}; + std::array counts{}; for (auto const &issue : state.issues) ++counts[static_cast(issue.type)]; state.status = format(STR_C("%zu paths checked: %d gaps, %d steps, %d kinks, %d cant jumps, %d grade breaks"), state.queue.size(), counts[0], counts[1], counts[2], counts[3], counts[4]); + if (state.model_check && state.model_unplaced) + state.status += STR_C(", the terrain model left out: the scenery isn't placed on the map (the orthophoto settings)"); + else if (state.model_check) + state.status += format(STR_C(", %d paths off the terrain model"), counts[5]); if (state.free_ends) - state.status += format(STR_C(", %d free ends"), counts[5]); + state.status += format(STR_C(", %d free ends"), counts[6]); WriteLog("Editor: joints check - " + state.status, logtype::generic); } @@ -295,7 +419,7 @@ void editor_mode::joints_focus(int const Index) bool editor_mode::joint_fix(joint_issue &Issue, std::vector> &States, std::vector &Changed) { - if (Issue.fixed || Issue.type == kind::open || Issue.track == nullptr || Issue.other == nullptr || Issue.track->m_editorremoved || Issue.other->m_editorremoved) + if (Issue.fixed || Issue.type == kind::open || Issue.type == kind::model || Issue.track == nullptr || Issue.other == nullptr || Issue.track->m_editorremoved || Issue.other->m_editorremoved) return false; auto const movable = [](TTrack const *Track) { std::string reason; @@ -387,44 +511,70 @@ 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); - bool const running{state.next < state.queue.size()}; + 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() || state.model_waiting}; if (running) { - auto const progress{static_cast(state.next) / static_cast(state.queue.size())}; - ImGui::ProgressBar(progress, ImVec2(-70.0f, 0.0f), format(STR_C("Checking %zu / %zu"), state.next, state.queue.size()).c_str()); + if (state.next < state.queue.size()) + { + auto const progress{static_cast(state.next) / static_cast(state.queue.size())}; + ImGui::ProgressBar(progress, ImVec2(-70.0f, 0.0f), format(STR_C("Checking %zu / %zu"), state.next, state.queue.size()).c_str()); + } + else + { + auto const progress{static_cast(state.run_next) / static_cast(std::max(1, state.runs.size()))}; + ImGui::ProgressBar(progress, ImVec2(-70.0f, 0.0f), format(STR_C("Terrain model %zu / %zu"), state.run_next, state.runs.size()).c_str()); + } ImGui::SameLine(); if (ImGui::SmallButton(STR_C("Cancel"))) + { state.queue.resize(state.next); + state.model_waiting = false; + } } - 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"); + limit("Off the terrain model (m)", "##jointmodel", state.model, "%.2f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The track lies higher or lower than the terrain model (NMT) says the ground is, by more than this.\nEach path is listed once, at its place the farthest off; bridges and tunnels are left out")); + ImGui::EndTable(); + } + toggle_chips("jointflags", {{STR_C("Free ends too"), &state.free_ends}, {STR_C("Against the terrain model"), &state.model_check}}); 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")); + state.model = std::max(0.05, state.model); + ImGui::TextDisabled("%s", STR_C("Change the thresholds, then Check again")); ImGui::TreePop(); } if (false == state.status.empty()) @@ -435,7 +585,8 @@ void editor_mode::render_joints_body() ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", STR_C("The scenery changed since the check")); int filter{state.filter + 1}; ImGui::SetNextItemWidth(150.0f); - auto const filters{std::string{STR_C("All")} + '\0' + STR_C("gap") + '\0' + STR_C("step") + '\0' + STR_C("kink") + '\0' + STR_C("cant jump") + '\0' + STR_C("grade break") + '\0' + STR_C("free end") + '\0' + '\0'}; + auto const filters{std::string{STR_C("All")} + '\0' + STR_C("gap") + '\0' + STR_C("step") + '\0' + STR_C("kink") + '\0' + STR_C("cant jump") + '\0' + STR_C("grade break") + '\0' + STR_C("off the terrain model") + '\0' + + STR_C("free end") + '\0' + '\0'}; if (ImGui::Combo("##jointfilter", &filter, filters.c_str())) state.filter = filter - 1; std::vector listed; @@ -446,7 +597,7 @@ void editor_mode::render_joints_body() if (state.filter >= 0 && static_cast(issue.type) != state.filter) continue; listed.push_back(i); - if (false == issue.fixed && issue.type != kind::open) + if (false == issue.fixed && issue.type != kind::open && issue.type != kind::model) fixable.push_back(i); } if (false == fixable.empty()) @@ -493,7 +644,7 @@ void editor_mode::render_joints_body() { auto &issue{state.issues[state.selected]}; ImGui::Text("%s %s: %s / %s", STR_C(kind_name(issue.type)), value_text(issue).c_str(), name_of(issue.track).c_str(), name_of(issue.other).c_str()); - if (false == issue.fixed && issue.type != kind::open && ImGui::Button(STR_C("Fix this one"))) + if (false == issue.fixed && issue.type != kind::open && issue.type != kind::model && ImGui::Button(STR_C("Fix this one"))) joints_fix({state.selected}); } } @@ -509,7 +660,7 @@ void editor_mode::draw_joints_overlay() const for (int i = 0; i < static_cast(state.issues.size()); ++i) { auto const &issue{state.issues[i]}; - if (issue.fixed || (state.filter >= 0 && static_cast(issue.type) != state.filter) || glm::distance(issue.position, camera) > kOverlayRange) + if (issue.fixed || (state.filter >= 0 && static_cast(issue.type) != state.filter) || (false == Global.EditorOrtho && glm::distance(issue.position, camera) > kOverlayRange)) continue; ImVec2 screen; if (false == projection.project(issue.position, screen)) diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp new file mode 100644 index 00000000..d66b7608 --- /dev/null +++ b/application/editormodelay.cpp @@ -0,0 +1,1460 @@ +/* +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: +*/ + + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "application/editormodeshared.h" +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" + +#include "utilities/Globals.h" +#include "rendering/renderer.h" +#include "world/Track.h" +#include "world/TrackFeatures.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include "imgui/ImGuizmo.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "application/editormodetrackshared.h" + +using namespace track_mode; + +void editor_mode::update_build_tools() +{ + glm::dvec3 const ground{cursor_ground()}; + auto const &io{ImGui::GetIO()}; + glm::vec2 const cursor{io.MousePos.x, io.MousePos.y}; + m_straights.detour_mouse = ground; + auto const joining = [&](editor_track::snap_target const &Target) { + if (Target.track == m_join_track && Target.point == m_join_point) + return; + m_join_track = Target.track; + m_join_point = Target.point; + m_join_radius = 0.0; + m_join_variant = 0; + }; + bool const pointing{m_cursor_override.has_value() || ImGui::IsMousePosValid()}; + if (m_extend.active) + { + if (pointing) + m_extend.mouse = cursor_terrain(m_extend.point.y); + m_extend.snap = m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; + if (m_extend.snap.track == nullptr) + m_extend.mouse.y = bed_height(m_extend.mouse, plan_of(m_extend.mouse - m_extend.point), m_extend.track); + joining(m_extend.snap); + } + if (m_switch.placing) + m_switch.mouse = cursor_level(m_switch.point.y); + auto &lay{m_lay}; + lay.preview.clear(); + lay.preview_error.clear(); + lay.preview_length = 0.0; + if (false == lay.active) + return; + // geometric ray, same as extend: Mouse_Position() is a delayed depth pick and lags the cursor + double const height{false == lay.points.empty() ? lay.points.back().y : lay.start.track != nullptr ? lay.start.position.y : ground.y}; + if (pointing) + lay.mouse = typed_lay(cursor_terrain(height)); + lay.skew.clear(); + if (pointing && ImGui::GetIO().KeyAlt && m_typed.empty() && false == lay.points.empty()) + lay.mouse = lay_angle_snap(lay.mouse, lay.skew); + lay.mouse_snap = m_typed.empty() ? snap_free_end(lay.mouse, cursor, nullptr, kRailCategory, {}) : editor_track::find_free_end(nullptr, kRailCategory, lay.mouse, kLaySnapRadius, {}); + if (lay.mouse_snap.track == nullptr) + lay.mouse = lay_bed(lay.mouse); + if (false == m_extend.active) + joining(lay.points.empty() ? editor_track::snap_target{} : lay.mouse_snap); + if (lay.points.empty()) + return; + auto points{lay.points}; + auto end{lay.mouse_snap}; + if (end.track != nullptr && glm::distance(end.position, points.front()) > 1.0) + points.push_back(end.position); + else + { + end = {}; + if (plan_distance(lay.mouse, points.back()) > 1.0) + points.push_back(lay.mouse); + } + lay.preview = lay_pieces(points, end, lay.preview_length, lay.preview_error); +} + +bool editor_mode::free_end(TTrack &Track, int const Path, bool const Atend, extend_tool &Tool) +{ + editor_track::point_ref const point{Path, Atend ? editor_track::point_kind::end : editor_track::point_kind::start}; + if (Path < 0 || Path >= static_cast(Track.m_paths.size()) || editor_track::is_connected(Track, point)) + return false; + auto const &path{Track.m_paths[Path]}; + auto const &start{path.points[segment_data::point::start]}; + auto const &end{path.points[segment_data::point::end]}; + auto const &control1{path.points[segment_data::point::control1]}; + auto const &control2{path.points[segment_data::point::control2]}; + glm::dvec3 tangent; + if (Atend) + tangent = control2 != glm::dvec3{} ? -control2 : end - (start + control1); + else + tangent = -(control1 != glm::dvec3{} ? control1 : (end + control2) - start); + glm::dvec2 const planar{tangent.x, tangent.z}; + if (glm::length(planar) < 1e-6) + return false; + Tool.path = Path; + Tool.atend = Atend; + Tool.track = &Track; + Tool.point = editor_track::point_position(Track, point); + Tool.direction = glm::normalize(planar); + Tool.grade = tangent.y / glm::length(planar); + Tool.mouse = Tool.point; + return true; +} + +bool editor_mode::chosen_free_end(extend_tool &Tool) +{ + auto *track{selected_track()}; + if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || false == editor_track::is_supported(*track)) + return false; + if (m_track_point.valid() && editor_track::is_end(m_track_point.kind) && free_end(*track, m_track_point.path, m_track_point.kind == editor_track::point_kind::end, Tool)) + return true; + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + for (auto const atend : {true, false}) + if (free_end(*track, i, atend, Tool)) + return true; + return false; +} + +bool editor_mode::start_extend(editor_track::snap_target const &End) +{ + auto *track{End.track}; + if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || false == editor_track::is_supported(*track)) + return false; + extend_tool tool; + if (false == free_end(*track, End.point.path, End.point.kind == editor_track::point_kind::end, tool)) + return false; + auto const &io{ImGui::GetIO()}; + m_extend = tool; + m_extend.origin = End; + m_extend.pressed = {io.MousePos.x, io.MousePos.y}; + m_extend.active = true; + if (track->eType == tt_Switch && End.point.path == 1 && End.point.kind == editor_track::point_kind::end) + m_extend.line = editor_track::find_straight(*track, m_straights.tolerance).tracks; + return true; +} + +bool editor_mode::extend_snap(editor_track::snap_target &Target) const +{ + auto const &tool{m_extend}; + if (false == tool.active || tool.track == nullptr) + return false; + Target = tool.snap; + return Target.track != nullptr && glm::distance(Target.position, tool.point) > 0.5; +} + +glm::dvec3 editor_mode::cursor_ground() const +{ + glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; + if (GfxRenderer->Mouse_Hit()) + return ground; + screen_projection const projection; + glm::dvec3 level; + if (projection.on_level(ImGui::GetIO().MousePos, 0.0, level)) + return level; + return {ground.x, 0.0, ground.z}; +} + +glm::dvec3 editor_mode::cursor_level(double const Height) const +{ + if (m_cursor_override.has_value()) + return glm::dvec3{m_cursor_override->x, Height, m_cursor_override->z}; + glm::dvec3 const ground{cursor_ground()}; + screen_projection const projection; + glm::dvec3 level; + if (false == projection.on_level(ImGui::GetIO().MousePos, Height, level) || (false == Global.editor_tracks && glm::distance(level, glm::dvec3{Global.pCamera.Pos}) > static_cast(kMaxPlacementDistance))) + return ground; + return level; +} + +editor_track::snap_target editor_mode::snap_free_end(glm::dvec3 const &Near, glm::vec2 const &Screen, TTrack const *Self, int const Category, std::vector const &Exclude) const +{ + screen_projection const projection; + auto const reach{Global.EditorOrtho ? std::max(kJointRange, static_cast(Global.EditorOrthoExtent) * 2.0) : kJointRange}; + editor_track::snap_target result; + auto best{kSnapPixels * kSnapPixels}; + for (auto const &candidate : editor_track::free_ends(Self, Category, glm::dvec3{Global.pCamera.Pos}, reach, Exclude)) + { + ImVec2 screen; + if (false == projection.project(candidate.position, screen)) + continue; + auto const distance{(screen.x - Screen.x) * (screen.x - Screen.x) + (screen.y - Screen.y) * (screen.y - Screen.y)}; + if (distance < best) + { + best = distance; + result = candidate; + } + } + if (result.track == nullptr && m_track_snap_radius > 0.0f) + result = editor_track::find_free_end(Self, Category, Near, m_track_snap_radius, Exclude); + return result; +} + +void editor_mode::draw_free_ends(TTrack const *Self, int const Category, std::vector const &Exclude) const +{ + 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) : std::min(kJointRange, kDotsRange)}; + for (auto const &candidate : editor_track::free_ends(Self, Category, glm::dvec3{Global.pCamera.Pos}, reach, Exclude)) + { + ImVec2 screen; + if (projection.project(candidate.position, screen) && screen.x >= 0.0f && screen.y >= 0.0f && screen.x <= display.x && screen.y <= display.y) + drawlist->AddCircle(screen, 7.0f, IM_COL32(255, 60, 255, 170), 12, 1.5f); + } +} + +bool editor_mode::switch_reach(glm::dvec3 const &Ground, glm::dvec3 &Point) +{ + auto *selected{selected_track()}; + if (m_track_tab != track_tab::turnout || m_switch.armed < 0 || selected == nullptr || selected->eType != tt_Normal || selected->m_paths.empty()) + return false; + auto const &line{current_straight()}; + if (false == line.tracks.empty()) + { + auto const along{std::clamp(line.along(Ground), 0.0, line.length)}; + Point = line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * along; + } + else + { + Point = editor_track::point_at(*selected, editor_track::nearest_parameter(*selected, Ground)); + } + return plan_distance(Point, Ground) <= kSwitchReach; +} + +void editor_mode::handle_drag_start(glm::dvec3 const &Anchor) +{ + auto const &io{ImGui::GetIO()}; + m_handle_drag = {}; + m_handle_drag.active = true; + m_handle_drag.from = {io.MousePos.x, io.MousePos.y}; + m_handle_drag.height = Anchor.y; + m_handle_drag.position = Anchor; + screen_projection const projection; + glm::dvec3 level; + if (projection.on_level(io.MousePos, Anchor.y, level)) + m_handle_drag.offset = {Anchor.x - level.x, 0.0, Anchor.z - level.z}; +} + +bool editor_mode::handle_drag_step(glm::dvec3 &Position) +{ + auto &drag{m_handle_drag}; + auto const &io{ImGui::GetIO()}; + if (false == drag.active) + return false; + if (false == io.MouseDown[0]) + { + drag = {}; + return false; + } + glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; + if (false == drag.moved && glm::length(mouse - drag.from) < kDragPixels) + return false; + drag.moved = true; + screen_projection const projection; + glm::dvec3 level; + if (projection.on_level(io.MousePos, drag.height, level) && (Global.editor_tracks || glm::distance(level, glm::dvec3{Global.pCamera.Pos}) <= static_cast(kMaxPlacementDistance))) + drag.position = level + drag.offset; + Position = drag.position; + return true; +} + +void editor_mode::point_drag_start(editor_track::point_ref const &Point) +{ + auto *track{selected_track()}; + m_track_point = Point; + m_point_drag = {}; + if (track == nullptr || false == Point.valid() || Point.path >= static_cast(track->m_paths.size())) + return; + auto const &io{ImGui::GetIO()}; + m_point_drag.active = true; + m_point_drag.from = {io.MousePos.x, io.MousePos.y}; + m_point_drag.height = editor_track::point_position(*track, Point).y; +} + +void editor_mode::point_drag_update() +{ + auto &drag{m_point_drag}; + if (false == drag.active) + return; + auto const &io{ImGui::GetIO()}; + if (false == io.MouseDown[0]) + { + point_drag_finish(); + return; + } + auto *track{selected_track()}; + if (track == nullptr || false == m_track_point.valid() || m_track_point.path >= static_cast(track->m_paths.size())) + { + drag = {}; + return; + } + glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; + if (false == drag.moved) + { + if (glm::length(mouse - drag.from) < kDragPixels) + return; + std::string reason; + if (false == editor_track::can_edit_geometry(*track, reason)) + { + ui()->set_status(reason, true); + drag = {}; + return; + } + drag.moved = true; + m_track_drag = {track}; + m_track_drag_points.clear(); + m_track_joints.clear(); + m_track_snap = {}; + if (editor_track::is_end(m_track_point.kind) && m_track_drag_connected) + { + for (auto const &connection : editor_track::connected_points(*track, editor_track::point_position(*track, m_track_point))) + { + std::string neighbourreason; + if (false == editor_track::can_edit_geometry(*connection.first, neighbourreason)) + continue; + m_track_drag_points.emplace_back(connection); + if (std::find(m_track_drag.begin(), m_track_drag.end(), connection.first) == m_track_drag.end()) + m_track_drag.emplace_back(connection.first); + } + } + std::vector> states; + for (auto *dragged : m_track_drag) + states.emplace_back(dragged, editor_track::capture(*dragged)); + push_track_snapshot(std::move(states)); + records_watch_begin(); + } + screen_projection const projection; + glm::dvec3 position; + if (false == projection.on_level(io.MousePos, drag.height, position) || (false == Global.editor_tracks && glm::distance(position, glm::dvec3{Global.pCamera.Pos}) > static_cast(kMaxPlacementDistance))) + return; + m_track_snap = {}; + if (editor_track::is_end(m_track_point.kind) && m_track_drag_points.empty()) + { + std::vector const exclude(m_track_drag.begin(), m_track_drag.end()); + m_track_snap = snap_free_end(position, mouse, track, track->iCategoryFlag & 15, exclude); + if (m_track_snap.track != nullptr) + position = m_track_snap.position; + } + editor_track::move_point(*track, m_track_point, position); + for (auto const &connection : m_track_drag_points) + editor_track::move_point(*connection.first, connection.second, position); + m_track_dirty = true; + commit_track_drag(false); +} + +void editor_mode::point_drag_finish() +{ + auto const drag{m_point_drag}; + m_point_drag = {}; + if (false == drag.active || false == drag.moved) + return; + if (auto *track{selected_track()}; track != nullptr && m_track_snap.track != nullptr && m_track_point.valid()) + { + editor_track::snap_point(*track, m_track_point, m_track_snap, m_track_align_tangent); + m_track_dirty = true; + } + m_track_snap = {}; + commit_track_drag(true); + records_watch_end(); + m_track_drag.clear(); + m_track_drag_points.clear(); + m_track_joints.clear(); +} + +std::vector editor_mode::extend_pieces() const +{ + std::vector result; + auto const &tool{m_extend}; + editor_track::snap_target target; + if (extend_snap(target)) + { + editor_track::branch_link link; + std::string reason; + if (plan_branch_link(link, reason)) + { + m_join_last = {}; + return link.pieces; + } + double length; + std::string error; + return join_pieces(tool.track, {tool.path, tool.atend ? editor_track::point_kind::end : editor_track::point_kind::start}, tool.point, tool.direction, target, length, error); + } + m_join_last = {}; + if (m_track_layout != track_layout::straight && tool.track != nullptr && tool.track->eType == tt_Switch && tool.track->m_paths.size() > 1 && tool.path == 1 && tool.atend) + { + auto const &diverging{tool.track->m_paths[1]}; + bezier const curve{diverging}; + 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 (auto const radius{editor_track::branch_radius(diverging)}; radius > 0.0) + { + int const side{turn > 0.0 ? 1 : -1}; + auto const &direction{tool.direction}; + auto const centre{plan_of(tool.point) + glm::dvec2{-direction.y, direction.x} * (side * radius)}; + auto const angle{geometry::signed_angle(plan_of(tool.point) - centre, plan_of(tool.mouse) - centre) * side}; + if (angle * radius < 0.5) + return result; + auto pieces{arc_pieces(tool.point, direction, tool.grade, radius, std::min(angle, glm::pi()), side)}; + for (auto &piece : pieces) + piece.rolls = {diverging.rolls[1], diverging.rolls[1]}; + follow_ground(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN(), tool.track); + return pieces; + } + } + auto radius{m_lay.radius}; + if (auto const values{typed_values()}; false == values.empty() && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) && std::abs(values[0]) >= 1.0) + radius = std::abs(values[0]); + auto pieces{editor_track::layout_pieces(m_track_layout, tool.point, tool.direction, tool.grade, tool.mouse, radius)}; + follow_ground(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN(), tool.track); + return pieces; +} + +double editor_mode::design_speed(TTrack const *From, TTrack const *To) const +{ + double speed{0.0}; + for (auto const *track : {From, To}) + { + if (track == nullptr) + continue; + auto const velocity{editor_track::velocity(*track)}; + if (velocity > 0.0) + speed = speed > 0.0 ? std::min(speed, velocity) : velocity; + } + if (speed <= 0.0) + speed = m_lay.style.velocity > 0.0 ? m_lay.style.velocity : 100.0; + return speed; +} + +std::vector editor_mode::join_pieces(TTrack const *From, editor_track::point_ref const &Frompoint, glm::dvec3 const &Start, glm::dvec2 const &Direction, editor_track::snap_target const &End, double &Length, + std::string &Error) const +{ + alignment::join_request request; + request.start.position = Start; + request.start.direction = Direction; + request.start.roll = From != nullptr ? -editor_track::joint_roll(*From, Frompoint) : 0.0; + request.end.position = End.position; + request.end.direction = plan_of(End.direction); + request.end.roll = End.track != nullptr ? editor_track::joint_roll(*End.track, End.point) : 0.0; + request.radius = m_join_radius; + request.variant = m_join_variant; + request.transitions = m_lay.transitions; + request.speed = design_speed(From, End.track); + request.kink = m_joints.angle * 0.5; + request.shape = m_route.design.shape; + request.norms = laying_norms(); + auto result{alignment::join(request)}; + if (m_lay.transitions && result.kind != alignment::join_kind::straight && result.kind != alignment::join_kind::arc && glm::length(request.end.direction) > 1e-9) + { + auto const grade{(End.position.y - Start.y) / std::max(1.0, plan_distance(Start, End.position))}; + auto const in{From != nullptr && From->eType == tt_Normal ? editor_track::ease_out_of(*From, Start, grade, request.speed, request.norms) : editor_track::easing{}}; + auto const out{End.track != nullptr && End.track->eType == tt_Normal ? editor_track::ease_out_of(*End.track, End.position, -grade, request.speed, request.norms) : editor_track::easing{}}; + if (auto eased{eased_join(request, in, out)}) + result = std::move(*eased); + } + Length = result.length; + std::vector notes; + for (auto const &warning : result.warnings) + 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")); + Error = lines_of(notes); + m_join_last = result; + return result.pieces; +} + +void editor_mode::finish_extend() +{ + auto const &io{ImGui::GetIO()}; + if (glm::length(glm::vec2{io.MousePos.x, io.MousePos.y} - m_extend.pressed) < kDragPixels) + { + m_extend.active = false; + if (m_lay.active && m_lay.points.empty() && m_extend.origin.track != nullptr) + { + m_lay.status.clear(); + m_lay.start = m_extend.origin; + m_lay.points.push_back(m_extend.origin.position); + } + else if (false == m_lay.active && m_extend.origin.track != nullptr && m_extend.origin.track == selected_track()) + m_track_point = m_extend.origin.point; + return; + } + std::string reason; + if (editor_track::branch_link link; plan_branch_link(link, reason)) + { + finish_branch_link(link); + return; + } + if (false == reason.empty()) + ui()->set_status(reason, true); + crossover_plan plan; + editor_track::snap_target target; + if (false == extend_snap(target) && plan_crossover(plan)) + { + auto *origin{m_extend.track}; + m_extend.active = false; + std::vector> states; + std::vector created; + std::vector removed; + if (false == place_switch_on_straight(plan.target, plan.shape, origin, plan.along, plan.direction, plan.side, false, states, created, removed, switch_name_for_new())) + return; + auto *placed{*std::find_if(created.begin(), created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; })}; + if (glm::distance(plan.insert.points[segment_data::point::start], plan.insert.points[segment_data::point::end]) > 0.01) + { + auto *insert{editor_track::create_path(*origin, plan.insert)}; + editor_track::commit({insert}); + created.push_back(insert); + } + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + if (place_switch_drive(*placed)) + join_last_steps(); + advance_switch_name(); + return; + } + auto const pieces{extend_pieces()}; + auto *style{m_extend.track}; + m_extend.active = false; + if (pieces.empty() || style == nullptr) + return; + std::vector created; + for (auto const &piece : pieces) + created.push_back(editor_track::create_path(*style, piece)); + editor_track::commit(created); + digitize_drawn(created); + push_track_snapshot({}, created); + first_step_done(first_step::laid); + if (target.track != nullptr) + first_step_done(first_step::join); + m_node = created.back(); + ui()->set_node(m_node); + straight_refresh(); +} + +void editor_mode::extend_add(int const Side) +{ + extend_tool tool; + if (false == chosen_free_end(tool)) + return; + auto const length{std::max(0.5, m_extend_length)}; + std::vector pieces; + if (Side == 0) + { + segment_data path; + path.points[segment_data::point::start] = tool.point; + path.points[segment_data::point::end] = {tool.point.x + tool.direction.x * length, tool.point.y + tool.grade * length, tool.point.z + tool.direction.y * length}; + pieces.push_back(path); + } + else + { + auto const radius{std::max(10.0, m_extend_radius)}; + pieces = arc_pieces(tool.point, tool.direction, tool.grade, radius, std::min(length / radius, glm::radians(180.0)), Side); + } + std::vector created; + for (auto const &piece : pieces) + created.push_back(editor_track::create_path(*tool.track, piece)); + editor_track::commit(created); + digitize_drawn(created); + push_track_snapshot({}, created); + m_node = created.back(); + ui()->set_node(m_node); + m_track_point = {}; + straight_refresh(); +} + +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"))) + return; + 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); + 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(0.0f, gap); + if (piece("##extendleft", STR_C("Curve left"), 1)) + extend_add(1); + 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 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) + { + 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::TextUnformatted(STR_C(names[current])); + ImGui::SameLine(); + ImGui::TextDisabled("%s", STR_C("(Shift+wheel)")); +} + +int editor_mode::wheel_step(double const Offset, bool const Gesture) +{ + auto const &io{ImGui::GetIO()}; + auto const now{ImGui::GetTime()}; + int const keys{(io.KeyCtrl ? 1 : 0) | (io.KeyShift ? 2 : 0) | (io.KeyAlt ? 4 : 0)}; + auto &wheel{m_wheel}; + if (now - wheel.time > 0.3 || keys != wheel.keys || (wheel.rest != 0.0 && (wheel.rest > 0.0) != (Offset > 0.0))) + { + wheel.rest = 0.0; + wheel.spent = false; + } + wheel.time = now; + wheel.keys = keys; + wheel.rest += Offset; + if ((Gesture && wheel.spent) || std::abs(wheel.rest) < 1.0 - 1e-6) + return 0; + int const step{wheel.rest > 0.0 ? 1 : -1}; + wheel.rest -= step; + wheel.spent = true; + return step; +} + +bool editor_mode::track_wheel(double const Offset) +{ + auto const &io{ImGui::GetIO()}; + if (m_switch.placing && m_switch.connection >= 0 && Offset != 0.0 && (io.KeyCtrl || io.KeyShift || io.KeyAlt)) + { + auto &tool{m_switch}; + int const step{wheel_step(Offset, false == io.KeyCtrl)}; + if (step == 0) + return true; + if (io.KeyShift) + tool.connection = (tool.connection + step + connection::kind_count) % connection::kind_count; + else if (io.KeyCtrl) + { + static double const radii[]{0.0, 150.0, 190.0, 250.0, 300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1200.0, 1500.0, 2000.0, 3000.0}; + int index{0}; + for (int i = 0; i < static_cast(std::size(radii)); ++i) + if (radii[i] <= tool.connection_radius + 1e-6) + index = i; + tool.connection_radius = radii[std::clamp(index + step, 0, static_cast(std::size(radii)) - 1)]; + } + else + tool.connection_together = false == tool.connection_together; + return true; + } + if ((false == io.KeyCtrl && false == io.KeyShift) || Offset == 0.0) + return false; + bool const laying{m_lay.active && false == m_lay.points.empty()}; + if (false == m_extend.active && false == laying) + return false; + int const step{wheel_step(Offset, false == io.KeyCtrl)}; + if (step == 0) + return true; + editor_track::snap_target target; + bool const joined{m_extend.active ? extend_snap(target) : m_lay.mouse_snap.track != nullptr && m_lay.start.track != nullptr}; + static double const radii[]{100.0, 150.0, 190.0, 250.0, 300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1200.0, 1500.0, 2000.0, 3000.0, 4000.0, 6000.0}; + auto const next_radius = [&](double const Current) { + if (step > 0) + { + for (auto const radius : radii) + if (radius > Current * 1.001) + return radius; + return std::end(radii)[-1]; + } + for (auto radius{std::rbegin(radii)}; radius != std::rend(radii); ++radius) + if (*radius < Current * 0.999) + return *radius; + return radii[0]; + }; + first_step_done(io.KeyCtrl ? first_step::wheel : first_step::shape); + if (io.KeyCtrl) + { + if (false == joined) + m_lay.radius = next_radius(m_lay.radius); + else if (m_join_radius <= 0.0) + m_join_radius = next_radius(m_join_last.radius > 0.0 ? m_join_last.radius : 500.0); + else if (step > 0 && m_join_radius >= std::end(radii)[-1]) + m_join_radius = 0.0; + else + m_join_radius = next_radius(m_join_radius); + return true; + } + if (joined) + m_join_variant += step; + else + m_track_layout = static_cast((static_cast(m_track_layout) + 4 - step) % 4); + return true; +} + +std::vector editor_mode::lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const +{ + auto const &tool{m_lay}; + Length = 0.0; + if (Points.size() < 2) + return {}; + if (Points.size() == 2) + { + auto const leaving{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; + if (glm::length(leaving) < 1e-6) + { + Error = STR_C("Two points lie on one another"); + return {}; + } + if (End.track != nullptr && tool.start.track != nullptr) + return join_pieces(tool.start.track, tool.start.point, Points[0], glm::normalize(leaving), End, Length, Error); + m_join_last = {}; + std::vector pieces; + if (tool.start.track != nullptr) + { + auto const grade{-grade_of(tool.start.direction)}; + pieces = editor_track::layout_pieces(m_track_layout, Points[0], glm::normalize(leaving), grade, Points[1], tool.radius); + Error = narrowed(pieces, m_track_layout, tool.radius); + follow_ground(pieces, Points[0].y, grade, Points[1].y, std::numeric_limits::quiet_NaN(), tool.start.track); + } + else if (End.track == nullptr) + { + auto const length{glm::length(leaving)}; + pieces = straight_piece(Points[0], glm::normalize(leaving), (Points[1].y - Points[0].y) / length, length); + follow_ground(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), Points[1].y, std::numeric_limits::quiet_NaN(), tool.copy_selected ? selected_track() : nullptr); + } + else if (glm::length(plan_of(End.direction)) > 1e-6) + { + auto const back{editor_track::layout_pieces(m_track_layout, End.position, glm::normalize(-plan_of(End.direction)), -grade_of(End.direction), Points[0], tool.radius)}; + for (auto piece{back.rbegin()}; piece != back.rend(); ++piece) + pieces.push_back(reversed(*piece)); + Error = narrowed(pieces, m_track_layout, tool.radius); + follow_ground(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), End.position.y, grade_of(End.direction), End.track); + } + for (auto const &piece : pieces) + Length += bezier{piece}.plan_length(); + if (pieces.empty()) + Error = STR_C("The track can't be laid through these points"); + return pieces; + } + auto const startdirection{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; + auto const enddirection{End.track != nullptr ? plan_of(End.direction) : plan_of(Points.back() - Points[Points.size() - 2])}; + if (glm::length(startdirection) < 1e-3 || glm::length(enddirection) < 1e-3) + { + Error = STR_C("Two points lie on one another"); + return {}; + } + alignment::design design; + auto const shape{lay_shape(Points, glm::normalize(startdirection), glm::normalize(enddirection), design)}; + if (false == shape.valid) + { + Error = shape.errors.empty() ? std::string{STR_C("The track can't be laid through these points")} : shape.errors.front(); + return {}; + } + Error = lines_of(shape.warnings); + auto const count = [&](alignment::result const &Shape, alignment::design const &Design) { + return std::max(alignment::minimum_pieces(Shape, Design), static_cast(std::ceil(Shape.length / std::max(5.0, tool.piece_length)))); + }; + // the curvature of the track laid has to go on from the one of the track it joins, a jump gets a transition curve between them + auto const grade{(Points.back().y - Points.front().y) / std::max(1.0, plan_distance(Points.front(), Points.back()))}; + auto const eased = [&](editor_track::snap_target const &Joint, double const Chainage, double const Sign) { + if (Joint.track == nullptr || Joint.track->eType != tt_Normal) + return editor_track::easing{}; + auto result{editor_track::ease_out_of(*Joint.track, Joint.position, grade * Sign, design.speed, design.norms)}; + auto const own{Sign * curvature_at(shape, Chainage)}; + if (std::abs(own - result.curvature) <= 0.05 * std::abs(result.curvature)) + result.pieces.clear(); + return result; + }; + auto const in{tool.transitions ? eased(tool.start, 0.0, 1.0) : editor_track::easing{}}; + auto const out{tool.transitions ? eased(End, shape.length, -1.0) : editor_track::easing{}}; + if ((false == in.pieces.empty() || false == out.pieces.empty()) && editor_track::eases_room(in, out, Points.front(), design.start_direction, Points.back(), design.end_direction)) + { + auto points{Points}; + auto starting{design.start_direction}; + auto ending{design.end_direction}; + if (false == in.pieces.empty()) + { + points.front() = in.end; + starting = in.direction; + } + if (false == out.pieces.empty()) + { + points.back() = out.end; + ending = -out.direction; + } + alignment::design easeddesign; + auto const easedshape{lay_shape(points, starting, ending, easeddesign)}; + if (easedshape.valid) + { + Length = editor_track::eased_length(in, easedshape.length, out); + Error = lines_of(easedshape.warnings); + return editor_track::eased_pieces(in, alignment::pieces(easedshape, easeddesign, count(easedshape, easeddesign)), out); + } + Error = STR("The curvature changes abruptly where the track joins, there's no room for a transition curve") + (easedshape.errors.empty() ? std::string{} : ": " + easedshape.errors.front()); + } + Length = shape.length; + return alignment::pieces(shape, design, count(shape, design)); +} + +alignment::result editor_mode::lay_shape(std::vector const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &design) const +{ + auto const &tool{m_lay}; + design = {}; + design.norms = laying_norms(); + design.shape = m_route.design.shape; + design.speed = design_speed(tool.start.track, nullptr); + design.start = Points.front(); + design.end = Points.back(); + design.start_direction = Startdirection; + design.end_direction = Enddirection; + + alignment::vertex corner; + corner.radius = std::max(1.0, tool.radius); + if (Points.size() == 2) + { + if (false == alignment::collinear(design)) + { + glm::dvec2 intersection; + if (alignment::tangent_intersection(design, intersection)) + { + design.vertices.push_back(corner); + } + else + { + corner.offset = plan_distance(design.start, design.end) / 3.0; + design.vertices = {corner, corner}; + } + } + } + else + { + for (std::size_t i = 1; i + 1 < Points.size(); ++i) + { + corner.position = plan_of(Points[i]); + design.vertices.push_back(corner); + } + if (design.vertices.size() >= 2) + { + design.vertices.front().offset = std::max(1.0, glm::dot(plan_of(Points[1] - Points.front()), design.start_direction)); + design.vertices.back().offset = std::max(1.0, glm::dot(plan_of(Points.back() - Points[Points.size() - 2]), design.end_direction)); + } + } + return alignment::compute_recommended(design, tool.transitions); +} + +void editor_mode::lay_click() +{ + auto &tool{m_lay}; + tool.status.clear(); + auto const snap{tool.mouse_snap}; + if (snap.track != nullptr && false == tool.points.empty() && glm::distance(snap.position, tool.points.front()) > 1.0) + { + // reaching a free end finishes the track there, in line with the path it joins + tool.points.push_back(snap.position); + lay_finish(snap); + return; + } + if (tool.points.empty()) + { + tool.chain.clear(); + tool.start = snap; + auto point{snap.track != nullptr ? snap.position : tool.mouse}; + if (auto const *axis{snap.track == nullptr && ImGui::GetIO().KeyAlt ? skew_reference(point, point, kSkewSeat) : nullptr}) + { + auto const &path{axis->m_paths.front()}; + auto const &from{path.points[segment_data::point::start]}; + auto const &to{path.points[segment_data::point::end]}; + auto const along{std::clamp(glm::dot(plan_of(point - from), plan_of(to - from)) / glm::dot(plan_of(to - from), plan_of(to - from)), 0.0, 1.0)}; + point = from + (to - from) * along; + } + tool.points.push_back(point); + return; + } + auto const &point{tool.mouse}; + if (plan_distance(point, tool.points.back()) < 1.0) + return; + m_typed.clear(); + if (tool.points.size() == 1) + { + lay_tool::step const previous{tool.start, tool.points.front()}; + tool.points.push_back(tool.mouse); + lay_finish({}); + if (false == tool.points.empty()) + { + tool.points.pop_back(); + return; + } + auto *last{dynamic_cast(m_node)}; + if (last == nullptr || last->m_paths.empty()) + return; + auto const &path{last->m_paths.front()}; + editor_track::snap_target next; + next.track = last; + next.point = {0, editor_track::point_kind::end}; + next.position = path.points[segment_data::point::end]; + auto const &control{path.points[segment_data::point::control2]}; + next.direction = glm::normalize(control != glm::dvec3{} ? control : path.points[segment_data::point::start] - next.position); + tool.chain.push_back(previous); + tool.start = next; + tool.points = {next.position}; + return; + } + tool.points.push_back(point); +} + +void editor_mode::lay_finish(editor_track::snap_target const &End) +{ + auto &tool{m_lay}; + double length; + std::string error; + auto const pieces{lay_pieces(tool.points, End, length, error)}; + tool.status_time = ImGui::GetTime(); + if (pieces.empty()) + { + tool.status = error.empty() ? std::string{STR_C("Click at least two points")} : error; + if (End.track != nullptr && false == tool.points.empty()) + tool.points.pop_back(); + return; + } + auto const *style{tool.start.track != nullptr ? tool.start.track : End.track}; + if (style == nullptr && tool.copy_selected) + style = selected_track(); + std::vector created; + for (auto const &piece : pieces) + created.push_back(style != nullptr ? editor_track::create_path(*style, piece) : editor_track::create_path(tool.style, piece)); + editor_track::commit(created); + digitize_drawn(created); + push_track_snapshot({}, created); + first_step_done(first_step::laid); + if (End.track != nullptr) + first_step_done(first_step::join); + 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; + { + std::vector> run; + if (tool.start.track != nullptr && tool.start.track->eType == tt_Normal) + run.emplace_back(tool.start.track, tool.start.point.kind == editor_track::point_kind::end); + for (auto *track : created) + run.emplace_back(track, true); + if (End.track != nullptr && End.track->eType == tt_Normal) + run.emplace_back(End.track, End.point.kind == editor_track::point_kind::start); + editor_track::curve curve; + if (run.size() > created.size() && editor_track::analyse_curve(run, m_route.design.norms.gauge, curve) && curve.compound) + for (std::size_t i = 1; i < curve.arcs.size(); ++i) + if (curve.arcs[i].transition < 1.0) + { + tool.status += "\n" + format(STR_C("Compound curve: R %.0f m meets R %.0f m with no transition curve between them"), curve.arcs[i - 1].radius, curve.arcs[i].radius); + break; + } + } + if (scene::Layers.active() == null_handle && style == nullptr) + tool.status += "\n" + STR("The scenery isn't opened for editing: new track won't be saved"); + WriteLog("Editor: " + tool.status, logtype::generic); + tool.points.clear(); + tool.start = {}; + m_node = created.back(); + ui()->set_node(m_node); + straight_refresh(); +} + +void editor_mode::lay_enter() +{ + auto &tool{m_lay}; + if (false == m_typed.empty()) + { + lay_click(); + return; + } + if (tool.points.empty()) + return; + auto const end{tool.mouse_snap}; + if (end.track != nullptr && glm::distance(end.position, tool.points.front()) > 1.0) + { + tool.points.push_back(end.position); + lay_finish(end); + return; + } + auto const point{placement_on_ground(tool.mouse)}; + bool const appended{plan_distance(point, tool.points.back()) > 1.0}; + if (appended) + tool.points.push_back(point); + auto const count{tool.points.size()}; + lay_finish({}); + if (appended && tool.points.size() == count) + tool.points.pop_back(); +} + +void editor_mode::lay_cancel() +{ + auto &tool{m_lay}; + tool.points.clear(); + tool.start = {}; + tool.chain.clear(); + tool.preview.clear(); + tool.status.clear(); +} + +void editor_mode::lay_after_history(bool const Undo) +{ + auto &tool{m_lay}; + if (tool.points.empty()) + return; + if (false == Undo) + { + lay_cancel(); + return; + } + while (tool.start.track != nullptr && tool.start.track->m_editorremoved) + { + if (tool.chain.empty()) + { + lay_cancel(); + return; + } + tool.start = tool.chain.back().start; + tool.points = {tool.chain.back().point}; + tool.chain.pop_back(); + } +} + +void editor_mode::lay_back() +{ + auto &tool{m_lay}; + if (false == tool.chain.empty()) + { + undo_last(); + tool.status = STR_C("The last piece was taken back"); + tool.status_time = ImGui::GetTime(); + return; + } + if (false == tool.points.empty()) + tool.points.pop_back(); + if (tool.points.empty()) + tool.start = {}; +} + +glm::dvec3 editor_mode::lay_angle_snap(glm::dvec3 const &Mouse, std::string &Label) const +{ + Label.clear(); + auto const &last{m_lay.points.back()}; + glm::dvec2 const offset{Mouse.x - last.x, Mouse.z - last.z}; + auto const length{glm::length(offset)}; + if (length < 1.0) + return Mouse; + auto const leaving{m_lay.start.track != nullptr ? -plan_of(m_lay.start.direction) : glm::dvec2{0.0}}; + glm::dvec2 reference{glm::length(leaving) > 1e-6 ? glm::normalize(leaving) : glm::dvec2{0.0, 1.0}}; + auto const step{glm::radians(15.0)}; + auto angle{std::round(signed_angle(reference, offset / length) / step) * step}; + if (auto const *axis{skew_reference(last, Mouse, kSkewReach)}) + { + auto const &path{axis->m_paths.front()}; + reference = glm::normalize(plan_of(path.points[segment_data::point::end] - path.points[segment_data::point::start])); + auto const relative{signed_angle(reference, offset / length)}; + angle = std::round(relative / step) * step; + double ratio{0.0}; + for (auto const n : kSkewRatios) + for (auto const base : {0.0, glm::pi()}) + for (auto const side : {-1.0, 1.0}) + { + auto const candidate{base + side * std::atan(1.0 / n)}; + if (std::abs(std::remainder(relative - candidate, 2.0 * glm::pi())) < std::abs(std::remainder(relative - angle, 2.0 * glm::pi()))) + { + angle = candidate; + ratio = n; + } + } + auto const name{axis->name().empty() ? std::string{STR_C("(no name)")} : axis->name()}; + auto const degrees{glm::degrees(std::abs(std::remainder(angle, glm::pi())))}; + Label = ratio > 0.0 ? format(STR_C("1:%g to %s"), ratio, name.c_str()) : degrees < 0.1 ? format(STR_C("parallel to %s"), name.c_str()) : format(STR_C("%.0f deg to %s"), degrees, name.c_str()); + } + auto const heading{turned(reference, angle) * length}; + return {last.x + heading.x, Mouse.y, last.z + heading.y}; +} + +TTrack const *editor_mode::skew_reference(glm::dvec3 const &From, glm::dvec3 const &To, double const Reach) const +{ + auto const from{plan_of(From)}; + auto const leg{plan_of(To) - from}; + auto const length{glm::length(leg)}; + TTrack const *nearest{nullptr}; + auto best{std::numeric_limits::max()}; + for (auto *section : simulation::Region->sections((From + To) * 0.5, static_cast(std::min(0.5 * length + Reach, 2000.0)))) + for (auto const &cell : section->m_cells) + for (auto *track : cell.m_directories.paths) + { + if (track == nullptr || track->eType != tt_Normal || track->m_editorremoved || (track->iCategoryFlag & 15) != kRailCategory || track->m_paths.size() != 1 || + false == editor_track::is_straight(*track, m_straights.tolerance)) + continue; + auto const &path{track->m_paths.front()}; + auto const a{plan_of(path.points[segment_data::point::start])}; + auto const axis{plan_of(path.points[segment_data::point::end]) - a}; + auto const span{glm::length(axis)}; + if (span < 1.0) + continue; + auto const along{std::clamp(glm::dot(from - a, axis) / span, 0.0, span)}; + auto distance{glm::distance(from, a + axis / span * along)}; + if (distance > Reach) + distance = std::numeric_limits::max(); + if (auto const determinant{cross(leg, axis)}; length > 1.0 && std::abs(determinant) > 1e-9) + { + auto const t{cross(a - from, axis) / determinant}; + auto const u{cross(a - from, leg) / determinant}; + if (t >= 0.0 && t <= 1.0 && u >= 0.0 && u <= 1.0) + distance = std::min(distance, t * length); + } + if (distance < best) + { + best = distance; + nearest = track; + } + } + return nearest; +} + +editor_mode::track_hover editor_mode::split_spot() const +{ + auto const *selected{selected_track()}; + if (selected == nullptr || ui()->mode() != nodebank_panel::TRACK || ImGui::GetIO().WantCaptureMouse) + return {}; + return track_under_cursor(selected, 20.0f); +} + +alignment::limits editor_mode::laying_norms() const +{ + auto norms{m_route.design.norms}; + norms.checked = false == m_norms_free; + 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() +{ + auto changed{false}; + 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) + { + if (i > 0) + ImGui::SameLine(0.0f, gap); + auto const chosen{m_norms_free == (i == 0)}; + 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; + } + } + ImGui::PopID(); + if (changed && false == m_route.chain.tracks.empty()) + route_update(); + return changed; +} + +void editor_mode::render_lay_ui() +{ + auto &tool{m_lay}; + render_norms_choice(); + ImGui::Separator(); + 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"); + 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(); + if (auto *track{selected_track()}; track != nullptr) + { + std::string reason; + if (editor_track::can_edit_geometry(*track, reason)) + render_extend_ui(); + } + if (false == ImGui::TreeNode(STR_C("More settings"))) + { + 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::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) + { + 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(); + }; + texture_row(0, STR_C("Rails"), tool.style.rail); + texture_row(1, STR_C("Trackbed"), tool.style.ballast); + } + ImGui::TreePop(); + render_lay_status(); +} + +void editor_mode::render_lay_status() +{ + auto const &tool{m_lay}; + if (scene::Layers.active() == null_handle) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), STR_C("The scenery isn't opened for editing: new track won't be saved")); + if (tool.active && false == tool.preview_error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", tool.preview_error.c_str()); + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp index f28ff72a..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" @@ -38,6 +39,7 @@ using geometry::plan_of; double const kSampleStep{2.0}; // m between the samples of the line double const kLineCorridor{15.0}; // m from the axis, posts farther away belong to another line double const kOverlayRange{3000.0}; +float const kFoulingPick{12.0f}; // px from a point of the fouling overlay a click takes it double const kFoulingTrace{250.0}; // m from the start of a switch the tracks are followed for double const kRailHeight{0.18}; // m from the points of a track to the heads of its rails int const kRailCategory{1}; @@ -524,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; } @@ -547,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; } @@ -567,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; } @@ -589,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()) { @@ -636,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")); @@ -653,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); @@ -666,14 +678,7 @@ void editor_mode::render_hekto_ui() auto const label{post.duplicate ? format(STR_C("km %s duplicate##%u"), km_text(post.hectometres).c_str(), post.post.instance) : format(STR_C("km %s stands %.1f m from its place##%u"), km_text(post.hectometres).c_str(), post.distance, post.post.instance)}; if (ImGui::Selectable(label.c_str())) { - glm::dvec3 const look{glm::normalize(glm::dvec3{-1.0, -0.8, -1.0})}; - m_focus_start_pos = Camera.Pos; - m_focus_start_angle = Camera.Angle; - m_focus_target_pos = post.post.location - look * 20.0; - m_focus_target_angle = glm::vec3(static_cast(std::asin(look.y)), static_cast(std::atan2(-look.x, -look.z)), 0.0f); - m_focus_active = true; - m_focus_time = 0.0; - m_focus_duration = 0.6; + fly_to(post.post.location, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 20.0); } } ImGui::EndChild(); @@ -832,14 +837,15 @@ void editor_mode::fouling_update() tool.status = format(STR_C("%zu switches, %zu fouling points: %d without a marker, %d markers standing wrong"), switches.size(), tool.points.size(), static_cast(missing), static_cast(wrong)); } -void editor_mode::fouling_place() +void editor_mode::fouling_place(int const Only) { auto &tool{m_fouling}; fouling_update(); std::vector items; - for (auto const &point : tool.points) + for (int i = 0; i < static_cast(tool.points.size()); ++i) { - if (point.standing >= 0) + auto const &point{tool.points[i]}; + if (point.standing >= 0 || (Only >= 0 && i != Only)) continue; template_item item; item.file = kFoulingModels[std::clamp(tool.model, 0, 3)]; @@ -851,19 +857,20 @@ void editor_mode::fouling_place() } 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; } -void editor_mode::fouling_fix() +void editor_mode::fouling_fix(int const Only) { auto &tool{m_fouling}; fouling_update(); std::vector> moves; - for (auto const &found : tool.found) + for (int i = 0; i < static_cast(tool.found.size()); ++i) { - if (false == found.wrong) + auto const &found{tool.found[i]}; + if (false == found.wrong || (Only >= 0 && i != Only)) continue; template_item item; auto const &point{tool.points[found.point]}; @@ -881,72 +888,147 @@ void editor_mode::fouling_fix() 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; } +std::optional> editor_mode::fouling_hit() const +{ + auto const &tool{m_fouling}; + screen_projection const projection; + auto const &mouse{ImGui::GetIO().MousePos}; + glm::dvec3 const camera{Global.pCamera.Pos}; + std::optional> result; + auto best{kFoulingPick * kFoulingPick}; + auto const consider = [&](glm::dvec3 const &Position, int const Index, bool const Marker) { + ImVec2 screen; + if (glm::distance(Position, camera) > kOverlayRange || false == projection.project(Position, screen)) + return; + auto const distance2{(screen.x - mouse.x) * (screen.x - mouse.x) + (screen.y - mouse.y) * (screen.y - mouse.y)}; + if (distance2 < best) + { + best = distance2; + result = std::make_pair(Index, Marker); + } + }; + for (int i = 0; i < static_cast(tool.points.size()); ++i) + if (tool.points[i].standing < 0) + consider(tool.points[i].position, i, false); + for (int i = 0; i < static_cast(tool.found.size()); ++i) + if (tool.found[i].wrong) + consider(tool.found[i].marker.location, i, true); + return result; +} + void editor_mode::render_fouling_ui() { auto &tool{m_fouling}; - ImGui::TextDisabled("%s", STR_C("W17 where the axes of the tracks meeting at a switch are the spacing apart,\nhalf way between them")); auto const changed = [&](bool const Changed) { 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)); - 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) + 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()); + else { - 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")); - tool.height_tolerance = std::clamp(tool.height_tolerance, 0.005, 1.0); + 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; })}; + 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) + { + 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) + { + // each one: a click flies there, the button fixes it alone; in the view a click on its point does the same + ImGui::TextDisabled("%s", STR_C("Click the green or red point in the view: that one only")); + ImGui::BeginChild("##foulinglist", ImVec2(0.0f, std::min(8, static_cast(missing + wrong)) * ImGui::GetFrameHeightWithSpacing() + 6.0f), true); + auto const look{glm::normalize(glm::dvec3{-1.0, -0.8, -1.0})}; + for (int i = 0; i < static_cast(tool.points.size()); ++i) + { + auto const &point{tool.points[i]}; + if (point.standing >= 0) + continue; + ImGui::PushID(i); + if (ImGui::SmallButton(STR_C("Place"))) + fouling_place(i); + ImGui::SameLine(); + if (ImGui::Selectable(format(STR_C("%s: no W17"), point.track->name().c_str()).c_str())) + fly_to(point.position, look, 25.0); + ImGui::PopID(); + } + for (int i = 0; i < static_cast(tool.found.size()); ++i) + { + auto const &found{tool.found[i]}; + if (false == found.wrong) + continue; + ImGui::PushID(1000000 + i); + if (ImGui::SmallButton(STR_C("Fix"))) + fouling_fix(i); + ImGui::SameLine(); + if (ImGui::Selectable(format(STR_C("%s: W17 %.1f m off, %+.2f m high"), tool.points[found.point].track->name().c_str(), found.error, found.height_error).c_str())) + fly_to(found.marker.location, look, 25.0); + ImGui::PopID(); + } + ImGui::EndChild(); + } } - 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))) + if (ImGui::TreeNode(STR_C("More settings"))) { - tool.turn = turn * 90; - tool.dirty = true; + if (begin_fields("##foulingfields", 0.6f)) + { + 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(); + } + 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::TreePop(); } - ImGui::PopItemWidth(); tool.reach = std::clamp(tool.reach, 50.0, 50000.0); tool.spacing = std::clamp(tool.spacing, 2.0, 10.0); tool.tolerance = std::clamp(tool.tolerance, 0.2, 20.0); - fouling_update(); - if (false == tool.error.empty()) - { - ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str()); - return; - } - 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(); - 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())) - fouling_fix(); - } } void editor_mode::draw_fouling_overlay() const diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp new file mode 100644 index 00000000..a540e010 --- /dev/null +++ b/application/editormodenmt.cpp @@ -0,0 +1,1551 @@ +/* +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 heightmap terrain made from the digital terrain model of GUGiK: the scenery placed as its orthophoto is, the heights +// of the model moved up or down to meet the tracks it has already, the chunks around the camera or along the tracks made as +// the model comes in, and the formation of each track with its slopes kept as a modifier of the terrain + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editormodeshared.h" +#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" +#include "utilities/translation.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include +#include +#include +#include + +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) +{ + 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 +bool shapes_ground(TTrack const &Track) +{ + return Track.eEnvironment != e_bridge && Track.eEnvironment != e_tunnel; +} + +} // namespace + +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 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])}; + auto const count{std::max(1, static_cast(std::ceil(chord * 1.1 / Step)))}; + for (int i = 0; i <= count; ++i) + { + auto const at{curve.point(static_cast(i) / count)}; + 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; +} + +void editor_mode::nmt_locate() +{ + auto const &config{m_orthophoto.settings()}; + m_nmt.place(config.north, config.east); +} + +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; + std::copy_if(places.begin(), places.end(), std::back_inserter(shaped), [](nmt_place const &Place) { return Place.shapes; }); + if (shaped.empty()) + { + state.offset = 0.0; + state.fitted = true; + state.status = STR("No tracks to fit the terrain model to: its heights are taken as they are"); + return; + } + // spread over the whole scenery, each place asking for its square of the model + auto const every{std::max(1, shaped.size() / fit_places)}; + for (std::size_t i = 0; i < shaped.size(); i += every) + state.fit.push_back(shaped[i]); + state.fitting = true; + 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.crown - height); + state.fitting = false; + if (differences.empty()) + { + state.status = STR("The terrain model has no data under the tracks: is the scenery placed where it is?"); + return; + } + std::sort(differences.begin(), differences.end()); + state.offset = differences[differences.size() / 2]; + std::vector deviations; + for (auto const difference : differences) + deviations.push_back(std::abs(difference - state.offset)); + std::sort(deviations.begin(), deviations.end()); + state.spread = deviations[deviations.size() / 2]; + state.fitted = true; + state.samples = differences.size(); + 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 +{ + std::vector result; + auto const centre{heightmap::chunk_at(Camera.Pos.x, Camera.Pos.z)}; + auto const first{-(m_terrain_add_count - 1) / 2}; + for (int dz = 0; dz < m_terrain_add_count; ++dz) + for (int dx = 0; dx < m_terrain_add_count; ++dx) + result.push_back({centre.first + first + dx, centre.second + first + dz}); + return result; +} + +std::vector editor_mode::nmt_keys_along_tracks() const +{ + std::set keys; + auto const reach{static_cast(std::ceil(m_nmt_state.band / heightmap::chunk_size))}; + for (auto const &place : nmt_track_places(heightmap::chunk_size * 0.5)) + { + auto const at{heightmap::chunk_at(place.position.x, place.position.z)}; + for (int dz = -reach; dz <= reach; ++dz) + for (int dx = -reach; dx <= reach; ++dx) + { + heightmap::chunk_key const key{at.first + dx, at.second + dz}; + auto const low{heightmap::chunk_corner(key)}; + // the chunk reaches within the band of the track + glm::dvec2 const nearest{std::clamp(place.position.x, low.x, low.x + heightmap::chunk_size), std::clamp(place.position.z, low.y, low.y + heightmap::chunk_size)}; + if (glm::distance(nearest, glm::dvec2{place.position.x, place.position.z}) <= m_nmt_state.band) + keys.insert(key); + } + } + return {keys.begin(), keys.end()}; +} + +void editor_mode::nmt_make(std::vector const &Keys, bool const Rebuild) +{ + auto &state{m_nmt_state}; + if (false == ensure_terrain()) + { + state.status = STR("The heightmap terrain can't be opened"); + 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()) + { + state.pending.push_back(key); + ++queued; + } + state.made = 0; + if (queued > 0) + state.grown = false; + // the terrain comes with its photos + if (queued > 0 && false == m_orthophoto.enabled()) + m_orthophoto.enabled(true); + 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) +{ + auto const low{heightmap::chunk_corner(Key)}; + auto const high{low + glm::dvec2{heightmap::chunk_size}}; + if (false == m_nmt.request(low - glm::dvec2{2.0}, high + glm::dvec2{2.0})) + return false; + auto const cells{heightmap::cells_for(m_terrain_spacing)}; + auto const side{static_cast(cells) + 1}; + 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) + { + auto const index{iz * side + ix}; + if (double height; m_nmt.height(low.x + ix * step, low.y + iz * step, height)) + { + heights[index] = height + offset_at(low.x + ix * step, low.y + iz * step); + found[index] = 1; + any = true; + } + } + // where the model has no data the chunk goes on level with its nearest point + if (any) + fill_from_nearest(heights, found, side); + m_streamer.add_chunk(Key.first, Key.second, m_terrain_spacing, [&](double const X, double const Z) { + auto const ix{static_cast(std::clamp(std::lround((X - low.x) / step), 0, cells))}; + auto const iz{static_cast(std::clamp(std::lround((Z - low.y) / step), 0, cells))}; + return static_cast(heights[iz * side + ix]); + }); + return true; +} + +std::size_t editor_mode::nmt_shape_tracks(std::vector const *Only) +{ + auto &state{m_nmt_state}; + if (false == ensure_terrain()) + return 0; + auto const wanted = [&](TTrack const *Track) { return Only == nullptr || std::find(Only->begin(), Only->end(), Track) != Only->end(); }; + // 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 + // 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)) + { + if (false == wanted(place.track)) + continue; + std::pair const path{place.track, place.path}; + if (path != previous) + broken = false; + previous = path; + if (false == place.shapes) + { + broken = true; + continue; + } + 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{}) + (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) + laid.push_back(std::move(modifier)); + if (Only != nullptr) + { + // the modifiers the tracks don't have any more: of the pieces of a path gone, of the tracks removed + std::vector gone; + for (auto const *track : *Only) + { + if (track == nullptr || track->name().empty()) + continue; + auto const own{"nmt " + track->name()}; + for (auto const &modifier : m_streamer.modifiers()) + if ((modifier.name == own || modifier.name.starts_with(own + " ")) && std::none_of(laid.begin(), laid.end(), [&](heightmap::modifier const &Laid) { return Laid.name == modifier.name; })) + gone.push_back(modifier.name); + } + for (auto const &name : gone) + m_streamer.remove_modifier(name); + } + auto const count{laid.size()}; + m_streamer.modifiers(std::move(laid)); + forget_ground(); + if (Only == nullptr) + state.shaped = modifiers.size(); + return count; +} + +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}; + for (auto const &place : state.fit) + 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; + if (all) + nmt_finish_fit(); + return; + } + nmt_grow(); + if (state.pending.empty()) + return; + // a few chunks a frame, those whose model is in + int made{0}; + for (auto it{state.pending.begin()}; it != state.pending.end() && made < 4;) + { + if (m_streamer.exists(*it)) + { + it = state.pending.erase(it); + continue; + } + if (add_chunk_from_nmt(*it)) + { + it = state.pending.erase(it); + ++made; + ++state.made; + continue; + } + ++it; + } + if (made > 0) + { + forget_ground(); + state.status = format(STR_C("Chunks made from the terrain model: %zu, waiting for it: %zu"), state.made, state.pending.size()); + if (state.pending.empty()) + { + if (state.shape && false == state.grown) + { + nmt_shape_tracks(); + 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 = STR(error); +} + +void editor_mode::nmt_grow() +{ + auto &state{m_nmt_state}; + auto const now{ImGui::GetTime()}; + if (false == state.grow || now - state.grow_time < 0.5 || (false == m_lay.active && false == m_extend.active) || m_streamer.resident() == 0 || false == m_nmt.placed()) + return; + state.grow_time = now; + auto const at{m_extend.active ? m_extend.mouse : m_lay.mouse}; + glm::dvec2 const cursor{at.x, at.z}; + double constexpr grow_reach{400.0}; // m around the cursor + auto const reach{static_cast(std::ceil(grow_reach / heightmap::chunk_size))}; + auto const centre{heightmap::chunk_at(at.x, at.z)}; + std::vector> missing; + for (int dz = -reach; dz <= reach; ++dz) + for (int dx = -reach; dx <= reach; ++dx) + { + heightmap::chunk_key const key{centre.first + dx, centre.second + dz}; + auto const low{heightmap::chunk_corner(key)}; + glm::dvec2 const nearest{std::clamp(cursor.x, low.x, low.x + heightmap::chunk_size), std::clamp(cursor.y, low.y, low.y + heightmap::chunk_size)}; + auto const distance{glm::distance(nearest, cursor)}; + if (distance <= grow_reach && false == m_streamer.exists(key) && std::find(state.pending.begin(), state.pending.end(), key) == state.pending.end()) + missing.emplace_back(distance, key); + } + if (missing.empty()) + return; + std::sort(missing.begin(), missing.end()); + if (state.pending.empty()) + { + state.grown = true; + state.made = 0; + } + for (auto const &chunk : missing) + state.pending.push_back(chunk.second); + if (false == m_orthophoto.enabled()) + m_orthophoto.enabled(true); + WriteLog(format("Editor: NMT - %zu chunks around the cursor to make", missing.size()), logtype::generic); +} + +void editor_mode::render_nmt_ui() +{ + auto &state{m_nmt_state}; + nmt_locate(); + auto const busy{state.fitting || state.stage != nmt_state::automatic::idle}; + step_heading(1, STR_C("Location")); + if (false == m_nmt.placed() || state.moving) + { + // 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; + } + 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 (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")); + } + toggle_chips("nmtgrow", {{STR_C("Make the terrain ahead while laying"), &state.grow, "Where a track is laid past the edge of the terrain, the terrain model is made around the cursor as it goes"}}); + // 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::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::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() +{ + auto &state{m_nmt_state}; + if (state.offered || false == m_lay.active || false == EditorSettings.terrain_offer()) + return; + state.offered = true; + if (m_streamer.resident() > 0 || std::any_of(simulation::Paths.sequence().begin(), simulation::Paths.sequence().end(), [](TTrack const *Track) { return Track != nullptr && rail(*Track); })) + return; + nmt_locate(); + state.north = m_orthophoto.settings().north; + state.east = m_orthophoto.settings().east; + state.prompt = true; +} + +void editor_mode::nmt_accept_offer() +{ + auto &state{m_nmt_state}; + if (false == m_nmt.placed()) + 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()); + state.prompt = false; +} + +void editor_mode::render_nmt_prompt() +{ + auto &state{m_nmt_state}; + if (false == state.prompt) + return; + char const *const title{STR_C("Terrain for the new scenery###nmtoffer")}; + if (false == ImGui::IsPopupOpen(title)) + ImGui::OpenPopup(title); + ImGui::SetNextWindowSize(ImVec2(520.0f * Global.ui_scale, 0.0f)); + if (false == ImGui::BeginPopupModal(title, nullptr, ImGuiWindowFlags_NoResize)) + return; + ImGui::TextWrapped("%s", STR_C("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.")); + bool const placed{m_nmt.placed()}; + if (false == placed) + { + ImGui::TextWrapped("%s", STR_C("Where is it? PUWG 1992 of this point, X to the north and Y to the east, as geoportal.gov.pl gives them:")); + ImGui::SetNextItemWidth(150.0f); + ImGui::InputDouble("X##nmtnorth", &state.north, 0.0, 0.0, "%.0f"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(150.0f); + ImGui::InputDouble("Y##nmteast", &state.east, 0.0, 0.0, "%.0f"); + } + auto const known{placed || (state.north > 100000.0 && state.east > 100000.0)}; + 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(); + } + 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)) + { + EditorSettings.terrain_offer(ask); + EditorSettings.save(); + } + 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); + } + // the includes moved by the place in their parameters are made anew there: their models aren't moved on their own, nor are + // the models left behind by earlier changes, deleted a moment later: moved, they'd get into the scene again + std::set remade; + for (auto const &move : moves) + if (scene::Layers.tracked(move.first.instance) && scene::Layers.removable(move.first.instance)) + remade.insert(move.first.instance); + // the models, a group as a whole, ahead of the includes, which retire theirs + std::set groups; + for (auto *instance : simulation::Instances.sequence()) + { + if (instance == nullptr || remade.contains(instance->m_instance) || + std::any_of(m_retired.begin(), m_retired.end(), [&](std::pair const &Retired) { return Retired.first == instance; })) + 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); + } + if (move_templates(moves) > 0 && m_history.size() >= 2) + m_history.back().joined = 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 24090fd0..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" @@ -23,6 +24,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/translation.h" #include #include +#include namespace { @@ -30,13 +32,100 @@ namespace using geometry::bezier; using geometry::cross; using geometry::plan_of; +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) @@ -76,6 +165,23 @@ double curvature(segment_data const &Path, bool const Atend) return speed > 1e-9 ? cross(first, second) / (speed * speed * speed) : 0.0; } +std::optional arc_curvature(segment_data const &Path) +{ + auto const [control1, control2] = controls(Path); + auto const along{plan_of(control1)}; + auto const back{-plan_of(control2)}; + if (glm::length(along) < 1e-9 || glm::length(back) < 1e-9) + return std::nullopt; + auto const turn{signed_angle(glm::normalize(along), glm::normalize(back))}; + if (std::abs(turn) < 1e-6) + return std::nullopt; + auto const radius{glm::length(plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])) / (2.0 * std::sin(std::abs(turn) * 0.5))}; + auto const control{4.0 / 3.0 * std::tan(std::abs(turn) * 0.25) * radius}; + if (std::abs(glm::length(along) - control) > 1e-4 * control || std::abs(glm::length(back) - control) > 1e-4 * control) + return std::nullopt; + return std::copysign(1.0 / radius, turn); +} + glm::dvec2 direction(segment_data const &Path, bool const Atend) { auto const [control1, control2] = controls(Path); @@ -188,8 +294,9 @@ void editor_mode::parallel_update() { auto const &source{sources[i].second}; auto const [control1, control2] = controls(source); - auto const startscale{1.0 - curvature(source, false) * offset}; - auto const endscale{1.0 - curvature(source, true) * offset}; + auto const arc{arc_curvature(source)}; + auto const startscale{1.0 - (arc.has_value() ? *arc : curvature(source, false)) * offset}; + auto const endscale{1.0 - (arc.has_value() ? *arc : curvature(source, true)) * offset}; if (startscale < 0.05 || endscale < 0.05) { tool.pieces.clear(); @@ -205,7 +312,7 @@ void editor_mode::parallel_update() piece.rolls = source.rolls; auto const middle{0.5 * (curvature(source, false) + curvature(source, true))}; if (source.radius != 0.0f && std::abs(middle) > 1e-7) - piece.radius = static_cast(std::copysign(std::abs((1.0 - middle * offset) / middle), static_cast(source.radius))); + piece.radius = static_cast(std::copysign(std::abs(std::abs(static_cast(source.radius)) - std::copysign(offset, middle)), static_cast(source.radius))); tool.length += bezier{piece}.plan_length(); tool.pieces.push_back(piece); tool.styles.push_back(sources[i].first); @@ -222,6 +329,7 @@ void editor_mode::parallel_build() for (std::size_t i = 0; i < tool.pieces.size(); ++i) created.push_back(editor_track::create_path(*tool.styles[i], tool.pieces[i])); editor_track::commit(created); + digitize_drawn(created); push_track_snapshot({}, created); tool.status = format(STR_C("Parallel track of %zu paths, %.1f m, %.2f m away; Ctrl+Z takes it back"), created.size(), tool.length, tool.spacing); if (scene::Layers.active() == null_handle) @@ -278,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()); @@ -333,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; @@ -352,11 +454,9 @@ bool editor_mode::parallel_preview_visible() const { if (m_parallel.pieces.empty() || parallel_source() != m_parallel.built_for) return false; - if (m_track_tab == track_tab::lay) - return m_lay.points.empty(); - if (m_track_tab == track_tab::path) + if (m_track_tab == track_tab::parallel) return true; - return m_track_tab == track_tab::lineside && m_parallel.open; + return m_track_tab == track_tab::path && parallel_target() != nullptr; } void editor_mode::draw_parallel_preview() const diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 4fafe1d3..c49b12b7 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -10,12 +10,20 @@ 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" +#include "editor/editorSwitchStore.hpp" #include "world/Track.h" #include "scene/scenelayers.h" #include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/glmHelpers.h" +#include "world/TrackSleepers.h" #include "utilities/Logs.h" #include "utilities/translation.h" @@ -38,6 +46,14 @@ 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)}; +ImU32 const kModel{IM_COL32(70, 200, 190, 230)}; // the ground of the terrain model, as measured +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)}; @@ -45,6 +61,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 @@ -97,6 +115,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: @@ -125,7 +165,9 @@ void editor_mode::profile_library_load() { stored_profile entry; entry.layer = layer; - glm::dvec3 start, end, middle; + glm::dvec3 start; + glm::dvec3 end; + glm::dvec3 middle; line >> entry.length >> start.x >> start.y >> start.z >> end.x >> end.y >> end.z >> middle.x >> middle.y >> middle.z; if (line.fail()) { @@ -144,7 +186,8 @@ void editor_mode::profile_library_load() std::string word; if (keyword == "speed") { - int start{1}, end{1}; + int start{1}; + int end{1}; line >> entry.line.speed >> word >> entry.origin >> word >> start >> end; entry.line.join_start = (start != 0); entry.line.join_end = (end != 0); @@ -154,6 +197,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; @@ -201,6 +246,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"); @@ -241,14 +288,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; } @@ -270,9 +511,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) @@ -290,7 +550,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; } @@ -305,15 +566,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()) @@ -330,19 +587,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)"); } @@ -351,6 +627,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) @@ -381,17 +658,82 @@ 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}; - state.from = state.picked_from = From; - state.to = state.picked_to = To; + state.picked_from = From; + state.from = state.picked_from; + state.picked_to = To; + 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()) + { profile_recognize(); + if (false == state.samples.empty()) + { + auto const km{alignment_store::chainage_at(state.samples.front().position)}; + auto const further{alignment_store::chainage_at(editor_track::sampled_position(state.samples, std::min(state.route.length, 5.0)))}; + if (km.has_value() && further.has_value() && *further > *km) + state.origin = *km; + } + } profile_fit_view(); + // the camera stays where the route was picked, it follows the plot once that's moved + state.view_seen = state.view_framed = {state.view_from, state.view_to}; } void editor_mode::profile_resample() @@ -409,22 +751,143 @@ void editor_mode::profile_resample() for (std::size_t i = 0; i < samples.size(); ++i) if (found[i] != 0) state.terrain[i] = heights[i]; + profile_sample_model(); + 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 = {}; context.length = state.route.length; if (samples.empty()) return; + std::optional open; for (auto const &span : state.route.spans) - if (span.track->eType != tt_Normal) + { + if (auto const found{switch_store::named(*span.track)}) + { + if (open.has_value() && open->name == found->name && span.from - context.switches.back().to <= std::max(found->a + found->b, context.switches.back().to - context.switches.back().from)) + { + context.switches.back().to = span.to; + continue; + } + auto const label{switch_store::label(*found)}; + context.switches.push_back({span.from, span.to, found->number != "-" ? found->number + ' ' + label : label}); + open = found; + } + else if (span.track->eType != tt_Normal) + { context.switches.push_back({span.from, span.to, span.track->name()}); + 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(); } +void editor_mode::profile_sample_model() +{ + auto &state{m_profile}; + auto const &samples{state.samples}; + state.model.assign(samples.size(), std::numeric_limits::quiet_NaN()); + state.model_waiting = false; + state.model_polled = ImGui::GetTime(); + if (samples.empty() || false == state.show_model) + return; + nmt_locate(); + if (false == m_nmt.placed()) + return; + // the squares along the route, not all those under the rectangle around it: a long line crosses the grid + std::size_t constexpr run{200}; + for (std::size_t first = 0; first < samples.size(); first += run) + { + glm::dvec2 low{samples[first].position.x, samples[first].position.z}; + auto high{low}; + for (auto i = first; i < std::min(samples.size(), first + run + 1); ++i) + { + low = glm::min(low, glm::dvec2{samples[i].position.x, samples[i].position.z}); + high = glm::max(high, glm::dvec2{samples[i].position.x, samples[i].position.z}); + } + if (false == m_nmt.request(low - glm::dvec2{2.0}, high + glm::dvec2{2.0})) + state.model_waiting = true; + } + std::vector differences; + for (std::size_t i = 0; i < samples.size(); ++i) + if (double height; m_nmt.height(samples[i].position.x, samples[i].position.z, height)) + { + state.model[i] = height; + differences.push_back(samples[i].position.y - height); + } + // a scenery put onto the model lies at its heights, or as far from them as the model was found to lie from it; one built at + // heights of its own, never put onto it, is compared by the shape: the model goes as far as the route lies from it + auto offset{m_nmt_state.offset}; + if (false == m_nmt_state.fitted && false == differences.empty()) + { + std::nth_element(differences.begin(), differences.begin() + differences.size() / 2, differences.end()); + if (auto const median{differences[differences.size() / 2]}; std::abs(median) > 30.0) + offset = median; + } + for (auto &height : state.model) + height += offset; +} + void editor_mode::profile_recognize() { auto &state{m_profile}; @@ -441,7 +904,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() @@ -457,32 +920,43 @@ 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); state.selected = -1; state.error.clear(); profile_check(); - double fill{0.0}, cut{0.0}; + double fill{0.0}; + double cut{0.0}; for (std::size_t i = 0; i < state.samples.size(); ++i) { 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}; @@ -504,7 +978,8 @@ std::pair editor_mode::profile_generate_ground(std::vector(cells) + 1}; - int made{0}, skipped{0}; + int made{0}; + int skipped{0}; for (auto const &key : keys) { if (m_streamer.exists(key)) @@ -626,7 +1101,8 @@ void editor_mode::profile_shape_ground() } modifier.points.push_back(points.back()); - double fill{0.0}, cut{0.0}; + double fill{0.0}; + double cut{0.0}; for (std::size_t i = 0; i < samples.size(); ++i) { if (i >= state.terrain.size() || false == std::isfinite(state.terrain[i])) @@ -641,11 +1117,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; @@ -760,16 +1236,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) @@ -778,19 +1257,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); @@ -798,12 +1287,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")); @@ -862,13 +1357,101 @@ void editor_mode::profile_fit_view() state.exaggeration = 0.0f; } -void editor_mode::profile_apply() +void editor_mode::profile_follow_view() +{ + auto &state{m_profile}; + std::pair const shown{state.view_from, state.view_to}; + auto const now{ImGui::GetTime()}; + if (shown != state.view_seen) + { + state.view_seen = shown; + state.view_seen_time = now; + return; + } + if (false == state.camera_follows || shown == state.view_framed || now - state.view_seen_time < 0.25) + return; + state.view_framed = shown; + profile_frame_view(); +} + +void editor_mode::profile_frame_view() +{ + auto const &state{m_profile}; + std::vector points; + for (auto const &sample : state.samples) + if (sample.chainage >= state.view_from && sample.chainage <= state.view_to) + points.push_back(sample.position); + auto const &io{ImGui::GetIO()}; + if (points.size() < 2 || io.DisplaySize.y <= 0.0f) + return; + glm::dvec3 low{points.front()}; + glm::dvec3 high{points.front()}; + for (auto const &point : points) + { + low = glm::min(low, point); + high = glm::max(high, point); + } + auto const centre{(low + high) * 0.5}; + // the room the docks and the strip of the plot leave to the view, and how far its middle lies from the middle of the screen, + // in pixels + glm::dvec2 const screen{io.DisplaySize.x, io.DisplaySize.y}; + auto const viewlow{editor_ui::view_min()}; + auto viewhigh{editor_ui::view_max()}; + if (state.strip_top > viewlow.y + 100.0f) + viewhigh.y = std::min(viewhigh.y, state.strip_top); + auto const width{std::max(50.0, static_cast(viewhigh.x - viewlow.x))}; + auto const height{std::max(50.0, static_cast(viewhigh.y - viewlow.y))}; + glm::dvec2 const shift{(viewlow.x + viewhigh.x) * 0.5 - screen.x * 0.5, (viewlow.y + viewhigh.y) * 0.5 - screen.y * 0.5}; + double constexpr margin{1.1}; + if (Global.EditorOrtho) + { + auto const yaw{static_cast(Camera.Angle.y)}; + double extent{5.0}; + for (auto const &point : points) + { + auto const local{RotateY(point - centre, -yaw)}; + extent = std::max({extent, std::abs(local.x) * margin * screen.y / width, std::abs(local.z) * margin * screen.y / height}); + } + Global.EditorOrthoExtent = std::clamp(static_cast(extent), 5.0f, 50000.0f); + auto const scale{2.0 * static_cast(Global.EditorOrthoExtent) / screen.y}; + auto const offset{RotateY(glm::dvec3{shift.x * scale, 0.0, shift.y * scale}, yaw)}; + Camera.Pos.x = centre.x - offset.x; + Camera.Pos.z = centre.z - offset.z; + Camera.Velocity = {}; + m_focus_active = false; + Global.pCamera = Camera; + return; + } + // seen from the side, as the plot shows it: the start on the left, from a little above + glm::dvec3 chord{points.back().x - points.front().x, 0.0, points.back().z - points.front().z}; + chord = glm::length(chord) > 1e-6 ? glm::normalize(chord) : glm::dvec3{1.0, 0.0, 0.0}; + glm::dvec3 const up{0.0, 1.0, 0.0}; + auto const look{glm::normalize(glm::cross(up, chord) + glm::dvec3{0.0, -0.4, 0.0})}; + auto const right{glm::normalize(glm::cross(look, up))}; + auto const above{glm::cross(right, look)}; + auto const tangent{std::tan(glm::radians(static_cast(Global.FieldOfView) / std::max(0.01, static_cast(Global.ZoomFactor))) * 0.5)}; + auto const across{tangent * width / screen.y}; + auto const upward{tangent * height / screen.y}; + double distance{10.0}; + for (auto const &point : points) + { + auto const offset{point - centre}; + auto const depth{glm::dot(offset, look)}; + distance = std::max({distance, std::abs(glm::dot(offset, right)) * margin / across - depth, std::abs(glm::dot(offset, above)) * margin / upward - depth}); + } + distance = std::min(distance, 8000.0); + auto const pixel{2.0 * distance * tangent / screen.y}; + fly_to(centre - right * (shift.x * pixel) + above * (shift.y * pixel), look, distance, 0.35); +} + +void editor_mode::profile_apply(bool const Terrain) { auto &state{m_profile}; state.status.clear(); state.error.clear(); if (state.route.spans.empty() || state.line.points.size() < 2) return; + auto const edits{m_track_edits}; if (has_errors(state.issues)) { state.error = STR_C("Correct the errors to apply the grade line"); @@ -883,17 +1466,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; @@ -903,24 +1490,84 @@ 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); + if (Terrain) + profile_follow_terrain(static_cast(m_track_edits - edits)); WriteLog("Editor: vertical profile - " + state.status, logtype::generic); } +void editor_mode::profile_follow_terrain(std::size_t const Edits) +{ + auto &state{m_profile}; + // the heightmap terrain the scenery has; the ground of a scenery without it stays as it is + if (false == m_streamer.active() || Edits == 0) + return; + std::vector tracks; + std::size_t found{0}; + for (auto step{m_history.rbegin()}; step != m_history.rend() && found < Edits; ++step) + { + if (step->action != EditorSnapshot::Action::TrackEdit) + continue; + ++found; + for (auto const &entry : step->tracks) + tracks.push_back(entry.first); + tracks.insert(tracks.end(), step->created.begin(), step->created.end()); + tracks.insert(tracks.end(), step->removed.begin(), step->removed.end()); + } + std::erase(tracks, nullptr); + std::sort(tracks.begin(), tracks.end()); + tracks.erase(std::unique(tracks.begin(), tracks.end()), tracks.end()); + if (tracks.empty()) + return; + // the route shaped by the earthworks of the profile keeps its modifier, shaped again to the new grade line; the tracks + // shaped one by one, as the terrain model lays them, get their formations anew + auto const name = [](TTrack const *Track) { return Track != nullptr && false == Track->name().empty() ? Track->name() : std::string{"?"}; }; + auto const route{"route " + name(state.from) + " - " + name(state.to)}; + auto const &modifiers{m_streamer.modifiers()}; + auto const has = [&](std::string const &Name) { return std::any_of(modifiers.begin(), modifiers.end(), [&](heightmap::modifier const &Modifier) { return Modifier.name == Name; }); }; + auto const shaped{has(route) || (false == state.earthworks.modifier.empty() && has(state.earthworks.modifier))}; + auto const own{std::any_of(tracks.begin(), tracks.end(), [&](TTrack const *Track) { + return false == Track->name().empty() && std::any_of(modifiers.begin(), modifiers.end(), [&](heightmap::modifier const &Modifier) { return Modifier.name == "nmt " + Track->name() || Modifier.name.starts_with("nmt " + Track->name() + " "); }); + })}; + auto status{state.status}; + std::size_t laid{0}; + if (false == shaped || own) + laid = nmt_shape_tracks(&tracks); + if (shaped) + { + profile_shape_ground(); + status += "\n" + state.status; + } + else + profile_resample(); + if (laid > 0) + status += format(STR_P("\nThe terrain followed the tracks: %zu formation laid anew", "\nThe terrain followed the tracks: %zu formations laid anew", laid), laid); + state.status = status; + // the terrain goes back with the grade line in one step + auto const before{m_history.empty() ? EditorSnapshot::Action::Other : m_history.back().action}; + terrain_history_step(); + if (before != EditorSnapshot::Action::Terrain && false == m_history.empty() && m_history.back().action == EditorSnapshot::Action::Terrain) + m_history.back().joined = true; +} + void editor_mode::profile_after_undo() { auto &state{m_profile}; @@ -937,27 +1584,43 @@ void editor_mode::profile_after_undo() { state.route = {}; state.samples.clear(); - state.selected = state.dragging = state.curve_grip = -1; + state.selected = -1; + state.dragging = -1; + state.curve_grip = -1; return; } 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)}; @@ -972,77 +1635,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(true); + 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())) { @@ -1053,7 +1849,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(); @@ -1065,11 +1861,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()) @@ -1085,18 +1889,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) @@ -1106,48 +1921,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; } @@ -1191,7 +1979,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) { @@ -1205,7 +1999,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 { @@ -1216,54 +2010,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; @@ -1363,54 +2169,92 @@ 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); + // the next item on the same line while it fits, past the edge of the plot on the next one + auto const edge{ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x}; + auto const next = [&](float const Gap, float const Width) { + ImGui::SameLine(0.0f, Gap); + if (ImGui::GetCursorScreenPos().x + Width > edge) + ImGui::NewLine(); }; - 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); + auto const pill = [](char const *Label) { return ImGui::CalcTextSize(Label).x + ImGui::GetFrameHeight() * 1.75f; }; + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", STR_C("Show:")); + next(ImGui::GetStyle().ItemSpacing.x, pill(STR_C("Track"))); + pill_toggle(STR_C("Track"), kTrack, state.show_track); + next(4.0f, pill(STR_C("Terrain"))); + pill_toggle(STR_C("Terrain"), kTerrain, state.show_terrain); + if (m_nmt.placed()) + { + next(4.0f, pill(STR_C("Terrain model"))); + if (pill_toggle(STR_C("Terrain model"), kModel, state.show_model)) + profile_sample_model(); + item_tooltip(state.model_waiting ? "The ground of the terrain model (NMT) as it was measured, before the earthworks; its squares along the route are still being fetched" + : "The ground of the terrain model (NMT) as it was measured, before the earthworks"); + } + next(4.0f, pill(STR_C("Plan curvature"))); + pill_toggle(STR_C("Plan curvature"), kPlan, state.show_plan); + next(6.0f, ImGui::CalcTextSize("(?)").x); + 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(kModel, "Terrain model"); + entry(kPlan, "Plan curvature"); + entry(kSwitchLabel, "Switches"); + entry(kSharedEdge, "Shared timbers"); + entry(kError, "Errors"); + entry(kWarning, "Warnings"); + ImGui::EndTooltip(); + } + next(14.0f, 120.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")); - ImGui::SameLine(); + next(ImGui::GetStyle().ItemSpacing.x, ImGui::CalcTextSize(STR_C("Fit")).x + ImGui::GetStyle().FramePadding.x * 2.0f); if (ImGui::Button(STR_C("Fit"))) profile_fit_view(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Shows the whole route")); + next(14.0f, pill(STR_C("Camera follows"))); + if (pill_toggle(STR_C("Camera follows"), kGradeLine, state.camera_follows) && state.camera_follows) + state.view_framed = {}; + item_tooltip("The view shows the part of the route the plot shows, once the plot is zoomed or moved"); } 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()}; if (state.exaggeration <= 0.0f) profile_fit_exaggeration(profile_view{state, corner, size}); + if (state.model_waiting && ImGui::GetTime() - state.model_polled > 0.5) + profile_sample_model(); profile_navigate(profile_view{state, corner, size}, hovered); + profile_follow_view(); profile_view const view{state, corner, size}; if (profile_canvas_edit(view, hovered)) profile_edited(); @@ -1428,24 +2272,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())) { @@ -1470,7 +2328,8 @@ void editor_mode::render_profile_canvas(float const Height) void editor_mode::profile_fit_exaggeration(profile_view const &View) { auto &state{m_profile}; - double low{std::numeric_limits::max()}, high{std::numeric_limits::lowest()}; + double low{std::numeric_limits::max()}; + double high{std::numeric_limits::lowest()}; for (auto const &sample : state.samples) { low = std::min(low, sample.position.y); @@ -1482,7 +2341,10 @@ void editor_mode::profile_fit_exaggeration(profile_view const &View) high = std::max(high, point.elevation); } if (low > high) - low = high = 0.0; + { + low = 0.0; + high = 0.0; + } auto const range{std::max(1.0, (high - low) * 1.3)}; state.exaggeration = static_cast(std::clamp(View.across / (range / View.height), 1.0, 500.0)); state.view_centre = (low + high) * 0.5; @@ -1574,9 +2436,20 @@ bool editor_mode::profile_canvas_edit(profile_view const &View, bool const Hover auto const point{hit_point()}; auto const grip{point < 0 ? hit_grip() : -1}; if (point >= 0) - state.selected = state.dragging = point; + { + 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.selected = state.curve_grip = grip; + { + state.curve_grip = grip; + state.selected = state.curve_grip; + state.drag_press = mouse; + state.drag_moved = false; + } else state.selected = -1; } @@ -1598,38 +2471,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; @@ -1670,9 +2547,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) { @@ -1723,7 +2631,11 @@ 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_model && state.model.size() == samples.size()) + polyline([&](std::size_t const I) { return state.model[I]; }, kModel, 1.5f); if (state.show_track) polyline([&](std::size_t const I) { return samples[I].position.y; }, kTrack, 1.5f); @@ -1834,7 +2746,13 @@ 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])); + } + if (state.show_model && index < state.model.size() && false == std::isnan(state.model[index])) + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(kModel), STR_C("Terrain model %.3f m, the grade line %+.2f m from it"), state.model[index], designed - state.model[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(); @@ -1862,16 +2780,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 new file mode 100644 index 00000000..711dba03 --- /dev/null +++ b/application/editormoderecords.cpp @@ -0,0 +1,419 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editormodeshared.h" +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "editor/editorGeometry.hpp" + +#include "world/Track.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include +#include +#include +#include + +namespace +{ + +using geometry::bezier; +using geometry::plan_of; +using geometry::signed_angle; +using geometry::turned; +using path_ends = std::pair; + +struct record_drift +{ + int followed{0}; + int departed{0}; + double departure{0.0}; + double cant{0.0}; + std::string layer; +}; + +glm::dvec2 chord_direction(glm::dvec3 const &Start, glm::dvec3 const &End) +{ + auto const direction{plan_of(End - Start)}; + return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0}; +} + +path_ends const &ends_before(std::vector const &Moving, std::vector const &Before, TTrack const *Track) +{ + return Before[static_cast(std::find(Moving.begin(), Moving.end(), Track) - Moving.begin())]; +} + +bool lies_on(int const Record, TTrack const &Track) +{ + bezier const curve{Track.m_paths.front()}; + std::array constexpr samples{0.0, 0.5, 1.0}; + return std::ranges::all_of(samples, [&](double const T) { return alignment_store::on_record(Record, curve.point(T), alignment_store::tolerance).has_value(); }); +} + +bool covers_record(int const Record, std::vector const &Moving) +{ + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && track->eType == tt_Normal && false == track->m_paths.empty() && std::find(Moving.begin(), Moving.end(), track) == Moving.end() && lies_on(Record, *track)) + return false; + return true; +} + +bool carried_rigidly(int const Record, std::vector const &Tracks, std::vector const &Moving, std::vector const &Before, record_drift &Drift) +{ + auto const &first{ends_before(Moving, Before, Tracks.front())}; + auto const &path{Tracks.front()->m_paths.front()}; + auto const &start{path.points[segment_data::point::start]}; + auto const from{chord_direction(first.first, first.second)}; + auto const to{chord_direction(start, path.points[segment_data::point::end])}; + auto const angle{signed_angle(from, to)}; + auto const carried = [&](glm::dvec3 const &Old, glm::dvec3 const &Current) { return glm::distance(plan_of(start) + turned(plan_of(Old) - plan_of(first.first), angle), plan_of(Current)) <= 0.01; }; + for (auto const *track : Tracks) + { + auto const &was{ends_before(Moving, Before, track)}; + auto const &now{track->m_paths.front()}; + if (false == carried(was.first, now.points[segment_data::point::start]) || false == carried(was.second, now.points[segment_data::point::end])) + return false; + } + if (glm::distance(first.first, start) > 1e-4 || std::abs(angle) > 1e-7) + { + alignment_store::transform(Record, first.first, from, start, to); + ++Drift.followed; + } + return true; +} + +void measure_departure(int const Record, std::vector const &Tracks, record_drift &Drift) +{ + auto const &records{alignment_store::records()}; + auto const gauge{records[Record].design.norms.gauge}; + for (auto const *track : Tracks) + { + if (track->m_editorremoved) + continue; + auto const &path{track->m_paths.front()}; + bezier const curve{path}; + double largest{0.0}; + for (auto const t : {0.0, 0.5, 1.0}) + { + auto const found{alignment_store::on_record(Record, curve.point(t), 1000.0)}; + largest = std::max(largest, found.has_value() ? found->offset : 1000.0); + } + if (largest > alignment_store::tolerance) + { + ++Drift.departed; + Drift.departure = std::max(Drift.departure, largest); + Drift.layer = scene::Layers.layer(records[Record].layer).name; + continue; + } + for (auto const end : {0, 1}) + if (auto const found{alignment_store::on_record(Record, curve.point(end), alignment_store::tolerance)}) + { + auto const actual{gauge * std::sin(std::abs(glm::radians(static_cast(path.rolls[end]))))}; + Drift.cant = std::max(Drift.cant, std::abs(actual - alignment_store::cant_at(Record, found->chainage))); + } + } +} + +std::vector drift_notes(record_drift const &Drift) +{ + std::vector notes; + 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_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; +} + +switch_store::record switch_entry(editor_track::switch_template const &Shape) +{ + switch_store::record entry; + if (Shape.source == nullptr) + { + entry.type = Shape.type; + entry.rail = Shape.rail; + entry.radius = Shape.radius; + entry.ratio = Shape.ratio; + entry.a = Shape.a; + entry.b = Shape.b; + entry.source = "designed"; + } + else if (auto const known{switch_store::of(*Shape.source)}; known.has_value() && known->kind == "switch") + { + entry = *known; + entry.number = "-"; + } + else + { + // 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"; + } + return entry; +} + +template +bool edit_dashed(char const *Label, std::string &Value) +{ + std::array text{}; + std::snprintf(text.data(), text.size(), "%s", Value == "-" ? "" : Value.c_str()); + if (false == ImGui::InputText(Label, text.data(), text.size(), ImGuiInputTextFlags_EnterReturnsTrue) && false == ImGui::IsItemDeactivatedAfterEdit()) + return false; + Value = text[0] != '\0' ? text.data() : "-"; + return true; +} + +} // namespace + +void editor_mode::records_watch_begin() +{ + auto &watch{m_record_watch}; + watch = {}; + watch.active = true; + watch.moving = m_track_drag; + for (auto const &link : m_track_gizmo_drag.switches) + for (auto const &member : link.cluster) + if (std::find(watch.moving.begin(), watch.moving.end(), member.first) == watch.moving.end()) + watch.moving.push_back(member.first); + for (auto *track : watch.moving) + { + auto const &path{track->m_paths.front()}; + watch.before.emplace_back(path.points[segment_data::point::start], path.points[segment_data::point::end]); + if (track->eType != tt_Normal) + continue; + if (auto const found{alignment_store::on_track(*track)}) + watch.on[found->record].push_back(track); + } + for (auto const &[record, tracks] : watch.on) + if (covers_record(record, watch.moving)) + watch.whole.insert(record); +} + +// what the drag of the gizmo did to the kept alignments and switches: an alignment all of whose paths were moved or turned as a +// whole goes along with them, paths pulled out of an alignment or given another cant are told about, as are switches bent out +// of the shape of their type; switches moved as a whole take their records along +void editor_mode::records_watch_end() +{ + auto &watch{m_record_watch}; + if (false == watch.active) + return; + watch.active = false; + record_drift drift; + for (auto const &[record, tracks] : watch.on) + if (watch.whole.count(record) == 0 || false == carried_rigidly(record, tracks, watch.moving, watch.before, drift)) + measure_departure(record, tracks, drift); + auto notes{drift_notes(drift)}; + switch_store::follow(); + for (auto const *track : watch.moving) + if (track->eType == tt_Switch && false == track->m_editorremoved) + if (auto const known{switch_store::named(*track)}; known.has_value() && false == switch_store::of(*track).has_value()) + notes.push_back(format(STR_C("Switch %s lost the shape of its type %s: Ctrl+Z takes it back"), track->name().c_str(), switch_store::label(*known).c_str())); + records_to_history(); + if (false == notes.empty()) + { + std::string text; + for (auto const ¬e : notes) + text += (text.empty() ? "" : "\n") + note; + ui()->set_status(text, drift.departed > 0 || drift.cant > 5.0 || notes.size() > static_cast(drift.followed > 0 ? 1 : 0)); + WriteLog("Editor: " + text, logtype::generic); + } + watch = {}; +} + +void editor_mode::records_to_history() +{ + auto alignments{alignment_store::take_changes()}; + auto switches{switch_store::take_changes()}; + if (m_history.empty() || m_history.back().action != EditorSnapshot::Action::TrackEdit) + return; + auto &last{m_history.back()}; + if (alignments.has_value() && false == last.alignments.has_value()) + last.alignments = std::move(alignments); + if (switches.has_value() && false == last.switch_records.has_value()) + last.switch_records = std::move(switches); +} + +// the switch is kept in the scenery with its type: of the catalogue template, of the switch of the scenery it was copied +// from, or measured; on a curve it is an arc switch, one-sided when it diverges to the side the curve turns to +void editor_mode::record_switch(TTrack &Track, editor_track::switch_template const &Shape) +{ + if (Track.eType != tt_Switch || Track.m_paths.size() < 2 || Track.name().empty() || scene::Layers.empty()) + return; + auto entry{switch_entry(Shape)}; + entry.kind = "switch"; + entry.name = Track.name(); + entry.parts = {entry.name}; + switch_store::place(entry, Track); + switch_store::classify(entry, Track); + switch_store::put(entry, alignment_store::layer_of(Track)); +} + +// a double switch is kept as one record of its two switches, with the type, the size and the side of the first one +void editor_mode::record_double(TTrack &First, TTrack &Second, editor_track::switch_template const &Shape) +{ + if (First.eType != tt_Switch || Second.eType != tt_Switch || First.m_paths.size() < 2 || First.name().empty() || Second.name().empty() || scene::Layers.empty()) + return; + auto entry{switch_entry(Shape)}; + entry.kind = "double"; + entry.name = First.name(); + entry.parts = {First.name(), Second.name()}; + switch_store::place(entry, First); + switch_store::classify(entry, First); + switch_store::put(entry, alignment_store::layer_of(First)); +} + +void editor_mode::record_connection(connection::kind const Kind, std::vector const &Switches, connection::result const &Plan, editor_track::switch_template const &Shape) +{ + if (Switches.empty() || Switches.front()->eType != tt_Switch || Switches.front()->m_paths.size() < 2 || scene::Layers.empty()) + return; + auto entry{switch_entry(Shape)}; + entry.kind = "connection"; + entry.name = Switches.front()->name(); + for (auto const *track : Switches) + if (track->name().empty()) + return; + else + entry.parts.push_back(track->name()); + switch_store::place(entry, *Switches.front()); + switch_store::classify(entry, *Switches.front()); + entry.type = connection::word(Kind); + entry.a = Plan.spacing; + entry.b = Plan.straights.empty() ? 0.0 : Plan.straights.front(); + switch_store::put(entry, alignment_store::layer_of(*Switches.front())); +} + +std::optional editor_mode::double_of(TTrack const &Track) const +{ + auto const of_switch = [](TTrack const &Switch) -> std::optional { + if (Switch.eType != tt_Switch || Switch.m_editorremoved) + return std::nullopt; + auto const known{switch_store::named(Switch)}; + if (false == known.has_value() || known->kind != "double" || known->parts.size() != 2) + return std::nullopt; + auto *first{simulation::Paths.find(known->parts[0])}; + auto *second{simulation::Paths.find(known->parts[1])}; + for (auto const *part : {first, second}) + if (part == nullptr || part->m_editorremoved || part->eType != tt_Switch || part->m_paths.size() != 2) + return std::nullopt; + if (first == second) + return std::nullopt; + auto const start{second->m_paths.front().points[segment_data::point::start]}; + for (auto const &[other, point] : editor_track::connected_points(*first, first->m_paths.front().points[segment_data::point::end])) + { + if (other->m_editorremoved || other->eType != tt_Normal || other->m_paths.size() != 1) + continue; + auto const &path{other->m_paths.front()}; + auto const opposite{path.points[point.kind == editor_track::point_kind::start ? segment_data::point::end : segment_data::point::start]}; + if (glm::distance(opposite, start) < geometry::same_point) + return double_switch{first, other, second}; + } + return std::nullopt; + }; + if (Track.eType == tt_Switch) + return of_switch(Track); + if (Track.eType != tt_Normal || Track.m_editorremoved || Track.m_paths.size() != 1) + return std::nullopt; + for (auto const index : {segment_data::point::start, segment_data::point::end}) + for (auto const &[other, point] : editor_track::connected_points(Track, Track.m_paths.front().points[index])) + if (other->eType == tt_Switch) + if (auto const found{of_switch(*other)}; found.has_value() && found->link == &Track) + return found; + return std::nullopt; +} + +std::vector editor_mode::double_paths(double_switch const &Set) +{ + auto link{Set.link->m_paths.front()}; + auto const joint{Set.first->m_paths.front().points[segment_data::point::end]}; + if (glm::distance(link.points[segment_data::point::start], joint) > glm::distance(link.points[segment_data::point::end], joint)) + link = geometry::reversed(link); + return {Set.first->m_paths[0], Set.first->m_paths[1], Set.second->m_paths[0], Set.second->m_paths[1], link}; +} + +void editor_mode::record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base, double const Middle) +{ + if (Parts.empty() || scene::Layers.empty()) + return; + switch_store::record entry; + entry.kind = Kind; + entry.name = Base; + entry.type = Type; + entry.radius = Radius; + entry.middle = Middle; + entry.ratio = Angle > 1e-9 ? 1.0 / std::tan(Angle) : 0.0; + for (auto const *part : Parts) + if (part != nullptr && false == part->name().empty() && (Kind == "crossing" || part->eType == tt_Switch)) + entry.parts.push_back(part->name()); + if (entry.parts.empty()) + return; + auto const *first{simulation::Paths.find(entry.parts.front())}; + if (first == nullptr || first->m_paths.empty()) + return; + switch_store::place(entry, *first); + switch_store::put(entry, alignment_store::layer_of(*first)); +} + +void editor_mode::render_switch_record(TTrack const &Track) const +{ + auto const known{switch_store::of(Track)}; + if (false == known.has_value()) + { + ImGui::TextDisabled("%s", STR_C("The scenery keeps no type for this switch, it's read from its shape")); + return; + } + auto entry{*known}; + ImGui::Text(STR_C("Type kept in the scenery: %s"), switch_store::label(entry).c_str()); + if (ImGui::IsItemHovered()) + { + 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(); + } + if (changed) + { + switch_store::put(entry, entry.layer); + switch_store::settle(); + } +} 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 new file mode 100644 index 00000000..a176a4b8 --- /dev/null +++ b/application/editormoderoute.cpp @@ -0,0 +1,1688 @@ +/* +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: +*/ + + +#include "stdafx.h" +#include "application/editormode.h" +#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" +#include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" + +#include "utilities/Globals.h" +#include "rendering/renderer.h" +#include "world/Track.h" +#include "world/TrackFeatures.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include "imgui/ImGuizmo.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "application/editormodetrackshared.h" + +using namespace track_mode; + +bool editor_mode::route_active() const +{ + return ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::route && false == m_route.chain.tracks.empty(); +} + +void editor_mode::route_recommend(alignment::vertex &Vertex) const +{ + auto const recommended{alignment::recommend(m_route.design.speed, Vertex.radius, m_route.design.norms)}; + Vertex.cant = recommended.cant; + Vertex.transition_in = recommended.transition; + Vertex.transition_out = recommended.transition; +} + +void editor_mode::route_bind_ends() +{ + auto const &chain{m_route.chain}; + auto &design{m_route.design}; + design.start = chain.start; + design.end = chain.end; + design.start_direction = glm::normalize(plan_of(chain.start_direction)); + design.end_direction = glm::normalize(plan_of(chain.end_direction)); + design.start_grade = grade_of(chain.start_direction); + design.end_grade = grade_of(chain.end_direction); + design.start_radius = chain.start_radius; + design.end_radius = chain.end_radius; + design.start_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, false, m_straights.tolerance)); + design.end_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, true, m_straights.tolerance)); +} + +void editor_mode::route_reset() +{ + auto &route{m_route}; + route.chain = {}; + route.error.clear(); + route.status.clear(); + route.vertex = -1; + route.grip = -1; + route.result = {}; + if (route.from == nullptr || route.to == nullptr) + return; + if (false == editor_track::find_chain(route.from, route.to, route.chain, route.error, true)) + return; + if (false == route.chain.switches.empty()) + route.status = format(STR_C("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"), + route.chain.switches.size()); + + auto const &chain{route.chain}; + alignment::design design; + design.norms = route.design.norms; + design.shape = route.design.shape; + design.transition_pieces = route.design.transition_pieces; + design.speed = chain.velocity > 0.0 ? chain.velocity : 100.0; + route.design = design; + route_bind_ends(); + design = route.design; + + glm::dvec2 intersection; + if (false == alignment::collinear(design)) + { + alignment::vertex vertex; + vertex.radius = chain.radius > 0.0 ? std::max(10.0, std::round(chain.radius / 10.0) * 10.0) : 1000.0; + route_recommend(vertex); + if (alignment::tangent_intersection(design, intersection)) + { + route.design.vertices.push_back(vertex); + } + else + { + auto const distance{plan_distance(design.start, design.end)}; + vertex.offset = distance / 3.0; + route.design.vertices.push_back(vertex); + route.design.vertices.push_back(vertex); + } + } + route_update(); +} + +bool editor_mode::route_from_curve(TTrack &Track) +{ + editor_track::curve curve; + if (false == editor_track::find_curve(Track, m_straights.tolerance, m_route.design.norms.gauge, curve)) + { + m_route.chain = {}; + m_route.result = {}; + m_route.vertex = -1; + m_route.grip = -1; + m_route.error = STR_C("No curve here: LMB on a curve"); + return false; + } + m_route.from = curve.from; + m_route.to = curve.to; + route_reset(); + if (m_route.chain.tracks.empty()) + return false; + auto &design{m_route.design}; + auto vertex{route_vertex_of(curve)}; + m_route.designed = curve.designed.has_value() ? 1 : 0; + if (curve.reversals == 0) + { + design.vertices = {vertex}; + } + else + { + auto const distance{plan_distance(design.start, design.end)}; + vertex.offset = distance / 3.0; + vertex.compound = false; + vertex.reverse_turn = false; + design.vertices = {vertex, vertex}; + } + route_update(); + 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; +} + +alignment::vertex editor_mode::route_vertex_of(editor_track::curve const &Curve) const +{ + if (Curve.designed.has_value()) + return *Curve.designed; + alignment::vertex vertex; + vertex.radius = Curve.radius > 0.0 ? std::max(1.0, Curve.radius) : 1000.0; + vertex.transition_in = std::max(0.0, Curve.transition_in); + vertex.transition_out = std::max(0.0, Curve.transition_out); + vertex.cant = std::max(0.0, Curve.cant); + vertex.reverse_turn = std::abs(Curve.turn) > glm::pi(); + compound_from(Curve, vertex); + return vertex; +} + + +bool editor_mode::route_load(TTrack &Track) +{ + m_route.designed = 0; + if (false == (m_route.single ? route_from_curve(Track) : route_from_line(Track))) + return false; + route_read_stored(Track); + return true; +} + +// the design kept in the scenery for the fragment takes the place of the one read from the paths; its chainage is carried on +void editor_mode::route_read_stored(TTrack &Track) +{ + auto &route{m_route}; + route.departed = -1; + route.departure = 0.0; + route.origin = 0.0; + if (route.chain.tracks.empty()) + return; + auto const chain{route.chain}; + bool reversed; + auto index{alignment_store::find(chain.start, chain.end, reversed)}; + route.departed = alignment_store::departed(chain.tracks, route.departure); + auto const loose{std::any_of(chain.tracks.begin(), chain.tracks.end(), [](TTrack const *Track) { return false == alignment_store::on_track(*Track).has_value(); })}; + bool digitized{false}; + int parts{0}; + int skipped{0}; + std::string failure; + if (index < 0 && route.departed < 0 && loose && false == route.single) + { + parts = digitize_parts(chain, &failure, skipped); + if (parts == 1 && skipped == 0) + { + alignment_store::settle(); + index = alignment_store::find(chain.start, chain.end, reversed); + digitized = true; + } + else if (parts > 0) + { + alignment_store::settle(); + route_from_line(Track); + route.departed = alignment_store::departed(route.chain.tracks, route.departure); + } + } + if (index >= 0 && false == reversed && false == route.single) + { + route_take_stored(index); + } + else if (auto const km{alignment_store::chainage_at(chain.start)}) + { + auto const ahead{chain.start + glm::dvec3{route.design.start_direction.x, 0.0, route.design.start_direction.y}}; + auto const further{alignment_store::chainage_at(ahead)}; + if (further.has_value() && *further > *km) + route.origin = *km; + } + if (digitized && route.designed == 0) + route.status += STR_C("\nThe line is digitized now, a straight"); + else if (digitized) + route.status += format(STR_C("\nThe line is digitized now, curves kept in the scenery: %d"), route.designed); + else if (parts > 0) + route.status += format(STR_C("\nThe line is digitized in parts: %d, paths left out: %d"), parts, skipped) + (failure.empty() ? std::string{} : " - " + failure); + else if (skipped > 0 && loose && skipped < static_cast(chain.tracks.size())) + route.status += format(STR_C("\nPaths left out of the digitized line: %d"), skipped) + (failure.empty() ? std::string{} : " - " + failure); + else if (false == failure.empty()) + route.status += STR_C("\nThe line can't be digitized: ") + failure; + else if (route.designed > 0) + route.status += format(STR_C("\nCurves read from the design kept in the scenery: %d"), route.designed); + else if (false == route.design.vertices.empty()) + route.status += STR_C("\nCurves recognized from the paths, the scenery keeps no design for them"); +} + +void editor_mode::route_take_stored(int const Index) +{ + auto &route{m_route}; + auto const &records{alignment_store::records()}; + if (Index < 0 || Index >= static_cast(records.size())) + return; + auto const &stored{records[Index]}; + auto &design{route.design}; + design.vertices = stored.design.vertices; + design.shape = stored.design.shape; + design.speed = stored.design.speed; + design.norms = stored.design.norms; + design.transition_pieces = stored.design.transition_pieces; + design.arc_piece_angle = stored.design.arc_piece_angle; + route.designed = static_cast(design.vertices.size()); + route.origin = stored.origin; + route.vertex = -1; + route.grip = -1; + route_update(); +} + +void editor_mode::render_route_stored() +{ + auto &route{m_route}; + auto km{route.origin / 1000.0}; + if (begin_fields("##routeorigin")) + { + 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(); + } + 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); + route.status = STR_C("The kept design is loaded, applying it lays the paths by it again"); + } + item_tooltip("Loads the design kept in the scenery in place of the one read from the paths; nothing changes until it's applied"); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Forget the kept design"))) + { + alignment_store::forget(route.departed); + alignment_store::settle(); + route.departed = -1; + 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"); +} + +void editor_mode::route_store() +{ + auto &route{m_route}; + if (false == route.result.valid || route.chain.tracks.empty() || scene::Layers.empty()) + return; + alignment_store::store(route.design, route.result, route.origin, alignment_store::layer_of(*route.chain.tracks.front())); + route.departed = -1; + route.departure = 0.0; +} + +bool editor_mode::digitize_drawn(std::vector const &Created, bool const Alone) +{ + std::vector drawn; + for (auto *track : Created) + if (track != nullptr && track->eType == tt_Normal && false == track->m_editorremoved && false == track->m_paths.empty()) + drawn.push_back(track); + if (drawn.empty() || scene::Layers.empty()) + return false; + editor_track::chain chain; + std::string error; + if (editor_track::find_run(*drawn.front(), kDrawnReach, chain, error) && digitize_chain(chain)) + return true; + if (Alone && editor_track::find_chain(drawn.front(), drawn.back(), chain, error) && chain.tracks.size() == drawn.size() && digitize_chain(chain)) + return true; + WriteLog("Editor: the drawn paths don't make a line of straights, arcs and transition curves, no design kept for them", logtype::generic); + return false; +} + +void editor_mode::digitize_all(int *Kept, int *Failed) +{ + std::set visited; + int kept{0}; + int failed{0}; + std::string first; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty() || false == editor_track::is_supported(*track) || visited.count(track) > 0) + continue; + if (alignment_store::on_track(*track).has_value()) + continue; + editor_track::chain chain; + std::string error; + if (false == editor_track::find_run(*track, kDrawnReach, chain, error) || chain.tracks.empty()) + { + visited.insert(track); + continue; + } + visited.insert(chain.tracks.begin(), chain.tracks.end()); + std::string reason; + kept += digitize_parts(chain, &reason, failed); + if (first.empty()) + 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; + WriteLog("Editor: " + text, logtype::generic); + ui()->set_status(text, failed > 0); + if (false == m_route.chain.tracks.empty()) + route_load(*m_route.chain.tracks[m_route.chain.tracks.size() / 2]); +} + +int editor_mode::digitize_parts(editor_track::chain const &Chain, std::string *Reason, int &Skipped) +{ + auto const count{Chain.tracks.size()}; + if (count == 0 || std::all_of(Chain.tracks.begin(), Chain.tracks.end(), [](TTrack const *Track) { return alignment_store::on_track(*Track).has_value(); })) + return 0; + TTrack *culprit{nullptr}; + std::string reason; + if (digitize_chain(Chain, &reason, &culprit)) + return 1; + if (count == 1) + { + ++Skipped; + 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; + return 0; + } + auto const found{static_cast(std::find(Chain.tracks.begin(), Chain.tracks.end(), culprit) - Chain.tracks.begin())}; + auto const split{found < count ? std::max(1, found) : count / 2}; + int parts{0}; + for (auto const &[first, last] : {std::pair{0, split - 1}, std::pair{split, count - 1}}) + { + editor_track::chain part; + std::string error; + if (editor_track::find_chain(Chain.tracks[first], Chain.tracks[last], part, error) && part.tracks.size() == last - first + 1) + parts += digitize_parts(part, Reason, Skipped); + else + Skipped += static_cast(last - first + 1); + } + return parts; +} + +bool editor_mode::digitize_chain(editor_track::chain const &Chain, std::string *Reason, TTrack **Culprit) +{ + if (Chain.tracks.empty()) + return false; + 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()}; + for (auto *track : Chain.tracks) + for (auto const end : {segment_data::point::start, segment_data::point::end}) + if (auto const d{glm::distance(plan_of(track->m_paths.front().points[end]), Point)}; d < distance) + { + distance = d; + found = track; + } + return found; + }; + alignment::design design; + design.norms = m_route.design.norms; + design.transition_pieces = m_route.design.transition_pieces; + design.arc_piece_angle = m_route.design.arc_piece_angle; + design.speed = Chain.velocity > 0.0 ? Chain.velocity : 100.0; + design.start = Chain.start; + design.end = Chain.end; + if (glm::length(plan_of(Chain.start_direction)) < 1e-9 || glm::length(plan_of(Chain.end_direction)) < 1e-9) + return false; + design.start_direction = glm::normalize(plan_of(Chain.start_direction)); + design.end_direction = glm::normalize(plan_of(Chain.end_direction)); + design.start_grade = grade_of(Chain.start_direction); + design.end_grade = grade_of(Chain.end_direction); + design.start_radius = Chain.start_radius; + design.end_radius = Chain.end_radius; + auto const preferred{m_route.design.shape}; + auto const other{preferred == alignment::transition_shape::clothoid ? alignment::transition_shape::cubic_parabola : alignment::transition_shape::clothoid}; + alignment::result result; + alignment::design best; + alignment::result bestresult; + double closest{std::numeric_limits::max()}; + TTrack *farthest{nullptr}; + bool fits{false}; + for (auto const attempt : {0, 1, 2, 3}) + { + auto const shape{attempt % 2 == 0 ? preferred : other}; + design.shape = shape; + if (attempt == 2) + { + design.norms.arc_min = 0.0; + design.norms.arc_min_factor = 0.0; + } + design.vertices.clear(); + for (auto const &entry : editor_track::recognize_line(Chain, m_straights.tolerance, design.norms.gauge, shape)) + { + if (entry.kink) + { + if (Reason != nullptr) + *Reason = format(STR_C("the straights meet with no curve at path %s"), name(nearest(entry.position)).c_str()); + if (Culprit != nullptr) + *Culprit = nearest(entry.position); + return false; + } + auto vertex{route_vertex_of(entry.shape)}; + vertex.position = entry.position; + design.vertices.push_back(vertex); + } + if (design.vertices.size() >= 2) + { + design.vertices.front().offset = std::max(0.0, glm::dot(design.vertices.front().position - plan_of(design.start), design.start_direction)); + design.vertices.back().offset = std::max(0.0, glm::dot(plan_of(design.end) - design.vertices.back().position, design.end_direction)); + } + result = alignment::compute(design); + if (alignment_store::follows(result, Chain.tracks)) + { + fits = true; + break; + } + TTrack *worst{nullptr}; + if (auto const away{alignment_store::deviation(result, Chain.tracks, worst)}; away < closest) + { + closest = away; + farthest = worst; + best = design; + bestresult = result; + } + } + double fit{0.0}; + if (false == fits && closest <= kDigitizeFit) + { + design = best; + result = bestresult; + fit = closest; + fits = true; + } + if (false == fits) + { + if (Culprit != nullptr) + *Culprit = farthest; + if (Reason != nullptr) + *Reason = closest < std::numeric_limits::max() ? format(STR_C("the curves read from the paths leave path %s by %.2f m"), name(farthest).c_str(), closest) : std::string{STR_C("the paths don't make a line of straights, arcs and transition curves")}; + return false; + } + auto const &records{alignment_store::records()}; + std::set covered; + for (auto *track : Chain.tracks) + if (auto const found{alignment_store::on_track(*track)}) + covered.insert(found->record); + double origin{0.0}; + for (auto const index : covered) + { + auto const &old{records[index]}; + auto const start{alignment_store::chainage_on(result, old.design.start)}; + auto const end{alignment_store::chainage_on(result, old.design.end)}; + if (false == start.has_value() || false == end.has_value()) + return false; + if (index == *covered.begin() && *end > *start) + origin = old.origin - *start; + } + auto const layer{covered.empty() ? alignment_store::layer_of(*Chain.tracks.front()) : records[*covered.begin()].layer}; + for (auto index{covered.rbegin()}; index != covered.rend(); ++index) + alignment_store::forget(*index); + alignment_store::store(design, result, origin, layer, "drawn", fit); + return true; +} + +bool editor_mode::route_from_line(TTrack &Track) +{ + auto &route{m_route}; + editor_track::chain run; + if (false == editor_track::find_run(Track, kLineReach, run, route.error)) + return false; + route.from = run.tracks.front(); + route.to = run.tracks.back(); + route_reset(); + if (route.chain.tracks.empty()) + return false; + auto &design{route.design}; + auto const found{editor_track::recognize_line(route.chain, m_straights.tolerance, design.norms.gauge, design.shape)}; + route.designed = 0; + if (false == found.empty()) + { + design.vertices.clear(); + for (auto const &entry : found) + { + alignment::vertex vertex; + if (entry.kink) + { + vertex.radius = std::max(10.0, std::round(alignment::recommend(design.speed, 1000.0, design.norms).radius_min / 10.0) * 10.0); + vertex.transition_in = 0.0; + vertex.transition_out = 0.0; + vertex.cant = 0.0; + } + else + { + vertex = route_vertex_of(entry.shape); + if (entry.shape.designed.has_value()) + ++route.designed; + } + vertex.position = entry.position; + design.vertices.push_back(vertex); + } + if (design.vertices.size() >= 2) + { + design.vertices.front().offset = std::max(0.0, glm::dot(design.vertices.front().position - plan_of(design.start), design.start_direction)); + design.vertices.back().offset = std::max(0.0, glm::dot(plan_of(design.end) - design.vertices.back().position, design.end_direction)); + } + route_update(); + } + 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; +} + +void editor_mode::route_apply() +{ + auto &route{m_route}; + route.status.clear(); + if (false == route.result.valid || route.chain.tracks.empty()) + return; + editor_track::chain chain; + if (false == editor_track::find_chain(route.from, route.to, chain, route.error, true)) + return; + if (glm::distance(chain.start, route.chain.start) > 1e-3 || glm::distance(chain.end, route.chain.end) > 1e-3) + { + route.error = STR_C("The fragment changed since it was selected, select it again"); + return; + } + auto involved{chain.tracks}; + for (auto const &passage : chain.switches) + 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; + // takes back what was done before the paths were re-laid + auto const undo = [&]() { + 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}); + } + }; + auto *from{route.from}; + auto *to{route.to}; + auto const startextension{route.result.start_extension}; + auto const endextension{route.result.end_extension}; + if (startextension > 0.0 || endextension > 0.0) + { + auto const before{chain}; + if (startextension > 0.0) + from = editor_track::take_straight(editor_track::straight_beyond(before, false, m_straights.tolerance), from, before.start, route.result.start, states, created); + if (endextension > 0.0) + to = editor_track::take_straight(editor_track::straight_beyond(before, true, m_straights.tolerance), to, before.end, route.result.end, states, created); + std::string error; + auto const found{editor_track::find_chain(from, to, chain, error, true)}; + auto const startoff{found ? plan_distance(chain.start, route.result.start) : 0.0}; + auto const endoff{found ? plan_distance(chain.end, route.result.end) : 0.0}; + if (false == found || startoff > 5.0 || endoff > 5.0) + { + auto const startrun{editor_track::straight_beyond(before, false, m_straights.tolerance)}; + auto const endrun{editor_track::straight_beyond(before, true, m_straights.tolerance)}; + WriteLog(format("editor: cutting the straights failed: found %d, start %.2f m off (extension %.2f, run %zu paths %.2f m), end %.2f m off (extension %.2f, run %zu paths %.2f m), chain %zu paths, from %s, to %s, error '%s'", found ? 1 : 0, startoff, + startextension, startrun.tracks.size(), startrun.length, endoff, endextension, endrun.tracks.size(), endrun.length, chain.tracks.size(), from != nullptr ? from->name().c_str() : "-", to != nullptr ? to->name().c_str() : "-", error.c_str())); + undo(); + route.error = error.empty() ? format(STR_C("The adjoining straights can't be cut at the ends of the curve: the start misses by %.1f m, the end by %.1f m"), startoff, endoff) : error; + return; + } + if (false == tracks_editable(chain.tracks, route.error)) + { + undo(); + return; + } + } + for (auto *track : chain.tracks) + if (std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == track; })) + states.emplace_back(track, editor_track::capture(*track)); + std::vector relaid; + if (chain.switches.empty()) + { + auto pieces{alignment::pieces(route.result, route.design, chain.tracks.size())}; + editor_track::keep_heights(chain, pieces); + relaid = editor_track::relay(chain, pieces, &to); + } + else if (false == route_relay_through_switches(chain, states, relaid, to)) + { + undo(); + return; + } + // the straights were cut along their own direction, the curve ends where the design says: the ends are brought together + auto const added{relaid.size()}; + created.insert(created.end(), relaid.begin(), relaid.end()); + push_track_snapshot(std::move(states), std::move(created)); + + route.from = from; + route.to = to; + editor_track::find_chain(route.from, route.to, route.chain, route.error, true); + if (startextension > 0.0 || endextension > 0.0) + { + route_bind_ends(); + auto &vertices{route.design.vertices}; + if (vertices.size() >= 2) + { + vertices.front().offset += startextension; + vertices.back().offset += endextension; + } + route_update(); + } + route_store(); + records_to_history(); + 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(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); +} + +// the switches of the chain are moved as a whole onto the designed line, the regular paths between them re-laid +bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, std::vector> &States, std::vector &Created, TTrack *&Last) +{ + auto &route{m_route}; + auto const &result{route.result}; + 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 + { + TTrack *track; + double from, length; // chainage along the new line + glm::dvec3 entry; // of the main track, where the chain comes in + double angle; + }; + // chainages along the old line, the switches keep their distance from the nearer end of the chain + double before{0.0}; + std::vector starts; + std::size_t next{0}; + for (std::size_t i = 0; i <= Chain.tracks.size(); ++i) + { + for (; next < Chain.switches.size() && Chain.switches[next].before == i; ++next) + { + starts.push_back(before); + before += bezier{Chain.switches[next].track->m_paths.front()}.plan_length(); + } + if (i < Chain.tracks.size()) + before += bezier{Chain.tracks[i]->m_paths.front()}.plan_length(); + } + auto const oldlength{before}; + std::vector placements; + std::vector breaks; + for (std::size_t k = 0; k < Chain.switches.size(); ++k) + { + auto const &passage{Chain.switches[k]}; + auto const &main{passage.track->m_paths.front()}; + auto const length{bezier{main}.plan_length()}; + 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 = 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]}; + auto const exit{main.points[passage.forward ? segment_data::point::end : segment_data::point::start]}; + bezier const curve{main}; + auto const direction{passage.forward ? plan(curve.first(0.0)) : -plan(curve.first(1.0))}; + auto const first{alignment::evaluate(result, from)}; + auto const last{alignment::evaluate(result, from + length)}; + auto const angle{signed_angle(direction, plan(first.direction))}; + // where the other end of the main track goes, set against the line + auto const moved{plan_of(first.position) + turned(plan_of(exit - entry), angle)}; + auto const departure{glm::distance(moved, plan_of(last.position))}; + if (departure > 0.02) + { + route.error = format(STR_C("The switch %s would leave the designed line by %.2f m at its other end: it lies in a curve of the design. " + "Move the vertices so that the switch falls on a straight"), + name(passage.track).c_str(), departure); + return false; + } + placements.push_back({passage.track, from, length, entry, angle}); + breaks.push_back(from); + breaks.push_back(from + length); + } + // the regular parts between the switches, one interval each, the switches taking the ones between + auto const parts{editor_track::chain_parts(Chain)}; + std::vector counts; + std::vector partof; + std::size_t part{0}; + for (std::size_t k = 0; k <= Chain.switches.size(); ++k) + { + auto const first{k == 0 ? std::size_t{0} : Chain.switches[k - 1].before}; + auto const last{k == Chain.switches.size() ? Chain.tracks.size() : Chain.switches[k].before}; + counts.push_back(last - first); + partof.push_back(last > first ? static_cast(part++) : -1); + if (k < Chain.switches.size()) + counts.push_back(1); + } + if (part != parts.size()) + { + route.error = STR_C("The paths between the switches can't be read as a chain"); + return false; + } + std::vector intervals; + auto const pieces{alignment::pieces(result, route.design, 0, breaks, counts, &intervals)}; + std::vector> groups(parts.size()); + for (std::size_t i = 0; i < pieces.size(); ++i) + if (intervals[i] % 2 == 0 && partof[intervals[i] / 2] >= 0) + groups[partof[intervals[i] / 2]].push_back(pieces[i]); + for (std::size_t i = 0; i < parts.size(); ++i) + if (groups[i].size() < parts[i].tracks.size()) + { + route.error = STR_C("The designed line between the switches is too short for the paths there"); + return false; + } + + // all checked, the changes go in + auto const remember = [&](TTrack *Track) { + if (std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == Track; })) + States.emplace_back(Track, editor_track::capture(*Track)); + }; + for (auto const &place : placements) + { + auto joints{editor_track::joints(*place.track)}; + joints.erase(std::remove_if(joints.begin(), joints.end(), [&](editor_track::joint const &Joint) { return std::find(Chain.tracks.begin(), Chain.tracks.end(), Joint.other) != Chain.tracks.end(); }), joints.end()); + remember(place.track); + std::vector changed{place.track}; + for (auto const &joint : joints) + { + remember(joint.other); + if (std::find(changed.begin(), changed.end(), joint.other) == changed.end()) + changed.push_back(joint.other); + } + // the heights stay, the paths between the switches keep theirs too + auto target{alignment::evaluate(result, place.from).position}; + target.y = place.entry.y; + editor_track::place(*place.track, place.entry, target, place.angle); + editor_track::follow(*place.track, joints); + editor_track::commit(changed); + } + for (std::size_t i = 0; i < parts.size(); ++i) + { + auto group{groups[i]}; + editor_track::keep_heights(parts[i], group); + TTrack *last{nullptr}; + auto const added{editor_track::relay(parts[i], group, &last)}; + if (last != nullptr && parts[i].tracks.back() == Chain.tracks.back()) + Last = last; + Created.insert(Created.end(), added.begin(), added.end()); + } + return true; +} + +glm::dvec3 editor_mode::route_vertex_position(int const Vertex) const +{ + auto const &design{m_route.design}; + auto const &result{m_route.result}; + auto const count{static_cast(design.vertices.size())}; + glm::dvec2 const start{design.start.x, design.start.z}; + glm::dvec2 const end{design.end.x, design.end.z}; + glm::dvec2 position; + if (Vertex < static_cast(result.vertices.size()) && false == result.vertices.empty()) + position = result.vertices[Vertex]; + else if (count == 1) + { + if (false == alignment::tangent_intersection(design, position)) + position = (start + end) * 0.5; + } + else if (Vertex == 0) + position = start + glm::normalize(design.start_direction) * design.vertices.front().offset; + else if (Vertex == count - 1) + position = end - glm::normalize(design.end_direction) * design.vertices.back().offset; + else + position = design.vertices[Vertex].position; + + auto const height{Vertex < static_cast(result.vertex_elevations.size()) ? result.vertex_elevations[Vertex] : design.start.y + (design.end.y - design.start.y) * (Vertex + 1) / (count + 1.0)}; + return {position.x, height, position.y}; +} + +bool editor_mode::route_hit(int &Vertex, int &Grip) const +{ + if (false == route_active()) + return false; + screen_projection const projection; + float best = kHandleRadius * kHandleRadius; + int hit{-1}; + for (int i = 0; i < static_cast(m_route.design.vertices.size()); ++i) + { + if (auto const distance{projection.mouse_distance2(route_vertex_position(i))}; distance < best) + { + best = distance; + hit = i; + } + } + int griphit{-1}; + for (int i = 0; i < static_cast(m_route.result.vertex_chainages.size()) && m_route.result.length > 0.0; ++i) + { + if (auto const distance{projection.mouse_distance2(alignment::evaluate(m_route.result, m_route.result.vertex_chainages[i]).position)}; distance < best) + { + best = distance; + griphit = i; + hit = -1; + } + } + Vertex = hit; + Grip = griphit; + return hit >= 0 || griphit >= 0; +} + +void editor_mode::draw_route_overlay() const +{ + auto const &route{m_route}; + auto const &design{route.design}; + auto const &result{route.result}; + screen_projection const projection; + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); + auto const &start{result.valid ? result.start : design.start}; + auto const &end{result.valid ? result.end : design.end}; + + std::vector polygon{start}; + for (int i = 0; i < static_cast(design.vertices.size()); ++i) + polygon.push_back(route_vertex_position(i)); + polygon.push_back(end); + for (std::size_t i = 0; i + 1 < polygon.size(); ++i) + projection.line(drawlist, polygon[i], polygon[i + 1], IM_COL32(255, 210, 60, 150), 1.5f); + + if (result.length > 0.0 && result.profile.size() >= 2) + { + for (auto const &e : result.elements) + { + ImU32 const color = e.kind == alignment::element_kind::straight ? IM_COL32(255, 255, 255, 230) : e.kind == alignment::element_kind::arc ? overlay_color::grip : IM_COL32(255, 140, 30, 255); + 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, color, 3.0f); + previous = next; + } + ImVec2 screen; + 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}) + { + auto const &point{atend ? end : start}; + auto const direction{atend ? design.end_direction : design.start_direction}; + ImVec2 screen; + if (projection.project(point, screen)) + drawlist->AddRectFilled(ImVec2(screen.x - 5.0f, screen.y - 5.0f), ImVec2(screen.x + 5.0f, screen.y + 5.0f), IM_COL32(70, 140, 255, 255)); + auto const arrow{glm::dvec3{direction.x, 0.0, direction.y} * (atend ? 15.0 : -15.0)}; + projection.line(drawlist, point, point + arrow, IM_COL32(70, 140, 255, 255), 2.0f); + } + for (int i = 0; i < static_cast(design.vertices.size()); ++i) + { + ImVec2 screen; + if (false == projection.project(route_vertex_position(i), screen)) + continue; + drawlist->AddCircleFilled(screen, 6.0f, overlay_color::marked); + if (i == route.vertex) + drawlist->AddCircle(screen, 11.0f, overlay_color::highlight, 16, 2.5f); + if (i < static_cast(result.vertex_chainages.size()) && result.length > 0.0 && projection.project(alignment::evaluate(result, result.vertex_chainages[i]).position, screen)) + { + drawlist->AddCircleFilled(screen, 6.0f, overlay_color::grip, 4); + if (i == route.grip) + drawlist->AddCircle(screen, 11.0f, overlay_color::highlight, 16, 2.5f); + auto const *report{i < static_cast(result.curves.size()) ? &result.curves[i] : nullptr}; + auto radius{format("R %.0f", report != nullptr ? report->radius : design.vertices[i].radius)}; + if (report != nullptr && report->radii.size() >= 2) + { + radius = "R"; + for (std::size_t k = 0; k < report->radii.size(); ++k) + radius += format(k == 0 ? " %.0f" : " / %.0f", report->radii[k]); + } + drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y + 4.0f), overlay_color::grip, radius.c_str()); + } + drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y - 18.0f), overlay_color::marked, format("W%d", i + 1).c_str()); + } +} + +void editor_mode::render_route_gizmo() +{ + auto &route{m_route}; + auto const count{static_cast(route.design.vertices.size())}; + bool const grip{route.grip >= 0 && route.grip < count && route.grip < static_cast(route.result.vertex_chainages.size())}; + if (false == grip && (route.vertex < 0 || route.vertex >= count)) + { + m_route_dragging = false; + return; + } + + auto const position{grip ? alignment::evaluate(route.result, route.result.vertex_chainages[route.grip]).position : route_vertex_position(route.vertex)}; + glm::dvec3 moved{position}; + if (false == handle_drag_step(moved)) + { + if (m_route_dragging) + { + m_route_dragging = false; + route_apply(); + } + return; + } + m_route_dragging = true; + if (grip) + { + auto &vertex{route.design.vertices[route.grip]}; + auto const corner{route_vertex_position(route.grip)}; + auto const distance{plan_distance(moved, corner)}; + for (auto const &curve : route.result.curves) + { + if (curve.vertex != route.grip || vertex.reverse_turn) + continue; + auto const secant{1.0 / std::cos(curve.deflection * 0.5) - 1.0}; + if (secant > 1e-6) + vertex.radius = std::max(10.0, std::round(distance / secant)); + } + route_update(); + return; + } + auto &vertex{route.design.vertices[route.vertex]}; + glm::dvec2 const point{moved.x, moved.z}; + if (count >= 2) + { + if (route.vertex == 0) + vertex.offset = std::max(0.1, glm::dot(point - plan_of(route.design.start), glm::normalize(route.design.start_direction))); + else if (route.vertex == count - 1) + vertex.offset = std::max(0.1, glm::dot(plan_of(route.design.end) - point, glm::normalize(route.design.end_direction))); + else + vertex.position = point; + } + route_update(); +} + +void editor_mode::render_route_ui() +{ + auto &route{m_route}; + render_norms_choice(); + ImGui::Separator(); + if (false == route.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", route.error.c_str()); + if (route.chain.tracks.empty()) + return; + + 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(); + changed |= render_route_vertices(); + if (ImGui::CollapsingHeader(STR_C("Design speed and limits"))) + changed |= render_route_parameters(); + if (changed) + route_update(); + + 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::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); + 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; + } + + if (ImGui::TreeNode(STR_C("Norms"))) + { + auto &norms{design.norms}; + 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); + ImGui::TreePop(); + } + return changed; +} + +bool editor_mode::render_route_vertices() +{ + bool changed{false}; + auto const count{static_cast(m_route.design.vertices.size())}; + for (int i = 0; i < count; ++i) + { + ImGui::PushID(i); + changed |= render_route_vertex(i); + ImGui::PopID(); + } + return changed; +} + +bool editor_mode::render_route_vertex(int const Index) +{ + auto &route{m_route}; + auto &design{route.design}; + auto &vertex{design.vertices[Index]}; + auto const count{static_cast(design.vertices.size())}; + bool changed{false}; + if (count > 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); + if (ImGui::Checkbox(STR_C("Compound curve (several radii)"), &vertex.compound)) + { + changed = true; + if (vertex.compound && vertex.arcs.empty()) + vertex.arcs.push_back({std::max(10.0, std::round(vertex.radius * 0.6 / 10.0) * 10.0), 0.0, 1.0}); + } + item_tooltip("Curve made of arcs of different radii, each one its own path, joined with transitions"); + if (vertex.compound) + { + auto total{std::max(0.0, vertex.share)}; + for (auto const &arc : vertex.arcs) + total += std::max(0.0, arc.share); + auto const percent = [&](double const Share) { return total > 0.0 ? 100.0 * std::max(0.0, Share) / total : 0.0; }; + changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), 1).c_str(), ImGuiDataType_Double, &vertex.share, 0.01f, nullptr, nullptr, "%.2f"); + ImGui::SameLine(); + ImGui::TextDisabled("%.0f%%", percent(vertex.share)); + int removed{-1}; + for (int k = 0; k < static_cast(vertex.arcs.size()); ++k) + { + auto &arc{vertex.arcs[k]}; + ImGui::PushID(k); + ImGui::Separator(); + changed |= ImGui::DragScalar(format(STR_C("Radius R%d (m)"), k + 2).c_str(), ImGuiDataType_Double, &arc.radius, 5.0f, nullptr, nullptr, "%.0f"); + ImGui::SameLine(); + if (ImGui::SmallButton("x")) + removed = k; + item_tooltip("Takes this radius out of the curve"); + changed |= ImGui::DragScalar(format(STR_C("Transition R%d-R%d (m)"), k + 1, k + 2).c_str(), ImGuiDataType_Double, &arc.transition, 1.0f, nullptr, nullptr, "%.0f"); + changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), k + 2).c_str(), ImGuiDataType_Double, &arc.share, 0.01f, nullptr, nullptr, "%.2f"); + ImGui::SameLine(); + ImGui::TextDisabled("%.0f%%", percent(arc.share)); + arc.radius = std::max(1.0, arc.radius); + arc.transition = std::max(0.0, arc.transition); + arc.share = std::max(0.0, arc.share); + ImGui::PopID(); + } + vertex.share = std::max(0.0, vertex.share); + if (removed >= 0) + { + vertex.arcs.erase(vertex.arcs.begin() + removed); + if (vertex.arcs.empty()) + vertex.compound = false; + changed = true; + } + if (ImGui::SmallButton(STR_C("Add a radius"))) + { + auto const last{vertex.arcs.empty() ? vertex.radius : vertex.arcs.back().radius}; + vertex.arcs.push_back({std::max(10.0, std::round(last * 0.8 / 10.0) * 10.0), 0.0, 1.0}); + changed = true; + } + item_tooltip("Another arc after the last one, the angle of the curve is shared out by the shares"); + ImGui::Separator(); + } + ImGui::PopItemWidth(); + vertex.radius = std::max(1.0, vertex.radius); + vertex.transition_in = std::max(0.0, vertex.transition_in); + vertex.transition_out = std::max(0.0, vertex.transition_out); + vertex.cant = std::max(0.0, vertex.cant); + if (false == m_norms_free) + { + if (ImGui::SmallButton(STR_C("Apply recommended for V"))) + { + route_recommend(vertex); + changed = true; + } + 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"))) + { + for (auto const &curve : route.result.curves) + { + if (curve.vertex != Index) + continue; + ImGui::Text(STR_C("Deflection %.4f deg, tangents %.2f / %.2f m, arc %.2f m"), glm::degrees(curve.deflection), curve.tangent_in, curve.tangent_out, curve.arc_length); + ImGui::Text(STR_C("Unbalanced acc. %.3f m/s2, cant rate %.1f / %.1f mm/s, acc. rate %.3f / %.3f m/s3"), curve.unbalanced, curve.cant_rate_in, curve.cant_rate_out, curve.jerk_in, curve.jerk_out); + } + changed |= ImGui::Checkbox(STR_C("Turn the longer way (over 180 deg)"), &vertex.reverse_turn); + item_tooltip("For the loops: the curve turns by more than a half circle"); + ImGui::TreePop(); + } + return changed; +} + +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 && (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); + 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(), 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(); + } + + if (result.valid) + { + 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 + { + ImGui::TextDisabled(STR_C("Correct the errors to apply the design")); + } + if (false == route.status.empty()) + ImGui::TextUnformatted(route.status.c_str()); +} + +editor_track::straight const &editor_mode::current_straight() +{ + auto *track{selected_track()}; + if (track != m_straights.current_for) + { + m_straights.current_for = track; + m_straights.handle = -1; + m_straights.current = track != nullptr ? editor_track::find_straight(*track, m_straights.tolerance) : editor_track::straight{}; + find_neighbour_straights(); + } + return m_straights.current; +} 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 98d4a1e2..2a7909bd 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Globals.h" #include "utilities/Logs.h" #include "world/Track.h" +#include "world/TrackRails.h" #include "world/MemCell.h" #include "world/Event.h" #include "vehicle/DynObj.h" @@ -29,21 +30,46 @@ http://mozilla.org/MPL/2.0/. #include "rendering/renderer.h" #include "world/Sweep.h" #include "input/keyboardinput.h" +#include "launcher/textures_scanner.h" #include #include #include #include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "scene/sceneterrain.h" +#include "editor/editorSpeed.hpp" +#include #include #include #include #include #include +#include "application/editormodeselftest.h" + +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, &editor_mode::selftest_monkey}) + if (auto const result{(this->*group)(command, words)}; result != selftest_result::unhandled) + return result; + return selftest_result::unhandled; +} + // the script is a text file, one action a line: // wait // camera from the first point, looking at the second // select +// selectat the path nearest the point, within 3 m, as a click on it +// blade [name] logs the blades of each switch cut from the rails: tip, 50 mm head, full head, heel, drop of the head, open flangeway, the gauge lines apart against x^2/2R, vertices in both positions +// frog [name] logs where the inner rails of each switch cross: distance on both paths, 1:n, throat, nose, check rails +// guards [name] logs each guard rail and what each of its ends and breaks is cut along; a fault when a cut is not along a rail or a glued end not on the guard rail of the other track +// railtime lays out the rails of every track, as when its geometry is made, and logs how long it took +// throw <0|1> sets every switch to the path and moves its blades there at once +// crossings [name] logs where rails of tracks cross rails of other tracks and of what kind (acute, obtuse, middle acute or obtuse, wide), checks that each acute crossing has one partner and one nose +// frogkey name [on|off] sets or clears the frogs key of a switch (one undo step); logs the key, whether it is written, the frog and the crossings +// geomhash [name] logs the number of vertices and a hash of the drawn geometry of each rail track (rails, trackbed, cut faces), to compare builds; exact: the bits of every value, userdata and tread materials too // sweep =... model, params (separated by commas), lateral, height, side (origin, near, far, // centre), top (origin, bottom, top, edge), mode (bend, repeat), step, // length (original, all, or metres), from, to, widen (0, 1), face (0, 1), tilt (0, 1) @@ -71,6 +97,8 @@ http://mozilla.org/MPL/2.0/. // checkspans logs whether the text the scenery files hold at each node's place starts and ends it // spread logs the paths the spread from the track takes // newscenery the editor starts again with the wizard of a new scenery +// mapstate logs the orthophoto, the top view and the //$o line of the scenery +// trainset [km/h] the trainset tool puts the vehicle in the middle of the track // vehicles logs each vehicle: its head, facing, and what its couplers hold // terrainmode the terrain field of work, with its texture browser // terrainsculpt , terrainpaint , @@ -88,9 +116,42 @@ http://mozilla.org/MPL/2.0/. // terrainturn the textures of the palette entry turned // terrainwater [size] material and repeat of a body of water; terrainwaterdefault // orthophoto [in scene 0|1] the imagery layer on, placed and fitted as given +// join [radius] [variant] [transitions 0|1] the free ends at the points joined, as a drag from one to the other +// layto from the free end at the first point towards the second, as a drag in the layout +// layclicks ... clicks of the lay tool at the points, a free end within 3 m joined +// joints ... the joints check over the scenery, the issues around the points logged, every issue with free ends without points +// route the route design of the path nearest the point, a curve or the whole line +// routevertex , routearc , routeshare , routekm +// routeapply, routetake as the buttons of the route design do it +// alignments logs the alignments kept in the scenery and the element under each path +// digitizeall every line not digitized yet kept in the scenery, as the button of the route design does it +// splitkind , nudgekind the first path over an element of the kind split in two, or moved +// switchplace