From f5d328c3a5bd1d847b21cc8b660dbf6417ef680c Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 6 Oct 2026 22:19:12 +0200 Subject: [PATCH 1/6] ci: translations of lang/ packaged with the builds; each push of a branch publishes its Windows and Linux builds as the rolling pre-release build-, downloadable without a GitHub account and removed along with the branch --- .github/workflows/build_bin.yml | 95 ++++++++++++++++++++++++++++- .github/workflows/build_cleanup.yml | 21 +++++++ 2 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/build_cleanup.yml diff --git a/.github/workflows/build_bin.yml b/.github/workflows/build_bin.yml index f0b1fdec..255c89a6 100644 --- a/.github/workflows/build_bin.yml +++ b/.github/workflows/build_bin.yml @@ -87,7 +87,7 @@ jobs: --config $env:CONFIG_WIN ` --parallel - - name: Copy shaders + - name: Copy shaders and translations shell: pwsh run: | $output = "build/bin/$env:CONFIG_WIN" @@ -103,6 +103,17 @@ jobs: -Recurse ` -Force + New-Item ` + -ItemType Directory ` + -Force ` + -Path "$output/lang" | Out-Null + + Copy-Item ` + -Path "lang/*" ` + -Destination "$output/lang" ` + -Recurse ` + -Force + - name: Upload Windows build uses: actions/upload-artifact@v4 with: @@ -238,10 +249,11 @@ jobs: "$PYTHON_HOME/bin/python" -c "from PIL import Image; print('Pillow:', Image.__version__)" - - name: Copy shaders + - name: Copy shaders and translations run: | - mkdir -p build/bin/shaders + mkdir -p build/bin/shaders build/bin/lang cp -R shaders/. build/bin/shaders/ + cp -R lang/. build/bin/lang/ - name: Upload Linux build uses: actions/upload-artifact@v4 @@ -251,3 +263,80 @@ jobs: build/bin/ if-no-files-found: error retention-days: 14 + + + # ============================================================ + # Public download: release assets need no GitHub account + # ============================================================ + + publish: + name: Publish public download + needs: [windows, linux] + if: github.event_name != 'pull_request' + runs-on: ubuntu-24.04 + permissions: + contents: write + concurrency: + group: publish-${{ github.ref }} + cancel-in-progress: false + + steps: + - name: Download Windows build + uses: actions/download-artifact@v4 + with: + name: maszyna-windows-x64-${{ env.CONFIG_WIN }} + path: dist/maszyna-windows-x64 + + - name: Download Linux build + uses: actions/download-artifact@v4 + with: + name: maszyna-linux-x64-${{ env.CONFIG_LINUX }} + path: dist/maszyna-linux-x64 + + - name: Pack + run: | + cd dist + + find maszyna-linux-x64 -type f -exec sh -c 'file -b "$1" | grep -q "^ELF" && chmod +x "$1"' _ {} \; + if [ -d maszyna-linux-x64/linuxpython64/bin ]; then + chmod +x maszyna-linux-x64/linuxpython64/bin/* + fi + + (cd maszyna-windows-x64 && zip -qr9 ../maszyna-windows-x64.zip .) + tar -czf maszyna-linux-x64.tar.gz -C maszyna-linux-x64 . + ls -la + + - name: Publish rolling release of the branch + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + BRANCH: ${{ github.ref_name }} + SHA: ${{ github.sha }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + slug="$(printf '%s' "$BRANCH" | tr '/ ' '--' | tr -cd '[:alnum:]._-')" + tag="build-$slug" + base="${{ github.server_url }}/${{ github.repository }}/releases/download/$tag" + + notes="$(printf '%s\n' \ + "Automatic build of branch \`$BRANCH\`, commit $SHA, replaced by every push to the branch." \ + "" \ + "Download without a GitHub account:" \ + "- Windows: $base/maszyna-windows-x64.zip" \ + "- Linux: $base/maszyna-linux-x64.tar.gz" \ + "" \ + "Unpack over a MaSzyna installation (the game data isn't included). Build log: $RUN_URL")" + + gh release delete "$tag" --yes --cleanup-tag || true + gh release create "$tag" \ + dist/maszyna-windows-x64.zip \ + dist/maszyna-linux-x64.tar.gz \ + --target "$SHA" \ + --prerelease \ + --latest=false \ + --title "Build: $BRANCH" \ + --notes "$notes" + + echo "### Public download" >> "$GITHUB_STEP_SUMMARY" + echo "- Windows: $base/maszyna-windows-x64.zip" >> "$GITHUB_STEP_SUMMARY" + echo "- Linux: $base/maszyna-linux-x64.tar.gz" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/build_cleanup.yml b/.github/workflows/build_cleanup.yml new file mode 100644 index 00000000..8b20be0d --- /dev/null +++ b/.github/workflows/build_cleanup.yml @@ -0,0 +1,21 @@ +name: Remove public build of a deleted branch + +on: + delete: + +permissions: + contents: write + +jobs: + cleanup: + if: github.event.ref_type == 'branch' + runs-on: ubuntu-24.04 + steps: + - name: Delete the rolling release of the branch + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + BRANCH: ${{ github.event.ref }} + run: | + slug="$(printf '%s' "$BRANCH" | tr '/ ' '--' | tr -cd '[:alnum:]._-')" + gh release delete "build-$slug" --yes --cleanup-tag || true From 084a705452ff2dd8f9a95d343ad96d1142677174 Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 6 Oct 2026 22:19:12 +0200 Subject: [PATCH 2/6] fix(vehicles): vehicle placed with a switch as its first path no longer reads the direction of a previous path it doesn't have; vehicle put back on the track keeps the direction of its axles --- vehicle/DynObj.cpp | 4 ++-- world/TrkFoll.cpp | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 0bf8815b..a6c57f56 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -1917,11 +1917,11 @@ void TDynamicObject::place_on_track(TTrack *Track, double fDist, bool Reversed) switch (iNumAxles) { // Ra: pojazdy wstawiane są na tor początkowy, a potem przesuwane case 2: // ustawianie osi na torze - Axle0.Init(Track, this, iDirection ? 1 : -1); Axle0.Reset(); + Axle0.Init(Track, this, iDirection ? 1 : -1); Axle0.Move((iDirection ? fDist : -fDist) + fAxleDistHalf, false); - Axle1.Init(Track, this, iDirection ? 1 : -1); Axle1.Reset(); + Axle1.Init(Track, this, iDirection ? 1 : -1); Axle1.Move((iDirection ? fDist : -fDist) - fAxleDistHalf, false); // false, żeby nie generować eventów break; } diff --git a/world/TrkFoll.cpp b/world/TrkFoll.cpp index b1884896..c437553d 100644 --- a/world/TrkFoll.cpp +++ b/world/TrkFoll.cpp @@ -51,6 +51,9 @@ TTrack * TTrackFollower::SetCurrentTrack(TTrack *pTrack, int end) { case tt_Switch: // jeśli zwrotnica, to przekładamy ją, aby uzyskać dobry segment { + if( pCurrentTrack == nullptr ) { + break; + } int i = end ? pCurrentTrack->iNextDirection : pCurrentTrack->iPrevDirection; if (i > 0) // jeżeli wjazd z ostrza pTrack->SwitchForced(i >> 1, Owner); // to przełożenie zwrotnicy - rozprucie! From 978b99b7e5ae74d4f8d5f495f538c8d84314217f Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 6 Oct 2026 22:19:32 +0200 Subject: [PATCH 3/6] feat(scenery): models laid along curves (sweep nodes), bent to follow the paths or repeated along them, loaded and saved with the scenery; trainsets put on a path while the simulation runs; trainset turned around by reversed after its header: the first vehicle stays at the offset and leads towards point 1, the rest stands behind it; launcher keeps reversed trainsets and can turn one around; memory cells saved with the new name of the path they name; events tell their targets and the events of a multiple event; renderer tells whether the cursor is over a surface; vehicle placed in the editor entered when the driving goes on --- CMakeLists.txt | 1 + application/drivermode.cpp | 13 + launcher/scenery_list.cpp | 6 +- launcher/scenery_scanner.cpp | 4 + launcher/scenery_scanner.h | 1 + rendering/opengl33renderer.cpp | 1 + rendering/opengl33renderer.h | 3 + rendering/renderer.h | 1 + scene/scene.h | 2 + scene/scenelayerssave.cpp | 95 ++- scene/scenenode.h | 8 + simulation/simulation.cpp | 6 + simulation/simulation.h | 4 + simulation/simulationstateserializer.cpp | 103 ++- simulation/simulationstateserializer.h | 3 + utilities/Classes.h | 2 + utilities/Globals.h | 1 + world/Event.cpp | 20 + world/Event.h | 5 + world/Sweep.cpp | 907 +++++++++++++++++++++++ world/Sweep.h | 158 ++++ 21 files changed, 1329 insertions(+), 15 deletions(-) create mode 100644 world/Sweep.cpp create mode 100644 world/Sweep.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 37f5c4ae..e2fa3abd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,7 @@ set(SOURCES "utilities/Timer.cpp" "world/Track.cpp" "world/Road.cpp" +"world/Sweep.cpp" "world/RoadPoint.cpp" "world/Traction.cpp" "world/TractionPower.cpp" diff --git a/application/drivermode.cpp b/application/drivermode.cpp index dbe3e176..a79c31b0 100644 --- a/application/drivermode.cpp +++ b/application/drivermode.cpp @@ -255,6 +255,19 @@ bool driver_mode::update() // variable step simulation time routines + if (false == Global.editor_enter_vehicle.empty()) + { + auto const name{std::move(Global.editor_enter_vehicle)}; + Global.editor_enter_vehicle.clear(); + if (auto *dynamic{simulation::Vehicles.find(name)}) + { + if (false == FreeFlyModeFlag) + InOutKey(); + m_relay.post(user_command::entervehicle, 0.0, simulation::Train ? simulation::Train->id() : 0, GLFW_PRESS, 0, dynamic->GetPosition(), &dynamic->name()); + change_train = dynamic->name(); + } + } + if (!change_train.empty()) { TTrain *train = simulation::Trains.find(change_train); diff --git a/launcher/scenery_list.cpp b/launcher/scenery_list.cpp index 77f69a35..a24d3cf9 100644 --- a/launcher/scenery_list.cpp +++ b/launcher/scenery_list.cpp @@ -119,6 +119,10 @@ void ui::scenerylist_panel::draw_launch_box() ImGui::TextWrapped(selected_trainset->description.c_str()); + ImGui::Checkbox(STR_C("Trainset turned around"), &selected_trainset->reversed); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The first vehicle stays where the trainset starts and leads the other way,\nthe rest of the trainset stands behind it")); + if (ImGui::Button(STR_C("Launch"), ImVec2(-1, 0))) { if (!launch_simulation()) ImGui::OpenPopup("missing_driver"); @@ -163,7 +167,7 @@ void ui::scenerylist_panel::add_replace_entry(const trainset_desc &trainset) set += trainset.name + " "; set += trainset.track + " "; set += std::to_string(trainset.offset) + " "; - set += std::to_string(trainset.velocity) + "\n"; + set += std::to_string(trainset.velocity) + (trainset.reversed ? " reversed\n" : "\n"); for (const auto &veh : trainset.vehicles) { if (!veh.skin) { ErrorLog("trainset contains invalid vehicle " + veh.name); diff --git a/launcher/scenery_scanner.cpp b/launcher/scenery_scanner.cpp index 7bfa8a07..bcd18ac9 100644 --- a/launcher/scenery_scanner.cpp +++ b/launcher/scenery_scanner.cpp @@ -95,6 +95,10 @@ void scenery_scanner::parse_trainset(cParser &parser) parser >> trainset.name >> trainset.track >> trainset.offset >> trainset.velocity; parser.getTokens(); + if (parser.peek() == "reversed") { + trainset.reversed = true; + parser.getTokens(); + } while (parser.peek() == "node") { trainset.vehicles.emplace_back(); dynamic_desc &dyn = trainset.vehicles.back(); diff --git a/launcher/scenery_scanner.h b/launcher/scenery_scanner.h index 44854438..fcda27da 100644 --- a/launcher/scenery_scanner.h +++ b/launcher/scenery_scanner.h @@ -30,6 +30,7 @@ struct trainset_desc { float offset { 0.f }; float velocity { 0.f }; + bool reversed { false }; std::vector vehicles; }; diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index b146d5d1..ff0e9951 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -5154,6 +5154,7 @@ glm::dvec3 opengl33_renderer::Update_Mouse_Position() if (pointdepth != std::numeric_limits::max()) { + m_mousehit = (GLAD_GL_ARB_clip_control || GLAD_GL_EXT_clip_control) ? pointdepth > 0.0f : pointdepth < 1.0f; if (GLAD_GL_ARB_clip_control || GLAD_GL_EXT_clip_control) { if (pointdepth > 0.0f) m_worldmousecoordinates = glm::unProjectZO(glm::vec3(bufferpos, pointdepth), glm::mat4(glm::mat3(m_colorpass.pass_camera.modelview())), m_colorpass.pass_camera.projection(), diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 506e9211..36c8417c 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -105,6 +105,8 @@ class opengl33_renderer : public gfx_renderer { Pick_Node() const override { return m_picksceneryitem; } glm::dvec3 Mouse_Position() const override { return m_worldmousecoordinates; } + bool + Mouse_Hit() const override { return m_mousehit; } glm::mat4 Camera_View_Matrix() const override { return glm::mat4( glm::mat3( m_colorpass.pass_camera.modelview() ) ); } glm::mat4 @@ -387,6 +389,7 @@ class opengl33_renderer : public gfx_renderer { std::vector m_picksceneryitems; scene::basic_node *m_picksceneryitem{nullptr}; glm::vec3 m_worldmousecoordinates { 0.f }; + bool m_mousehit { false }; #ifdef EU07_USE_DEBUG_CAMERA renderpass_config m_worldcamera; // debug item #endif diff --git a/rendering/renderer.h b/rendering/renderer.h index f7b6d762..225e26c0 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -73,6 +73,7 @@ public: virtual auto Pick_Node() const -> scene::basic_node const * = 0; virtual auto Mouse_Position() const -> glm::dvec3 = 0; + virtual auto Mouse_Hit() const -> bool { return true; } // editor helpers: matrices/position of the most recent color pass camera. // the view matrix is camera-relative (rotation only, camera at origin), matching the // camera-relative rendering used by the engine; build object matrices relative to Camera_Position(). diff --git a/scene/scene.h b/scene/scene.h index 74b3ba4c..6b1ba175 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -69,7 +69,9 @@ struct scratch_data { std::vector vehicles; std::vector couplings; TDynamicObject * driver { nullptr }; + TTrack * path { nullptr }; // given directly, for a trainset placed in the editor on a path which may have no name bool is_open { false }; + bool reversed { false }; std::unordered_map assignment; } trainset; diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index 98da1f12..7751a860 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "scene/scenelayers.h" #include "world/Track.h" #include "world/Road.h" +#include "world/Sweep.h" #include "world/RoadPoint.h" #include "simulation/simulation.h" @@ -231,7 +232,7 @@ bool patch_model(std::string &Text, model_placement const &Placement, std::strin } // definition layout: node memcell endmemcell -bool patch_memcell(std::string &Text, glm::dvec3 const &Location, std::string &Error) +bool patch_memcell(std::string &Text, std::optional const &Location, std::string const &Track, std::string &Error) { auto const tokens{tokenize(Text)}; if (tokens.size() < 13 || tokens[0].text != "node" || tokens[4].text != "memcell" || tokens.back().text != "endmemcell") @@ -239,7 +240,13 @@ bool patch_memcell(std::string &Text, glm::dvec3 const &Location, std::string &E Error = "unexpected layout of memory cell definition"; return false; } - apply(Text, {{tokens[5].begin, tokens[5].end, number(Location.x)}, {tokens[6].begin, tokens[6].end, number(Location.y)}, {tokens[7].begin, tokens[7].end, number(Location.z)}}); + std::vector changes; + if (Location) + changes.insert(changes.end(), {{tokens[5].begin, tokens[5].end, number(Location->x)}, {tokens[6].begin, tokens[6].end, number(Location->y)}, {tokens[7].begin, tokens[7].end, number(Location->z)}}); + auto const &track{tokens[tokens.size() - 2]}; + if (false == Track.empty() && track.text != Track) + changes.push_back({track.begin, track.end, Track}); + apply(Text, changes); return true; } @@ -699,7 +706,9 @@ save_result node_layers::save(std::vector const &Rootstatements) auto const rotated{false == glm::all(glm::epsilonEqual(angles, source.angles, 1e-3f))}; auto const scaled{false == glm::all(glm::epsilonEqual(scale, source.scale, 1e-4f))}; auto const renamed{Model != nullptr && m_renamed.find(Node) != m_renamed.end()}; - if (false == (moved || rotated || scaled || renamed)) + auto const *cell{Model == nullptr ? static_cast(Node) : nullptr}; + auto const track{cell != nullptr && cell->Track != nullptr ? cell->Track->name() : std::string{}}; + if (false == (moved || rotated || scaled || renamed || (false == track.empty() && Node->dirty()))) { return true; } @@ -739,13 +748,17 @@ save_result node_layers::save(std::vector const &Rootstatements) } else { - patched = patch_memcell(text, source.context.to_local(location), error); + patched = patch_memcell(text, moved ? std::optional{source.context.to_local(location)} : std::nullopt, track, error); } if (false == patched) { state.error = error + " of \"" + Node->name() + "\" in file \"" + layer(source.layer).name + "\""; return false; } + if (text.size() == static_cast(source.span.end - source.span.begin) && content.compare(static_cast(source.span.begin), text.size(), text) == 0) + { + return true; + } file_patch::edit edit; edit.begin = source.span.begin; edit.end = source.span.end; @@ -988,6 +1001,72 @@ save_result node_layers::save(std::vector const &Rootstatements) state.patches[source.layer].edits.emplace_back(std::move(edit)); rewritten.emplace(road); } + // models laid along curves, likewise + std::vector savedsweeps; + std::vector droppedsweeps; + auto const sweep_text = [](sweep_node &Sweep, glm::dvec3 const &Offset) { + auto const definition{Sweep.definition()}; + auto local{definition}; + for (auto &piece : local.pieces) + { + piece.points[segment_data::point::start] -= Offset; + piece.points[segment_data::point::end] -= Offset; + } + Sweep.define(local); + std::string text; + Sweep.export_as_text(text); + Sweep.define(definition); + text.erase(text.find_last_not_of(" \t\r\n") + 1); + return text; + }; + for (auto *sweep : simulation::Sweeps.sequence()) + { + if (sweep == nullptr || sweep->from_template()) + { + continue; + } + auto const nodelayer{resolve(sweep->layer())}; + if (false == is_output(nodelayer)) + { + continue; + } + auto const lookup{m_sources.find(sweep)}; + if (lookup == m_sources.end()) + { + if (sweep->m_editorremoved || false == sweep->dirty()) + { + continue; + } + created[nodelayer].emplace_back(sweep, sweep_text(*sweep, layer(nodelayer).context_insert().offset)); + savedsweeps.push_back(sweep); + continue; + } + auto const &source{lookup->second}; + if ((false == sweep->m_editorremoved && false == sweep->dirty()) || false == writable(source.layer)) + { + continue; + } + if (false == load(source.layer)) + { + return fail(state.error); + } + file_patch::edit edit; + edit.begin = source.span.begin; + edit.end = source.span.end; + if (sweep->m_editorremoved) + { + droppedsweeps.push_back(sweep); + } + else + { + edit.text = sweep_text(*sweep, source.context.offset); + edit.length = edit.text.size(); + edit.node = sweep; + savedsweeps.push_back(sweep); + } + state.patches[source.layer].edits.emplace_back(std::move(edit)); + rewritten.emplace(sweep); + } // road junctions, likewise std::vector savedjunctions; std::vector droppedjunctions; @@ -1647,6 +1726,14 @@ save_result node_layers::save(std::vector const &Rootstatements) { m_sources.erase(road); } + for (auto *sweep : savedsweeps) + { + sweep->m_dirty = false; + } + for (auto const *sweep : droppedsweeps) + { + m_sources.erase(sweep); + } for (auto *junction : savedjunctions) { junction->m_dirty = false; diff --git a/scene/scenenode.h b/scene/scenenode.h index dd844a65..b7fa9fda 100644 --- a/scene/scenenode.h +++ b/scene/scenenode.h @@ -149,6 +149,9 @@ public: // set lighting (used by generated geometry painted with plain colour) void lighting( lighting_data const &Lighting ); + // sets whether the shape is drawn with the translucent geometry (used by generated geometry) + void + translucent( bool const Translucent ); // data access shapenode_data const & data() const; @@ -186,6 +189,11 @@ void shape_node::lighting( lighting_data const &Lighting ) { m_data.lighting = Lighting; } +inline +void +shape_node::translucent( bool const Translucent ) { + m_data.translucent = Translucent; +} // data access inline shape_node::shapenode_data const & diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index 035a4f37..3cbe6c69 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include "world/MemCell.h" #include "world/Track.h" #include "world/Road.h" +#include "world/Sweep.h" #include "world/RoadPoint.h" #include "world/Traction.h" #include "world/TractionPower.h" @@ -39,6 +40,7 @@ event_manager Events; memory_table Memory; path_table Paths; road_table Roads; +sweep_table Sweeps; junction_table Junctions; roadpoint_table Roadpoints; traction_table Traction; @@ -497,6 +499,10 @@ TAnimModel * state_manager::create_model(const std::string &src, const std::stri return m_serializer.create_model(src, name, position); } +std::vector state_manager::insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed) { + return m_serializer.insert_trainset(Name, Path, Offset, Vehicles, Reversed); +} + TEventLauncher * state_manager::create_eventlauncher(const std::string &src, const std::string &name, const glm::dvec3 &position) { return m_serializer.create_eventlauncher(src, name, position); } diff --git a/simulation/simulation.h b/simulation/simulation.h index 89337583..0e96da68 100644 --- a/simulation/simulation.h +++ b/simulation/simulation.h @@ -40,6 +40,9 @@ public: // create model from node string TAnimModel * create_model(const std::string &src, const std::string &name, const glm::dvec3 &position); + // places the vehicles, given as their node statements, as a trainset on the path. returns: the vehicles made + std::vector + insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed = false); // create eventlauncher from node string TEventLauncher * create_eventlauncher(const std::string &src, const std::string &name, const glm::dvec3 &position); @@ -86,6 +89,7 @@ extern event_manager Events; extern memory_table Memory; extern path_table Paths; extern road_table Roads; +extern sweep_table Sweeps; extern junction_table Junctions; extern roadpoint_table Roadpoints; extern traction_table Traction; diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 8736e56d..fe5486e8 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -28,6 +28,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/lightarray.h" #include "world/TractionPower.h" #include "world/Road.h" +#include "world/Sweep.h" #include "world/RoadPoint.h" #include "application/application.h" #include "rendering/renderer.h" @@ -35,8 +36,19 @@ http://mozilla.org/MPL/2.0/. #include "utilities/utilities.h" #include "editor/editorTerrainStreamer.hpp" + namespace simulation { +namespace { + +double vehicle_length( std::string const &Folder, std::string const &Type ) { + + TMoverParameters probe( 0.0, Type, Type, 0 ); + return probe.LoadFIZ( paths::dynamic + Folder + "/" ) ? probe.Dim.L : 0.0; +} + +} // namespace + std::shared_ptr state_serializer::deserialize_begin( std::string const &Scenariofile ) { @@ -123,7 +135,8 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) { { "trainset", &state_serializer::deserialize_trainset }, { "terrain", &state_serializer::deserialize_terrain }, { "editorterrain", &state_serializer::deserialize_editorterrain }, - { "endtrainset", &state_serializer::deserialize_endtrainset } }; + { "endtrainset", &state_serializer::deserialize_endtrainset }, + { "reversed", &state_serializer::deserialize_reversed } }; for( auto &function : functionlist ) { state->functionmap.emplace( function.first, std::bind( function.second, this, std::ref( state->input ), std::ref( state->scratchpad ) ) ); @@ -191,6 +204,7 @@ state_serializer::deserialize_continue(std::shared_ptr state simulation::Roads.create_geometry( Scratchpad ); simulation::Junctions.create_geometry( Scratchpad ); simulation::Roadpoints.create_geometry(); + simulation::Sweeps.create_geometry( Scratchpad ); return false; } @@ -554,6 +568,20 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch road->create_lanes(); scene::Layers.track( road, { sourcebegin, Input.TokenEnd() } ); } + else if( nodedata.type == "sweep" ) { + + auto *sweep { new sweep_node( nodedata ) }; + sweep->import( + Input, + ( Scratchpad.location.offset.empty() ? + glm::dvec3 { 0.0 } : + glm::dvec3 { Scratchpad.location.offset.top() } ) ); + if( false == simulation::Sweeps.insert( sweep ) ) { + ErrorLog( "Bad scenario: duplicate sweep name \"" + sweep->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); + } + scene::Groups.insert( scene::Groups.handle(), sweep ); + scene::Layers.track( sweep, { sourcebegin, Input.TokenEnd() } ); + } else if( nodedata.type == "junction" ) { auto *junction { new junction_node( nodedata ) }; @@ -1224,6 +1252,17 @@ include_terrain_file( std::string File, std::string &Includes, scene::scratch_da } // namespace +void +state_serializer::deserialize_reversed( cParser &Input, scene::scratch_data &Scratchpad ) { + + if( false == Scratchpad.trainset.is_open + || false == Scratchpad.trainset.vehicles.empty() ) { + ErrorLog( "Bad trainset: \"reversed\" has to follow the trainset header, ahead of its vehicles, in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" ); + return; + } + Scratchpad.trainset.reversed = true; +} + void state_serializer::deserialize_terrain(cParser &Input, scene::scratch_data &Scratchpad) { @@ -1312,9 +1351,16 @@ state_serializer::deserialize_endtrainset( cParser &Input, scene::scratch_data & } if( vehicleindex > 0 ) { // from second vehicle on couple it with the previous one - Scratchpad.trainset.vehicles[ vehicleindex - 1 ]->AttachNext( - vehicle, - Scratchpad.trainset.couplings[ vehicleindex - 1 ] ); + if( Scratchpad.trainset.reversed ) { + vehicle->AttachNext( + Scratchpad.trainset.vehicles[ vehicleindex - 1 ], + Scratchpad.trainset.couplings[ vehicleindex - 1 ] ); + } + else { + Scratchpad.trainset.vehicles[ vehicleindex - 1 ]->AttachNext( + vehicle, + Scratchpad.trainset.couplings[ vehicleindex - 1 ] ); + } } ++vehicleindex; } @@ -1494,7 +1540,7 @@ state_serializer::deserialize_dynamic( cParser &Input, scene::scratch_data &Scra loadtype = ""; } - auto *path = simulation::Paths.find( pathname ); + auto *path = Scratchpad.trainset.path != nullptr ? Scratchpad.trainset.path : simulation::Paths.find( pathname ); if( path == nullptr ) { ErrorLog( "Bad scenario: vehicle \"" + Nodedata.name + "\" placed on nonexistent path \"" + pathname + "\" in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); @@ -1502,7 +1548,9 @@ state_serializer::deserialize_dynamic( cParser &Input, scene::scratch_data &Scra return nullptr; } - if( true == Scratchpad.trainset.vehicles.empty() // jeśli pierwszy pojazd, + auto const reversedset { Scratchpad.trainset.is_open && Scratchpad.trainset.reversed }; + if( false == reversedset + && true == Scratchpad.trainset.vehicles.empty() // jeśli pierwszy pojazd, && false == path->m_events0.empty() // tor ma Event0 && std::abs(velocity) <= 1.f // a skład stoi && Scratchpad.trainset.offset >= 0.0 // ale może nie sięgać na owy tor @@ -1512,23 +1560,34 @@ state_serializer::deserialize_dynamic( cParser &Input, scene::scratch_data &Scra } auto *vehicle = new TDynamicObject(); - + + auto const gap { offset == -1.0 ? 0.0 : offset }; + auto const turned { ( offset == -1.0 ) != reversedset }; + auto const expected { reversedset ? vehicle_length( datafolder, mmdfile ) : 0.0 }; auto const length = vehicle->Init( Nodedata.name, datafolder, skinfile, mmdfile, path, - offset == -1.0 ? Scratchpad.trainset.offset : Scratchpad.trainset.offset - offset, + reversedset ? Scratchpad.trainset.offset + gap + expected : Scratchpad.trainset.offset - gap, drivertype, velocity, Scratchpad.trainset.name, loadcount, loadtype, - offset == -1.0, + turned, params ); + if( length != 0.0 && reversedset ) { + if( std::abs( length - expected ) > 0.01 ) { + vehicle->place_on_track( path, Scratchpad.trainset.offset + gap + length, turned ); + } + Scratchpad.trainset.offset += length; + } if( length != 0.0 ) { // zero oznacza błąd // przesunięcie dla kolejnego, minus bo idziemy w stronę punktu 1 - Scratchpad.trainset.offset -= length; + if( false == reversedset ) { + Scratchpad.trainset.offset -= length; + } // automatically establish permanent connections for couplers which specify them in their definitions if( coupling != 0 && vehicle->MoverParameters->Couplers[(offset == -1.0 ? end::front : end::rear)].AllowedFlag & coupling::permanent ) { @@ -1774,6 +1833,11 @@ state_serializer::export_nodes_to_stream(std::ostream &scmfile, bool Dirty) cons point->export_as_text( scmfile ); } } + for( auto const *sweep : Sweeps.sequence() ) { + if( sweep != nullptr && false == sweep->m_editorremoved && sweep->dirty() == Dirty && sweep->group() == null_handle ) { + sweep->export_as_text( scmfile ); + } + } // traction scmfile << "// traction\n"; for( auto const *traction : Traction.sequence() ) { @@ -1826,6 +1890,25 @@ TAnimModel *state_serializer::create_model(const std::string &src, const std::st return cloned; } +std::vector state_serializer::insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed) { + scene::scratch_data scratch; + scratch.trainset.is_open = true; + scratch.trainset.name = Name; + scratch.trainset.track = Path != nullptr ? Path->name() : std::string{}; + scratch.trainset.path = Path; + scratch.trainset.offset = static_cast(Offset); + scratch.trainset.reversed = Reversed; + cParser parser(Vehicles, cParser::buffer_TEXT, Global.asCurrentSceneryPath, Global.bLoadTraction); + auto token { parser.getToken() }; + while (false == token.empty()) { + if (token == "node") { deserialize_node(parser, scratch); } + token = parser.getToken(); + } + auto const vehicles { scratch.trainset.vehicles }; + if (false == vehicles.empty()) { deserialize_endtrainset(parser, scratch); } + return vehicles; +} + std::pair state_serializer::preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance) { // statements which take more than a single token, with the tokens ending them static std::unordered_map const nodeends { diff --git a/simulation/simulationstateserializer.h b/simulation/simulationstateserializer.h index 001f6ad3..a5cbc32f 100644 --- a/simulation/simulationstateserializer.h +++ b/simulation/simulationstateserializer.h @@ -44,6 +44,8 @@ public: export_as_text( std::string const &Scenariofile ) const; // create new model from node stirng TAnimModel * create_model(std::string const &src, std::string const &name, const glm::dvec3 &position); + // places the vehicles, given as their node statements, as a trainset on the path, while the simulation runs. returns: the vehicles made + std::vector insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed = false); // create new eventlauncher from node stirng TEventLauncher * create_eventlauncher(std::string const &src, std::string const &name, const glm::dvec3 &position); // creates model instances defined by an include directive placed or changed in the scenery editor, to show the outcome. @@ -80,6 +82,7 @@ private: void deserialize_terrain( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_editorterrain( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_endtrainset( cParser &Input, scene::scratch_data &Scratchpad ); + void deserialize_reversed( cParser &Input, scene::scratch_data &Scratchpad ); TTrack * deserialize_path( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); TTraction * deserialize_traction( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); TTractionPowerSource * deserialize_tractionpowersource( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); diff --git a/utilities/Classes.h b/utilities/Classes.h index ec60965c..6a5ca167 100644 --- a/utilities/Classes.h +++ b/utilities/Classes.h @@ -45,6 +45,8 @@ struct light_array; class particle_manager; class road_node; // kawałek drogi, z którego powstają pasy ruchu class road_table; +class sweep_node; // model ułożony wzdłuż krzywej: wygięty albo powtarzany +class sweep_table; class junction_node; class junction_table; class roadpoint_node; // przejazd kolejowy albo miejsce, w którym pojazdy drogowe się pojawiają lub znikają diff --git a/utilities/Globals.h b/utilities/Globals.h index acf0c00f..36574ab1 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -84,6 +84,7 @@ struct global_settings { std::string szDefaultExt{ szTexturesDDS }; std::string SceneryFile; std::string local_start_vehicle{ "EU07-424" }; + std::string editor_enter_vehicle; // vehicle placed in the editor, to be entered when the driving goes on bool editor_session{ false }; // scenery was opened for editing (-edit): loader keeps track of scenery layers and hands over to the editor int iConvertModels{ 0 }; // tworzenie plików binarnych int iConvertIndexRange{ 1000 }; // range of duplicate vertex scan diff --git a/world/Event.cpp b/world/Event.cpp index 4e042b29..fa752078 100644 --- a/world/Event.cpp +++ b/world/Event.cpp @@ -1243,6 +1243,26 @@ multi_event::init() { } } +std::vector multi_event::children() const { + std::vector result; + for (auto const &childevent : m_children) { + if (std::get(childevent) != nullptr) { + result.push_back(std::get(childevent)); + } + } + return result; +} + +std::vector basic_event::target_nodes() const { + std::vector result; + for (auto const &target : m_targets) { + if (std::get(target) != nullptr) { + result.push_back(std::get(target)); + } + } + return result; +} + std::vector multi_event::dump_children_names() const { std::vector result; for (auto const &childevent : m_children) { diff --git a/world/Event.h b/world/Event.h index 9b8f32d8..69d66e3c 100644 --- a/world/Event.h +++ b/world/Event.h @@ -47,6 +47,7 @@ public: // destructor virtual ~basic_event(); // methods + std::vector target_nodes() const; // restores event data from provided stream virtual void @@ -334,6 +335,7 @@ public: void init() override; std::vector dump_children_names() const; + std::vector children() const; private: // types @@ -699,6 +701,9 @@ public: // returns all eventlaunchers in radius ignoring height std::vector find_eventlaunchers(glm::vec2 center, float radius) const; + std::deque const & + sequence() const { + return m_events; } // all event launchers, global ones included std::vector launchers() const { diff --git a/world/Sweep.cpp b/world/Sweep.cpp new file mode 100644 index 00000000..ef3af81b --- /dev/null +++ b/world/Sweep.cpp @@ -0,0 +1,907 @@ +/* +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 "world/Sweep.h" +#include "editor/editorGauge.hpp" + +#include "model/MdlMngr.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "scene/scene.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/parser.h" +#include "vehicle/DynObj.h" + +#include +#include + +#include +#include +#include +#include + +namespace +{ + +double const sample_spacing{0.5}; // m along the curve between the samples +std::size_t const vertex_limit{4000000}; // copies beyond this many vertices are left out +double const slice_width{1.0}; // m of the model, the faces of a bent copy are cut into +double const edge_band{0.3}; // m from the side facing the curve, where the top of the edge is looked for + +struct cubic +{ + glm::dvec3 p0, p1, p2, p3; + + explicit cubic(segment_data const &Piece) + { + p0 = Piece.points[segment_data::point::start]; + p3 = Piece.points[segment_data::point::end]; + auto const &control1{Piece.points[segment_data::point::control1]}; + auto const &control2{Piece.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + { + p1 = glm::mix(p0, p3, 1.0 / 3.0); + p2 = glm::mix(p0, p3, 2.0 / 3.0); + } + else + { + p1 = p0 + control1; + p2 = p3 + control2; + } + } + glm::dvec3 point(double const T) const + { + auto const u{1.0 - T}; + return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; + } + glm::dvec3 first(double const T) const + { + auto const u{1.0 - T}; + return 3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2); + } +}; + +double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B) +{ + return glm::length(glm::dvec2{B.x - A.x, B.z - A.z}); +} + +// triangles of the model which share the look, in the space of the model +struct model_part +{ + material_handle material{null_handle}; + lighting_data lighting; + bool translucent{false}; + std::vector vertices; +}; + +void gather_parts(TSubModel *Submodel, glm::dmat4 const &Transform, material_data const &Skins, std::vector &Parts) +{ + for (auto *submodel = Submodel; submodel != nullptr; submodel = submodel->Next) + { + auto transform{Transform}; + if ((submodel->iFlags & 0xC000) != 0 && submodel->GetMatrix() != nullptr) + { + transform = Transform * glm::dmat4(glm::make_mat4(submodel->GetMatrix()->readArray())); + } + // the nearest level of detail only, the others would be drawn over it + if (submodel->eType == GL_TRIANGLES && submodel->fSquareMinDist <= 0.f && (submodel->m_geometry.handle.bank != 0 || submodel->m_geometry.handle.chunk != 0)) + { + auto material{submodel->m_material}; + if (material < 0) + { + material = Skins.replacable_skins[std::clamp(-material, 1, 4)]; + } + model_part part; + part.material = material; + part.lighting.ambient = submodel->f4Ambient; + part.lighting.diffuse = submodel->f4Diffuse; + part.lighting.specular = submodel->f4Specular; + part.translucent = material != null_handle && GfxRenderer->Material(material)->get_or_guess_opacity() == 0.0f; + auto const normals{glm::dmat3(transform)}; + auto const &vertices{GfxRenderer->Vertices(submodel->m_geometry.handle)}; + auto const &indices{GfxRenderer->Indices(submodel->m_geometry.handle)}; + auto const take = [&](gfx::basic_vertex const &Vertex) { + auto vertex{Vertex.to_world()}; + vertex.position = glm::dvec3(transform * glm::dvec4(vertex.position, 1.0)); + auto const normal{normals * glm::dvec3(vertex.normal)}; + vertex.normal = glm::length(normal) > 1e-9 ? glm::vec3(glm::normalize(normal)) : vertex.normal; + part.vertices.push_back(vertex); + }; + if (false == indices.empty()) + { + for (std::size_t i = 0; i + 2 < indices.size(); i += 3) + { + take(vertices[indices[i]]); + take(vertices[indices[i + 1]]); + take(vertices[indices[i + 2]]); + } + } + else + { + for (std::size_t i = 0; i + 2 < vertices.size(); i += 3) + { + take(vertices[i]); + take(vertices[i + 1]); + take(vertices[i + 2]); + } + } + if (false == part.vertices.empty()) + { + Parts.push_back(std::move(part)); + } + } + if (submodel->Child != nullptr) + { + gather_parts(submodel->Child, transform, Skins, Parts); + } + } +} + +world_vertex blend(world_vertex const &A, world_vertex const &B, double const T) +{ + world_vertex result; + result.position = glm::mix(A.position, B.position, T); + auto const normal{glm::mix(A.normal, B.normal, static_cast(T))}; + result.normal = glm::length(normal) > 1e-6f ? glm::normalize(normal) : A.normal; + result.texture = glm::mix(A.texture, B.texture, static_cast(T)); + return result; +} + +// the part of the polygon on one side of the plane Along = Cut: Side 1 keeps what's beyond it, -1 what's before it +std::vector clip(std::vector const &Polygon, std::function const &Along, double const Cut, double const Side) +{ + std::vector result; + for (std::size_t i = 0; i < Polygon.size(); ++i) + { + auto const ¤t{Polygon[i]}; + auto const &next{Polygon[(i + 1) % Polygon.size()]}; + auto const a{Side * (Along(current.position) - Cut)}; + auto const b{Side * (Along(next.position) - Cut)}; + if (a >= 0.0) + result.push_back(current); + if ((a >= 0.0) != (b >= 0.0)) + result.push_back(blend(current, next, a / (a - b))); + } + return result; +} + +// triangles cut across the axis of the model into slices, so a long face bends with the curve instead of spanning it straight +std::vector slices(std::vector const &Vertices, std::function const &Along, double const Low, double const Width) +{ + std::vector result; + result.reserve(Vertices.size()); + for (std::size_t i = 0; i + 2 < Vertices.size(); i += 3) + { + auto const a0{Along(Vertices[i].position)}; + auto const a1{Along(Vertices[i + 1].position)}; + auto const a2{Along(Vertices[i + 2].position)}; + auto const first{static_cast(std::floor((std::min({a0, a1, a2}) - Low) / Width))}; + auto const last{static_cast(std::ceil((std::max({a0, a1, a2}) - Low) / Width)) - 1}; + if (last <= first) + { + result.insert(result.end(), Vertices.begin() + i, Vertices.begin() + i + 3); + continue; + } + std::vector const triangle{Vertices[i], Vertices[i + 1], Vertices[i + 2]}; + for (int slice = first; slice <= last; ++slice) + { + auto const polygon{clip(clip(triangle, Along, Low + slice * Width, 1.0), Along, Low + (slice + 1) * Width, -1.0)}; + for (std::size_t k = 1; k + 1 < polygon.size(); ++k) + { + result.push_back(polygon[0]); + result.push_back(polygon[k]); + result.push_back(polygon[k + 1]); + } + } + } + return result; +} + +} // namespace + +// height of the head of the rail on the side of the offset over the points of the track, less the height of the rail; 0 for no side +double sweep_node::rail_head(double const Roll, double const Lateral) +{ + auto const roll{glm::radians(Roll)}; + auto const raise{Global.bRollFix ? 0.75 * std::abs(std::sin(roll)) : 0.0}; + if (Lateral == 0.0) + return raise; + return raise - (Lateral > 0.0 ? 1.0 : -1.0) * 0.75 * std::sin(roll); +} + +bool sweep_node::state::operator==(state const &Other) const +{ + if (model != Other.model || skin != Other.skin || bend != Other.bend || step != Other.step || from != Other.from || to != Other.to || lateral != Other.lateral || height != Other.height || along_x != Other.along_x || + flip != Other.flip || mirror != Other.mirror || tilt != Other.tilt || face != Other.face || widen != Other.widen || parameters != Other.parameters || side_anchor != Other.side_anchor || height_anchor != Other.height_anchor || pieces.size() != Other.pieces.size()) + { + return false; + } + for (std::size_t i = 0; i < pieces.size(); ++i) + { + if (pieces[i].points != Other.pieces[i].points || pieces[i].rolls != Other.pieces[i].rolls) + { + return false; + } + } + return true; +} + +sweep_node::sweep_node(scene::node_data const &Nodedata) : basic_node(Nodedata) {} + +void sweep_node::import(cParser &Input, glm::dvec3 const &Offset) +{ + m_state = state{}; + m_state.model = Input.getToken(); + m_state.skin = Input.getToken(); + auto token{Input.getToken()}; + while (false == token.empty() && token != "endsweep") + { + if (token == "mode") + { + m_state.bend = Input.getToken() != "repeat"; + } + else if (token == "step") + { + Input.getTokens(); + Input >> m_state.step; + } + else if (token == "range") + { + Input.getTokens(2); + Input >> m_state.from >> m_state.to; + } + else if (token == "offset") + { + Input.getTokens(2); + Input >> m_state.lateral >> m_state.height; + } + else if (token == "axis") + { + m_state.along_x = Input.getToken() == "x"; + } + else if (token == "flip") + { + m_state.flip = true; + } + else if (token == "mirror") + { + m_state.mirror = true; + } + else if (token == "tilt") + { + m_state.tilt = true; + } + else if (token == "widen") + { + m_state.widen = true; + } + else if (token == "noface") + { + m_state.face = false; + } + else if (token == "parameters") + { + int count{0}; + Input.getTokens(); + Input >> count; + m_state.parameters.clear(); + for (int i = 0; i < count; ++i) + m_state.parameters.push_back(Input.getToken(false)); + } + else if (token == "anchor") + { + auto const side{Input.getToken()}; + auto const height{Input.getToken()}; + m_state.side_anchor = side == "near" ? 1 : side == "far" ? 2 : side == "centre" ? 3 : 0; + m_state.height_anchor = height == "bottom" ? 1 : height == "top" ? 2 : height == "edge" ? 3 : 0; + } + else if (token == "piece") + { + segment_data piece; + piece.deserialize(Input, Offset); + m_state.pieces.push_back(piece); + } + else + { + ErrorLog("Bad sweep: unknown property \"" + token + "\" of \"" + m_name + "\" in file \"" + Input.Name() + "\""); + } + token = Input.getToken(); + } + rebuild_samples(); +} + +void sweep_node::define(state const &State) +{ + auto const shown{m_shapes.shown()}; + if (shown) + { + hide(); + } + m_state = State; + rebuild_samples(); + if (shown) + { + show(); + } +} + +void sweep_node::rebuild_samples() +{ + m_samples.clear(); + double station{0.0}; + for (auto const &piece : m_state.pieces) + { + cubic const curve{piece}; + auto const count{std::clamp(static_cast(plan_distance(curve.p0, curve.p3) / sample_spacing), 4, 4000)}; + for (int k = m_samples.empty() ? 0 : 1; k <= count; ++k) + { + auto const t{static_cast(k) / count}; + auto const position{curve.point(t)}; + if (false == m_samples.empty()) + { + station += plan_distance(m_samples.back().position, position); + } + auto tangent{curve.first(t)}; + tangent = glm::length(tangent) > 1e-9 ? glm::normalize(tangent) : glm::dvec3{0.0, 0.0, 1.0}; + m_samples.push_back({station, position, tangent, piece.rolls[0] + (piece.rolls[1] - piece.rolls[0]) * t, {}, {}}); + } + } + if (m_samples.size() > 2) + { + // the structure gauge widens ahead of the curves and of the cant, the same way the gauge check of the editor has it + std::vector chainage, curvature, cant; + for (std::size_t i = 0; i < m_samples.size(); ++i) + { + auto const &before{m_samples[i == 0 ? 0 : i - 1]}; + auto const &after{m_samples[std::min(i + 1, m_samples.size() - 1)]}; + glm::dvec2 const a{before.tangent.x, before.tangent.z}; + glm::dvec2 const b{after.tangent.x, after.tangent.z}; + auto const span{after.station - before.station}; + auto const turn{glm::length(a) > 1e-9 && glm::length(b) > 1e-9 ? std::atan2(a.x * b.y - a.y * b.x, glm::dot(a, b)) : 0.0}; + chainage.push_back(m_samples[i].station); + curvature.push_back(span > 1e-9 ? turn / span : 0.0); + cant.push_back(1.5 * std::abs(std::sin(glm::radians(m_samples[i].roll)))); + } + auto const sections{gauge::sections(gauge::kind::unified, chainage, curvature, cant)}; + for (std::size_t i = 0; i < m_samples.size() && i < sections.size(); ++i) + { + m_samples[i].widening = sections[i].lower; + m_samples[i].cant = sections[i].cant; + } + } + if (false == m_samples.empty()) + { + location(frame_at(0.5 * length()).position); + } + m_area.radius = -1.f; +} + +double sweep_node::length() const +{ + return m_samples.empty() ? 0.0 : m_samples.back().station; +} + +double sweep_node::start() const +{ + return std::clamp(m_state.from, 0.0, length()); +} + +double sweep_node::end() const +{ + return m_state.to < 0.0 ? length() : std::clamp(m_state.to, start(), length()); +} + +sweep_node::frame sweep_node::frame_at(double const Station) const +{ + frame result; + if (m_samples.empty()) + { + return result; + } + auto const found{std::upper_bound(m_samples.begin(), m_samples.end(), Station, [](double const Value, sample const &Sample) { return Value < Sample.station; })}; + auto const after{found == m_samples.end() ? std::prev(found) : found}; + auto const before{after == m_samples.begin() ? after : std::prev(after)}; + auto const span{after->station - before->station}; + auto const f{span > 1e-9 ? std::clamp((Station - before->station) / span, 0.0, 1.0) : 0.0}; + result.position = glm::mix(before->position, after->position, f); + // beyond the ends the curve goes on straight + if (Station < m_samples.front().station) + { + result.position = m_samples.front().position + m_samples.front().tangent * (Station - m_samples.front().station); + } + else if (Station > m_samples.back().station) + { + result.position = m_samples.back().position + m_samples.back().tangent * (Station - m_samples.back().station); + } + auto forward{glm::mix(before->tangent, after->tangent, f)}; + forward = glm::length(forward) > 1e-9 ? glm::normalize(forward) : glm::dvec3{0.0, 0.0, 1.0}; + glm::dvec3 left{-forward.z, 0.0, forward.x}; + left = glm::length(left) > 1e-9 ? glm::normalize(left) : glm::dvec3{-1.0, 0.0, 0.0}; + glm::dvec3 up{glm::normalize(glm::cross(left, forward))}; + result.roll = before->roll + (after->roll - before->roll) * f; + for (int side = 0; side < 2; ++side) + { + result.widening[side] = before->widening[side] + (after->widening[side] - before->widening[side]) * f; + result.cant[side] = before->cant[side] + (after->cant[side] - before->cant[side]) * f; + } + if (m_state.tilt) + { + auto const roll{glm::radians(result.roll)}; + auto const turnedleft{left * std::cos(roll) - up * std::sin(roll)}; + up = up * std::cos(roll) + left * std::sin(roll); + left = turnedleft; + } + result.forward = forward; + result.left = left; + result.up = up; + return result; +} + +// models a template is made of, with their placement in the space of the template +std::vector sweep_node::items() const +{ + std::vector result; + auto const &definition{m_state}; + auto const ends = [](std::string const &Name, std::string const &Extension) { return Name.size() >= Extension.size() && Name.compare(Name.size() - Extension.size(), Extension.size(), Extension) == 0; }; + auto name{definition.model}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + if (false == ends(name, ".inc")) + { + item single; + single.model = TModelsManager::GetModel(definition.model, false, false); + if (false == definition.skin.empty() && definition.skin != "none") + single.skins.assign(definition.skin); + if (single.model != nullptr) + result.push_back(single); + return result; + } + cParser parser(definition.model, cParser::buffer_FILE, Global.asCurrentSceneryPath, Global.bLoadTraction, definition.parameters); + std::vector offsets{glm::dvec3{0.0}}; + glm::dvec3 rotation{0.0}; + auto token{parser.getToken()}; + while (false == token.empty()) + { + if (token == "origin") + { + glm::dvec3 offset; + parser.getTokens(3); + parser >> offset.x >> offset.y >> offset.z; + offsets.push_back(offsets.back() + offset); + } + else if (token == "endorigin") + { + if (offsets.size() > 1) + offsets.pop_back(); + } + else if (token == "rotate") + { + parser.getTokens(3); + parser >> rotation.x >> rotation.y >> rotation.z; + } + else if (token == "node") + { + double rangemax, rangemin; + std::string nodename, type; + parser.getTokens(4); + parser >> rangemax >> rangemin >> nodename >> type; + if (type == "model") + { + glm::dvec3 location; + double yaw; + parser.getTokens(4); + parser >> location.x >> location.y >> location.z >> yaw; + auto const model{parser.getToken()}; + auto const skin{parser.getToken()}; + auto rest{parser.getToken()}; + while (false == rest.empty() && rest != "endmodel") + rest = parser.getToken(); + item entry; + entry.model = TModelsManager::GetModel(model, false, false); + if (skin != "none" && false == skin.empty() && skin.front() != '*') + entry.skins.assign(skin); + // placed the way the scenery places a model in an origin turned by a rotate statement + auto const place{glm::rotateY(location, glm::radians(rotation.y)) + offsets.back()}; + glm::dmat4 transform{glm::translate(glm::dmat4(1.0), place)}; + transform = glm::rotate(transform, glm::radians(rotation.y + yaw), glm::dvec3{0.0, 1.0, 0.0}); + transform = glm::rotate(transform, glm::radians(rotation.x), glm::dvec3{1.0, 0.0, 0.0}); + transform = glm::rotate(transform, glm::radians(rotation.z), glm::dvec3{0.0, 0.0, 1.0}); + entry.transform = transform; + if (entry.model != nullptr && rangemin >= 0.0) + result.push_back(entry); + } + else + { + // whatever else the template defines isn't laid along the curve + auto rest{parser.getToken()}; + while (false == rest.empty() && rest != "end" + type && rest != "end" && rest != "endtri" && rest != "endline") + rest = parser.getToken(); + } + } + token = parser.getToken(); + } + return result; +} + +double sweep_node::project(glm::dvec3 const &Point, double &Lateral) const +{ + double best{std::numeric_limits::max()}; + double station{0.0}; + Lateral = 0.0; + glm::dvec2 const point{Point.x, Point.z}; + for (std::size_t i = 1; i < m_samples.size(); ++i) + { + glm::dvec2 const a{m_samples[i - 1].position.x, m_samples[i - 1].position.z}; + glm::dvec2 const b{m_samples[i].position.x, m_samples[i].position.z}; + auto const ab{b - a}; + auto const length2{glm::dot(ab, ab)}; + if (length2 < 1e-12) + continue; + auto const t{std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0)}; + auto const distance{glm::length(point - (a + ab * t))}; + if (distance < best) + { + best = distance; + station = m_samples[i - 1].station + (m_samples[i].station - m_samples[i - 1].station) * t; + auto const side{ab.x * (point - a).y - ab.y * (point - a).x}; + Lateral = side >= 0.0 ? distance : -distance; + } + } + return station; +} + +double sweep_node::model_length() const +{ + auto const pieces{items()}; + double low{std::numeric_limits::max()}, high{-std::numeric_limits::max()}; + for (auto const &entry : pieces) + { + std::vector parts; + gather_parts(entry.model->Root, entry.transform, entry.skins, parts); + for (auto const &part : parts) + for (auto const &vertex : part.vertices) + { + auto const along{m_state.along_x ? vertex.position.x : vertex.position.z}; + low = std::min(low, along); + high = std::max(high, along); + } + } + return high > low ? high - low : 0.0; +} + +std::vector sweep_node::create_shapes() const +{ + std::vector shapes; + auto const &definition{m_state}; + if (m_samples.size() < 2) + return shapes; + // the parts of each model of the template, and where the model stands in the template + struct piece + { + std::vector parts; + glm::dvec3 origin{0.0}; + glm::dvec3 low{std::numeric_limits::max()}; + glm::dvec3 high{-std::numeric_limits::max()}; + bool bent{false}; + }; + std::vector pieces; + for (auto const &entry : items()) + { + piece model; + gather_parts(entry.model->Root, entry.transform, entry.skins, model.parts); + model.origin = glm::dvec3(entry.transform[3]); + if (false == model.parts.empty()) + pieces.push_back(std::move(model)); + } + if (pieces.empty()) + return shapes; + // the side of the model facing the curve is the one its lateral axis points away from: -x, or -z for a model laid along x + auto const needsflip{definition.along_x ? definition.lateral < 0.0 : definition.lateral > 0.0}; + auto const flip{definition.flip != (definition.face && needsflip)}; + // the model's own axes: along the curve, sideways (+ to the right of it), and up + auto const axes = [&](glm::dvec3 const &Vector) { + glm::dvec3 result{definition.along_x ? Vector.x : Vector.z, definition.along_x ? Vector.z : -Vector.x, Vector.y}; + if (flip) + { + result.x = -result.x; + result.y = -result.y; + } + if (definition.mirror) + { + result.y = -result.y; + } + return result; + }; + glm::dvec3 low{std::numeric_limits::max()}, high{-std::numeric_limits::max()}; + for (auto &model : pieces) + { + for (auto const &part : model.parts) + for (auto const &vertex : part.vertices) + { + auto const local{axes(vertex.position)}; + model.low = glm::min(model.low, local); + model.high = glm::max(model.high, local); + } + low = glm::min(low, model.low); + high = glm::max(high, model.high); + } + // the long models of a template follow the curve, the others stand at their places along it + for (auto &model : pieces) + model.bent = definition.bend && (pieces.size() == 1 || model.high.x - model.low.x >= 0.5 * (high.x - low.x)); + // what's bent sets the length of a copy, its sides and its top; the rest only goes along with it + if (std::any_of(pieces.begin(), pieces.end(), [](piece const &Model) { return Model.bent; })) + { + low = glm::dvec3{std::numeric_limits::max()}; + high = glm::dvec3{-std::numeric_limits::max()}; + for (auto const &model : pieces) + if (model.bent) + { + low = glm::min(low, model.low); + high = glm::max(high, model.high); + } + } + auto const own{high.x - low.x}; + // the point of the model the offset puts at its place, sideways and up + auto const nearside{definition.lateral >= 0.0 ? low.y : high.y}; + auto const farside{definition.lateral >= 0.0 ? high.y : low.y}; + double edgetop{-std::numeric_limits::max()}; + for (auto const &model : pieces) + if (model.bent || false == definition.bend) + for (auto const &part : model.parts) + for (auto const &vertex : part.vertices) + { + auto const local{axes(vertex.position)}; + if (std::abs(local.y - nearside) <= edge_band) + edgetop = std::max(edgetop, local.z); + } + if (edgetop == -std::numeric_limits::max()) + edgetop = high.z; + glm::dvec3 const anchor_shift{0.0, + definition.side_anchor == 1 ? -nearside : definition.side_anchor == 2 ? -farside : definition.side_anchor == 3 ? -0.5 * (low.y + high.y) : 0.0, + definition.height_anchor == 1 ? -low.z : definition.height_anchor == 2 ? -high.z : definition.height_anchor == 3 ? -edgetop : 0.0}; + auto const from{start()}; + auto const to{end()}; + auto const stretch{to - from}; + if (own < 1e-3 || stretch < 1e-3) + return shapes; + if (definition.bend) + { + auto const along = [&](glm::dvec3 const &Position) { return axes(Position).x; }; + for (auto &model : pieces) + if (model.bent) + for (auto &part : model.parts) + part.vertices = slices(part.vertices, along, low.x, slice_width); + } + auto const copy_length{definition.step > 0.01 ? definition.step : own}; + // copies end to end fill the stretch exactly, each one squeezed or stretched a bit; repeated ones go at even steps + auto const copies{definition.bend ? std::max(1, static_cast(std::round(stretch / copy_length))) : static_cast(std::floor(stretch / copy_length + 1e-6)) + 1}; + auto const spacing{definition.bend ? stretch / copies : copy_length}; + auto const scale{definition.bend ? spacing / own : 1.0}; + std::size_t percopy{0}; + for (auto const &model : pieces) + for (auto const &part : model.parts) + percopy += part.vertices.size(); + auto const limit{std::max(1, vertex_limit / std::max(1, percopy))}; + if (static_cast(copies) > limit) + ErrorLog("Bad sweep: \"" + m_name + "\" would make " + std::to_string(copies) + " copies of \"" + definition.model + "\", only " + std::to_string(limit) + " are made"); + auto const count{std::min(copies, limit)}; + struct batch + { + material_handle material; + lighting_data lighting; + bool translucent; + std::vector vertices; + }; + std::vector batches; + auto const target = [&](model_part const &Part) -> std::vector & { + for (auto &entry : batches) + if (entry.material == Part.material && entry.lighting == Part.lighting && entry.translucent == Part.translucent) + return entry.vertices; + batches.push_back({Part.material, Part.lighting, Part.translucent, {}}); + return batches.back().vertices; + }; + // set back by the widening of the gauge: from the radius, and on the inner side of a canted curve by the lean of the cars + // at the height of the edge + auto const side{definition.lateral >= 0.0 ? 1 : 0}; + auto const edgeheight{std::max(0.0, definition.height - gauge::rail_height)}; + auto const setback = [&](frame const &At) { + if (false == definition.widen) + return 0.0; + auto const widening{At.widening[side] + std::max(0.0, At.cant[side]) * edgeheight / 1.5}; + return definition.lateral >= 0.0 ? widening : -widening; + }; + // heights are given from the head of the rail on the side of the copies. the cant turns the track about its inner rail when the + // simulation raises it for the cant, about the axis otherwise; a copy which leans with the cant goes up with the axis + auto const cone = [&](frame const &At) { return rail_head(At.roll, definition.tilt ? 0.0 : definition.lateral); }; + auto const upright = [](frame At) { + glm::dvec3 forward{At.forward.x, 0.0, At.forward.z}; + forward = glm::length(forward) > 1e-9 ? glm::normalize(forward) : glm::dvec3{0.0, 0.0, 1.0}; + At.forward = forward; + At.left = glm::dvec3{-forward.z, 0.0, forward.x}; + At.up = glm::dvec3{0.0, 1.0, 0.0}; + return At; + }; + glm::dvec3 const offset{0.0, definition.height, 0.0}; + for (std::size_t copy = 0; copy < count; ++copy) + { + auto const base{from + spacing * static_cast(copy)}; + for (auto const &model : pieces) + { + // a model which doesn't bend stands upright at its point: the place of its origin along the curve + auto const anchor{axes(model.origin) + anchor_shift}; + auto const stand{upright(frame_at(definition.bend ? base + (anchor.x - low.x) * scale : base))}; + for (auto const &part : model.parts) + { + auto &vertices{target(part)}; + auto const first{vertices.size()}; + for (auto const &vertex : part.vertices) + { + auto const local{axes(vertex.position) + anchor_shift}; + auto const direction{axes(glm::dvec3(vertex.normal))}; + world_vertex placed{vertex}; + if (model.bent) + { + auto const at{frame_at(base + (local.x - low.x) * scale)}; + placed.position = at.position + at.left * (local.y + definition.lateral + setback(at)) + at.up * local.z + offset + glm::dvec3{0.0, cone(at), 0.0}; + placed.normal = glm::vec3(glm::normalize(at.forward * direction.x + at.left * direction.y + at.up * direction.z)); + } + else + { + auto const along{definition.bend ? local.x - anchor.x : local.x}; + placed.position = stand.position + stand.forward * along + stand.left * (local.y + definition.lateral + setback(stand)) + stand.up * local.z + offset + glm::dvec3{0.0, cone(stand), 0.0}; + placed.normal = glm::vec3(glm::normalize(stand.forward * direction.x + stand.left * direction.y + stand.up * direction.z)); + } + vertices.push_back(placed); + } + if (definition.mirror) + { + // a mirror image turns the triangles inside out + for (auto i = first; i + 2 < vertices.size(); i += 3) + std::swap(vertices[i + 1], vertices[i + 2]); + } + } + } + } + for (auto &entry : batches) + { + if (entry.vertices.empty()) + continue; + scene::shape_node shape; + shape.make_terrain(entry.material, std::move(entry.vertices), glm::dvec3{0.0}); + shape.lighting(entry.lighting); + shape.translucent(entry.translucent); + shapes.emplace_back(std::move(shape)); + } + return shapes; +} + +void sweep_node::show() +{ + if (m_merged || m_shapes.shown() || m_editorremoved) + { + return; + } + m_shapes.show(create_shapes(), location()); +} + +void sweep_node::hide() +{ + m_shapes.hide(); +} + +float sweep_node::radius_() +{ + return static_cast(0.5 * length() + std::abs(m_state.lateral) + 25.0); +} + +void sweep_node::serialize_(std::ostream &Output) const +{ + // TODO: implement +} + +void sweep_node::deserialize_(std::istream &Input) +{ + // TODO: implement +} + +void sweep_node::export_as_text_(std::ostream &Output) const +{ + auto const &sweep{m_state}; + Output << "sweep " << sweep.model << ' ' << (sweep.skin.empty() ? std::string{"none"} : sweep.skin) << ' '; + Output << "mode " << (sweep.bend ? "bend" : "repeat") << ' '; + if (sweep.step > 0.0) + { + Output << "step " << sweep.step << ' '; + } + if (sweep.from > 0.0 || sweep.to >= 0.0) + { + Output << "range " << sweep.from << ' ' << sweep.to << ' '; + } + if (sweep.lateral != 0.0 || sweep.height != 0.0) + { + Output << "offset " << sweep.lateral << ' ' << sweep.height << ' '; + } + if (sweep.along_x) + { + Output << "axis x "; + } + if (sweep.flip) + { + Output << "flip "; + } + if (sweep.mirror) + { + Output << "mirror "; + } + if (sweep.tilt) + { + Output << "tilt "; + } + if (false == sweep.face) + { + Output << "noface "; + } + if (sweep.widen) + { + Output << "widen "; + } + if (false == sweep.parameters.empty()) + { + Output << "parameters " << sweep.parameters.size() << ' '; + for (auto const ¶meter : sweep.parameters) + Output << (parameter.empty() ? std::string{"none"} : parameter) << ' '; + } + if (sweep.side_anchor != 0 || sweep.height_anchor != 0) + { + char const *sides[] = {"origin", "near", "far", "centre"}; + char const *heights[] = {"origin", "bottom", "top", "edge"}; + Output << "anchor " << sides[std::clamp(sweep.side_anchor, 0, 3)] << ' ' << heights[std::clamp(sweep.height_anchor, 0, 3)] << ' '; + } + auto const precision{Output.precision(std::numeric_limits::digits10)}; + for (auto const &piece : sweep.pieces) + { + auto const &start{piece.points[segment_data::point::start]}; + auto const &control1{piece.points[segment_data::point::control1]}; + auto const &control2{piece.points[segment_data::point::control2]}; + auto const &end{piece.points[segment_data::point::end]}; + Output << "\npiece " << start.x << ' ' << start.y << ' ' << start.z << ' ' << piece.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' + << end.x << ' ' << end.y << ' ' << end.z << ' ' << piece.rolls[1] << ' ' << piece.radius; + } + Output.precision(precision); + Output << "\nendsweep\n"; +} + +void sweep_table::create_geometry(scene::scratch_data &Scratchpad) +{ + if (simulation::Region == nullptr) + { + return; + } + for (auto *sweep : m_items) + { + if (sweep == nullptr) + { + continue; + } + if (Global.editor_session && false == Global.NvRenderer) + { + // scenery opened for editing: the copies are kept apart, so they can be changed later + sweep->show(); + continue; + } + for (auto &shape : sweep->create_shapes()) + { + simulation::Region->insert(shape, Scratchpad, false); + } + sweep->merged(true); + } +} diff --git a/world/Sweep.h b/world/Sweep.h new file mode 100644 index 00000000..7da708d7 --- /dev/null +++ b/world/Sweep.h @@ -0,0 +1,158 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include + +#include + +#include "utilities/Classes.h" +#include "utilities/Names.h" +#include "scene/scenenode.h" +#include "world/Road.h" +#include "world/Segment.h" +#include "vehicle/DynObj.h" + +namespace scene +{ +struct scratch_data; +} + +// a model laid along a curve: copies of it bent to follow the curve end to end, or set along it at even steps. +// the node isn't drawn itself, it makes ordinary shapes of the copies, so the renderers deal with it as with the rest +// of the scenery. animations, lights and other submodels which aren't plain triangles are left out of the copies. +// scenery entry: +// node sweep [ ]... piece ... endsweep +// properties: mode , step , +// range (m along the curve, to below 0: up to its end), offset , +// anchor (what of the model the offset puts there: sideways its origin, its side +// facing the curve, the other side or its middle; up its origin, its bottom or its top), +// axis (axis of the model laid along the curve), flip (the model goes the other way), mirror (its sides swapped), +// widen (the offset grows by the widening of the structure gauge in the curves, the way the platforms are set back), +// noface (the model isn't turned to have its -x side, or -z for one laid along x, facing the curve), +// parameters ... (of a template: the model can be an *.inc file, its long models are bent, the others stand +// upright at their places along the curve), +// anchor sides: origin, near, far, centre; heights: origin, bottom, top, edge (the top of the side facing the curve), +// tilt (bent copies lean with the cant of the curve; otherwise they stay upright, as the platforms and the walls by a track do, +// at the height the plane of the cant has at their sideways offset) +class sweep_node : public scene::basic_node +{ + + public: + struct state + { + std::string model; + std::string skin{"none"}; + bool bend{true}; + double step{0.0}; + double from{0.0}; + double to{-1.0}; + double lateral{0.0}; + double height{0.0}; + bool along_x{false}; + bool flip{false}; + bool mirror{false}; + bool tilt{false}; + bool face{true}; // the model is turned so its -x side (-z for a model laid along x) faces the curve + bool widen{false}; // moved away from the curve by the widening of the structure gauge in the curves + std::vector parameters; // of a template (*.inc) laid along the curve + int side_anchor{0}; // 0: origin, 1: the side facing the curve, 2: the far side, 3: the middle + int height_anchor{0}; // 0: origin, 1: bottom, 2: top, 3: top of the side facing the curve + std::vector pieces; + + bool operator==(state const &Other) const; + }; + // point of the curve, with the directions the copies are laid out by + struct frame + { + glm::dvec3 position{0.0}; + glm::dvec3 forward{0.0, 0.0, 1.0}; + glm::dvec3 left{-1.0, 0.0, 0.0}; + glm::dvec3 up{0.0, 1.0, 0.0}; + double roll{0.0}; // degrees, of the curve there + std::array widening{}; // of the structure gauge, below the platforms: right and left side, metres + std::array cant{}; // metres, positive on the inner side of the curve + }; + + explicit sweep_node(scene::node_data const &Nodedata); + + void import(cParser &Input, glm::dvec3 const &Offset); + void define(state const &State); + state const &definition() const + { + return m_state; + } + // length of the curve, in the plan + double length() const; + frame frame_at(double const Station) const; + // station of the point of the curve nearest to the point in the plan, and how far to the right of the curve it is + double project(glm::dvec3 const &Point, double &Lateral) const; + // stretch of the curve the copies take + double start() const; + double end() const; + // length of a copy along the curve, as the model has it + double model_length() const; + std::vector create_shapes() const; + // a model of what's laid along the curve, placed in its space + struct item + { + TModel3d *model{nullptr}; + material_data skins; + glm::dmat4 transform{1.0}; + }; + std::vector items() const; + // height of the head of the rail on the side of the offset over the points of the track, less the height of the rail + static double rail_head(double const Roll, double const Lateral); + // puts the copies in the scene as shapes of their own, which can be taken back + void show(); + void hide(); + void merged(bool const Merged) + { + m_merged = Merged; + } + bool merged() const + { + return m_merged; + } + + bool m_editorremoved{false}; + + private: + void rebuild_samples(); + float radius_() override; + void serialize_(std::ostream &Output) const override; + void deserialize_(std::istream &Input) override; + void export_as_text_(std::ostream &Output) const override; + + struct sample + { + double station; + glm::dvec3 position; + glm::dvec3 tangent; + double roll; // degrees + std::array widening; + std::array cant; + }; + state m_state; + std::vector m_samples; + bool m_merged{false}; + owned_shapes m_shapes; +}; + +// collection of the models laid along curves present in the scene +class sweep_table : public basic_table +{ + + public: + // generates geometry of the copies and puts it in the scene + void create_geometry(scene::scratch_data &Scratchpad); +}; From 0a3cd55e59210f7e58f2e7b80be2cb30398ec712 Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 6 Oct 2026 22:19:40 +0200 Subject: [PATCH 4/6] feat(launcher): -edit without a scenery opens the wizard of a new scenery: file name, title and centre, picked on the OpenStreetMap map of geoportal.gov.pl in PUWG 1992 or given in degrees; the scenery is written with the centre as its //$g reference and opened in the editor; a bare -edit no longer loads the scenery of eu07.ini --- CMakeLists.txt | 1 + application/application.cpp | 16 +- editor/editorOrthophoto.cpp | 11 + editor/editorOrthophoto.hpp | 2 + launcher/launcheruilayer.cpp | 7 +- launcher/launcheruilayer.h | 2 + launcher/scenery_wizard.cpp | 382 +++++++++++++++++++++++++++++++++++ launcher/scenery_wizard.h | 70 +++++++ 8 files changed, 486 insertions(+), 5 deletions(-) create mode 100644 launcher/scenery_wizard.cpp create mode 100644 launcher/scenery_wizard.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e2fa3abd..090e6050 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -259,6 +259,7 @@ set(SOURCES "launcher/keymapper.cpp" "launcher/textures_scanner.cpp" "launcher/scenery_scanner.cpp" +"launcher/scenery_wizard.cpp" "vr/vr_interface.cpp" ) diff --git a/application/application.cpp b/application/application.cpp index db77c976..13fa8620 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -1126,6 +1126,8 @@ int eu07_application::init_settings(int Argc, char *Argv[]) } // process command line arguments + bool sceneryfromcommandline{false}; + bool editwithoutscenery{false}; for (int i = 1; i < Argc; ++i) { @@ -1136,6 +1138,7 @@ int eu07_application::init_settings(int Argc, char *Argv[]) if (i + 1 < Argc) { Global.SceneryFile = ToLower(Argv[++i]); + sceneryfromcommandline = true; } } else if (token == "-v") @@ -1148,12 +1151,15 @@ int eu07_application::init_settings(int Argc, char *Argv[]) else if (token == "-edit") { // edit session: the scenery is loaded without player vehicle, straight into the editor - if (i + 1 < Argc) + Global.editor_session = true; + Global.local_start_vehicle = "ghostview"; + if (i + 1 < Argc && Argv[i + 1][0] != '-') { Global.SceneryFile = ToLower(Argv[++i]); - Global.local_start_vehicle = "ghostview"; - Global.editor_session = true; + sceneryfromcommandline = true; } + else + editwithoutscenery = true; } else if (token == "-seed") { @@ -1169,11 +1175,13 @@ int eu07_application::init_settings(int Argc, char *Argv[]) { std::cout << "usage: " << std::string(Argv[0]) << " [-s sceneryfilepath]" << " [-v vehiclename]" - << " [-edit sceneryfilepath]" + << " [-edit [sceneryfilepath]]" << " [-seed number|word]" << std::endl; return -1; } } + if (editwithoutscenery && false == sceneryfromcommandline) + Global.SceneryFile.clear(); return 0; } diff --git a/editor/editorOrthophoto.cpp b/editor/editorOrthophoto.cpp index 935fd825..26bbb646 100644 --- a/editor/editorOrthophoto.cpp +++ b/editor/editorOrthophoto.cpp @@ -256,6 +256,17 @@ bool http_client::get(std::string const &, std::vector &Out, std:: #endif +} // namespace + +bool editor_http_get(std::string const &Url, std::vector &Out, std::string &Error) +{ + thread_local http_client client; + return client.get(Url, Out, Error, []() { return false; }); +} + +namespace +{ + // --------------------------------------------------------------------------------------------- // geoportal requests and the disk cache diff --git a/editor/editorOrthophoto.hpp b/editor/editorOrthophoto.hpp index cfaf925a..a2e5b761 100644 --- a/editor/editorOrthophoto.hpp +++ b/editor/editorOrthophoto.hpp @@ -27,6 +27,8 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Classes.h" // material_handle #include "rendering/geometrybank.h" // gfx::geometry_handle +bool editor_http_get(std::string const &Url, std::vector &Out, std::string &Error); + namespace scene { class basic_region; diff --git a/launcher/launcheruilayer.cpp b/launcher/launcheruilayer.cpp index 91974bbc..45baccb0 100644 --- a/launcher/launcheruilayer.cpp +++ b/launcher/launcheruilayer.cpp @@ -11,8 +11,9 @@ launcher_ui::launcher_ui() : m_scenery_scanner(m_vehicles_bank), m_scenerylist_p add_external_panel(&m_scenerylist_panel); add_external_panel(&m_keymapper_panel); add_external_panel(&m_vehiclepicker_panel); + add_external_panel(&m_scenerywizard_panel); - open_panel(&m_scenerylist_panel); + open_panel(Global.editor_session ? static_cast(&m_scenerywizard_panel) : &m_scenerylist_panel); m_suppress_menu = true; } @@ -50,6 +51,9 @@ void launcher_ui::render_() if (ImGui::Button(STR_C("Keymapper"), topbar_button_size)) open_panel(&m_keymapper_panel); ImGui::SameLine(); + if (ImGui::Button(STR_C("New scenery"), topbar_button_size)) + open_panel(&m_scenerywizard_panel); + ImGui::SameLine(); if (ImGui::Button(STR_C("Quit"), topbar_button_size)) Application.queue_quit(false); } @@ -61,6 +65,7 @@ void launcher_ui::close_panels() m_scenerylist_panel.is_open = false; m_vehiclepicker_panel.is_open = false; m_keymapper_panel.is_open = false; + m_scenerywizard_panel.is_open = false; } void launcher_ui::open_panel(ui_panel* panel) diff --git a/launcher/launcheruilayer.h b/launcher/launcheruilayer.h index 91e36bbd..6c9cb937 100644 --- a/launcher/launcheruilayer.h +++ b/launcher/launcheruilayer.h @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "launcher/vehicle_picker.h" #include "launcher/textures_scanner.h" #include "launcher/scenery_scanner.h" +#include "launcher/scenery_wizard.h" class launcher_ui : public ui_layer { public: @@ -33,6 +34,7 @@ private: ui::scenerylist_panel m_scenerylist_panel; ui::keymapper_panel m_keymapper_panel; ui::vehiclepicker_panel m_vehiclepicker_panel; + ui::scenerywizard_panel m_scenerywizard_panel; ui_panel *m_current_panel; }; diff --git a/launcher/scenery_wizard.cpp b/launcher/scenery_wizard.cpp new file mode 100644 index 00000000..a6c96520 --- /dev/null +++ b/launcher/scenery_wizard.cpp @@ -0,0 +1,382 @@ +/* +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 "launcher/scenery_wizard.h" +#include "application/application.h" +#include "editor/editorFormat.hpp" +#include "editor/editorOrthophoto.hpp" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/translation.h" + +#include "stb/stb_image.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +double constexpr kSemiMajor{6378137.0}; +double constexpr kFlattening{1.0 / 298.257222101}; +double constexpr kScale{0.9993}; +double constexpr kFalseEasting{500000.0}; +double constexpr kFalseNorthing{-5300000.0}; +double constexpr kCentralMeridian{19.0}; +glm::dvec2 const kPolandLow{140000.0, 120000.0}; +glm::dvec2 const kPolandHigh{890000.0, 800000.0}; +std::string const kMapService{"https://mapy.geoportal.gov.pl/wss/ext/OSM/BaseMap/service"}; + +double seconds() +{ + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); +} + +glm::dvec2 to_puwg(double const Latitude, double const Longitude) +{ + auto const e2{2.0 * kFlattening - kFlattening * kFlattening}; + auto const ep2{e2 / (1.0 - e2)}; + auto const phi{glm::radians(Latitude)}; + auto const sine{std::sin(phi)}; + auto const cosine{std::cos(phi)}; + auto const n{kSemiMajor / std::sqrt(1.0 - e2 * sine * sine)}; + auto const t{std::tan(phi) * std::tan(phi)}; + auto const c{ep2 * cosine * cosine}; + auto const a{glm::radians(Longitude - kCentralMeridian) * cosine}; + auto const m{kSemiMajor * ((1.0 - e2 / 4.0 - 3.0 * e2 * e2 / 64.0 - 5.0 * e2 * e2 * e2 / 256.0) * phi - (3.0 * e2 / 8.0 + 3.0 * e2 * e2 / 32.0 + 45.0 * e2 * e2 * e2 / 1024.0) * std::sin(2.0 * phi) + + (15.0 * e2 * e2 / 256.0 + 45.0 * e2 * e2 * e2 / 1024.0) * std::sin(4.0 * phi) - (35.0 * e2 * e2 * e2 / 3072.0) * std::sin(6.0 * phi))}; + auto const x{kScale * n * (a + (1.0 - t + c) * std::pow(a, 3) / 6.0 + (5.0 - 18.0 * t + t * t + 72.0 * c - 58.0 * ep2) * std::pow(a, 5) / 120.0)}; + auto const y{kScale * (m + n * std::tan(phi) * (a * a / 2.0 + (5.0 - t + 9.0 * c + 4.0 * c * c) * std::pow(a, 4) / 24.0 + (61.0 - 58.0 * t + t * t + 600.0 * c - 330.0 * ep2) * std::pow(a, 6) / 720.0))}; + return {kFalseEasting + x, y + kFalseNorthing}; +} + +glm::dvec2 to_degrees(glm::dvec2 const &Point) +{ + auto const e2{2.0 * kFlattening - kFlattening * kFlattening}; + auto const ep2{e2 / (1.0 - e2)}; + auto const x{Point.x - kFalseEasting}; + auto const m{(Point.y - kFalseNorthing) / kScale}; + auto const mu{m / (kSemiMajor * (1.0 - e2 / 4.0 - 3.0 * e2 * e2 / 64.0 - 5.0 * e2 * e2 * e2 / 256.0))}; + auto const e1{(1.0 - std::sqrt(1.0 - e2)) / (1.0 + std::sqrt(1.0 - e2))}; + auto const phi1{mu + (3.0 * e1 / 2.0 - 27.0 * std::pow(e1, 3) / 32.0) * std::sin(2.0 * mu) + (21.0 * e1 * e1 / 16.0 - 55.0 * std::pow(e1, 4) / 32.0) * std::sin(4.0 * mu) + + (151.0 * std::pow(e1, 3) / 96.0) * std::sin(6.0 * mu) + (1097.0 * std::pow(e1, 4) / 512.0) * std::sin(8.0 * mu)}; + auto const sine{std::sin(phi1)}; + auto const c1{ep2 * std::cos(phi1) * std::cos(phi1)}; + auto const t1{std::tan(phi1) * std::tan(phi1)}; + auto const n1{kSemiMajor / std::sqrt(1.0 - e2 * sine * sine)}; + auto const r1{kSemiMajor * (1.0 - e2) / std::pow(1.0 - e2 * sine * sine, 1.5)}; + auto const d{x / (n1 * kScale)}; + auto const phi{phi1 - (n1 * std::tan(phi1) / r1) * (d * d / 2.0 - (5.0 + 3.0 * t1 + 10.0 * c1 - 4.0 * c1 * c1 - 9.0 * ep2) * std::pow(d, 4) / 24.0 + + (61.0 + 90.0 * t1 + 298.0 * c1 + 45.0 * t1 * t1 - 252.0 * ep2 - 3.0 * c1 * c1) * std::pow(d, 6) / 720.0)}; + auto const lambda{(d - (1.0 + 2.0 * t1 + c1) * std::pow(d, 3) / 6.0 + (5.0 - 2.0 * c1 + 28.0 * t1 - 3.0 * c1 * c1 + 8.0 * ep2 + 24.0 * t1 * t1) * std::pow(d, 5) / 120.0) / std::cos(phi1)}; + return {glm::degrees(phi), kCentralMeridian + glm::degrees(lambda)}; +} + +bool valid_name(std::string const &Name) +{ + return false == Name.empty() && std::all_of(Name.begin(), Name.end(), [](char const Character) { return (Character >= 'a' && Character <= 'z') || (Character >= '0' && Character <= '9') || Character == '_' || Character == '-'; }); +} + +} // namespace + +ui::scenerywizard_panel::scenerywizard_panel() : ui_panel(STR("New scenery"), false) {} + +ui::scenerywizard_panel::~scenerywizard_panel() +{ + if (m_request.valid()) + m_request.wait(); + if (m_texture != 0) + glDeleteTextures(1, &m_texture); +} + +void ui::scenerywizard_panel::render_contents() +{ + upload_map(); + auto const available{ImGui::GetContentRegionAvail()}; + auto const form{360.f * Global.ui_scale}; + glm::vec2 const map{std::max(200.f, available.x - form - ImGui::GetStyle().ItemSpacing.x), std::max(200.f, available.y)}; + ImGui::BeginChild("##wizardmap", ImVec2(map.x, map.y), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + render_map(map - glm::vec2{16.f}); + ImGui::EndChild(); + ImGui::SameLine(); + ImGui::BeginChild("##wizardform", ImVec2(0.f, map.y), true); + render_form(); + ImGui::EndChild(); +} + +void ui::scenerywizard_panel::render_map(glm::vec2 const &Size) +{ + auto const origin{ImGui::GetCursorScreenPos()}; + if (false == m_fitted) + { + m_fitted = true; + m_centre = (kPolandLow + kPolandHigh) * 0.5; + m_scale = std::max((kPolandHigh.x - kPolandLow.x) / Size.x, (kPolandHigh.y - kPolandLow.y) / Size.y); + } + ImGui::InvisibleButton("##map", ImVec2(Size.x, Size.y)); + auto const hovered{ImGui::IsItemHovered()}; + auto const active{ImGui::IsItemActive()}; + auto const &io{ImGui::GetIO()}; + glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; + glm::vec2 const middle{origin.x + Size.x * 0.5f, origin.y + Size.y * 0.5f}; + auto const to_world = [&](glm::vec2 const &Screen) { return glm::dvec2{m_centre.x + (Screen.x - middle.x) * m_scale, m_centre.y - (Screen.y - middle.y) * m_scale}; }; + auto const to_screen = [&](glm::dvec2 const &World) { return ImVec2(middle.x + static_cast((World.x - m_centre.x) / m_scale), middle.y - static_cast((World.y - m_centre.y) / m_scale)); }; + + if (hovered && io.MouseWheel != 0.f) + { + auto const under{to_world(mouse)}; + m_scale = std::clamp(m_scale * std::pow(0.8, static_cast(io.MouseWheel)), 0.25, 2000.0); + m_centre = {under.x - (mouse.x - middle.x) * m_scale, under.y + (mouse.y - middle.y) * m_scale}; + m_dirty = true; + m_changed = seconds(); + } + if (ImGui::IsItemActivated()) + { + m_pressed = mouse; + m_dragging = false; + } + if (active) + { + if (glm::length(mouse - m_pressed) > 4.f) + m_dragging = true; + if (m_dragging && (io.MouseDelta.x != 0.f || io.MouseDelta.y != 0.f)) + { + m_centre.x -= io.MouseDelta.x * m_scale; + m_centre.y += io.MouseDelta.y * m_scale; + m_dirty = true; + m_changed = seconds(); + } + } + if (ImGui::IsItemDeactivated() && false == m_dragging) + pick(to_world(mouse)); + + if (m_dirty && seconds() - m_changed > 0.25 && false == m_request.valid()) + request_map(Size); + + auto *drawlist{ImGui::GetWindowDrawList()}; + drawlist->PushClipRect(origin, ImVec2(origin.x + Size.x, origin.y + Size.y), true); + drawlist->AddRectFilled(origin, ImVec2(origin.x + Size.x, origin.y + Size.y), IM_COL32(200, 214, 222, 255)); + if (m_texture != 0) + drawlist->AddImage(reinterpret_cast(static_cast(m_texture)), to_screen({m_image_low.x, m_image_high.y}), to_screen({m_image_high.x, m_image_low.y})); + if (m_picked) + { + auto const at{to_screen(m_point)}; + auto const colour{IM_COL32(220, 30, 30, 255)}; + drawlist->AddCircle(at, 10.f, colour, 24, 2.5f); + drawlist->AddLine(ImVec2(at.x - 16.f, at.y), ImVec2(at.x + 16.f, at.y), colour, 2.f); + drawlist->AddLine(ImVec2(at.x, at.y - 16.f), ImVec2(at.x, at.y + 16.f), colour, 2.f); + } + std::string info; + if (hovered) + { + auto const world{to_world(mouse)}; + auto const degrees{to_degrees(world)}; + info = format(STR_C("E %.3f km, N %.3f km (%.5f N, %.5f E)"), world.x / 1000.0, world.y / 1000.0, degrees.x, degrees.y); + } + if (m_request.valid()) + info += (info.empty() ? "" : " ") + std::string{STR_C("loading the map...")}; + else if (false == m_map_error.empty()) + info += (info.empty() ? "" : " ") + m_map_error; + if (false == info.empty()) + { + auto const size{ImGui::CalcTextSize(info.c_str())}; + drawlist->AddRectFilled(ImVec2(origin.x + 4.f, origin.y + 4.f), ImVec2(origin.x + 12.f + size.x, origin.y + 8.f + size.y), IM_COL32(0, 0, 0, 170), 4.f); + drawlist->AddText(ImVec2(origin.x + 8.f, origin.y + 6.f), IM_COL32(255, 255, 255, 240), info.c_str()); + } + drawlist->PopClipRect(); +} + +void ui::scenerywizard_panel::request_map(glm::vec2 const &Size) +{ + m_dirty = false; + auto const width{std::clamp(static_cast(Size.x), 64, 2048)}; + auto const height{std::clamp(static_cast(Size.y), 64, 2048)}; + glm::dvec2 const half{width * 0.5 * m_scale, height * 0.5 * m_scale}; + auto const low{m_centre - half}; + auto const high{m_centre + half}; + char bbox[160]; + std::snprintf(bbox, sizeof(bbox), "%.1f,%.1f,%.1f,%.1f", low.x, low.y, high.x, high.y); + auto const url{kMapService + "?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=osm&STYLES=&SRS=EPSG:2180&BBOX=" + bbox + "&WIDTH=" + std::to_string(width) + "&HEIGHT=" + std::to_string(height) + "&FORMAT=image/png"}; + m_request = std::async(std::launch::async, [url, low, high, width, height]() { + map_result result; + result.low = low; + result.high = high; + std::vector data; + if (false == editor_http_get(url, data, result.error)) + return result; + int w{0}, h{0}, components{0}; + auto *pixels{stbi_load_from_memory(data.data(), static_cast(data.size()), &w, &h, &components, 4)}; + if (pixels == nullptr) + { + result.error = STR("The map service sent no image"); + return result; + } + result.width = w; + result.height = h; + result.pixels.assign(pixels, pixels + static_cast(w) * h * 4); + stbi_image_free(pixels); + return result; + }); +} + +void ui::scenerywizard_panel::upload_map() +{ + if (false == m_request.valid() || m_request.wait_for(std::chrono::seconds(0)) != std::future_status::ready) + return; + auto result{m_request.get()}; + if (false == result.error.empty() || result.pixels.empty()) + { + m_map_error = result.error.empty() ? std::string{STR("The map service sent no image")} : STR("No map: ") + result.error; + return; + } + m_map_error.clear(); + if (m_texture == 0) + glGenTextures(1, &m_texture); + glBindTexture(GL_TEXTURE_2D, m_texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, result.width, result.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, result.pixels.data()); + glBindTexture(GL_TEXTURE_2D, 0); + m_image_low = result.low; + m_image_high = result.high; +} + +void ui::scenerywizard_panel::pick(glm::dvec2 const &Point) +{ + m_point = Point; + m_picked = true; + auto const degrees{to_degrees(Point)}; + std::snprintf(m_latlon, sizeof(m_latlon), "%.6f, %.6f", degrees.x, degrees.y); +} + +void ui::scenerywizard_panel::render_form() +{ + ImGui::TextUnformatted(STR_C("New scenery")); + ImGui::Separator(); + ImGui::TextWrapped("%s", STR_C("Find the place on the map: the wheel zooms, dragging moves the map. A click sets the centre of the scenery, its point 0 0 0.")); + ImGui::Spacing(); + + ImGui::SetNextItemWidth(-1.f); + ImGui::InputTextWithHint("##name", STR_C("file name, e.g. my_line"), m_name, sizeof(m_name), ImGuiInputTextFlags_CharsNoBlank); + std::string name{m_name}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + auto const path{Global.asCurrentSceneryPath + name + ".scn"}; + auto const named{valid_name(name)}; + auto const exists{named && std::filesystem::exists(path)}; + if (name.empty()) + ImGui::TextDisabled("%s", STR_C("scenery/.scn")); + else if (false == named) + ImGui::TextColored(ImVec4(1.f, 0.45f, 0.35f, 1.f), "%s", STR_C("Lower case letters, digits, _ and - only")); + else if (exists) + ImGui::TextColored(ImVec4(1.f, 0.45f, 0.35f, 1.f), STR_C("%s exists already"), path.c_str()); + else + ImGui::TextDisabled("%s", path.c_str()); + + ImGui::SetNextItemWidth(-1.f); + ImGui::InputTextWithHint("##title", STR_C("title, written in the file"), m_title, sizeof(m_title)); + ImGui::Spacing(); + + ImGui::TextUnformatted(STR_C("Centre of the scenery, PUWG 1992")); + double east{m_point.x / 1000.0}; + double north{m_point.y / 1000.0}; + ImGui::SetNextItemWidth(130.f); + auto changed{ImGui::InputDouble(STR_C("E, km##east"), &east, 0.0, 0.0, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue)}; + ImGui::SetNextItemWidth(130.f); + changed |= ImGui::InputDouble(STR_C("N, km##north"), &north, 0.0, 0.0, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue); + if (changed) + { + pick({east * 1000.0, north * 1000.0}); + m_centre = m_point; + m_dirty = true; + } + ImGui::SetNextItemWidth(-90.f); + auto const entered{ImGui::InputTextWithHint("##latlon", STR_C("latitude, longitude"), m_latlon, sizeof(m_latlon), ImGuiInputTextFlags_EnterReturnsTrue)}; + ImGui::SameLine(); + if (ImGui::Button(STR_C("Show")) || entered) + { + std::string text{m_latlon}; + std::replace(text.begin(), text.end(), ',', ' '); + std::istringstream words(text); + double latitude{0.0}, longitude{0.0}; + if (words >> latitude >> longitude && latitude > 45.0 && latitude < 58.0 && longitude > 10.0 && longitude < 28.0) + { + pick(to_puwg(latitude, longitude)); + m_centre = m_point; + m_scale = std::min(m_scale, 4.0); + m_dirty = true; + m_status.clear(); + } + else + m_status = STR("Give the latitude and the longitude in degrees, e.g. 52.2297, 21.0122"); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Degrees, as given by the online maps; the map moves there")); + if (m_picked) + { + auto const degrees{to_degrees(m_point)}; + ImGui::TextDisabled(STR_C("%.5f N, %.5f E"), degrees.x, degrees.y); + } + ImGui::Spacing(); + ImGui::TextWrapped("%s", STR_C("The scenery keeps the centre as its geographic reference (//$g PUWG1992): the orthophoto of the editor lies under it, in place.")); + ImGui::Spacing(); + + auto const ready{named && false == exists && m_picked}; + if (false == ready) + ImGui::TextDisabled("%s", m_picked ? STR_C("Give the file a name") : STR_C("Click the centre on the map")); + if (ImGui::Button(STR_C("Create and open in the editor"), ImVec2(-1.f, 0.f)) && ready) + create_scenery(); + if (false == m_status.empty()) + ImGui::TextWrapped("%s", m_status.c_str()); + ImGui::Spacing(); + ImGui::TextDisabled("%s", STR_C("Map: (c) OpenStreetMap contributors, served by geoportal.gov.pl")); +} + +bool ui::scenerywizard_panel::create_scenery() +{ + std::string name{m_name}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + auto const file{name + ".scn"}; + std::ofstream output(Global.asCurrentSceneryPath + file, std::ios::binary); + if (false == output.is_open()) + { + m_status = STR("The file can't be written"); + return false; + } + auto const now{std::time(nullptr)}; + char date[32]; + std::strftime(date, sizeof(date), "%Y-%m-%d", std::localtime(&now)); + char reference[96]; + std::snprintf(reference, sizeof(reference), "//$g PUWG1992 %.3f %.3f", m_point.x / 1000.0, m_point.y / 1000.0); + output << reference << "\r\n"; + if (m_title[0] != '\0') + output << "// " << m_title << "\r\n"; + output << "// created in the editor " << date << "\r\n\r\n"; + output << "atmo 0.423 0.702 1.0 1700 2000 0.70 0.80 0.9 endatmo\r\n\r\n"; + output << "FirstInit\r\n"; + output.close(); + WriteLog("Scenery wizard: created " + file + " at " + reference); + + Global.SceneryFile = file; + Global.local_start_vehicle = "ghostview"; + Global.editor_session = true; + Application.pop_mode(); + Application.push_mode(eu07_application::mode::scenarioloader); + return true; +} diff --git a/launcher/scenery_wizard.h b/launcher/scenery_wizard.h new file mode 100644 index 00000000..d47362d0 --- /dev/null +++ b/launcher/scenery_wizard.h @@ -0,0 +1,70 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "application/uilayer.h" + +#include +#include +#include +#include + +namespace ui +{ + +class scenerywizard_panel : public ui_panel +{ + public: + scenerywizard_panel(); + ~scenerywizard_panel() override; + + void render_contents() override; + + private: + struct map_result + { + glm::dvec2 low{0.0}; + glm::dvec2 high{0.0}; + int width{0}; + int height{0}; + std::vector pixels; + std::string error; + }; + + void render_map(glm::vec2 const &Size); + void render_form(); + void request_map(glm::vec2 const &Size); + void upload_map(); + void pick(glm::dvec2 const &Point); + bool create_scenery(); + + glm::dvec2 m_centre{515000.0, 460000.0}; + double m_scale{1000.0}; + bool m_fitted{false}; + double m_changed{0.0}; + bool m_dirty{true}; + glm::vec2 m_pressed{0.f}; + bool m_dragging{false}; + + std::future m_request; + unsigned int m_texture{0}; + glm::dvec2 m_image_low{0.0}; + glm::dvec2 m_image_high{0.0}; + std::string m_map_error; + + bool m_picked{false}; + glm::dvec2 m_point{0.0}; + char m_name[64]{}; + char m_title[128]{}; + char m_latlon[64]{}; + std::string m_status; +}; + +} // namespace ui From 24923dd472a8f5479d1fe968fa593ced3b746a57 Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 6 Oct 2026 22:19:54 +0200 Subject: [PATCH 5/6] feat(editor): track editor driven by what the cursor is over: the outline and a label show what a click or a drag does, snapping to free ends on the screen, handles and ends dragged directly, no rotation gizmo; laying from a free end the same in every mode, straight with Shift, the curve of a switch going on past its diverging end; height 0 where there's no terrain; placed switch leaves the switch mode, double slips listed apart, a drive of the switch placed with it by the //$e switch description of its template; compound curves of any number of radii; dragged end of a straight fits the curve beside it; name of a path, followed by the memory cells and the includes taking it; turntable placed from a template described as one, tracks led out of it and the tracks around fitted to it, its positions written to the include; infrastructure bound to the path whose events read its memory cell; terrain made where the route has none and shaped to the formation under the ballast; models along the track over the selected path and on along the joined ones while dragged, rarely used settings folded; vehicles and trainsets placed on any path, switches included, facing the way the arrow shows; joined paths spread into the selection; New scenery in the File menu starts the editor again with the wizard; ortho zoom towards the cursor; scripted self tests; Polish translation --- CMakeLists.txt | 8 + application/editormode.cpp | 384 +++++-- application/editormode.h | 630 +++++++++++- application/editormodeinfra.cpp | 459 +++++++-- application/editormodejoints.cpp | 521 ++++++++++ application/editormodelineside.cpp | 983 ++++++++++++++++++ application/editormodeparallel.cpp | 234 +++++ application/editormodeprofile.cpp | 188 +++- application/editormodeselftest.cpp | 385 +++++++ application/editormodespeed.cpp | 17 +- application/editormodesweep.cpp | 955 ++++++++++++++++++ application/editormodetrack.cpp | 1128 ++++++++++++++++----- application/editormodetrackux.cpp | 1359 +++++++++++++++++++++++++ application/editormodeturntable.cpp | 1025 +++++++++++++++++++ application/editormodevehicle.cpp | 315 ++++++ application/editorprojection.h | 29 + application/editoruilayer.cpp | 4 + application/editoruilayer.h | 2 + editor/editorAlignment.cpp | 71 +- editor/editorAlignment.hpp | 14 +- editor/editorGeometry.hpp | 31 + editor/editorIncludeInfo.cpp | 17 +- editor/editorIncludeInfo.hpp | 7 +- editor/editorTrack.cpp | 227 ++++- editor/editorTrack.hpp | 25 +- lang/editor_pl.po | 1441 ++++++++++++++++++++++++++- 26 files changed, 9991 insertions(+), 468 deletions(-) create mode 100644 application/editormodejoints.cpp create mode 100644 application/editormodelineside.cpp create mode 100644 application/editormodeparallel.cpp create mode 100644 application/editormodeselftest.cpp create mode 100644 application/editormodesweep.cpp create mode 100644 application/editormodetrackux.cpp create mode 100644 application/editormodeturntable.cpp create mode 100644 application/editormodevehicle.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 090e6050..116f032d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -185,11 +185,19 @@ set(SOURCES "editor/editorOrthophoto.cpp" "application/editormode.cpp" "application/editormodetrack.cpp" +"application/editormodetrackux.cpp" "application/editormoderoad.cpp" "application/editormodeprofile.cpp" "application/editormodegauge.cpp" "application/editormodeinfra.cpp" "application/editormodespeed.cpp" +"application/editormodejoints.cpp" +"application/editormodeparallel.cpp" +"application/editormodelineside.cpp" +"application/editormodevehicle.cpp" +"application/editormodesweep.cpp" +"application/editormodeturntable.cpp" +"application/editormodeselftest.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" "application/editoruipanels.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index f5b6d716..93b9bf4c 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -10,6 +10,12 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" #include "application/editoruilayer.h" +#ifndef _WIN32 +#include +#include +extern char **environ; +#endif +#include "application/editorprojection.h" #include "application/application.h" #include "editor/editorSettings.hpp" @@ -477,10 +483,12 @@ editor_mode::editor_mode() { ui()->set_fill_options([this]() { render_area_fill(); }); ui()->set_gizmo_options([this]() { render_gizmo_options(); }); ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); }); + ui()->set_new_scenery([this]() { m_newscenery_asked = true; }); ui()->set_menu_options([this]() { render_object_menu(); render_track_menu(); render_road_menu(); + render_map_menu(); }); m_roadtool.settings.normalize(); road_select(nullptr); @@ -893,6 +901,20 @@ void editor_mode::undo_last() EditorSnapshot snap = m_history.back(); m_history.pop_back(); + if (false == snap.sweeps.empty() || false == snap.sweeps_toggled.empty()) + { + restore_sweeps(snap); + g_redo.push_back(std::move(snap)); + return; + } + + if (false == snap.instances.empty() || false == snap.directives.empty()) + { + restore_includes(snap); + g_redo.push_back(std::move(snap)); + return; + } + if (snap.instance != 0) { restore_include(snap); @@ -997,6 +1019,20 @@ void editor_mode::redo_last() EditorSnapshot snap = g_redo.back(); g_redo.pop_back(); + if (false == snap.sweeps.empty() || false == snap.sweeps_toggled.empty()) + { + restore_sweeps(snap); + m_history.push_back(std::move(snap)); + return; + } + + if (false == snap.instances.empty() || false == snap.directives.empty()) + { + restore_includes(snap); + m_history.push_back(std::move(snap)); + return; + } + if (snap.instance != 0) { restore_include(snap); @@ -1100,6 +1136,14 @@ void editor_mode::redo_last() bool editor_mode::update() { + selftest_step(); + if (m_vehicle.leave) + { + m_vehicle.leave = false; + Application.pop_mode(); + return true; + } + Timer::UpdateTimers(true); simulation::State.update_clocks(); @@ -1256,13 +1300,36 @@ bool editor_mode::update() draw_straights_overlay(); if (ui()->mode() == nodebank_panel::TRACK) { + if (m_track_box.active && false == ImGui::GetIO().MouseDown[0]) + finish_track_box(); + update_track_hover(); + point_drag_update(); + if (m_track_tab == track_tab::turntable) + draw_turntable_overlay(); + if (m_track_tab == track_tab::lineside) + { + draw_parallel_preview(); + draw_hekto_overlay(); + draw_fouling_overlay(); + draw_vehicle_marker(); + sweep_drag(); + draw_sweep_overlay(); + } + render_track_context(); update_build_tools(); + update_track_intent(); + draw_track_hover(); + draw_track_set(); + draw_track_spread(); draw_build_overlay(); + draw_track_intent(); speed_step(); + joints_step(); draw_track_hints(); draw_profile_overlay(); draw_infra_overlay(); draw_speed_overlay(); + draw_joints_overlay(); } update_gauge(); render_gauge_window(); @@ -1276,6 +1343,9 @@ bool editor_mode::update() if (ui()->mode() == nodebank_panel::FILL) draw_area_fill_outline(); + render_orthophoto_window(); + render_new_scenery_popup(); + // --- ImGui: Editor Settings & History windows --- if(m_settings_open) render_settings(); @@ -1318,11 +1388,6 @@ void editor_mode::render_settings() render_terrain_ui(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("Orthophoto")) - { - render_orthophoto_ui(); - ImGui::EndTabItem(); - } ImGui::EndTabBar(); } @@ -1517,7 +1582,13 @@ void editor_mode::load_orthophoto_settings() config.in_scene = stored.in_scene; config.north = 0.0; config.east = 0.0; - EditorSettings.orthophoto_origin(scenery, config.north, config.east); + m_georeference_read = false; + read_georeference(); + if (false == std::as_const(EditorSettings).orthophoto_origin(scenery, config.north, config.east) && m_georeference) + { + config.north = m_georeference_origin.x; + config.east = m_georeference_origin.y; + } m_orthophoto.settings(config); m_orthophoto_origin_edit = {config.north, config.east}; } @@ -1567,6 +1638,21 @@ void editor_mode::render_orthophoto_ui() config.east = m_orthophoto_origin_edit.y; changed = persist = true; } + if (m_georeference) + { + ImGui::TextDisabled("%s", m_georeference_line.c_str()); + ImGui::TextDisabled(STR_C("in the scenery file: Y (easting) %.0f m, X (northing) %.0f m"), m_georeference_origin.y, m_georeference_origin.x); + if (config.north != m_georeference_origin.x || config.east != m_georeference_origin.y) + { + 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; + } + } + } if (config.north == 0.0 && config.east == 0.0) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f), "Enter the origin of this scenery first"); else @@ -2103,13 +2189,13 @@ void editor_mode::commit_terrain() m_streamer.flush(); // save resident edited chunks to disk } -void editor_mode::save() +bool editor_mode::save() { if (scene::Layers.empty()) { // without the sources tracked during the load there's no telling where the changes belong ui()->set_status("The scenery wasn't opened for editing. Start the simulator with: -edit ", true); - return; + return false; } // terrain made in the editor is kept in files of its own, the scenery only needs the directive which loads it @@ -2137,6 +2223,7 @@ void editor_mode::save() } ui()->set_status(result.message, false == result.success); WriteLog("Editor: " + result.message, logtype::generic); + return result.success; } void editor_mode::toggle_include(scene::instance_handle const Instance) @@ -2175,6 +2262,33 @@ void editor_mode::restore_include(EditorSnapshot &Snapshot) select_include(scene::Layers.removable(Snapshot.instance) ? Snapshot.instance : 0); } +void editor_mode::restore_includes(EditorSnapshot &Snapshot) +{ + std::size_t gone{0}; + for (auto const instance : Snapshot.instances) + { + if (false == scene::Layers.tracked(instance) || scene::Layers.instance(instance).dead) + { + ++gone; + continue; + } + scene::Layers.removed(instance, false == scene::Layers.instance(instance).removed); + } + for (auto &entry : Snapshot.directives) + { + auto current = scene::Layers.directive(entry.first); + if (current.empty() || scene::Layers.instance(entry.first).dead) + { + ++gone; + continue; + } + set_include_directive(entry.first, entry.second); + 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); +} + void editor_mode::select_include(scene::instance_handle const Instance) { m_instance = (scene::Layers.tracked(Instance) ? Instance : 0); @@ -2541,6 +2655,146 @@ void editor_mode::drop_model_instances() ui()->set_status(status); } +void editor_mode::read_georeference() +{ + if (m_georeference_read) + return; + m_georeference_read = true; + m_georeference = false; + m_georeference_line.clear(); + // the name of the scenery is kept in lower case, the file may have capitals in its name + std::filesystem::path file{Global.asCurrentSceneryPath + Global.SceneryFile}; + std::error_code error; + if (false == std::filesystem::exists(file, error)) + { + for (auto const &entry : std::filesystem::directory_iterator(Global.asCurrentSceneryPath, error)) + { + auto name{entry.path().filename().string()}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + if (name == Global.SceneryFile) + { + file = entry.path(); + break; + } + } + } + std::ifstream stream(file); + std::string line; + for (int count = 0; count < 200 && std::getline(stream, line); ++count) + { + auto const mark = line.find("//$g"); + if (mark == std::string::npos) + continue; + // 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}; + 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; + m_georeference_origin = {north * scale, east * scale}; + m_georeference_line = line.substr(mark); + if (false == m_georeference_line.empty() && m_georeference_line.back() == '\r') + m_georeference_line.pop_back(); + m_georeference = true; + break; + } +} + +void editor_mode::render_map_menu() +{ + if (false == ImGui::BeginMenu(STR_C("Map"))) + return; + bool enabled = m_orthophoto.enabled(); + if (ImGui::MenuItem(STR_C("Orthophoto"), nullptr, enabled)) + m_orthophoto.enabled(false == enabled); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Aerial imagery of geoportal.gov.pl under the scenery, laid out by the origin of the scenery")); + ImGui::MenuItem(STR_C("Orthophoto settings..."), nullptr, &m_orthophoto_window); + ImGui::EndMenu(); +} + +void editor_mode::render_new_scenery_popup() +{ + if (m_newscenery_asked) + { + ImGui::OpenPopup(STR_C("New scenery##restart")); + m_newscenery_asked = false; + } + if (false == ImGui::BeginPopupModal(STR_C("New scenery##restart"), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + return; + ImGui::TextUnformatted(STR_C("The editor starts again, with the wizard of a new scenery.")); + auto const editsession{false == scene::Layers.empty()}; + if (false == m_history.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "%s", STR_C("What isn't saved of this scenery is lost.")); + if (editsession && ImGui::Button(STR_C("Save and start the wizard"))) + { + if (save() && false == restart_for_new_scenery()) + ui()->set_status(STR_C("The editor couldn't be started again"), true); + ImGui::CloseCurrentPopup(); + } + if (editsession) + ImGui::SameLine(); + if (ImGui::Button(STR_C("Start the wizard"))) + { + if (false == restart_for_new_scenery()) + ui()->set_status(STR_C("The editor couldn't be started again"), true); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); +} + +bool editor_mode::restart_for_new_scenery() +{ +#ifdef _WIN32 + _putenv("EU07_EDITOR_SELFTEST="); + wchar_t path[MAX_PATH]; + if (GetModuleFileNameW(nullptr, path, MAX_PATH) == 0) + return false; + std::wstring command{L"\"" + std::wstring{path} + L"\" -edit"}; + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + if (false == CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup, &process)) + return false; + CloseHandle(process.hThread); + CloseHandle(process.hProcess); +#else + unsetenv("EU07_EDITOR_SELFTEST"); + char path[4096]; + auto const size{readlink("/proc/self/exe", path, sizeof(path) - 1)}; + if (size <= 0) + return false; + path[size] = '\0'; + char edit[]{"-edit"}; + char *arguments[]{path, edit, nullptr}; + posix_spawnattr_t attributes; + posix_spawnattr_init(&attributes); + posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETSID); + pid_t child; + auto const result{posix_spawn(&child, path, nullptr, &attributes, arguments, environ)}; + posix_spawnattr_destroy(&attributes); + if (result != 0) + return false; +#endif + WriteLog("Editor: started again for a new scenery"); + Application.queue_quit(true); + return true; +} + +void editor_mode::render_orthophoto_window() +{ + if (false == m_orthophoto_window) + return; + ImGui::SetNextWindowSize(ImVec2(430.0f, 0.0f), ImGuiCond_FirstUseEver); + if (ImGui::Begin(STR_C("Orthophoto##window"), &m_orthophoto_window, ImGuiWindowFlags_AlwaysAutoResize)) + render_orthophoto_ui(); + ImGui::End(); +} + void editor_mode::render_object_menu() { if (ImGui::BeginMenu("Objects")) @@ -2899,6 +3153,12 @@ void editor_mode::render_gizmo() return; } + if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::path && m_track_set.size() >= 2) + { + render_track_set_gizmo(); + return; + } + if (straights_active()) { render_straight_gizmo(); @@ -2920,7 +3180,7 @@ void editor_mode::render_gizmo() if (selected_track()) { // in the track modes the whole path is moved only while selecting, or the switch in its mode - if (ui()->mode() != nodebank_panel::TRACK || m_track_tab == track_tab::path || m_track_tab == track_tab::turnout) + if (ui()->mode() != nodebank_panel::TRACK || (m_track_tab == track_tab::path && false == m_point_drag.active) || (m_track_tab == track_tab::turnout && selected_track()->eType == tt_Switch)) render_track_gizmo(); else m_track_gizmo_using = false; @@ -3153,7 +3413,16 @@ void editor_mode::on_scroll(double const Xoffset, double const Yoffset) { if (false == Global.EditorOrtho || ImGui::GetIO().WantCaptureMouse) return; - Global.EditorOrthoExtent = std::clamp(Global.EditorOrthoExtent * static_cast(std::pow(0.85, Yoffset)), 5.0f, 5000.0f); + auto const before{Global.EditorOrthoExtent}; + auto const after{std::clamp(before * static_cast(std::pow(0.85, Yoffset)), 5.0f, 5000.0f)}; + screen_projection const projection; + glm::dvec3 origin, 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; + Camera.Pos.z = origin.z + (Camera.Pos.z - origin.z) * scale; + Global.EditorOrthoExtent = after; + Global.pCamera = Camera; } void editor_mode::update_camera(double const Deltatime) @@ -3286,6 +3555,9 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in if (!anyModifier && m_userinterface->on_key(Key, Action)) return; + if (ui()->mode() == nodebank_panel::TRACK && is_press(Action) && (Mods & (GLFW_MOD_CONTROL | GLFW_MOD_ALT)) == 0 && typed_key(Key)) + return; + // gizmo transform shortcuts (Q/W/E/R) — only when the camera isn't being flown (RMB up). // handled before the camera keyboard step because Q/W/E are also the fly-mode movement keys, // which would otherwise consume them. @@ -3316,6 +3588,16 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in return; } + if (Global.ctrlState && false == Global.shiftState && Key == GLFW_KEY_F && ui()->mode() == nodebank_panel::TRACK) + { + if (is_press(Action)) + { + show_track_tab(m_track_tab); + m_track_search.focus = true; + } + return; + } + // then internal input handling if (m_input.keyboard.key(Key, Action)) return; @@ -3324,7 +3606,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in return; // shortcuts: undo/redo. not in the middle of a drag, it would go on from the state which was undone - if (Global.ctrlState && (Key == GLFW_KEY_Z || Key == GLFW_KEY_Y) && is_press(Action) && (m_track_gizmo_using || m_straights.dragging || m_extend.active || m_switch.placing)) + if (Global.ctrlState && (Key == GLFW_KEY_Z || Key == GLFW_KEY_Y) && is_press(Action) && (m_track_gizmo_using || m_track_set_using || m_straights.dragging || m_extend.active || m_switch.placing || m_point_drag.active || m_handle_drag.active)) return; if (Global.ctrlState && Key == GLFW_KEY_Z && is_press(Action)) { @@ -3401,6 +3683,10 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in ui()->set_node(nullptr); simulation::State.delete_model(model); } + else if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::path && m_track_set.size() >= 2) + { + track_set_delete(); + } else if (selected_track() != nullptr) { delete_selected_track(); @@ -3423,8 +3709,16 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in cancel_track_tools(); m_straights.tool = 0; } - else + 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) show_track_tab(track_tab::path); + else if (selected_track() != nullptr) + { + m_node = nullptr; + m_track_point = {}; + ui()->set_node(nullptr); + } } break; @@ -3443,7 +3737,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in case GLFW_KEY_ENTER: case GLFW_KEY_KP_ENTER: if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && m_lay.active) - lay_finish({}); + lay_enter(); break; case GLFW_KEY_K: @@ -3452,16 +3746,7 @@ 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) - { - auto *track{selected_track()}; - auto const parameter{editor_track::nearest_parameter(*track, Global.pCamera.Pos + GfxRenderer->Mouse_Position())}; - auto const before{editor_track::capture(*track)}; - if (auto *created{editor_track::split_path(*track, parameter)}) - { - push_track_snapshot({{track, before}}, {created}); - straight_refresh(); - } - } + split_track_at(*selected_track(), Global.pCamera.Pos + GfxRenderer->Mouse_Position()); break; case GLFW_KEY_F: @@ -3563,45 +3848,7 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const if (mode == nodebank_panel::TRACK) { - if (m_lay.active) - { - GfxRenderer->Pick_Node_Callback([this](scene::basic_node * /*node*/) { - if (viewport_click()) - lay_click(); - }); - m_input.mouse.button(Button, Action); - return; - } - if (start_extend() || start_switch_placement()) - { - m_input.mouse.button(Button, Action); - return; - } - if ((Mods & GLFW_MOD_ALT) != 0 && m_track_tab == track_tab::straights) - { - GfxRenderer->Pick_Node_Callback([this](scene::basic_node *node) { - if (viewport_click() && dynamic_cast(node) != nullptr) - toggle_straight_set(*dynamic_cast(node)); - }); - m_input.mouse.button(Button, Action); - return; - } - if ((Mods & (GLFW_MOD_CONTROL | GLFW_MOD_SHIFT)) != 0 && m_track_tab == track_tab::straights && false == current_straight().tracks.empty()) - { - if ((Mods & GLFW_MOD_CONTROL) != 0) - start_straight_gesture(1); - else - add_detour_point(); - m_input.mouse.button(Button, Action); - return; - } - if (false == ImGuizmo::IsOver() && false == pick_route_vertex() && false == place_straight_tool() && false == pick_straight_handle() && false == pick_track_handle()) - { - GfxRenderer->Pick_Node_Callback([this](scene::basic_node *node) { - if (viewport_click()) - select_track(node); - }); - } + track_press(track_intent_at(Mods)); m_input.mouse.button(Button, Action); return; } @@ -3700,12 +3947,18 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const if (is_release(Action)) { mouseHold = false; + finish_track_box(); + point_drag_finish(); + m_handle_drag = {}; + sweep_release(); if (m_straights.tool_mouse) finish_straight_gesture(); if (m_extend.active) finish_extend(); if (m_switch.placing) finish_switch_placement(); + if (m_turntable.placing) + turntable_finish_placement(); } m_dragging = false; @@ -3713,6 +3966,13 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const } else if (Button == GLFW_MOUSE_BUTTON_RIGHT) { + if (ui()->mode() == nodebank_panel::TRACK) + { + if (is_press(Action)) + track_context_press(); + else if (is_release(Action)) + track_context_release(); + } // game-engine style look: hide & grab the cursor while flying, restore it on release Application.set_cursor(is_press(Action) ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL); } diff --git a/application/editormode.h b/application/editormode.h index fb44b830..fdec7ae8 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -26,6 +26,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorProfile.hpp" #include "editor/editorInfra.hpp" #include "editor/editorGauge.hpp" +#include "world/Sweep.h" #include #include @@ -36,6 +37,12 @@ http://mozilla.org/MPL/2.0/. class TAnimModel; class TTrack; +namespace ui +{ +class vehicles_bank; +struct vehicle_desc; +struct skin_set; +} class editor_mode : public application_mode, private editor_track::observer { @@ -51,6 +58,19 @@ class editor_mode : public application_mode, private editor_track::observer int path{0}; glm::dvec2 direction{0.0, 1.0}; // of the track there, in the plan }; + // joint of the paths found wrong by the joints check + struct joint_issue + { + enum class kind { gap, step, kink, cant, grade, 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 + bool fixed{false}; + }; // constructors editor_mode(); ~editor_mode() override; @@ -120,6 +140,10 @@ class editor_mode : public application_mode, private editor_track::observer UID uuid; // node UUID for reference, used as fallback lookup for deleted/recreated nodes scene::layer_handle layer{null_handle}; // scenery layer of the node, so a recreated node returns to it scene::instance_handle instance{0}; // include of a scenery template which was placed or removed, instead of a node + std::vector instances; // includes placed or removed together + std::vector> directives; // includes changed together, with the directives to go back to + std::vector> sweeps; // models along curves changed, with the definitions to go back to + std::vector sweeps_toggled; // models along curves made or removed: undo flips whether they're there std::vector> tracks; std::vector created; std::vector removed; @@ -218,7 +242,7 @@ class editor_mode : public application_mode, private editor_track::observer // commits authored terrain to disk and enables streaming void commit_terrain(); // writes the changes to the files of scenery opened for editing (Ctrl+S) - void save(); + bool save(); // places include of specified scenery template at the cursor, in the active layer void place_include(std::string const &File, int RotationMode, float FixedRotation); // undo and redo of placing or removing an include: brings removed include back, or removes it @@ -231,6 +255,8 @@ class editor_mode : public application_mode, private editor_track::observer void set_include_directive(scene::instance_handle Instance, std::string const &Directive); // undo and redo of an include snapshot void restore_include(EditorSnapshot &Snapshot); + // undo and redo of the includes placed, removed or changed together + void restore_includes(EditorSnapshot &Snapshot); // gizmo for the selected include, limited to what the parameters of its template can express void render_include_gizmo(glm::mat4 const &View, glm::mat4 const &Projection, glm::dvec3 const &Camerapos); // exports the scenery in legacy (text) format, with layers hidden in the editor left out of the picture @@ -269,6 +295,18 @@ class editor_mode : public application_mode, private editor_track::observer // geoportal orthophoto layer drawn under the other viewport overlays void render_orthophoto_ui(); + void render_map_menu(); + void render_orthophoto_window(); + bool m_newscenery_asked{false}; + void render_new_scenery_popup(); + bool restart_for_new_scenery(); + bool m_orthophoto_window{false}; + // origin of the scenery in PUWG 1992 the scenery file gives in its //$g line, if it does + bool m_georeference_read{false}; + bool m_georeference{false}; + glm::dvec2 m_georeference_origin{0.0}; // northing, easting + std::string m_georeference_line; + void read_georeference(); void draw_orthophoto(); // picks up the stored settings and the origin of the current scenery void load_orthophoto_settings(); @@ -321,7 +359,7 @@ class editor_mode : public application_mode, private editor_track::observer TTrack *selected_track() const; void render_path_ui(); void draw_track_overlay() const; - bool pick_track_handle(); + editor_track::point_ref track_handle_hit() const; void select_track(scene::basic_node *Node); void render_track_gizmo(); void commit_track_drag(bool const Force); @@ -432,7 +470,6 @@ class editor_mode : public application_mode, private editor_track::observer bool route_relay_through_switches(editor_track::chain const &Chain, std::vector> &States, std::vector &Created); void route_recommend(alignment::vertex &Vertex) const; void draw_route_overlay() const; - bool pick_route_vertex(); void render_route_gizmo(); glm::dvec3 route_vertex_position(int const Vertex) const; bool route_active() const; @@ -457,6 +494,8 @@ class editor_mode : public application_mode, private editor_track::observer bool fitting{false}; editor_track::curve fit_before; editor_track::curve fit_after; + bool fit_has_before{false}; + bool fit_has_after{false}; std::vector> drag_states; glm::dvec3 preview_start{0.0}; glm::dvec3 preview_end{0.0}; @@ -487,7 +526,6 @@ class editor_mode : public application_mode, private editor_track::observer void draw_straights_overlay() const; editor_track::straight const ¤t_straight(); bool straights_active(); - bool pick_straight_handle(); void render_straight_gizmo(); void straight_apply(editor_track::straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End); void straights_apply(std::vector const &Lines, std::vector> const &Ends); @@ -498,7 +536,7 @@ class editor_mode : public application_mode, private editor_track::observer 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); void straight_refresh(); - bool start_curve_fit(editor_track::straight const &Line); + bool start_curve_fit(editor_track::straight const &Line, int const Handle); void update_curve_fit(glm::dvec3 const &Start, glm::dvec3 const &End); void add_detour_point(); void apply_detour(); @@ -531,9 +569,89 @@ class editor_mode : public application_mode, private editor_track::observer glm::dvec3 mouse{0.0}; editor_track::straight line; std::string status; + char name[64]{}; + struct drive_family + { + std::string name; + std::array files; + }; + std::vector drives; + bool drives_scanned{false}; + int drive{-1}; + int drive_side{0}; }; switch_tool m_switch; + void scan_switch_drives(); + std::string switch_name_for_new() const; + bool place_switch_drive(TTrack &Switch); + bool render_switch_drive_choice(); int m_turnout_template{0}; + struct turntable_template + { + std::string file; + std::string name; + double length{0.0}; + int angles{0}; + int positions{0}; + int track{0}; + int yaw{0}; + std::vector fixed; + }; + struct turntable_fit + { + editor_track::snap_target end; + double heading{0.0}; + double radius{0.0}; + double length{0.0}; + std::vector pieces; + std::string issue; + bool chosen{true}; + TTrack *replaces{nullptr}; + }; + struct turntable_tool + { + std::vector templates; + bool scanned{false}; + int chosen{-1}; + char name[64]{}; + bool placing{false}; + glm::dvec3 centre{0.0}; + double yaw{0.0}; + glm::vec2 pressed{0.f}; + editor_track::snap_target snap; + TTrack *table{nullptr}; + scene::instance_handle include{0}; + int kind{-1}; + double length{0.0}; + double table_yaw{0.0}; + std::vector exits; + float reach{80.f}; + float radius{150.f}; + float stub{25.f}; + float step{0.f}; + std::vector fits; + int hovered{-1}; + double heading{0.0}; + double exit_length{0.0}; + std::string status; + std::string error; + }; + turntable_tool m_turntable; + void scan_turntables(); + std::string turntable_name_for_new() const; + bool turntable_read(TTrack *Table); + glm::dvec3 turntable_centre() const; + std::vector turntable_lines() const; + double turntable_snap(double const Heading) const; + void turntable_finish_placement(); + void turntable_place(glm::dvec3 const &Centre, double const Yaw); + bool turntable_fit_end(editor_track::snap_target const &End, turntable_fit &Fit) const; + void turntable_find_fits(); + void turntable_build(std::vector const &Fits); + bool turntable_exit_at(glm::dvec3 const &Ground, double &Heading, double &Length) const; + void turntable_store_exits(); + void render_turntable_ui(); + void draw_turntable_overlay() const; struct extend_tool { bool active{false}; @@ -544,6 +662,9 @@ class editor_mode : public application_mode, private editor_track::observer glm::dvec3 mouse{0.0}; int path{0}; bool atend{true}; + editor_track::snap_target origin; + editor_track::snap_target snap; + glm::vec2 pressed{0.f}; }; extend_tool m_extend; // lays new track through the clicked points, with no path needed to start from @@ -570,18 +691,21 @@ class editor_mode : public application_mode, private editor_track::observer 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; void render_switch_ui(); bool start_switch_placement(); void finish_switch_placement(); std::vector switch_preview() const; void insert_switch(editor_track::straight const &Line, double const Along, int const Direction, int const Side); + void switch_placed(std::vector const &Created); static std::vector sample_chain(editor_track::chain const &Chain); static curve_sample sample_at(std::vector const &Frame, double const Station); 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); - 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); + 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 { editor_track::straight target; @@ -597,7 +721,7 @@ class editor_mode : public application_mode, private editor_track::observer 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); - bool start_extend(); + bool start_extend(editor_track::snap_target const &End); void finish_extend(); std::vector extend_pieces() const; // the free end of the path as the start of a new track; false if it isn't free or has no direction @@ -614,7 +738,7 @@ class editor_mode : public application_mode, private editor_track::observer 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, infra, lay }; + enum class track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable }; struct track_mode { char const *label; @@ -622,7 +746,10 @@ class editor_mode : public application_mode, private editor_track::observer track_tab tab; char const *tooltip; }; - static std::array const &track_modes(); + static std::array const &track_modes(); + // the objects along the track: a tab for each tool + int m_lineside_tab{0}; + void render_lineside_ui(); void render_track_menu(); void arm_switch(); bool m_track_window_open{false}; @@ -630,11 +757,21 @@ class editor_mode : public application_mode, private editor_track::observer void render_track_inspector(); void render_track_modes(TTrack *Track); void show_track_tab(track_tab const Tab); - bool track_analysis_tab() const { return m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || m_track_tab == track_tab::infra; } + 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 std::string track_mode_name() const; void render_turnout_ui(); + struct track_name_edit + { + TTrack const *track{nullptr}; + std::string synced; + char text[128]{}; + editor_track::name_uses uses; + std::string status; + }; + 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}; @@ -651,6 +788,431 @@ class editor_mode : public application_mode, private editor_track::observer void render_track_guide(); std::string track_readout() const; void draw_track_hints(); + // path under the cursor, outlined before a click so it's known which one the click takes + struct track_hover + { + TTrack *track{nullptr}; + int path{0}; + glm::dvec3 point{0.0}; + double since{0.0}; + }; + track_hover m_hover; + track_hover track_under_cursor() const; + void update_track_hover(); + 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 }; + kind what{kind::none}; + TTrack *track{nullptr}; + int path{0}; + editor_track::point_ref point; + int index{-1}; + glm::dvec3 position{0.0}; + editor_track::snap_target snap; + std::string action; + }; + track_intent m_intent; + track_intent track_intent_at(int const Mods); + bool track_gesture_on() const; + void update_track_intent(); + void track_press(track_intent const &Intent); + void turntable_press(track_intent const &Intent); + track_intent turntable_intent(glm::dvec3 const &Ground, bool const Shift); + void draw_track_intent() const; + editor_track::snap_target snap_free_end(glm::dvec3 const &Near, glm::vec2 const &Screen, TTrack const *Self, int const Category, std::vector const &Exclude) const; + void draw_free_ends(TTrack const *Self, int const Category, std::vector const &Exclude) const; + struct point_drag + { + bool active{false}; + bool moved{false}; + glm::vec2 from{0.f}; + double height{0.0}; + }; + point_drag m_point_drag; + struct handle_drag + { + bool active{false}; + bool moved{false}; + glm::vec2 from{0.f}; + double height{0.0}; + glm::dvec3 offset{0.0}; + glm::dvec3 position{0.0}; + }; + handle_drag m_handle_drag; + void handle_drag_start(glm::dvec3 const &Anchor); + bool handle_drag_step(glm::dvec3 &Position); + void point_drag_start(editor_track::point_ref const &Point); + void point_drag_update(); + void point_drag_finish(); + bool route_hit(int &Vertex, int &Grip) const; + int straight_handle_hit(); + bool switch_reach(glm::dvec3 const &Ground, glm::dvec3 &Point); + glm::dvec3 cursor_ground() const; + glm::dvec3 cursor_level(double const Height) const; + bool sweep_grabs(glm::dvec3 const &Point); + // menu of the path a click of the right mouse button lands on + struct track_context + { + track_hover target; + std::chrono::steady_clock::time_point pressed; + glm::vec3 angle{0.f}; + glm::vec2 mouse{0.f}; + bool armed{false}; + bool pending{false}; + }; + track_context m_track_context; + void track_context_press(); + void track_context_release(); + 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); + // paths found by their names + struct track_search + { + char text[64]{}; + std::string searched; + std::vector found; + std::size_t total{0}; + bool focus{false}; + }; + track_search m_track_search; + void render_track_search(); + // paths selected together: Shift+LMB adds or takes one out, Shift+drag adds those in the box + std::vector m_track_set; + struct track_box + { + bool active{false}; + 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; + void track_set_toggle(TTrack &Track); + void track_set_prune(); + void start_track_box(); + void finish_track_box(); + void draw_track_set() const; + void render_track_set_ui(); + void render_track_set_gizmo(); + void track_set_delete(); + struct track_spread + { + int stop{0}; + bool same_rails{false}; + bool same_bed{false}; + bool same_speed{false}; + float reach{0.f}; + TTrack const *from{nullptr}; + int signature{-1}; + std::vector preview; + double length{0.0}; + }; + mutable track_spread m_spread; + std::vector track_spread_from(TTrack &Start) const; + std::vector const &track_spread_preview(TTrack &Start) const; + void track_spread_apply(TTrack &Start); + 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); + // second track alongside the selected path or the line through it, at a spacing, curves included + struct parallel_tool + { + bool open{false}; + bool expand{false}; + double spacing{4.0}; + int side{1}; // 1: to the right, -1: to the left, of the direction of the line + int scope{1}; // 0: the selected path, 1: the line through it, up to the switches + TTrack *built_for{nullptr}; + double built_spacing{0.0}; + int built_side{0}; + int built_scope{-1}; + std::size_t built_history{0}; + std::vector pieces; + std::vector styles; + double length{0.0}; + std::string error; + std::string status; + }; + parallel_tool m_parallel; + void parallel_update(); + void render_parallel_ui(); + void parallel_build(); + void draw_parallel_preview() const; + + // scenery templates standing by the track, placed and checked by the editor + struct template_item + { + std::string file; + glm::dvec3 location{0.0}; // world space + double yaw{0.0}; + std::vector rest; // parameters after the rotation + bool described{false}; + std::string track; + }; + struct standing_template + { + scene::instance_handle instance{0}; + std::string file; // lower case, forward slashes + glm::dvec3 location{0.0}; + double yaw{0.0}; + std::vector values; + }; + // 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; + // 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 + std::size_t move_templates(std::vector> const &Moves); + // takes them out, as one step of the undo + std::size_t remove_templates(std::vector const &Instances); + + // hectometre posts along the line, at every 100 m of the kilometrage + struct hekto_post + { + int hectometres{0}; + double chainage{0.0}; + glm::dvec3 position{0.0}; + double yaw{0.0}; + int standing{-1}; // post already there with this number + }; + struct hekto_found + { + standing_template post; + int hectometres{0}; + double chainage{0.0}; // where it stands along the line + double expected{0.0}; // where its number belongs + double error{0.0}; // m along the line + double distance{0.0}; // m from where it belongs, in the plan + bool wrong{false}; + bool duplicate{false}; // another post with the same number stands nearer to its place + bool outside{false}; // its number belongs past the ends of the line + }; + struct hekto_tool + { + bool open{false}; + bool expand{false}; + TTrack *track{nullptr}; // the line runs through it + glm::dvec3 point{0.0}; // where the kilometrage is given + double km{0.0}; + bool km_read{false}; // the kilometrage was read from the posts already standing + int growth{1}; // 1: grows along the line, -1: against + int side{1}; // 1: to the right of the growing kilometrage, -1: to the left, 0: alternately + double offset{3.0}; // m from the axis + int style{0}; // nowy, stary, zgnity, mixed + int turn{0}; // degrees added to the rotation + double reach{10000.0}; // m each way + double tolerance{5.0}; // m along the line, for the posts standing + bool dirty{true}; + std::size_t history{0}; + editor_track::route route; + std::vector samples; + double origin{0.0}; // chainage of the point + std::vector posts; + std::vector found; + std::string status; + std::string error; + }; + hekto_tool m_hekto; + void hekto_start(TTrack &Track, glm::dvec3 const &Point); + void hekto_update(); + void hekto_read_kilometrage(); + void hekto_place(); + void hekto_fix(); + void hekto_remove_duplicates(); + void render_hekto_ui(); + void draw_hekto_overlay() const; + + // fouling point markers (W17) where the tracks meeting at a switch are the spacing apart + struct fouling_point + { + TTrack *track{nullptr}; // the switch + glm::dvec3 position{0.0}; + double yaw{0.0}; + int standing{-1}; // marker already there + }; + struct fouling_found + { + standing_template marker; + int point{-1}; // the fouling point it belongs to + double error{0.0}; // m from where it belongs + double height_error{0.0}; // m over where it belongs + bool wrong{false}; + }; + struct fouling_tool + { + bool open{false}; + bool expand{false}; + int scope{0}; // 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 + int height_mode{1}; // 0: on the ground, 1: at the height given over the rail heads + double height{0.0}; // m over the rail heads + double height_tolerance{0.03}; // m, a marker standing higher or lower than this is wrong + int model{0}; // w17, w17_new, w17_old, w17_dwuteo + int turn{0}; + bool dirty{true}; + std::size_t history{0}; + TTrack *selected{nullptr}; + std::vector points; + std::vector found; + std::string status; + std::string error; + }; + fouling_tool m_fouling; + void fouling_update(); + void fouling_place(); + void fouling_fix(); + 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 + struct vehicle_tool + { + bool open{false}; + bool expand{false}; + std::shared_ptr bank; + char search[64]{}; + bool controllable_only{true}; + std::shared_ptr vehicle; + std::shared_ptr skin; + int driver{0}; // 0: a driver, to be driven; 1: nobody, parked + TTrack *track{nullptr}; + glm::dvec3 point{0.0}; + std::string placed; + std::string status; + bool leave{false}; // the editor gives way to the driving at the start of the next frame + struct consist_vehicle + { + std::shared_ptr vehicle; + std::shared_ptr skin; + bool turned{false}; + }; + std::vector consist; + int facing{1}; + }; + vehicle_tool m_vehicle; + void vehicle_start(TTrack &Track, glm::dvec3 const &Point); + bool vehicle_place(); + void render_vehicle_ui(); + void draw_vehicle_marker() const; + + // a model laid along the track: bent to follow it, or repeated along it + struct sweep_tool + { + bool open{false}; + bool expand{false}; + char model[256]{}; + char skin[128]{"none"}; + char search[64]{}; + std::vector models; + bool scanned{false}; + // parameters of a template laid along the track, with their names + std::string parameters_for; + std::vector> parameters; + std::vector parameter_labels; + std::vector parameter_placement; // filled in with zeros: the template is placed in its own space + std::vector> parameter_choices; // variants of the textures a parameter is a part of the name of + int length_mode{0}; // 0: one copy as long as the original, 1: the length given, 2: the whole stretch of the line + double length{200.0}; + sweep_node::state settings; + int axis{0}; // 0: the longer one of the model, 1: z, 2: x + double model_length{0.0}; + std::string measured; // model the length is of + // the curve: the axis of the track + TTrack *curve_for{nullptr}; + std::size_t curve_history{0}; + struct piece + { + TTrack *track{nullptr}; + int path{0}; + bool forward{true}; + }; + std::vector curve_pieces; + std::vector curve; + double curve_length{0.0}; + std::vector outline; // of the curve, for the preview + std::vector stations; + // marking the stretch in the view: press where it starts, the distance from the track sets the offset, drag to the end + bool marking{false}; + bool dragging{false}; + double anchor{0.0}; // station the drag started at + sweep_node *edited{nullptr}; + std::string status; + }; + sweep_tool m_sweep; + // paths the models along curves were laid on, so they follow the changes of the paths + struct sweep_link + { + sweep_node *sweep{nullptr}; + std::size_t piece{0}; + TTrack *track{nullptr}; + int path{0}; + bool reversed{false}; + }; + std::vector m_sweep_links; + void sweep_curve(); + void sweep_outline(); + bool sweep_extend(bool const Atend, glm::dvec3 const &Cursor); + double sweep_station(glm::dvec3 const &Point, double &Lateral) const; + void sweep_scan_models(); + void sweep_measure(); + sweep_node::state sweep_definition() const; + void sweep_create(); + void sweep_apply(); + void sweep_delete(sweep_node &Sweep); + void sweep_edit(sweep_node &Sweep); + std::vector sweeps_near(TTrack const &Track) const; + bool sweep_press(); + void sweep_drag(); + void sweep_release(); + // the same at a point of the world: Anywhere takes the press however far from the track it is + bool sweep_press_at(glm::dvec3 const &Point, bool const Anywhere); + void sweep_drag_at(glm::dvec3 const &Point); + void render_sweep_ui(); + void draw_sweep_overlay() const; + void restore_sweeps(EditorSnapshot &Snapshot); + void sweeps_captured(TTrack const &Track); + void sweeps_committed(std::vector const &Tracks); + void sweeps_split(TTrack &Original, TTrack &Created); + + // script of editor actions given in the EU07_EDITOR_SELFTEST file, run a step at a time, for testing without a person + struct selftest + { + bool loaded{false}; + std::vector lines; + std::size_t next{0}; + int wait{0}; + bool quitting{false}; + }; + selftest m_selftest; + void selftest_step(); + // a value typed during a gesture, taken instead of the one the cursor gives + std::string m_typed; + // what the typed value sets in the gesture under way, nullptr when the gesture takes none + 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}; // vertical profile (grade line) along a route @@ -674,6 +1236,7 @@ class editor_mode : public application_mode, private editor_track::observer // the editor terrain led to the grade line: formation under the track, slopes down or up to the ground struct earthworks { + bool from_ballast{true}; double depth{0.7}; // m, formation below the top of the rail double half_width{3.5}; // m, from the axis of the route to the edge of the formation double slope{1.5}; // horizontal run of the slopes per metre of height @@ -681,6 +1244,9 @@ class editor_mode : public application_mode, private editor_track::observer double rounding{1.5}; // m, over which the slopes bend into the formation and into the ground // heights of the terrain before the last shaping std::vector>> undo; + bool generate{true}; + std::vector> generated; + bool generated_streaming{false}; } earthworks; double view_from{0.0}; double view_to{100.0}; @@ -736,7 +1302,9 @@ class editor_mode : public application_mode, private editor_track::observer void profile_resample(); void profile_recognize(); void profile_fit_ground(); + static std::pair profile_ballast(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(); @@ -762,6 +1330,13 @@ class editor_mode : public application_mode, private editor_track::observer std::string reason; bool chosen{false}; bool bound{false}; + TTrack *reader{nullptr}; + std::string event; // of that path + std::string cell; + bool loose{false}; + TTrack *nearer{nullptr}; + glm::dvec3 nearer_foot{0.0}; + bool unread{false}; }; struct infra_window { @@ -774,6 +1349,11 @@ class editor_mode : public application_mode, private editor_track::observer std::vector rules{infra::default_rules()}; std::vector tracks; // of the scope std::vector candidates; + int filter{0}; + int read{0}; + int overruled{0}; + int unread{0}; + int elsewhere{0}; int hovered{-1}; std::string status; std::string error; @@ -827,6 +1407,36 @@ class editor_mode : public application_mode, private editor_track::observer void speed_apply(std::vector const &Indices, bool const Branches); std::vector speed_listed() const; void draw_speed_overlay() const; + + // joints of the paths: ends which almost meet, steps, kinks, jumps of the cant and of the grade + struct joints_state + { + bool open{false}; + int scope{0}; // 0: around the camera, 1: the whole scenery + double reach{2000.0}; + double gap{1.0}; // m, free ends closer than this to another end + double step{0.005}; // m + double angle{0.2}; // deg + double cant{5.0}; // mm + double grade{10.0}; // per mille + bool free_ends{false}; + std::vector queue; + std::size_t next{0}; + std::vector issues; + int filter{-1}; + int selected{-1}; + std::size_t history{0}; + std::string status; + }; + joints_state m_joints; + void joints_start(); + void joints_step(); + void joints_check(TTrack &Track); + 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); + void render_joints_body(); + void draw_joints_overlay() const; void infra_rebase(); void render_infra_search(); void render_infra_candidates(); diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index 00e6a60a..056b6b00 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -43,6 +43,7 @@ http://mozilla.org/MPL/2.0/. namespace { +using geometry::bezier; using geometry::plan_distance; using geometry::plan_of; @@ -419,6 +420,159 @@ class track_index std::map> m_cells; }; +struct cell_reader +{ + TTrack *track{nullptr}; + std::string event; + std::string cell; + bool loose{false}; +}; + +class event_links +{ + public: + event_links() + { + std::unordered_map named; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && false == track->name().empty() && track->name() != "none") + named.emplace(track->name(), track); + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved) + continue; + for (auto const *sequence : {&track->m_events0, &track->m_events1, &track->m_events2, &track->m_events0all, &track->m_events1all, &track->m_events2all}) + for (auto const &entry : *sequence) + read(track, entry.second != nullptr ? entry.second : simulation::Events.FindEvent(entry.first), entry.first.empty()); + } + static std::set const kinds{"event0", "event1", "event2", "eventall0", "eventall1", "eventall2"}; + std::unordered_set models(simulation::Instances.sequence().begin(), simulation::Instances.sequence().end()); + std::unordered_set cells(simulation::Memory.sequence().begin(), simulation::Memory.sequence().end()); + for (auto *event : simulation::Events.sequence()) + { + if (event == nullptr) + continue; + auto const &name{event->name()}; + auto const colon{name.rfind(':')}; + if (colon != std::string::npos && colon > 0 && kinds.count(name.substr(colon + 1)) > 0) + { + auto const track{named.find(name.substr(0, colon))}; + if (track != named.end()) + read(track->second, event, true); + } + auto const *multi{dynamic_cast(event)}; + if (multi == nullptr) + continue; + std::vector shown; + std::vector set; + for (auto *child : multi->children()) + { + auto const setter{dynamic_cast(child) != nullptr || dynamic_cast(child) != nullptr}; + for (auto *target : child->target_nodes()) + { + if (models.count(target) > 0 && false == contains(shown, static_cast(target))) + shown.push_back(target); + else if (setter && cells.count(target) > 0 && false == contains(set, static_cast(target))) + set.push_back(static_cast(target)); + } + } + if (shown.size() > 4 || set.size() > 4) + continue; + for (auto const *model : shown) + for (auto *cell : set) + if (plan_distance(model->location(), cell->location()) <= kCellReach && false == contains(m_cells[model], cell)) + m_cells[model].push_back(cell); + } + for (auto *cell : simulation::Memory.sequence()) + { + if (cell == nullptr) + continue; + if (cell->m_instance != 0) + m_included[cell->m_instance].push_back(cell); + if (false == cell->name().empty() && cell->name() != "none") + m_named.emplace_back(cell->name(), cell); + } + std::sort(m_named.begin(), m_named.end(), [](auto const &A, auto const &B) { return A.first < B.first; }); + } + std::vector cells_of(infra::binding const &Binding) const + { + std::vector result; + switch (Binding.type) + { + case infra::kind::memcell: result.push_back(static_cast(Binding.node)); break; + case infra::kind::include: + { + auto const found{m_included.find(Binding.include)}; + if (found != m_included.end()) + result = found->second; + break; + } + case infra::kind::model: + { + auto const found{m_cells.find(Binding.node)}; + if (found != m_cells.end()) + result = found->second; + auto const &name{Binding.node->name()}; + if (result.empty() && name.size() >= 3 && name != "none") + for (auto entry{std::lower_bound(m_named.begin(), m_named.end(), name, [](auto const &Entry, std::string const &Name) { return Entry.first < Name; })}; + entry != m_named.end() && entry->first.compare(0, name.size(), name) == 0; ++entry) + if (plan_distance(entry->second->location(), Binding.node->location()) <= kCellReach) + result.push_back(entry->second); + break; + } + default: break; + } + return result; + } + std::vector readers_of(std::vector const &Cells) const + { + std::vector result; + for (auto const *cell : Cells) + { + auto const found{m_readers.find(cell)}; + if (found == m_readers.end()) + continue; + for (auto const &reader : found->second) + if (std::none_of(result.begin(), result.end(), [&](cell_reader const &Reader) { return Reader.track == reader.track; })) + result.push_back(reader); + } + return result; + } + + private: + static constexpr double kCellReach{60.0}; + void read(TTrack *Track, basic_event *Event, bool const Loose) + { + std::vector pending{Event}; + std::unordered_set seen; + while (false == pending.empty()) + { + auto *event{pending.back()}; + pending.pop_back(); + if (event == nullptr || false == seen.insert(event).second) + continue; + if (auto const *multi{dynamic_cast(event)}) + { + auto const children{multi->children()}; + pending.insert(pending.end(), children.begin(), children.end()); + continue; + } + if (dynamic_cast(event) == nullptr) + continue; + auto const targets{event->target_nodes()}; + if (targets.empty()) + continue; + auto &list{m_readers[targets.front()]}; + if (std::none_of(list.begin(), list.end(), [&](cell_reader const &Reader) { return Reader.track == Track; })) + list.push_back({Track, Event->name(), targets.front()->name(), Loose}); + } + } + std::unordered_map> m_readers; + std::unordered_map> m_cells; + std::unordered_map> m_included; + std::vector> m_named; +}; + // lookup of points by a grid of 1 m cells in the plan template class point_grid @@ -711,8 +865,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; track_index index; std::unordered_set names; + std::unordered_set const scope(state.tracks.begin(), state.tracks.end()); for (auto *track : state.tracks) { index.add(*track); @@ -720,6 +876,18 @@ void editor_mode::infra_recognize() names.insert(track->name()); } Live.refresh(); + event_links const links; + std::unordered_map> threads; + auto const thread_of = [&](TTrack *Reader) -> std::vector const & { + auto found{threads.find(Reader)}; + if (found != threads.end()) + return found->second; + std::vector areas; + for (auto const &span : editor_track::run_route(*Reader, 250.0).spans) + if (scope.count(span.track) > 0 && std::none_of(areas.begin(), areas.end(), [&](track_area const &Area) { return Area.track == span.track; })) + areas.push_back(area_of(*span.track)); + return threads.emplace(Reader, std::move(areas)).first->second; + }; auto const corridor{static_cast(state.corridor)}; auto const add = [&](infra::binding Binding, std::string const &Reason, double const Reach) { @@ -727,12 +895,57 @@ void editor_mode::infra_recognize() std::optional yaw; if (false == object_points(Binding, points, yaw)) return; - for (auto const &point : points) + infra_candidate candidate; + auto const cells{points.size() == 1 ? links.cells_of(Binding) : std::vector{}}; + auto const readers{links.readers_of(cells)}; + infra::station geometric; + auto const near{index.nearest(points.front(), Reach, geometric)}; + if (false == readers.empty()) { + auto best{-1.0}; infra::station station; - if (false == index.nearest(point, Reach, station)) + for (auto const &reader : readers) + { + infra::station at; + auto const &thread{thread_of(reader.track)}; + if (false == nearest_beside(thread, points.front(), std::max(Reach, 25.0), at)) + continue; + auto const distance{plan_distance(infra::frame_at(at).point, points.front())}; + if (best < 0.0 || distance < best) + { + best = distance; + station = at; + candidate.reader = reader.track; + candidate.event = reader.event; + candidate.cell = reader.cell; + candidate.loose = reader.loose; + } + } + if (candidate.reader == nullptr) + { + state.elsewhere += near ? 1 : 0; return; - Binding.anchors.push_back(infra::make_anchor(station, point)); + } + auto const &thread{thread_of(candidate.reader)}; + if (near && geometric.track != station.track && std::none_of(thread.begin(), thread.end(), [&](track_area const &Area) { return Area.track == geometric.track; })) + { + candidate.nearer = geometric.track; + candidate.nearer_foot = infra::frame_at(geometric).point; + } + Binding.anchors.push_back(infra::make_anchor(station, points.front())); + if (Binding.group == infra::category::other && Binding.type != infra::kind::memcell) + Binding.group = infra::category::signal; + } + else + { + for (auto const &point : points) + { + infra::station station; + if (false == index.nearest(point, Reach, station)) + return; + Binding.anchors.push_back(infra::make_anchor(station, point)); + } + candidate.unread = (false == cells.empty() && Binding.type != infra::kind::memcell && Binding.group == infra::category::signal); } if (std::abs(Binding.anchors.front().height) > 40.0) return; @@ -740,12 +953,14 @@ void editor_mode::infra_recognize() if (Binding.turns) Binding.yaw = turn_of(Binding, *yaw); Binding.label = object_label(Binding); - infra_candidate candidate; 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.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"; }; @@ -805,6 +1020,8 @@ void editor_mode::infra_recognize() } 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())); + if (state.elsewhere > 0) + state.status += format(STR_C("; %d left out: they stand by the scope, but the paths which read them are elsewhere"), state.elsewhere); } void editor_mode::infra_bind_chosen() @@ -910,6 +1127,7 @@ void editor_mode::infra_buffer(infra::binding const &Binding, bool const Refresh void editor_mode::track_captured(TTrack const &Track) { + sweeps_captured(Track); if (m_infra_suspended || false == m_infra.follow) return; infra_load(); @@ -947,6 +1165,7 @@ void editor_mode::track_captured(TTrack const &Track) void editor_mode::tracks_committed(std::vector const &Tracks) { + sweeps_committed(Tracks); if (m_infra_suspended || false == m_infra.follow) return; infra_load(); @@ -1031,6 +1250,7 @@ void editor_mode::track_retired(TTrack &) void editor_mode::track_split(TTrack &Original, TTrack &Created, segment_data const &First) { + sweeps_split(Original, Created); if (m_infra_suspended || Created.m_paths.empty()) return; auto const length{infra::plan_length(First)}; @@ -1185,9 +1405,9 @@ void editor_mode::infra_rebase() void editor_mode::render_infra_search() { auto &state{m_infra}; - static char const *const scopes[] = {"Selected path", "Line through the selected path", "Route of the vertical profile", "Whole scenery"}; + 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("Look along", &state.scope, scopes, IM_ARRAYSIZE(scopes)); + ImGui::Combo(STR_C("Look along"), &state.scope, scopes, IM_ARRAYSIZE(scopes)); if (state.scope == 1) { ImGui::SetNextItemWidth(120.f); @@ -1224,77 +1444,140 @@ void editor_mode::render_infra_candidates() { auto &state{m_infra}; auto &candidates{state.candidates}; + ImVec4 const readcolour{0.45f, 0.85f, 1.f, 1.f}; + ImVec4 const warncolour{1.f, 0.75f, 0.3f, 1.f}; + auto const shown = [&](infra_candidate const &Candidate) { + switch (state.filter) + { + case 1: return Candidate.reader != nullptr; + case 2: return Candidate.nearer != nullptr; + case 3: return Candidate.unread; + default: return true; + } + }; auto const choose = [&](std::optional const Group, bool const Chosen) { for (auto &candidate : candidates) - if (false == Group.has_value() || candidate.binding.group == *Group) + if ((false == Group.has_value() || candidate.binding.group == *Group) && shown(candidate)) candidate.chosen = Chosen && false == candidate.bound; }; state.hovered = -1; - if (false == candidates.empty()) - { - int chosen{0}; - for (auto const &candidate : candidates) - chosen += candidate.chosen ? 1 : 0; - ImGui::SameLine(); - if (ImGui::Button(format(STR_C("Bind the chosen (%d)"), chosen).c_str())) - infra_bind_chosen(); - ImGui::SameLine(); - if (ImGui::SmallButton(STR_C("All"))) - choose({}, true); - ImGui::SameLine(); - if (ImGui::SmallButton(STR_C("None"))) - choose({}, false); + if (candidates.empty()) + return; + int chosen{0}; + for (auto const &candidate : candidates) + chosen += candidate.chosen ? 1 : 0; + ImGui::SameLine(); + if (ImGui::Button(format(STR_C("Bind the chosen (%d)"), chosen).c_str())) + infra_bind_chosen(); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("All"))) + choose({}, true); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("None"))) + choose({}, false); - ImGui::BeginChild("##infracandidates", ImVec2(0.f, 260.f), true); - for (auto const group : infra::categories()) + 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) { - auto const count{std::count_if(candidates.begin(), candidates.end(), [&](infra_candidate const &Candidate) { return Candidate.binding.group == group; })}; - if (count == 0) - continue; - ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(colour(group))); - auto const open{ImGui::TreeNodeEx(format("%s (%d)", infra::name(group), static_cast(count)).c_str(), group == infra::category::other ? 0 : ImGuiTreeNodeFlags_DefaultOpen)}; - ImGui::PopStyleColor(); ImGui::SameLine(); - if (ImGui::SmallButton((std::string{STR_C("all##g")} + infra::name(group)).c_str())) - choose(group, true); - ImGui::SameLine(); - if (ImGui::SmallButton((std::string{STR_C("none##g")} + infra::name(group)).c_str())) - choose(group, false); - if (false == open) - continue; - // the candidates are sorted by the category - auto const first{static_cast(std::distance(candidates.begin(), std::find_if(candidates.begin(), candidates.end(), [&](infra_candidate const &Candidate) { return Candidate.binding.group == group; })))}; - ImGuiListClipper clipper(static_cast(count)); - while (clipper.Step()) - for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) - { - auto const i{static_cast(first + row)}; - auto &candidate{candidates[i]}; - ImGui::PushID(static_cast(i)); - if (candidate.bound) - { - ImGui::TextDisabled(STR_C("[bound]")); - } - else - { - ImGui::Checkbox("##chosen", &candidate.chosen); - } - ImGui::SameLine(); - ImGui::Text("%s", candidate.binding.label.c_str()); - if (ImGui::IsItemHovered()) - { - state.hovered = static_cast(i); - ImGui::SetTooltip(STR_C("%s, %s\n%.2f m %s of the axis, %.2f m above the rail top\n%s"), infra::name(candidate.binding.type), candidate.binding.anchors.front().at.track->name().c_str(), - std::abs(candidate.offset), candidate.offset >= 0.0 ? "right" : "left", candidate.binding.anchors.front().height, candidate.reason.c_str()); - } - ImGui::SameLine(); - ImGui::TextDisabled("%s %+.1f m", infra::name(candidate.binding.type), candidate.offset); - ImGui::PopID(); - } - ImGui::TreePop(); + 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); } - ImGui::EndChild(); } + + ImGui::BeginChild("##infracandidates", ImVec2(0.f, 260.f), true); + std::vector rows; + for (auto const group : infra::categories()) + { + rows.clear(); + for (int i = 0; i < static_cast(candidates.size()); ++i) + if (candidates[i].binding.group == group && shown(candidates[i])) + rows.push_back(i); + 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)}; + ImGui::PopStyleColor(); + ImGui::SameLine(); + if (ImGui::SmallButton((std::string{STR_C("all##g")} + infra::name(group)).c_str())) + choose(group, true); + ImGui::SameLine(); + if (ImGui::SmallButton((std::string{STR_C("none##g")} + infra::name(group)).c_str())) + choose(group, false); + if (false == open) + continue; + ImGuiListClipper clipper(static_cast(rows.size())); + while (clipper.Step()) + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + auto const i{rows[row]}; + auto &candidate{candidates[i]}; + auto const &anchor{candidate.binding.anchors.front()}; + ImGui::PushID(i); + if (candidate.bound) + ImGui::TextDisabled(STR_C("[bound]")); + else + ImGui::Checkbox("##chosen", &candidate.chosen); + ImGui::SameLine(); + if (ImGui::Selectable(candidate.binding.label.c_str(), false, ImGuiSelectableFlags_None, ImVec2(ImGui::CalcTextSize(candidate.binding.label.c_str()).x, 0.f)) && anchor.at.track != nullptr) + focus_track(*anchor.at.track, anchor.at.path, anchor.foot); + if (ImGui::IsItemHovered()) + { + 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->name().c_str(), std::abs(candidate.offset), + candidate.offset >= 0.0 ? "right" : "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{}); + if (candidate.nearer != nullptr) + text += format(STR_C("\nThe path %s is nearer, but it doesn't read the object"), candidate.nearer->name().c_str()); + if (candidate.unread) + text += STR("\nNo path reads the memory cell of the signal"); + text += STR("\nClick to look at it"); + ImGui::SetTooltip("%s", text.c_str()); + } + ImGui::SameLine(); + ImGui::TextDisabled("%s %+.1f m", infra::name(candidate.binding.type), candidate.offset); + if (candidate.reader != nullptr) + { + ImGui::SameLine(); + ImGui::TextColored(readcolour, STR_C("read by %s"), candidate.reader->name().c_str()); + } + if (candidate.nearer != nullptr) + { + ImGui::SameLine(); + ImGui::TextColored(warncolour, STR_C("(nearer: %s)"), candidate.nearer->name().c_str()); + } + if (candidate.unread) + { + ImGui::SameLine(); + ImGui::TextColored(warncolour, "%s", STR_C("not read")); + } + ImGui::PopID(); + } + ImGui::TreePop(); + } + ImGui::EndChild(); } void editor_mode::render_infra_bound() @@ -1369,11 +1652,53 @@ void editor_mode::draw_infra_overlay() const }; for (auto const &binding : m_bindings) draw(binding, binding.lost ? overlay_color::invalid : IM_COL32(90, 230, 110, 220), 3.5f); + auto const object = [&](infra_candidate const &Candidate) { + std::vector points; + std::optional yaw; + return object_points(Candidate.binding, points, yaw) ? points.front() : Candidate.binding.anchors.front().foot; + }; + auto const dashed = [&](glm::dvec3 const &From, glm::dvec3 const &To, ImU32 const Colour) { + auto const steps{std::clamp(static_cast(glm::distance(From, To) / 0.5), 2, 40)}; + for (int k = 0; k < steps; k += 2) + projection.line(drawlist, glm::mix(From, To, static_cast(k) / steps), glm::mix(From, To, static_cast(k + 1) / steps), Colour, 2.f); + }; for (int i = 0; i < static_cast(state.candidates.size()); ++i) { auto const &candidate{state.candidates[i]}; + if (candidate.nearer != nullptr && alive(candidate.binding) && glm::distance(candidate.nearer_foot, camera) <= 2000.0) + { + auto const point{object(candidate)}; + ImVec2 screen; + if (projection.project(point, screen)) + drawlist->AddCircle(screen, i == state.hovered ? 13.f : 8.f, IM_COL32(255, 190, 70, 230), 16, 2.f); + if (i == state.hovered) + dashed(point, candidate.nearer_foot, IM_COL32(255, 190, 70, 230)); + } if (candidate.bound) continue; draw(candidate.binding, colour(candidate.binding.group, candidate.chosen ? 255 : 110), i == state.hovered ? 9.f : (candidate.chosen ? 5.f : 3.f)); } + if (state.hovered >= 0 && state.hovered < static_cast(state.candidates.size())) + { + auto const &candidate{state.candidates[state.hovered]}; + auto const outline = [&](TTrack const *Track, ImU32 const Colour, float const Width) { + if (Track == nullptr || Track->m_editorremoved) + return; + for (auto const &path : Track->m_paths) + { + bezier const curve{path}; + auto const count{std::clamp(static_cast(curve.plan_length() / 3.0), 6, 64)}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= count; ++k) + { + auto const next{curve.point(static_cast(k) / count)}; + projection.line(drawlist, previous, next, Colour, Width); + previous = next; + } + } + }; + outline(candidate.binding.anchors.front().at.track, IM_COL32(255, 255, 255, 200), 3.f); + if (candidate.reader != candidate.binding.anchors.front().at.track) + outline(candidate.reader, IM_COL32(110, 215, 255, 220), 3.f); + } } diff --git a/application/editormodejoints.cpp b/application/editormodejoints.cpp new file mode 100644 index 00000000..71ea8976 --- /dev/null +++ b/application/editormodejoints.cpp @@ -0,0 +1,521 @@ +/* +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/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::grade_of; +using geometry::plan_of; + +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}; + +using kind = editor_mode::joint_issue::kind; + +bool rail(TTrack const &Track) +{ + return (Track.iCategoryFlag & 15) == kRailCategory && false == Track.m_editorremoved && editor_track::is_supported(Track); +} + +bool at_start(editor_track::point_ref const &Point) +{ + return Point.kind == editor_track::point_kind::start; +} + +// direction of the path at its end, into the path +glm::dvec3 into(TTrack const &Track, editor_track::point_ref const &Point) +{ + auto const &path{Track.m_paths[Point.path]}; + bool const start{at_start(Point)}; + auto const &point{path.points[start ? segment_data::point::start : segment_data::point::end]}; + auto const &control{path.points[start ? segment_data::point::control1 : segment_data::point::control2]}; + glm::dvec3 const direction{control != glm::dvec3{} ? control : path.points[start ? segment_data::point::end : segment_data::point::start] - point}; + return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec3{0.0, 0.0, 1.0}; +} + +// roll of the path at its end, for the travel into the path, in degrees +double roll_into(TTrack const &Track, editor_track::point_ref const &Point) +{ + auto const &path{Track.m_paths[Point.path]}; + return at_start(Point) ? path.rolls[0] : -path.rolls[1]; +} + +double plan_angle(glm::dvec3 const &A, glm::dvec3 const &B) +{ + auto const a{plan_of(A)}; + auto const b{plan_of(B)}; + if (glm::length(a) < 1e-9 || glm::length(b) < 1e-9) + return 0.0; + return glm::degrees(std::acos(std::clamp(glm::dot(glm::normalize(a), glm::normalize(b)), -1.0, 1.0))); +} + +using end_key = std::tuple; + +end_key key_of(TTrack const *Track, editor_track::point_ref const &Point) +{ + return {Track, Point.path, static_cast(Point.kind)}; +} + +// pairs of the ends already looked at in the check under way +std::set> reported; + +char const *kind_name(kind const Kind) +{ + switch (Kind) + { + case kind::gap: return "gap"; + case kind::step: return "step"; + case kind::kink: return "kink"; + case kind::cant: return "cant jump"; + case kind::grade: return "grade break"; + default: return "free end"; + } +} + +std::string value_text(editor_mode::joint_issue const &Issue) +{ + switch (Issue.type) + { + 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::cant: return format("%.1f mm", Issue.value); + case kind::grade: return format("%.1f %%o", Issue.value); + default: return "-"; + } +} + +ImU32 kind_colour(kind const Kind) +{ + switch (Kind) + { + case kind::gap: return IM_COL32(255, 70, 60, 235); + case kind::step: return IM_COL32(255, 140, 50, 230); + 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); + default: return IM_COL32(200, 200, 200, 200); + } +} + +std::string name_of(TTrack const *Track) +{ + if (Track == nullptr) + return "-"; + return Track->name().empty() ? std::string{"(noname)"} : Track->name(); +} + +} // namespace + +void editor_mode::joints_start() +{ + auto &state{m_joints}; + state.queue.clear(); + state.next = 0; + state.issues.clear(); + state.selected = -1; + state.status.clear(); + reported.clear(); + glm::dvec3 const camera{Global.pCamera.Pos}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == rail(*track)) + continue; + if (state.scope == 0 && glm::distance(track->location(), camera) > state.reach) + continue; + state.queue.push_back(track); + } + state.history = m_history.size(); +} + +void editor_mode::joints_check(TTrack &Track) +{ + auto &state{m_joints}; + auto const once = [&](end_key const &A, end_key const &B) { return reported.insert(A < B ? std::make_pair(A, B) : std::make_pair(B, A)).second; }; + for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) + { + for (auto const kind_of_end : {editor_track::point_kind::start, editor_track::point_kind::end}) + { + editor_track::point_ref const point{i, kind_of_end}; + auto const position{editor_track::point_position(Track, point)}; + auto const connections{editor_track::connected_points(Track, position)}; + if (connections.empty()) + { + joint_issue nearest; + nearest.value = std::numeric_limits::max(); + for (auto *section : simulation::Region->sections(position, static_cast(state.gap))) + for (auto const &cell : section->m_cells) + for (auto *other : cell.m_directories.paths) + { + if (other == nullptr || other == &Track || false == rail(*other)) + continue; + for (int k = 0; k < static_cast(other->m_paths.size()); ++k) + for (auto const theirkind : {editor_track::point_kind::start, editor_track::point_kind::end}) + { + editor_track::point_ref const theirs{k, theirkind}; + auto const distance{glm::distance(position, editor_track::point_position(*other, theirs))}; + if (distance > state.gap || distance >= nearest.value) + continue; + nearest.track = &Track; + nearest.point = point; + nearest.other = other; + nearest.theirs = theirs; + nearest.value = distance; + } + } + if (nearest.other != nullptr) + { + if (once(key_of(&Track, point), key_of(nearest.other, nearest.theirs))) + { + nearest.type = kind::gap; + nearest.position = position; + state.issues.push_back(nearest); + } + } + else if (state.free_ends) + { + joint_issue open; + open.type = kind::open; + open.track = &Track; + open.point = point; + open.position = position; + state.issues.push_back(open); + } + continue; + } + for (auto const &connection : connections) + { + auto *other{connection.first}; + auto const &theirs{connection.second}; + if (false == rail(*other) || false == once(key_of(&Track, point), key_of(other, theirs))) + continue; + joint_issue issue; + issue.track = &Track; + issue.point = point; + issue.other = other; + issue.theirs = theirs; + issue.position = position; + auto const step{glm::distance(position, editor_track::point_position(*other, theirs))}; + auto const ours{into(Track, point)}; + auto const their{into(*other, theirs)}; + auto const angle{plan_angle(ours, -their)}; + auto const cant{kGauge * 1000.0 * std::abs(std::sin(glm::radians(roll_into(Track, point) + roll_into(*other, theirs))))}; + auto const grade{std::abs(grade_of(ours) + grade_of(their)) * 1000.0}; + auto const add = [&](kind const Kind, double const Value) { + issue.type = Kind; + issue.value = Value; + state.issues.push_back(issue); + }; + if (step > state.step) + add(kind::step, step); + if (angle > state.angle) + add(kind::kink, angle); + if (cant > state.cant) + add(kind::cant, cant); + if (grade > state.grade) + add(kind::grade, grade); + } + } + } +} + +void editor_mode::joints_step() +{ + auto &state{m_joints}; + if (state.next >= state.queue.size()) + 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) + { + auto *track{state.queue[state.next++]}; + if (track->m_editorremoved) + continue; + joints_check(*track); + } + if (state.next < state.queue.size()) + 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; + }); + 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.free_ends) + state.status += format(STR_C(", %d free ends"), counts[5]); + WriteLog("Editor: joints check - " + state.status, logtype::generic); +} + +void editor_mode::joints_focus(int const Index) +{ + auto &state{m_joints}; + if (Index < 0 || Index >= static_cast(state.issues.size())) + return; + state.selected = Index; + auto const &issue{state.issues[Index]}; + auto const &paths{simulation::Paths.sequence()}; + if (std::find(paths.begin(), paths.end(), issue.track) == paths.end() || issue.track->m_editorremoved) + return; + m_node = issue.track; + m_track_point = {}; + ui()->set_node(m_node); + focus_track(*issue.track, issue.point.path, issue.position); +} + +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) + return false; + auto const movable = [](TTrack const *Track) { + std::string reason; + return Track->eType == tt_Normal && editor_track::can_edit_geometry(*Track, reason) && scene::Layers.editable(Track); + }; + TTrack *mover{nullptr}; + editor_track::point_ref moved; + TTrack *fixed{nullptr}; + editor_track::point_ref held; + if (movable(Issue.track)) + { + mover = Issue.track; + moved = Issue.point; + fixed = Issue.other; + held = Issue.theirs; + } + else if (movable(Issue.other) && (Issue.type != kind::gap || false == editor_track::is_connected(*Issue.other, Issue.theirs))) + { + mover = Issue.other; + moved = Issue.theirs; + fixed = Issue.track; + held = Issue.point; + } + if (mover == nullptr) + return false; + if (std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == mover; })) + States.emplace_back(mover, editor_track::capture(*mover)); + if (std::find(Changed.begin(), Changed.end(), mover) == Changed.end()) + Changed.push_back(mover); + editor_track::snap_target target; + target.track = fixed; + target.point = held; + target.position = editor_track::point_position(*fixed, held); + target.direction = into(*fixed, held); + switch (Issue.type) + { + case kind::gap: + case kind::kink: + case kind::grade: + editor_track::snap_point(*mover, moved, target, true); + break; + case kind::step: + editor_track::snap_point(*mover, moved, target, false); + break; + case kind::cant: + { + auto &path{mover->m_paths[moved.path]}; + auto const roll{static_cast(roll_into(*fixed, held))}; + if (at_start(moved)) + path.rolls[0] = -roll; + else + path.rolls[1] = roll; + break; + } + default: + break; + } + Issue.fixed = true; + return true; +} + +void editor_mode::joints_fix(std::vector const &Indices) +{ + auto &state{m_joints}; + std::vector> states; + std::vector changed; + std::vector chosen; + for (auto const index : Indices) + if (index >= 0 && index < static_cast(state.issues.size())) + chosen.push_back(&state.issues[index]); + std::size_t count{0}; + for (auto *issue : chosen) + if (joint_fix(*issue, states, changed)) + ++count; + if (count == 0) + { + state.status = STR_C("Nothing could be fixed: the paths are switches or in the files which can't be changed"); + return; + } + push_track_snapshot(std::move(states)); + editor_track::commit(changed); + for (auto *track : changed) + track->mark_dirty(); + state.history = m_history.size(); + state.status = format(STR_C("%zu joints fixed, Ctrl+Z takes them back"), count); + WriteLog("Editor: joints check - " + state.status, logtype::generic); +} + +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()}; + 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()); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Cancel"))) + state.queue.resize(state.next); + } + else if (ImGui::Button(STR_C("Check"))) + { + joints_start(); + } + if (ImGui::TreeNode(STR_C("Limits"))) + { + 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(); + state.reach = std::clamp(state.reach, 50.0, 50000.0); + state.gap = std::clamp(state.gap, 0.03, 20.0); + state.step = std::max(0.001, state.step); + state.angle = std::max(0.01, state.angle); + state.cant = std::max(0.5, state.cant); + state.grade = std::max(0.5, state.grade); + ImGui::TextDisabled("%s", STR_C("Change the limits, then Check again")); + ImGui::TreePop(); + } + if (false == state.status.empty()) + ImGui::TextWrapped("%s", state.status.c_str()); + if (state.issues.empty()) + return; + if (state.history != m_history.size()) + 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'}; + if (ImGui::Combo("##jointfilter", &filter, filters.c_str())) + state.filter = filter - 1; + std::vector listed; + std::vector fixable; + for (int i = 0; i < static_cast(state.issues.size()); ++i) + { + auto const &issue{state.issues[i]}; + if (state.filter >= 0 && static_cast(issue.type) != state.filter) + continue; + listed.push_back(i); + if (false == issue.fixed && issue.type != kind::open) + fixable.push_back(i); + } + if (false == fixable.empty()) + { + ImGui::SameLine(); + if (ImGui::Button(format(STR_C("Fix %zu listed"), fixable.size()).c_str())) + joints_fix(fixable); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Gaps: the free end goes to the other end, along it. Steps: the end goes to the other one.\n" + "Kinks and grade breaks: the end is turned in line with the other path. Cant: the cant of the other path.\n" + "The plain path moves, a switch stays as it is. One Ctrl+Z takes it all back")); + } + auto const namewidth{std::max(110.0f, ImGui::GetContentRegionAvail().x * 0.32f)}; + bool const chosen{state.selected >= 0 && state.selected < static_cast(state.issues.size())}; + auto const details{chosen ? ImGui::GetFrameHeightWithSpacing() + ImGui::GetTextLineHeightWithSpacing() * 2.0f : 0.0f}; + ImGui::BeginChild("jointresults", ImVec2(0.0f, -details), true); + ImGui::Columns(4, "jointrows", false); + ImGui::SetColumnWidth(0, 90.0f); + ImGui::SetColumnWidth(1, 80.0f); + ImGui::SetColumnWidth(2, namewidth); + ImGuiListClipper clipper(static_cast(listed.size())); + while (clipper.Step()) + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + auto const index{listed[row]}; + auto const &issue{state.issues[index]}; + auto const colour{ImGui::ColorConvertU32ToFloat4(kind_colour(issue.type))}; + ImGui::PushStyleColor(ImGuiCol_Text, issue.fixed ? ImVec4(0.5f, 0.8f, 0.5f, 1.0f) : colour); + if (ImGui::Selectable(format("%s##joint%d", STR_C(kind_name(issue.type)), index).c_str(), state.selected == index, ImGuiSelectableFlags_SpanAllColumns)) + joints_focus(index); + ImGui::PopStyleColor(); + ImGui::NextColumn(); + ImGui::TextUnformatted(issue.fixed ? STR_C("fixed") : value_text(issue).c_str()); + ImGui::NextColumn(); + ImGui::TextUnformatted(name_of(issue.track).c_str()); + ImGui::NextColumn(); + ImGui::TextUnformatted(name_of(issue.other).c_str()); + ImGui::NextColumn(); + } + ImGui::Columns(1); + ImGui::EndChild(); + if (chosen) + { + 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"))) + joints_fix({state.selected}); + } +} + +void editor_mode::draw_joints_overlay() const +{ + auto const &state{m_joints}; + if (false == state.open || state.issues.empty()) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList()}; + glm::dvec3 const camera{Global.pCamera.Pos}; + 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) + continue; + ImVec2 screen; + if (false == projection.project(issue.position, screen)) + continue; + auto const colour{i == state.selected ? IM_COL32(255, 255, 80, 255) : kind_colour(issue.type)}; + drawlist->AddCircle(screen, 11.0f, colour, 16, 2.5f); + auto const text{std::string{STR_C(kind_name(issue.type))} + " " + value_text(issue)}; + drawlist->AddText(ImVec2(screen.x + 13.0f, screen.y - 7.0f), colour, text.c_str()); + } +} diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp new file mode 100644 index 00000000..3fdb7131 --- /dev/null +++ b/application/editormodelineside.cpp @@ -0,0 +1,983 @@ +/* +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/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" + +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include + +namespace +{ + +using geometry::bezier; +using geometry::plan_distance; +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}; +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}; +char const *const kHektoStyles[] = {"nowy", "stary", "zgnity"}; +char const *const kFoulingModels[] = {"ip/pkp/w17.inc", "ip/pkp/w17_new.inc", "ip/pkp/w17_old.inc", "ip/pkp/w17_dwuteo.inc"}; + +std::string normalized(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(Character == '\\' ? '/' : std::tolower(Character)); }); + return Text; +} + +bool numeric(std::string const &Text, double &Value) +{ + char *end{nullptr}; + Value = std::strtod(Text.c_str(), &end); + return false == Text.empty() && *end == '\0'; +} + +std::string number(double const Value, int const Decimals) +{ + auto text{format("%.*f", Decimals, Value)}; + if (text == "-0" || text.find_first_not_of("-0.") == std::string::npos) + text.erase(0, text.front() == '-' ? 1 : 0); + return text; +} + +double heading_of(glm::dvec2 const &Direction) +{ + auto const degrees{glm::degrees(std::atan2(Direction.x, Direction.y))}; + return degrees < 0.0 ? degrees + 360.0 : degrees; +} + +// index of the sample at or before the chainage +std::size_t sample_index(std::vector const &Samples, double const Chainage) +{ + auto const found{std::upper_bound(Samples.begin(), Samples.end(), Chainage, [](double const Value, editor_track::route_sample const &Sample) { return Value < Sample.chainage; })}; + return found == Samples.begin() ? 0 : std::min(std::distance(Samples.begin(), found) - 1, Samples.size() - 2); +} + +glm::dvec2 sampled_direction(std::vector const &Samples, double const Chainage) +{ + auto const index{sample_index(Samples, Chainage)}; + auto const &a{Samples[index]}; + auto const &b{Samples[index + 1]}; + auto const span{b.chainage - a.chainage}; + auto const f{span > 1e-9 ? std::clamp((Chainage - a.chainage) / span, 0.0, 1.0) : 0.0}; + auto const direction{a.direction * (1.0 - f) + b.direction * f}; + return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0}; +} + +// chainage of the point of the line nearest to the point, and how far from the line it is +double project(std::vector const &Samples, glm::dvec3 const &Point, double &Distance) +{ + Distance = std::numeric_limits::max(); + double chainage{0.0}; + auto const point{plan_of(Point)}; + for (std::size_t i = 1; i < Samples.size(); ++i) + { + auto const a{plan_of(Samples[i - 1].position)}; + auto const b{plan_of(Samples[i].position)}; + auto const ab{b - a}; + auto const length2{glm::dot(ab, ab)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0) : 0.0}; + auto const distance{glm::length(point - (a + ab * t))}; + if (distance < Distance) + { + Distance = distance; + chainage = Samples[i - 1].chainage + (Samples[i].chainage - Samples[i - 1].chainage) * t; + } + } + return chainage; +} + +// the digits after the rotation make the number of the post, the last one the hectometre +bool hectometres_of(std::vector const &Values, int &Hectometres) +{ + if (Values.size() < 6) + return false; + Hectometres = 0; + for (std::size_t i = 5; i < Values.size(); ++i) + { + double digit; + if (false == numeric(Values[i], digit) || digit < 0.0 || digit > 9.0) + return false; + Hectometres = Hectometres * 10 + static_cast(digit); + } + return true; +} + +std::string hekto_file(int const Hectometres, int const Style) +{ + auto style{Style}; + if (style > 2) + { + auto const pick{LocalRandom(0.0, 1.0)}; + style = pick < 0.6 ? 0 : pick < 0.9 ? 1 : 2; + } + auto const size{Hectometres < 100 ? "1" : Hectometres < 1000 ? "10" : "100"}; + return std::string{"przytorowe/slupek_hekto_"} + kHektoStyles[style] + "_" + size + ".inc"; +} + +std::vector hekto_digits(int const Hectometres) +{ + return {std::to_string((Hectometres / 1000) % 10), std::to_string((Hectometres / 100) % 10), std::to_string((Hectometres / 10) % 10), std::to_string(Hectometres % 10)}; +} + +std::string km_text(int const Hectometres) +{ + return format("%d.%d", Hectometres / 10, Hectometres % 10); +} + +// points along the track from the end of a path on, the way a train goes: through a switch along its main track +std::vector trace(TTrack &Track, int const Path, bool const Forward, double const Limit) +{ + std::vector points; + auto *track{&Track}; + auto path{Path}; + auto forward{Forward}; + double length{0.0}; + for (int guard = 0; guard < 64 && track != nullptr && length < Limit; ++guard) + { + bezier const curve{track->m_paths[path]}; + auto const count{std::clamp(static_cast(curve.plan_length() / 1.0), 4, 400)}; + for (int k = points.empty() ? 0 : 1; k <= count; ++k) + { + auto const point{curve.point(forward ? static_cast(k) / count : 1.0 - static_cast(k) / count)}; + if (false == points.empty()) + length += plan_distance(points.back(), point); + points.push_back(point); + } + auto const exit{points.back()}; + TTrack *next{nullptr}; + int nextpath{0}; + bool nextforward{true}; + for (auto const &connection : editor_track::connected_points(*track, exit)) + { + auto *other{connection.first}; + if (other == track || (other->iCategoryFlag & 15) != kRailCategory) + continue; + bool const start{connection.second.kind == editor_track::point_kind::start}; + // entering a switch at its start the train takes the main track + if (next == nullptr || (other == next && start && connection.second.path == 0)) + { + next = other; + nextpath = connection.second.path; + nextforward = start; + } + } + track = next; + path = nextpath; + forward = nextforward; + } + return points; +} + +// nearest point of the polyline +glm::dvec3 nearest_on(std::vector const &Line, glm::dvec3 const &Point, double &Distance) +{ + Distance = std::numeric_limits::max(); + glm::dvec3 result{Point}; + auto const point{plan_of(Point)}; + for (std::size_t i = 1; i < Line.size(); ++i) + { + auto const a{plan_of(Line[i - 1])}; + auto const ab{plan_of(Line[i]) - a}; + auto const length2{glm::dot(ab, ab)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0) : 0.0}; + auto const distance{glm::length(point - (a + ab * t))}; + if (distance < Distance) + { + Distance = distance; + result = glm::mix(Line[i - 1], Line[i], t); + } + } + return result; +} + +void draw_label(ImDrawList *Draw, ImVec2 const At, ImU32 const Colour, std::string const &Text) +{ + auto const size{ImGui::CalcTextSize(Text.c_str())}; + Draw->AddRectFilled(ImVec2(At.x + 8.0f, At.y - 8.0f), ImVec2(At.x + 14.0f + size.x, At.y - 6.0f + size.y), IM_COL32(0, 0, 0, 150), 3.0f); + Draw->AddText(ImVec2(At.x + 11.0f, At.y - 7.0f), Colour, Text.c_str()); +} + +} // namespace + +std::vector editor_mode::standing_templates(std::function const &Accept) const +{ + std::vector result; + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + if (false == scene::Layers.tracked(handle)) + continue; + auto const &included{scene::Layers.instance(handle)}; + if (included.removed || included.dead || included.file == nullptr) + continue; + auto const file{normalized(*included.file)}; + if (false == Accept(file) || included.context.rotation != glm::vec3{0.f} || included.context.scale != glm::vec3{1.f}) + continue; + std::string target; + std::vector values; + if (false == editor_includes::parse_directive(scene::Layers.directive(handle), target, values) || values.size() < 5) + continue; + glm::dvec3 location; + double yaw; + if (false == numeric(values[1], location.x) || false == numeric(values[2], location.y) || false == numeric(values[3], location.z) || false == numeric(values[4], yaw)) + continue; + standing_template standing; + standing.instance = handle; + standing.file = file; + standing.location = included.context.offset + location; + standing.yaw = yaw; + standing.values = std::move(values); + result.push_back(std::move(standing)); + } + return result; +} + +std::size_t editor_mode::place_templates(std::vector const &Items, std::string &Error) +{ + if (Items.empty()) + return 0; + if (scene::Layers.empty()) + { + Error = STR_C("Templates can be placed only in a scenery opened for editing (-edit)"); + return 0; + } + auto const layer{scene::Layers.resolve(scene::Layers.active())}; + if (std::string reason; false == scene::Layers.accepts(layer, &reason)) + { + Error = STR_C("The active layer can't take them: ") + (reason.empty() ? std::string{STR_C("there's no active layer")} : reason); + return 0; + } + auto const context{scene::Layers.layer(layer).context_insert()}; + if (context.rotation != glm::vec3{0.f} || context.scale != glm::vec3{1.f}) + { + Error = STR_C("The file of the active layer would receive them with a rotation or a scale in effect, pick another layer"); + return 0; + } + EditorSnapshot snap; + for (auto const &item : Items) + { + auto const local{context.to_local(item.location)}; + std::string directive; + if (item.described) + { + include_info info; + if (false == editor_includes::load(item.file, info, Error)) + continue; + directive = editor_includes::directive(item.file, info, editor_includes::parameter_count(item.file), local, static_cast(item.yaw), item.track); + } + else + { + std::vector values{"none", number(local.x, 3), number(local.y, 3), number(local.z, 3), number(item.yaw, 2)}; + values.insert(values.end(), item.rest.begin(), item.rest.end()); + directive = editor_includes::compose_directive(item.file, values); + } + auto const instance{scene::Layers.place(layer, item.file, directive)}; + if (instance == 0) + continue; + simulation::State.preview_include(directive, context, layer, instance); + snap.instances.push_back(instance); + } + if (snap.instances.empty()) + { + Error = STR_C("The active layer can't take them"); + return 0; + } + auto const count{snap.instances.size()}; + snap.node_name = Items.front().file; + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + return count; +} + +std::size_t editor_mode::move_templates(std::vector> const &Moves) +{ + EditorSnapshot snap; + for (auto const &[standing, item] : Moves) + { + if (false == scene::Layers.tracked(standing.instance) || false == scene::Layers.removable(standing.instance)) + continue; + auto const &included{scene::Layers.instance(standing.instance)}; + auto values{standing.values}; + auto const local{item.location - included.context.offset}; + values[1] = number(local.x, 3); + values[2] = number(local.y, 3); + values[3] = number(local.z, 3); + values[4] = number(item.yaw, 2); + auto before{scene::Layers.directive(standing.instance)}; + set_include_directive(standing.instance, editor_includes::compose_directive(*included.file, values)); + snap.directives.emplace_back(standing.instance, std::move(before)); + } + if (snap.directives.empty()) + return 0; + auto const count{snap.directives.size()}; + snap.node_name = Moves.front().first.file; + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + return count; +} + +std::size_t editor_mode::remove_templates(std::vector const &Instances) +{ + EditorSnapshot snap; + for (auto const instance : Instances) + { + if (false == scene::Layers.tracked(instance) || false == scene::Layers.removable(instance)) + continue; + scene::Layers.removed(instance, true); + snap.instances.push_back(instance); + } + if (snap.instances.empty()) + return 0; + auto const count{snap.instances.size()}; + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + select_include(0); + return count; +} + +void editor_mode::hekto_start(TTrack &Track, glm::dvec3 const &Point) +{ + auto &tool{m_hekto}; + tool.track = &Track; + tool.point = Point; + tool.km_read = false; + tool.dirty = true; + tool.expand = true; + tool.status.clear(); +} + +void editor_mode::hekto_read_kilometrage() +{ + auto &tool{m_hekto}; + auto const &found{tool.found}; + if (found.empty()) + return; + if (found.size() == 1) + { + // one post doesn't tell the way the kilometrage grows: it's taken to grow along the longer part of the line past the post + auto const length{tool.samples.empty() ? 0.0 : tool.samples.back().chainage}; + tool.growth = found.front().chainage > 0.5 * length ? -1 : 1; + tool.km = found.front().hectometres / 10.0 + tool.growth * (tool.origin - found.front().chainage) / 1000.0; + tool.status = format(STR_C("Kilometrage read from the post %s, check which way it grows"), km_text(found.front().hectometres).c_str()); + return; + } + // a straight line through the numbers of the posts against where they stand + double sc{0.0}, sk{0.0}, scc{0.0}, sck{0.0}; + for (auto const &post : found) + { + auto const km{post.hectometres / 10.0}; + sc += post.chainage; + sk += km; + scc += post.chainage * post.chainage; + sck += post.chainage * km; + } + auto const n{static_cast(found.size())}; + auto const denominator{n * scc - sc * sc}; + if (std::abs(denominator) < 1e-9) + return; + auto const slope{(n * sck - sc * sk) / denominator}; + auto const intercept{(sk - slope * sc) / n}; + if (std::abs(std::abs(slope) * 1000.0 - 1.0) > 0.2) + { + tool.status = format(STR_C("The numbers of the %zu posts along the line don't go by 100 m, the kilometrage wasn't read from them"), found.size()); + return; + } + tool.growth = slope > 0.0 ? 1 : -1; + tool.km = std::round((intercept + slope * tool.origin) * 1000.0) / 1000.0; + tool.status = format(STR_C("Kilometrage read from %zu posts standing along the line"), found.size()); +} + +void editor_mode::hekto_update() +{ + auto &tool{m_hekto}; + if (false == tool.dirty && tool.history == m_history.size()) + return; + tool.dirty = false; + tool.history = m_history.size(); + tool.posts.clear(); + tool.found.clear(); + tool.error.clear(); + tool.samples.clear(); + if (tool.track == nullptr || tool.track->m_editorremoved) + { + tool.track = nullptr; + return; + } + tool.route = editor_track::run_route(*tool.track, tool.reach); + if (tool.route.spans.empty()) + { + tool.error = STR_C("No line through this path"); + return; + } + tool.samples = editor_track::sample_route(tool.route, kSampleStep); + if (tool.samples.size() < 2) + { + tool.error = STR_C("No line through this path"); + return; + } + double distance; + tool.origin = project(tool.samples, tool.point, distance); + auto const length{tool.samples.back().chainage}; + for (auto const &standing : standing_templates([](std::string const &File) { return File.find("hekto") != std::string::npos; })) + { + hekto_found post; + post.post = standing; + if (false == hectometres_of(standing.values, post.hectometres)) + continue; + post.chainage = project(tool.samples, standing.location, distance); + if (distance > kLineCorridor || post.chainage <= 0.0 || post.chainage >= length) + continue; + tool.found.push_back(post); + } + if (false == tool.km_read) + { + tool.km_read = true; + hekto_read_kilometrage(); + } + auto const chainage_of = [&](int const Hectometres) { return tool.origin + tool.growth * (Hectometres / 10.0 - tool.km) * 1000.0; }; + for (auto &post : tool.found) + { + post.expected = chainage_of(post.hectometres); + post.error = post.chainage - post.expected; + post.outside = true; + } + auto const km_at = [&](double const Chainage) { return tool.km + tool.growth * (Chainage - tool.origin) / 1000.0; }; + auto const low{std::max(0, static_cast(std::ceil(std::min(km_at(0.0), km_at(length)) * 10.0 - 1e-6)))}; + auto const high{static_cast(std::floor(std::max(km_at(0.0), km_at(length)) * 10.0 + 1e-6))}; + for (int hectometres = low; hectometres <= high && tool.posts.size() < 5000; ++hectometres) + { + hekto_post post; + post.hectometres = hectometres; + post.chainage = chainage_of(hectometres); + if (post.chainage < 0.0 || post.chainage > length) + continue; + auto const axis{editor_track::sampled_position(tool.samples, post.chainage)}; + auto const growing{sampled_direction(tool.samples, post.chainage) * static_cast(tool.growth)}; + glm::dvec2 const left{-growing.y, growing.x}; + // alternately: the even hectometres on the right, the odd ones on the left of the growing kilometrage + auto const side{tool.side != 0 ? tool.side : (hectometres % 2 == 0 ? 1 : -1)}; + auto const planar{plan_of(axis) + left * (side * tool.offset)}; + post.position = placement_on_ground({planar.x, axis.y, planar.y}); + post.yaw = std::fmod(heading_of(growing) + tool.turn, 360.0); + // of the posts with this number the one nearest to the place stands for it, the others are duplicates. it stands wrong + // when it's farther from the place than allowed: along the line, across it, or on the other side + auto best{std::numeric_limits::max()}; + for (int i = 0; i < static_cast(tool.found.size()); ++i) + { + auto &found{tool.found[i]}; + if (found.hectometres != hectometres) + continue; + found.outside = false; + found.distance = plan_distance(found.post.location, post.position); + if (found.distance < best) + { + best = found.distance; + post.standing = i; + } + } + for (int i = 0; i < static_cast(tool.found.size()); ++i) + { + auto &found{tool.found[i]}; + if (found.hectometres != hectometres) + continue; + found.duplicate = i != post.standing; + found.wrong = false == found.duplicate && found.distance > tool.tolerance; + } + tool.posts.push_back(post); + } +} + +void editor_mode::hekto_remove_duplicates() +{ + auto &tool{m_hekto}; + hekto_update(); + std::vector instances; + for (auto const &found : tool.found) + 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); + WriteLog("Editor: " + tool.status); + tool.dirty = true; +} + +void editor_mode::hekto_place() +{ + auto &tool{m_hekto}; + hekto_update(); + std::vector items; + for (auto const &post : tool.posts) + { + if (post.standing >= 0) + continue; + template_item item; + item.file = hekto_file(post.hectometres, tool.style); + item.location = post.position; + item.yaw = post.yaw; + item.rest = hekto_digits(post.hectometres); + items.push_back(std::move(item)); + } + 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; + WriteLog("Editor: " + tool.status, logtype::generic); + tool.dirty = true; +} + +void editor_mode::hekto_fix() +{ + auto &tool{m_hekto}; + hekto_update(); + std::vector> moves; + for (auto const &post : tool.posts) + { + if (post.standing < 0 || false == tool.found[post.standing].wrong) + continue; + template_item item; + item.location = post.position; + item.yaw = post.yaw; + 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); + WriteLog("Editor: " + tool.status, logtype::generic); + tool.dirty = true; +} + +void editor_mode::render_hekto_ui() +{ + auto &tool{m_hekto}; + auto *selected{selected_track()}; + if (tool.track == nullptr || (selected != nullptr && selected != tool.track && ImGui::SmallButton(STR_C("Along the line of the selected path")))) + { + if (selected == nullptr) + { + ImGui::TextDisabled("%s", STR_C("Select a path of the line, or RMB on it: Hectometre posts")); + return; + } + hekto_start(*selected, bezier{selected->m_paths.front()}.point(0.0)); + } + auto const changed = [&](bool const Changed) { + 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"))) + { + tool.km_read = false; + tool.dirty = true; + } + 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)); + 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))) + { + tool.turn = turn * 90; + tool.dirty = true; + } + changed(ImGui::InputDouble(STR_C("Along the line each way (m)"), &tool.reach, 0.0, 0.0, "%.0f")); + 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()) + { + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str()); + return; + } + 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(); + if (duplicates > 0) + { + if (missing > 0) + ImGui::SameLine(); + if (ImGui::Button(format(STR_C("Remove %d duplicate posts"), 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")); + } + if (wrong > 0 || duplicates > 0) + { + if (wrong > 0) + { + if (missing > 0 || duplicates > 0) + ImGui::SameLine(); + if (ImGui::Button(format(STR_C("Move %d wrong posts to their places"), 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); + for (auto const &post : tool.found) + { + if (false == post.wrong && false == post.duplicate) + continue; + 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; + } + } + ImGui::EndChild(); + } + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} + +void editor_mode::draw_hekto_overlay() const +{ + auto const &tool{m_hekto}; + if (false == tool.open || tool.track == nullptr) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList()}; + glm::dvec3 const camera{Global.pCamera.Pos}; + for (auto const &post : tool.posts) + { + if (glm::distance(post.position, camera) > kOverlayRange) + continue; + ImVec2 screen; + if (false == projection.project(post.position, screen)) + continue; + bool const standing{post.standing >= 0 && false == tool.found[post.standing].wrong}; + auto const colour{standing ? IM_COL32(180, 180, 180, 200) : IM_COL32(90, 255, 120, 235)}; + drawlist->AddCircleFilled(screen, 4.0f, colour); + draw_label(drawlist, screen, colour, km_text(post.hectometres)); + } + for (auto const &post : tool.found) + { + if ((false == post.wrong && false == post.duplicate) || glm::distance(post.post.location, camera) > kOverlayRange) + continue; + ImVec2 screen; + if (false == projection.project(post.post.location, screen)) + continue; + auto const colour{post.duplicate ? IM_COL32(255, 160, 40, 235) : IM_COL32(255, 70, 60, 235)}; + drawlist->AddCircle(screen, 9.0f, colour, 16, 2.5f); + draw_label(drawlist, screen, colour, post.duplicate ? format(STR_C("km %s duplicate"), km_text(post.hectometres).c_str()) : format(STR_C("km %s, %.1f m from its place"), km_text(post.hectometres).c_str(), post.distance)); + if (post.duplicate) + continue; + for (auto const &target : tool.posts) + if (target.hectometres == post.hectometres) + projection.line(drawlist, post.post.location, target.position, IM_COL32(255, 70, 60, 160), 1.5f); + } + ImVec2 screen; + if (projection.project(tool.point, screen)) + { + drawlist->AddCircle(screen, 7.0f, IM_COL32(90, 200, 255, 235), 16, 2.5f); + draw_label(drawlist, screen, IM_COL32(90, 200, 255, 235), format("km %.3f", tool.km)); + } +} + +void editor_mode::fouling_update() +{ + auto &tool{m_fouling}; + auto *selected{selected_track()}; + if (tool.scope == 0 && selected != tool.selected) + tool.dirty = true; + if (false == tool.dirty && tool.history == m_history.size()) + return; + tool.dirty = false; + tool.history = m_history.size(); + tool.selected = selected; + tool.points.clear(); + tool.found.clear(); + tool.error.clear(); + std::vector switches; + glm::dvec3 const camera{Global.pCamera.Pos}; + if (tool.scope == 0) + { + if (selected == nullptr || selected->eType != tt_Switch) + { + tool.error = STR_C("Select a switch, or check around the camera"); + return; + } + switches.push_back(selected); + } + else + { + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && track->eType == tt_Switch && false == track->m_editorremoved && (track->iCategoryFlag & 15) == kRailCategory && track->m_paths.size() >= 2 && + (tool.scope == 2 || glm::distance(track->location(), camera) <= tool.reach)) + switches.push_back(track); + } + for (auto *track : switches) + { + if (track->m_paths.size() < 2 || false == editor_track::is_supported(*track)) + continue; + auto const main{trace(*track, 0, true, kFoulingTrace)}; + auto const branch{trace(*track, 1, true, kFoulingTrace)}; + if (main.size() < 2 || branch.size() < 2) + continue; + glm::dvec3 previous{branch.front()}; + double previousdistance{0.0}; + for (std::size_t i = 1; i < branch.size(); ++i) + { + double distance; + nearest_on(main, branch[i], distance); + if (distance < tool.spacing) + { + previous = branch[i]; + previousdistance = distance; + continue; + } + auto const f{distance - previousdistance > 1e-9 ? (tool.spacing - previousdistance) / (distance - previousdistance) : 1.0}; + auto const onbranch{glm::mix(previous, branch[i], std::clamp(f, 0.0, 1.0))}; + double unused; + auto const onmain{nearest_on(main, onbranch, unused)}; + fouling_point point; + point.track = track; + auto const middle{(onbranch + onmain) * 0.5}; + // the points of the paths are at the rail foot, the heads of the rails are the height of the rail over them + point.position = tool.height_mode == 1 ? glm::dvec3{middle.x, 0.5 * (onbranch.y + onmain.y) + kRailHeight + tool.height, middle.z} : placement_on_ground({middle.x, std::max(onbranch.y, onmain.y), middle.z}); + auto const back{plan_of(previous - branch[i])}; + point.yaw = std::fmod(heading_of(glm::length(back) > 1e-9 ? glm::normalize(back) : glm::dvec2{0.0, 1.0}) + tool.turn, 360.0); + tool.points.push_back(point); + break; + } + } + // markers standing: each goes to the nearest fouling point, the others are left alone + auto const markers{standing_templates([](std::string const &File) { return File.find("w17") != std::string::npos || File.find("ukres") != std::string::npos; })}; + for (auto const &marker : markers) + { + int nearest{-1}; + double best{std::numeric_limits::max()}; + for (int i = 0; i < static_cast(tool.points.size()); ++i) + { + auto const distance{plan_distance(marker.location, tool.points[i].position)}; + if (distance < best) + { + best = distance; + nearest = i; + } + } + if (nearest < 0 || best > 40.0) + continue; + auto &point{tool.points[nearest]}; + if (point.standing >= 0 && tool.found[point.standing].error <= best) + continue; + fouling_found found; + found.marker = marker; + found.point = nearest; + found.error = best; + found.height_error = marker.location.y - point.position.y; + found.wrong = best > tool.tolerance || (tool.height_mode == 1 && std::abs(found.height_error) > tool.height_tolerance); + if (point.standing >= 0) + tool.found[point.standing] = found; + else + { + point.standing = static_cast(tool.found.size()); + tool.found.push_back(found); + } + } + 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; })}; + 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() +{ + auto &tool{m_fouling}; + fouling_update(); + std::vector items; + for (auto const &point : tool.points) + { + if (point.standing >= 0) + continue; + template_item item; + item.file = kFoulingModels[std::clamp(tool.model, 0, 3)]; + item.location = point.position; + item.yaw = point.yaw; + if (tool.model != 2) + item.rest = {"0"}; + items.push_back(std::move(item)); + } + 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; + WriteLog("Editor: " + tool.status, logtype::generic); + tool.dirty = true; +} + +void editor_mode::fouling_fix() +{ + auto &tool{m_fouling}; + fouling_update(); + std::vector> moves; + for (auto const &found : tool.found) + { + if (false == found.wrong) + continue; + template_item item; + auto const &point{tool.points[found.point]}; + if (found.error <= tool.tolerance) + { + // in its place, only too high or too low + item.location = {found.marker.location.x, point.position.y, found.marker.location.z}; + item.yaw = found.marker.yaw; + } + else + { + item.location = point.position; + item.yaw = point.yaw; + } + 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); + WriteLog("Editor: " + tool.status, logtype::generic); + tool.dirty = true; +} + +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) + { + 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); + } + char const *models[] = {"w17", "w17_new", "w17_old", "w17_dwuteo"}; + ImGui::Combo(STR_C("Marker"), &tool.model, models, IM_ARRAYSIZE(models)); + char const *turns[] = {"0", "90", "180", "270"}; + int turn{tool.turn / 90}; + if (ImGui::Combo(STR_C("Turned by (deg)"), &turn, turns, IM_ARRAYSIZE(turns))) + { + tool.turn = turn * 90; + tool.dirty = true; + } + 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 +{ + auto const &tool{m_fouling}; + if (false == tool.open) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList()}; + glm::dvec3 const camera{Global.pCamera.Pos}; + for (auto const &point : tool.points) + { + if (glm::distance(point.position, camera) > kOverlayRange) + continue; + ImVec2 screen; + if (false == projection.project(point.position, screen)) + continue; + bool const standing{point.standing >= 0 && false == tool.found[point.standing].wrong}; + auto const colour{standing ? IM_COL32(180, 180, 180, 200) : IM_COL32(90, 255, 120, 235)}; + drawlist->AddCircleFilled(screen, 4.0f, colour); + draw_label(drawlist, screen, colour, "W17"); + } + for (auto const &found : tool.found) + { + if (false == found.wrong || glm::distance(found.marker.location, camera) > kOverlayRange) + continue; + ImVec2 screen; + if (false == projection.project(found.marker.location, screen)) + continue; + drawlist->AddCircle(screen, 9.0f, IM_COL32(255, 70, 60, 235), 16, 2.5f); + draw_label(drawlist, screen, IM_COL32(255, 70, 60, 235), format(STR_C("W17 %.1f m off, %+.2f m high"), found.error, found.height_error)); + projection.line(drawlist, found.marker.location, tool.points[found.point].position, IM_COL32(255, 70, 60, 160), 1.5f); + } +} diff --git a/application/editormodeparallel.cpp b/application/editormodeparallel.cpp new file mode 100644 index 00000000..7eaa92f4 --- /dev/null +++ b/application/editormodeparallel.cpp @@ -0,0 +1,234 @@ +/* +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/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include + +namespace +{ + +using geometry::bezier; +using geometry::cross; +using geometry::plan_of; + +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}; + +segment_data reversed(segment_data const &Path) +{ + segment_data result{Path}; + result.points[segment_data::point::start] = Path.points[segment_data::point::end]; + result.points[segment_data::point::end] = Path.points[segment_data::point::start]; + result.points[segment_data::point::control1] = Path.points[segment_data::point::control2]; + result.points[segment_data::point::control2] = Path.points[segment_data::point::control1]; + result.rolls = {-Path.rolls[1], -Path.rolls[0]}; + return result; +} + +// the control vectors as they're drawn, also for a straight given without them +std::pair controls(segment_data const &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]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + return {(end - start) / 3.0, (start - end) / 3.0}; + return {control1, control2}; +} + +// curvature in the plan at the start (false) or the end (true) of the path, 1/m, positive turning left +double curvature(segment_data const &Path, bool const Atend) +{ + auto const [control1, control2] = controls(Path); + auto const p0{plan_of(Path.points[segment_data::point::start])}; + auto const p3{plan_of(Path.points[segment_data::point::end])}; + auto const p1{p0 + plan_of(control1)}; + auto const p2{p3 + plan_of(control2)}; + auto const first{Atend ? 3.0 * (p3 - p2) : 3.0 * (p1 - p0)}; + auto const second{Atend ? 6.0 * (p1 - 2.0 * p2 + p3) : 6.0 * (p0 - 2.0 * p1 + p2)}; + auto const speed{glm::length(first)}; + return speed > 1e-9 ? cross(first, second) / (speed * speed * speed) : 0.0; +} + +glm::dvec2 direction(segment_data const &Path, bool const Atend) +{ + auto const [control1, control2] = controls(Path); + auto const along{Atend ? -plan_of(control2) : plan_of(control1)}; + return glm::length(along) > 1e-9 ? glm::normalize(along) : glm::dvec2{0.0, 1.0}; +} + +} // namespace + +void editor_mode::parallel_update() +{ + auto &tool{m_parallel}; + auto *track{selected_track()}; + if (track == tool.built_for && tool.spacing == tool.built_spacing && tool.side == tool.built_side && tool.scope == tool.built_scope && m_history.size() == tool.built_history) + return; + tool.built_for = track; + tool.built_spacing = tool.spacing; + tool.built_side = tool.side; + tool.built_scope = tool.scope; + tool.built_history = m_history.size(); + tool.pieces.clear(); + tool.styles.clear(); + tool.length = 0.0; + tool.error.clear(); + if (track == nullptr) + return; + if (track->eType != tt_Normal || false == editor_track::is_supported(*track)) + { + tool.error = STR_C("A switch has no line of its own: select a plain path"); + return; + } + std::vector> sources; + if (tool.scope == 1) + { + editor_track::chain run; + std::string error; + if (editor_track::find_run(*track, kParallelReach, run, error)) + for (std::size_t i = 0; i < run.tracks.size(); ++i) + sources.emplace_back(run.tracks[i], run.forward[i] ? run.tracks[i]->m_paths.front() : reversed(run.tracks[i]->m_paths.front())); + } + if (sources.empty()) + sources.emplace_back(track, track->m_paths.front()); + auto const offset{tool.spacing * tool.side}; + // the joints are moved once, along the mean direction of the paths meeting there, so the new paths meet too + std::vector joints; + for (std::size_t k = 0; k <= sources.size(); ++k) + { + bool const first{k == 0}; + bool const last{k == sources.size()}; + auto const &before{sources[last ? k - 1 : (first ? 0 : k - 1)].second}; + auto const &after{sources[last ? k - 1 : k].second}; + auto const point{first ? after.points[segment_data::point::start] : before.points[segment_data::point::end]}; + glm::dvec2 heading{first ? direction(after, false) : last ? direction(before, true) : direction(before, true) + direction(after, false)}; + heading = glm::length(heading) > 1e-9 ? glm::normalize(heading) : glm::dvec2{0.0, 1.0}; + auto const roll{first ? after.rolls[0] : last ? before.rolls[1] : 0.5f * (before.rolls[1] + after.rolls[0])}; + glm::dvec2 const left{-heading.y, heading.x}; + auto const planar{plan_of(point) + left * offset}; + joints.emplace_back(planar.x, point.y - offset * std::tan(glm::radians(static_cast(roll))), planar.y); + } + for (std::size_t i = 0; i < sources.size(); ++i) + { + 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}; + if (startscale < 0.05 || endscale < 0.05) + { + tool.pieces.clear(); + tool.styles.clear(); + tool.error = format(STR_C("The spacing is larger than the radius of %s"), sources[i].first->name().c_str()); + return; + } + segment_data piece; + piece.points[segment_data::point::start] = joints[i]; + piece.points[segment_data::point::end] = joints[i + 1]; + piece.points[segment_data::point::control1] = control1 * startscale; + piece.points[segment_data::point::control2] = control2 * endscale; + 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))); + tool.length += bezier{piece}.plan_length(); + tool.pieces.push_back(piece); + tool.styles.push_back(sources[i].first); + } +} + +void editor_mode::parallel_build() +{ + auto &tool{m_parallel}; + parallel_update(); + if (tool.pieces.empty()) + return; + std::vector created; + 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); + 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) + tool.status += "\n" + std::string{STR_C("The scenery isn't opened for editing, the new track won't be saved")}; + WriteLog("Editor: " + tool.status, logtype::generic); + tool.built_for = nullptr; +} + +void editor_mode::render_parallel_ui() +{ + auto &tool{m_parallel}; + ImGui::TextDisabled("%s", STR_C("Second track alongside, curves included: radius R minus or plus the spacing,\nthe same transitions and cant, heights on the plane of the cant")); + 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"); + 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; + } + parallel_update(); + if (false == tool.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str()); + else if (false == tool.pieces.empty()) + { + if (ImGui::Button(format(STR_C("Build %zu paths, %.1f m"), tool.pieces.size(), tool.length).c_str())) + parallel_build(); + } + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} + +void editor_mode::draw_parallel_preview() const +{ + auto const &tool{m_parallel}; + if (false == tool.open || tool.pieces.empty() || selected_track() != tool.built_for) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList()}; + for (auto const &piece : tool.pieces) + { + bezier const curve{piece}; + auto const count{std::clamp(static_cast(curve.plan_length() / 2.0), 4, 128)}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= count; ++k) + { + auto const next{curve.point(static_cast(k) / count)}; + projection.line(drawlist, previous, next, k % 2 ? IM_COL32(90, 255, 120, 230) : IM_COL32(90, 255, 120, 90), 3.0f); + previous = next; + } + } + ImVec2 screen; + if (projection.project(tool.pieces.front().points[segment_data::point::start], screen)) + drawlist->AddCircleFilled(screen, 5.0f, IM_COL32(90, 255, 120, 230)); +} diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index db2bc684..45643a28 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -26,6 +26,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include namespace @@ -475,6 +476,122 @@ void editor_mode::profile_fit_ground() 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); } +std::pair editor_mode::profile_ballast(TTrack const &Track) +{ + auto const depth{std::max(0.0, static_cast(Track.fTexHeight1 - Track.fTexHeightOffset))}; + auto const foot{0.5 * std::abs(Track.fTrackWidth) + std::abs(Track.fTexWidth) + std::abs(Track.fTexSlope)}; + return {depth, std::max(0.5, foot)}; +} + +std::pair editor_mode::profile_generate_ground(std::vector> const &Beds, double const Reach) +{ + auto &state{m_profile}; + auto &works{state.earthworks}; + auto const &samples{state.samples}; + bool const streaming{m_streamer.active()}; + double const size{streaming ? m_streamer.cells() * m_streamer.cellsize() : chunk_grid_size()}; + int const cells{streaming ? m_streamer.cells() : std::clamp(m_terrain_cells, 1, 256)}; + if (size <= 0.0 || cells <= 0 || samples.empty()) + return {0, 0}; + double const cellsize{size / cells}; + std::set> keys; + for (auto const &sample : samples) + { + auto const &p{sample.position}; + for (auto x = static_cast(std::floor((p.x - Reach) / size)); x <= static_cast(std::floor((p.x + Reach) / size)); ++x) + for (auto z = static_cast(std::floor((p.z - Reach) / size)); z <= static_cast(std::floor((p.z + Reach) / size)); ++z) + keys.insert({x, z}); + } + auto const terrains{active_terrains()}; + std::string const texture{m_terrain_texture}; + int made{0}, skipped{0}; + std::size_t const side{static_cast(cells) + 1}; + for (auto const &key : keys) + { + glm::dvec2 const low{key.first * size, key.second * size}; + glm::dvec2 const middle{low + glm::dvec2{size * 0.5}}; + if (streaming ? m_streamer.terrain_at(middle.x, middle.y) != nullptr : m_grid_chunks.count(key) > 0) + continue; + std::vector near; + for (std::size_t i = 0; i < samples.size(); ++i) + if (std::abs(samples[i].position.x - middle.x) <= size * 0.5 + Reach && std::abs(samples[i].position.z - middle.y) <= size * 0.5 + Reach) + near.push_back(i); + if (near.empty()) + continue; + std::vector points; + std::vector nearest; + points.reserve(side * side); + nearest.reserve(side * side); + for (std::size_t iz = 0; iz < side; ++iz) + for (std::size_t ix = 0; ix < side; ++ix) + { + glm::dvec3 point{low.x + ix * cellsize, 0.0, low.y + iz * cellsize}; + auto best{near.front()}; + auto distance{std::numeric_limits::max()}; + for (auto const i : near) + { + auto const d{plan_distance(point, samples[i].position)}; + if (d < distance) + { + distance = d; + best = i; + } + } + point.y = profile::elevation(state.line, samples[best].chainage); + points.push_back(point); + nearest.push_back(best); + } + std::vector found; + auto heights{ground_heights(points, true, &found)}; + std::size_t own{0}; + for (std::size_t i = 0; i < points.size(); ++i) + if (found[i] != 0 && std::none_of(terrains.begin(), terrains.end(), [&](editor_terrain const *Terrain) { return Terrain->contains(points[i].x, points[i].z); })) + ++own; + if (own * 20 > points.size()) + { + ++skipped; + continue; + } + for (std::size_t i = 0; i < points.size(); ++i) + { + if (found[i] != 0) + continue; + auto const sample{nearest[i]}; + heights[i] = sample < state.terrain.size() && std::isfinite(state.terrain[sample]) ? state.terrain[sample] : profile::elevation(state.line, samples[sample].chainage) - Beds[sample].first; + } + auto const sampler = [&](double const X, double const Z, double &Height) { + auto const ix{static_cast(std::clamp(std::lround((X - low.x) / cellsize), 0, cells))}; + auto const iz{static_cast(std::clamp(std::lround((Z - low.y) / cellsize), 0, cells))}; + Height = heights[iz * side + ix]; + return true; + }; + if (streaming) + { + m_streamer.add_chunk(key.first, key.second); + auto *terrain{m_streamer.terrain_at(middle.x, middle.y)}; + if (terrain == nullptr) + continue; + terrain->reshape([&](double const X, double const Z, float &Height) { + double height; + sampler(X, Z, height); + Height = static_cast(height); + return true; + }); + } + else + { + auto chunk{std::make_unique()}; + if (false == chunk->create(glm::dvec3{middle.x, m_terrain_baseheight, middle.y}, cells, static_cast(cellsize), texture, sampler)) + continue; + m_grid_chunks[key] = std::move(chunk); + } + works.generated.push_back(key); + works.generated_streaming = streaming; + ++made; + } + return {made, skipped}; +} + void editor_mode::profile_shape_ground() { auto &state{m_profile}; @@ -483,11 +600,23 @@ void editor_mode::profile_shape_ground() if (samples.size() < 2 || state.line.points.size() < 2) return; auto const slope{std::max(0.1, works.slope)}; - auto const reach{works.half_width + works.reach}; + std::vector> beds(samples.size(), {works.depth, works.half_width}); + if (works.from_ballast) + for (std::size_t i = 0; i < samples.size(); ++i) + if (samples[i].span < state.route.spans.size()) + if (auto const *track{state.route.spans[samples[i].span].track}; track != nullptr) + beds[i] = profile_ballast(*track); + double widest{0.0}; + for (auto const &bed : beds) + widest = std::max(widest, bed.second); + auto const reach{widest + works.reach}; + works.undo.clear(); + works.generated.clear(); + auto const generated{works.generate ? profile_generate_ground(beds, reach) : std::pair{0, 0}}; auto const terrains{active_terrains()}; if (terrains.empty()) { - state.error = STR_C("There's no editor terrain to shape: convert the scenery ground first"); + state.error = generated.second > 0 ? STR_C("The scenery has its own ground under the route: convert it to editor terrain first") : STR_C("There's no editor terrain to shape: convert the scenery ground first"); return; } @@ -509,7 +638,6 @@ void editor_mode::profile_shape_ground() high = glm::max(high, glm::dvec2{std::max(a.x, b.x), std::max(a.z, b.z)}); } - works.undo.clear(); std::size_t moved{0}; double fill{0.0}, cut{0.0}; for (auto *terrain : terrains) @@ -520,6 +648,7 @@ void editor_mode::profile_shape_ground() return false; double distance{reach}; double chainage{-1.0}; + std::size_t nearest{0}; auto const from{key(X - reach, Z - reach)}; auto const to{key(X + reach, Z + reach)}; for (auto x = from.first; x <= to.first; ++x) @@ -540,12 +669,14 @@ void editor_mode::profile_shape_ground() { distance = offset; chainage = samples[i].chainage + (samples[i + 1].chainage - samples[i].chainage) * t; + nearest = t < 0.5 ? i : i + 1; } } } if (chainage < 0.0) return false; - auto const formation{profile::elevation(state.line, chainage) - works.depth}; + auto const &bed{beds[nearest]}; + auto const formation{profile::elevation(state.line, chainage) - bed.first}; auto const ground{static_cast(Height)}; // the bends at the edge of the formation and at the foot of the slope are rounded off over the given length auto const rounding{std::max(0.0, works.rounding)}; @@ -555,7 +686,7 @@ void editor_mode::profile_shape_ground() auto const h{std::max(K - std::abs(A - B), 0.0) / K}; return std::max(A, B) + h * h * K * 0.25; }; - auto const beyond{soft_max(0.0, distance - works.half_width, rounding)}; + auto const beyond{soft_max(0.0, distance - bed.second, rounding)}; auto const bend{rounding / slope}; auto const target{ground > formation ? -soft_max(-ground, -(formation + beyond / slope), bend) : soft_max(ground, formation - beyond / slope, bend)}; if (std::abs(target - ground) < 0.005) @@ -569,14 +700,21 @@ void editor_mode::profile_shape_ground() if (shaped) works.undo.emplace_back(terrain, before); } + auto const chunks = [&](std::string Text) { + if (generated.first > 0) + Text += format(STR_C("; %d chunk(s) of terrain made under the route"), generated.first); + if (generated.second > 0) + Text += format(STR_C("; %d chunk(s) left out: the scenery has its own ground there, convert it to shape it"), generated.second); + return Text; + }; if (moved == 0) { - state.status = STR_C("The editor terrain doesn't reach the route, or it's shaped already"); + state.status = chunks(STR_C("The editor terrain doesn't reach the route, or it's shaped already")); return; } state.error.clear(); profile_resample(); - state.status = format(STR_C("Terrain shaped at %zu points: embankment up to %.2f m, cutting up to %.2f m"), moved, fill, cut); + state.status = chunks(format(STR_C("Terrain shaped at %zu points: embankment up to %.2f m, cutting up to %.2f m"), moved, fill, cut)); WriteLog("Editor: vertical profile - " + state.status, logtype::generic); } @@ -592,6 +730,15 @@ void editor_mode::profile_restore_ground() ++restored; } state.earthworks.undo.clear(); + for (auto const &key : state.earthworks.generated) + { + if (state.earthworks.generated_streaming) + m_streamer.remove_chunk(key.first, key.second); + else + remove_grid_chunk(key.first, key.second); + ++restored; + } + state.earthworks.generated.clear(); profile_resample(); state.status = restored > 0 ? STR_C("The terrain is back as it was before the shaping") : STR_C("The shaped terrain isn't loaded any more"); } @@ -659,10 +806,27 @@ void editor_mode::render_profile_earthworks() 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); - drag("Formation below the rail top (m)", works.depth, 0.01f, "%.2f"); - drag("Half width of the formation (m)", works.half_width, 0.05f, "%.2f"); + ImGui::Checkbox(STR_C("Make terrain where the route has none"), &works.generate); 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::SetTooltip("%s", STR_C("Chunks of editor terrain are made along the route where there's none, from the ground around,\nthen shaped. Where the scenery has its own ground triangles the chunk is left out: convert them instead")); + 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")); + if (works.from_ballast) + { + if (false == state.route.spans.empty() && state.route.spans.front().track != nullptr) + { + auto const bed{profile_ballast(*state.route.spans.front().track)}; + ImGui::TextDisabled(STR_C("Ballast %.2f m deep, its foot %.2f m from the axis"), bed.first, bed.second); + } + } + else + { + 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")); + } 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"); @@ -678,13 +842,13 @@ void editor_mode::render_profile_earthworks() 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(); - if (false == works.undo.empty()) + if (false == works.undo.empty() || false == works.generated.empty()) { ImGui::SameLine(); if (ImGui::Button(STR_C("Restore the terrain"))) profile_restore_ground(); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Puts back the terrain as it was before the last shaping")); + ImGui::SetTooltip("%s", STR_C("Puts back the terrain as it was before the last shaping, and takes away the terrain it made")); } if (false == active_terrains().empty()) { diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp new file mode 100644 index 00000000..8142eef9 --- /dev/null +++ b/application/editormodeselftest.cpp @@ -0,0 +1,385 @@ +/* +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/application.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" + +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Track.h" +#include "vehicle/DynObj.h" +#include "editor/editorFormat.hpp" + +#include +#include +#include +#include + +// the script is a text file, one action a line: +// wait +// camera from the first point, looking at the second +// select +// 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) +// spread logs the paths the spread from the track takes +// newscenery the editor starts again with the wizard of a new scenery +// vehicles logs each vehicle: its head, facing, and what its couplers hold +// screenshot +// save +// log +// quit +void editor_mode::selftest_step() +{ + auto &test{m_selftest}; + if (false == test.loaded) + { + test.loaded = true; + auto const *file{std::getenv("EU07_EDITOR_SELFTEST")}; + if (file == nullptr) + return; + std::ifstream stream(file); + std::string line; + while (std::getline(stream, line)) + if (false == line.empty() && line.front() != '#') + test.lines.push_back(line); + WriteLog("SELFTEST: " + std::to_string(test.lines.size()) + " steps from " + file); + } + if (test.wait > 0) + { + --test.wait; + return; + } + while (test.next < test.lines.size()) + { + std::istringstream words(test.lines[test.next++]); + std::string command; + words >> command; + if (command == "wait") + { + words >> test.wait; + return; + } + if (command == "camera") + { + glm::dvec3 from, to; + words >> from.x >> from.y >> from.z >> to.x >> to.y >> to.z; + auto const look{glm::normalize(to - from)}; + Camera.Pos = from; + Camera.Angle = glm::vec3(static_cast(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast(std::atan2(-look.x, -look.z)), 0.0f); + Global.pCamera = Camera; + m_focus_active = false; + } + else if (command == "select") + { + std::string name; + words >> name; + m_node = simulation::Paths.find(name); + m_track_point = {}; + ui()->set_node(m_node); + WriteLog("SELFTEST select " + name + (m_node != nullptr ? "" : ": no such track")); + } + else if (command == "sweep") + { + std::map values; + std::string pair; + while (words >> pair) + { + auto const equals{pair.find('=')}; + if (equals != std::string::npos) + values[pair.substr(0, equals)] = pair.substr(equals + 1); + } + auto &tool{m_sweep}; + auto const get = [&](char const *Key, std::string const &Default) { auto const found{values.find(Key)}; return found != values.end() ? found->second : Default; }; + tool.edited = nullptr; + tool.curve_for = nullptr; + std::snprintf(tool.model, sizeof(tool.model), "%s", get("model", "").c_str()); + tool.parameters.clear(); + std::stringstream parameters(get("params", "")); + std::string parameter; + while (std::getline(parameters, parameter, ',')) + { + tool.parameters.emplace_back(); + std::snprintf(tool.parameters.back().data(), tool.parameters.back().size(), "%s", parameter.c_str()); + } + tool.parameters_for = tool.model; + auto &settings{tool.settings}; + settings.lateral = std::stod(get("lateral", "0")); + settings.height = std::stod(get("height", "0")); + auto const side{get("side", "origin")}; + settings.side_anchor = side == "near" ? 1 : side == "far" ? 2 : side == "centre" ? 3 : 0; + auto const top{get("top", "origin")}; + settings.height_anchor = top == "bottom" ? 1 : top == "top" ? 2 : top == "edge" ? 3 : 0; + settings.bend = get("mode", "bend") != "repeat"; + settings.step = std::stod(get("step", "0")); + settings.widen = get("widen", "0") == "1"; + settings.face = get("face", "1") == "1"; + settings.tilt = get("tilt", "0") == "1"; + settings.from = std::stod(get("from", "0")); + settings.to = std::stod(get("to", "-1")); + auto const length{get("length", "all")}; + tool.length_mode = length == "original" ? 0 : length == "all" ? 2 : 1; + if (tool.length_mode == 1) + tool.length = std::stod(length); + tool.measured.clear(); + tool.open = true; + sweep_curve(); + sweep_measure(); + if (get("create", "1") == "1") + { + sweep_create(); + WriteLog("SELFTEST sweep: " + tool.status); + } + } + else if (command == "drag") + { + glm::dvec3 from{0.0}, to{0.0}; + words >> from.x >> from.z >> to.x >> to.z; + m_sweep.open = true; + if (false == sweep_press_at(from, false)) + { + WriteLog("SELFTEST drag: the press wasn't taken"); + continue; + } + sweep_drag_at(to); + sweep_release(); + WriteLog("SELFTEST drag: " + m_sweep.status); + } + else if (command == "probe") + { + // edge of the last laid model as it was made: its distance from the axis and the height of its top over the rail head + std::string name; + words >> name; + auto *sweep{name.empty() ? m_sweep.edited : simulation::Sweeps.find(name)}; + if (sweep == nullptr) + { + WriteLog("SELFTEST probe: nothing laid"); + continue; + } + auto const &definition{sweep->definition()}; + double const sign{definition.lateral >= 0.0 ? 1.0 : -1.0}; + struct bin + { + double edge{std::numeric_limits::max()}; + std::vector> points; // lateral, height over the axis + }; + std::map bins; + for (auto const &shape : sweep->create_shapes()) + for (auto const &vertex : shape.data().vertices) + { + double lateral; + auto const station{sweep->project(vertex.position, lateral)}; + if (lateral * sign <= 0.0) + continue; + auto const axis{sweep->frame_at(station)}; + auto &entry{bins[static_cast(station / 20.0)]}; + entry.edge = std::min(entry.edge, std::abs(lateral)); + entry.points.emplace_back(std::abs(lateral), vertex.position.y - axis.position.y); + } + WriteLog(format("SELFTEST probe %s: %zu stretches with the model, %.1f - %.1f m of %.1f m", sweep->name().c_str(), bins.size(), sweep->start(), sweep->end(), sweep->length())); + for (auto const &[index, entry] : bins) + { + auto const station{index * 20.0 + 10.0}; + if (station < sweep->start() || station > sweep->end()) + continue; + double top{-1000.0}; + for (auto const &point : entry.points) + if (point.first < entry.edge + 0.15) + top = std::max(top, point.second); + auto const at{sweep->frame_at(station)}; + auto const head{0.18 + sweep_node::rail_head(at.roll, definition.lateral)}; + WriteLog(format("SELFTEST probe %s at %.0f m: edge %.3f m from the axis, its top %.3f m over the rail head (cant %.1f deg, gauge widening %.3f m, cant part %.3f m)", sweep->name().c_str(), station, entry.edge, top - head, at.roll, + at.widening[definition.lateral >= 0.0 ? 1 : 0], std::max(0.0, at.cant[definition.lateral >= 0.0 ? 1 : 0]))); + } + } + else if (command == "fouling") + { + std::map values; + std::string pair; + while (words >> pair) + { + auto const equals{pair.find('=')}; + if (equals != std::string::npos) + values[pair.substr(0, equals)] = pair.substr(equals + 1); + } + auto &tool{m_fouling}; + tool.scope = std::stoi(values.count("scope") ? values["scope"] : "2"); + tool.height_mode = values["mode"] == "ground" ? 0 : 1; + tool.height = std::stod(values.count("height") ? values["height"] : "0"); + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling: " + tool.status); + int shown{0}; + for (auto const &found : tool.found) + if (found.wrong && shown++ < 5) + WriteLog(format("SELFTEST fouling: %s %.2f m off, %+.3f m high", found.marker.file.c_str(), found.error, found.height_error)); + if (values["place"] == "1") + { + fouling_place(); + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling after placing: " + tool.status); + } + if (values["fix"] == "1") + { + fouling_fix(); + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling after the fix: " + tool.status); + } + } + else if (command == "hekto") + { + std::map values; + std::string pair; + while (words >> pair) + { + auto const equals{pair.find('=')}; + if (equals != std::string::npos) + values[pair.substr(0, equals)] = pair.substr(equals + 1); + } + auto *track{simulation::Paths.find(values["track"])}; + if (track == nullptr) + { + WriteLog("SELFTEST hekto: no such track"); + continue; + } + hekto_start(*track, track->m_paths.front().points[segment_data::point::start]); + auto &tool{m_hekto}; + tool.dirty = true; + hekto_update(); + if (values.count("km")) + { + tool.km = std::stod(values["km"]); + tool.growth = values["growth"] == "-1" ? -1 : 1; + } + tool.side = values["side"] == "alt" ? 0 : values["side"] == "left" ? -1 : 1; + tool.dirty = true; + hekto_update(); + auto const missing{std::count_if(tool.posts.begin(), tool.posts.end(), [](hekto_post const &Post) { return Post.standing < 0; })}; + WriteLog(format("SELFTEST hekto: km %.3f growth %d, %zu posts along %.0f m, %d to place, %zu standing. %s", tool.km, tool.growth, tool.posts.size(), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, static_cast(missing), tool.found.size(), tool.status.c_str())); + for (std::size_t i = 0; i < tool.posts.size() && i < 4; ++i) + { + auto const &post{tool.posts[i]}; + double lateral; + glm::dvec3 const axis{editor_track::sampled_position(tool.samples, post.chainage)}; + auto const direction{editor_track::sampled_position(tool.samples, post.chainage + 1.0) - axis}; + glm::dvec2 const offset{post.position.x - axis.x, post.position.z - axis.z}; + lateral = glm::length(offset); + auto const right{(direction.x * offset.y - direction.z * offset.x) * tool.growth >= 0.0}; + WriteLog(format("SELFTEST hekto post %d.%d at %.1f m, %.2f m from the axis, %s of the growing kilometrage, at %.1f %.1f %.1f", post.hectometres / 10, post.hectometres % 10, post.chainage, lateral, right ? "right" : "left", post.position.x, post.position.y, + post.position.z)); + } + if (values["place"] == "1") + { + hekto_place(); + WriteLog("SELFTEST hekto: " + tool.status); + } + } + else if (command == "tracks") + { + int tab{0}; + words >> tab; + show_track_tab(track_tab::lineside); + m_sweep.expand = tab == 0; + m_hekto.expand = tab == 1; + m_fouling.expand = tab == 2; + m_parallel.expand = tab == 3; + m_vehicle.expand = tab == 4; + } + else if (command == "curve") + { + sweep_curve(); + for (auto const &piece : m_sweep.curve) + { + auto const &a{piece.points[segment_data::point::start]}; + auto const &b{piece.points[segment_data::point::end]}; + WriteLog(format("SELFTEST curve piece %.1f %.1f -> %.1f %.1f", a.x, a.z, b.x, b.z)); + } + if (false == m_sweep.outline.empty()) + WriteLog(format("SELFTEST curve outline %zu points, %.1f %.1f -> %.1f %.1f, %.1f m", m_sweep.outline.size(), m_sweep.outline.front().x, m_sweep.outline.front().z, m_sweep.outline.back().x, m_sweep.outline.back().z, m_sweep.curve_length)); + } + else if (command == "projtest") + { + screen_projection const projection; + for (std::size_t i = 0; i < m_sweep.outline.size() && i < 45; i += 4) + { + ImVec2 screen; + auto const shown{projection.project(m_sweep.outline[i], screen)}; + auto const clip{projection.clip(m_sweep.outline[i])}; + WriteLog(format("SELFTEST proj %zu: %.1f %.1f %.1f -> %d %.0f %.0f w %.2f", i, m_sweep.outline[i].x, m_sweep.outline[i].y, m_sweep.outline[i].z, shown ? 1 : 0, screen.x, screen.y, clip.w)); + } + } + else if (command == "spread") + { + std::string name; + words >> name >> m_spread.stop >> m_spread.reach; + if (auto *track{simulation::Paths.find(name)}) + { + std::string names; + for (auto const *other : track_spread_from(*track)) + names += ' ' + other->name(); + WriteLog("SELFTEST spread " + name + ":" + names + " | prev " + (track->trPrev ? track->trPrev->name() : std::string{"-"}) + " next " + (track->trNext ? track->trNext->name() : std::string{"-"})); + } + } + else if (command == "newscenery") + { + WriteLog(restart_for_new_scenery() ? "SELFTEST newscenery: started" : "SELFTEST newscenery: failed"); + return; + } + else if (command == "vehicles") + { + for (auto *vehicle : simulation::Vehicles.sequence()) + { + if (vehicle == nullptr) + continue; + auto const head{vehicle->HeadPosition()}; + auto const front{vehicle->VectorFront()}; + auto const coupler = [&](int const End) { + auto const &coupler{vehicle->MoverParameters->Couplers[End]}; + return coupler.Connected != nullptr ? coupler.Connected->Name + "/" + std::to_string(coupler.CouplingFlag) : std::string{"-"}; + }; + WriteLog(format("SELFTEST vehicle %s head %.2f %.2f front %.2f %.2f dir %d coupler0 %s coupler1 %s", vehicle->name().c_str(), head.x, head.z, front.x, front.z, vehicle->iDirection, coupler(0).c_str(), coupler(1).c_str())); + } + } + else if (command == "screenshot") + { + Application.queue_screenshot(); + test.wait = 3; + return; + } + else if (command == "save") + { + save(); + } + else if (command == "log") + { + std::string text; + std::getline(words, text); + WriteLog("SELFTEST" + text); + } + else if (command == "quit") + { + if (false == test.quitting) + { + WriteLog("SELFTEST done"); + test.quitting = true; + test.wait = 30; + --test.next; + return; + } + Application.queue_quit(true); + return; + } + } +} diff --git a/application/editormodespeed.cpp b/application/editormodespeed.cpp index 5c381805..f1c55666 100644 --- a/application/editormodespeed.cpp +++ b/application/editormodespeed.cpp @@ -29,7 +29,6 @@ namespace { double const kSpeedLineReach{5000.0}; // m each way, for the line through the selected path -double const kSpeedViewDistance{25.0}; double const kSpeedOverlayRange{3000.0}; bool rail(TTrack const &Track) @@ -148,21 +147,7 @@ void editor_mode::speed_focus(int const Index) return; m_node = verdict.track; ui()->set_node(m_node); - auto const &path{verdict.track->m_paths[std::min(verdict.path, verdict.track->m_paths.size() - 1)]}; - geometry::bezier const curve{path}; - glm::dvec3 look{curve.first(0.5)}; - look.y = 0.0; - look = glm::length(look) > 1e-6 ? glm::normalize(look) : glm::dvec3(0.0, 0.0, 1.0); - if (glm::dot(Camera.Pos - verdict.position, look) > 0.0) - look = -look; - look = glm::normalize(look - glm::dvec3(0.0, 0.25, 0.0)); - m_focus_start_pos = Camera.Pos; - m_focus_start_angle = Camera.Angle; - m_focus_target_pos = verdict.position - look * kSpeedViewDistance; - 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; + focus_track(*verdict.track, verdict.path, verdict.position); } void editor_mode::speed_apply(std::vector const &Indices, bool const Branches) diff --git a/application/editormodesweep.cpp b/application/editormodesweep.cpp new file mode 100644 index 00000000..38c78dac --- /dev/null +++ b/application/editormodesweep.cpp @@ -0,0 +1,955 @@ +/* +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/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" + +#include "rendering/renderer.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Sweep.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::bezier; +using geometry::plan_distance; +using geometry::plan_of; + +double const kSweepRouteReach{2000.0}; // m each way through the switches +double const kSweepOutlineStep{1.0}; +double const kSweepNear{4.0}; // m, a model along a curve this close to the selected path belongs to its line +double const kSweepGrab{25.0}; // m from the track, a press this close marks the stretch of the model +std::size_t const kSweepModelList{400}; +double const kRailTop{0.18}; // m from the points of a track to the top of its rails + +segment_data reversed(segment_data const &Path) +{ + segment_data result{Path}; + result.points[segment_data::point::start] = Path.points[segment_data::point::end]; + result.points[segment_data::point::end] = Path.points[segment_data::point::start]; + result.points[segment_data::point::control1] = Path.points[segment_data::point::control2]; + result.points[segment_data::point::control2] = Path.points[segment_data::point::control1]; + result.rolls = {-Path.rolls[1], -Path.rolls[0]}; + return result; +} + +bool same_piece(segment_data const &A, segment_data const &B) +{ + for (int i = 0; i < 4; ++i) + if (glm::distance(A.points[i], B.points[i]) > 1e-3) + return false; + return std::abs(A.rolls[0] - B.rolls[0]) < 1e-3f && std::abs(A.rolls[1] - B.rolls[1]) < 1e-3f; +} + +std::string lower(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + return Text; +} + +} // namespace + +void editor_mode::sweep_scan_models() +{ + auto &tool{m_sweep}; + if (tool.scanned) + return; + tool.scanned = true; + std::error_code error; + std::set found; + for (auto const &entry : std::filesystem::recursive_directory_iterator("models", std::filesystem::directory_options::skip_permission_denied, error)) + { + if (false == entry.is_regular_file(error)) + continue; + auto const extension{lower(entry.path().extension().string())}; + if (extension != ".e3d" && extension != ".t3d") + continue; + auto name{std::filesystem::relative(entry.path(), "models", error).generic_string()}; + name.erase(name.size() - extension.size()); + found.insert(name); + } + for (auto const &entry : std::filesystem::recursive_directory_iterator("scenery", std::filesystem::directory_options::skip_permission_denied, error)) + { + if (entry.is_regular_file(error) && lower(entry.path().extension().string()) == ".inc") + found.insert(std::filesystem::relative(entry.path(), "scenery", error).generic_string()); + } + tool.models.assign(found.begin(), found.end()); +} + + +void editor_mode::sweep_curve() +{ + auto &tool{m_sweep}; + auto *track{selected_track()}; + if (track == tool.curve_for && tool.curve_history == m_history.size()) + return; + tool.curve_for = track; + tool.curve_history = m_history.size(); + tool.curve.clear(); + tool.curve_pieces.clear(); + if (track != nullptr && false == track->m_paths.empty() && editor_track::is_supported(*track)) + { + tool.curve.push_back(track->m_paths.front()); + tool.curve_pieces.push_back({track, 0, true}); + } + sweep_outline(); +} + +void editor_mode::sweep_outline() +{ + auto &tool{m_sweep}; + tool.outline.clear(); + tool.stations.clear(); + tool.curve_length = 0.0; + for (auto const &piece : tool.curve) + { + bezier const curve{piece}; + auto const count{std::clamp(static_cast(curve.plan_length() / kSweepOutlineStep), 2, 2000)}; + for (int k = tool.outline.empty() ? 0 : 1; k <= count; ++k) + { + auto const point{curve.point(static_cast(k) / count)}; + if (false == tool.outline.empty()) + tool.curve_length += plan_distance(tool.outline.back(), point); + tool.outline.push_back(point); + tool.stations.push_back(tool.curve_length); + } + } +} + +bool editor_mode::sweep_extend(bool const Atend, glm::dvec3 const &Cursor) +{ + auto &tool{m_sweep}; + if (tool.curve_pieces.empty() || tool.curve_length > kSweepRouteReach) + return false; + auto const &edge{Atend ? tool.curve_pieces.back() : tool.curve_pieces.front()}; + auto const &edgepiece{Atend ? tool.curve.back() : tool.curve.front()}; + auto const point{Atend ? edgepiece.points[segment_data::point::end] : edgepiece.points[segment_data::point::start]}; + std::optional best; + segment_data bestpiece; + auto bestdistance{std::numeric_limits::max()}; + for (auto const &[other, end] : editor_track::connected_points(*edge.track, point)) + { + if (other == nullptr || other->m_editorremoved || other->eType == tt_Table || false == editor_track::is_supported(*other) || end.path >= static_cast(other->m_paths.size())) + continue; + if (std::any_of(tool.curve_pieces.begin(), tool.curve_pieces.end(), [&](sweep_tool::piece const &Piece) { return Piece.track == other && Piece.path == end.path; })) + continue; + auto const startshere{end.kind == editor_track::point_kind::start}; + auto const forward{Atend ? startshere : false == startshere}; + auto const piece{forward ? other->m_paths[end.path] : reversed(other->m_paths[end.path])}; + bezier const curve{piece}; + auto distance{std::numeric_limits::max()}; + for (int k = 0; k <= 32; ++k) + distance = std::min(distance, plan_distance(curve.point(k / 32.0), Cursor)); + if (distance < bestdistance) + { + bestdistance = distance; + best = sweep_tool::piece{other, end.path, forward}; + bestpiece = piece; + } + } + if (false == best.has_value()) + return false; + if (Atend) + { + tool.curve_pieces.push_back(*best); + tool.curve.push_back(bestpiece); + sweep_outline(); + } + else + { + auto const before{tool.curve_length}; + tool.curve_pieces.insert(tool.curve_pieces.begin(), *best); + tool.curve.insert(tool.curve.begin(), bestpiece); + sweep_outline(); + tool.anchor += tool.curve_length - before; + } + return true; +} + +double editor_mode::sweep_station(glm::dvec3 const &Point, double &Lateral) const +{ + auto const &tool{m_sweep}; + double best{std::numeric_limits::max()}; + double station{0.0}; + Lateral = 0.0; + auto const point{plan_of(Point)}; + for (std::size_t i = 1; i < tool.outline.size(); ++i) + { + auto const a{plan_of(tool.outline[i - 1])}; + auto const ab{plan_of(tool.outline[i]) - a}; + auto const length2{glm::dot(ab, ab)}; + if (length2 < 1e-12) + continue; + auto const t{std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0)}; + auto const distance{glm::length(point - (a + ab * t))}; + if (distance < best) + { + best = distance; + station = tool.stations[i - 1] + (tool.stations[i] - tool.stations[i - 1]) * t; + Lateral = geometry::cross(glm::normalize(ab), point - a) >= 0.0 ? distance : -distance; + } + } + return station; +} + +void editor_mode::sweep_measure() +{ + auto &tool{m_sweep}; + std::string const model{tool.model}; + if (model == tool.measured) + return; + tool.measured = model; + tool.model_length = 0.0; + if (model.empty()) + return; + scene::node_data data; + sweep_node probe{data}; + auto state{sweep_definition()}; + state.pieces.clear(); + state.along_x = false; + probe.define(state); + auto const alongz{probe.model_length()}; + state.along_x = true; + probe.define(state); + auto const alongx{probe.model_length()}; + tool.settings.along_x = tool.axis == 2 || (tool.axis == 0 && alongx > alongz); + tool.model_length = tool.settings.along_x ? alongx : alongz; +} + +sweep_node::state editor_mode::sweep_definition() const +{ + auto const &tool{m_sweep}; + auto state{tool.settings}; + state.model = tool.model; + state.skin = tool.skin[0] != '\0' ? tool.skin : "none"; + state.pieces = tool.curve; + state.height = tool.settings.height + kRailTop; + if (tool.length_mode == 0 && tool.model_length > 0.0) + state.to = state.from + tool.model_length; + else if (tool.length_mode == 1) + state.to = state.from + std::max(0.5, tool.length); + state.parameters.clear(); + for (auto const &value : tool.parameters) + state.parameters.emplace_back(value[0] != '\0' ? value.data() : "none"); + return state; +} + +void editor_mode::sweep_create() +{ + auto &tool{m_sweep}; + sweep_curve(); + if (tool.curve.empty() || tool.model[0] == '\0') + return; + static int counter{0}; + scene::node_data data; + data.type = "sweep"; + data.range_max = -1.0; + data.layer = scene::Layers.active(); + do + { + data.name = "sweep_" + std::to_string(++counter); + } while (simulation::Sweeps.find(data.name) != nullptr); + auto *sweep{new sweep_node(data)}; + sweep->define(sweep_definition()); + sweep->mark_dirty(); + simulation::Sweeps.insert(sweep); + scene::Layers.count(sweep->layer(), scene::layer_item::model); + sweep->show(); + // the pieces are the paths of the line, so the model follows them when they change + for (auto const &piece : tool.curve_pieces) + if (piece.track != nullptr && false == piece.track->m_editorremoved) + sweeps_captured(*piece.track); + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Other; + snap.node_name = sweep->name(); + snap.sweeps_toggled.push_back(sweep); + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + tool.edited = sweep; + tool.status = format(STR_C("%s laid along %.1f m, Ctrl+Z takes it back"), tool.model, sweep->end() - sweep->start()); + if (scene::Layers.active() == null_handle) + tool.status += "\n" + std::string{STR_C("The scenery isn't opened for editing, it won't be saved")}; + WriteLog("Editor: " + tool.status, logtype::generic); +} + +void editor_mode::sweep_apply() +{ + auto &tool{m_sweep}; + auto *sweep{tool.edited}; + if (sweep == nullptr || sweep->m_editorremoved) + return; + auto state{sweep_definition()}; + state.pieces = sweep->definition().pieces; + if (state == sweep->definition()) + return; + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Other; + snap.node_name = sweep->name(); + snap.sweeps.emplace_back(sweep, sweep->definition()); + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + sweep->define(state); + sweep->mark_dirty(); +} + +void editor_mode::sweep_delete(sweep_node &Sweep) +{ + Sweep.m_editorremoved = true; + Sweep.hide(); + Sweep.mark_dirty(); + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Other; + snap.node_name = Sweep.name(); + snap.sweeps_toggled.push_back(&Sweep); + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + if (m_sweep.edited == &Sweep) + m_sweep.edited = nullptr; +} + +void editor_mode::sweep_edit(sweep_node &Sweep) +{ + auto &tool{m_sweep}; + tool.edited = &Sweep; + auto const &state{Sweep.definition()}; + tool.settings = state; + tool.settings.height = state.height - kRailTop; + tool.settings.pieces.clear(); + std::snprintf(tool.model, sizeof(tool.model), "%s", state.model.c_str()); + std::snprintf(tool.skin, sizeof(tool.skin), "%s", state.skin.c_str()); + tool.axis = state.along_x ? 2 : 1; + tool.measured.clear(); + tool.parameters_for.clear(); + tool.parameters.clear(); + for (auto const &value : state.parameters) + { + tool.parameters.emplace_back(); + std::snprintf(tool.parameters.back().data(), tool.parameters.back().size(), "%s", value.c_str()); + } + tool.parameters_for = state.model; +} + +void editor_mode::restore_sweeps(EditorSnapshot &Snapshot) +{ + for (auto &entry : Snapshot.sweeps) + { + auto current{entry.first->definition()}; + entry.first->define(entry.second); + entry.first->mark_dirty(); + entry.second = std::move(current); + } + for (auto *sweep : Snapshot.sweeps_toggled) + { + sweep->m_editorremoved = false == sweep->m_editorremoved; + if (sweep->m_editorremoved) + sweep->hide(); + else + sweep->show(); + sweep->mark_dirty(); + if (m_sweep.edited == sweep && sweep->m_editorremoved) + m_sweep.edited = nullptr; + } +} + +std::vector editor_mode::sweeps_near(TTrack const &Track) const +{ + std::vector result; + std::vector points; + for (auto const &path : Track.m_paths) + { + bezier const curve{path}; + for (int k = 0; k <= 8; ++k) + points.push_back(curve.point(k / 8.0)); + } + for (auto *sweep : simulation::Sweeps.sequence()) + { + if (sweep == nullptr || sweep->m_editorremoved) + continue; + auto const &definition{sweep->definition()}; + bool near{false}; + for (auto const &piece : definition.pieces) + { + bezier const curve{piece}; + for (int k = 0; k <= 8 && false == near; ++k) + { + auto const point{curve.point(k / 8.0)}; + near = std::any_of(points.begin(), points.end(), [&](glm::dvec3 const &Other) { return plan_distance(point, Other) < kSweepNear; }); + } + if (near) + break; + } + if (near) + result.push_back(sweep); + } + return result; +} + +bool editor_mode::sweep_press() +{ + return sweep_press_at(Global.pCamera.Pos + GfxRenderer->Mouse_Position(), false); +} + +bool editor_mode::sweep_grabs(glm::dvec3 const &Point) +{ + auto &tool{m_sweep}; + if (false == tool.open || tool.model[0] == '\0') + return false; + sweep_curve(); + if (tool.outline.size() < 2) + return false; + sweep_measure(); + if (tool.model_length <= 0.0) + return false; + double lateral; + sweep_station(Point, lateral); + return std::abs(lateral) <= kSweepGrab; +} + +bool editor_mode::sweep_press_at(glm::dvec3 const &Point, bool const Anywhere) +{ + auto &tool{m_sweep}; + sweep_curve(); + if (false == tool.open || tool.outline.size() < 2 || tool.model[0] == '\0') + return false; + sweep_measure(); + if (tool.model_length <= 0.0) + return false; + auto *track{selected_track()}; + if (track != nullptr && track->eType == tt_Switch && track->m_paths.size() > 1 && tool.curve_pieces.size() == 1) + { + auto nearest{0}; + auto best{std::numeric_limits::max()}; + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + bezier const curve{track->m_paths[i]}; + for (int k = 0; k <= 32; ++k) + if (auto const distance{plan_distance(curve.point(k / 32.0), Point)}; distance < best) + { + best = distance; + nearest = i; + } + } + tool.curve = {track->m_paths[nearest]}; + tool.curve_pieces = {{track, nearest, true}}; + sweep_outline(); + } + double lateral; + auto const station{sweep_station(Point, lateral)}; + // a press away from the track is left to the selection of the tracks + if (false == Anywhere && std::abs(lateral) > kSweepGrab) + return false; + // the press tells the side, the distance from the track is the one set + auto const distance{std::abs(tool.settings.lateral) > 1e-6 ? std::abs(tool.settings.lateral) : std::round(std::abs(lateral) / 0.05) * 0.05}; + tool.settings.lateral = lateral >= 0.0 ? distance : -distance; + if (tool.settings.side_anchor == 0) + tool.settings.side_anchor = 1; + tool.anchor = std::round(station * 10.0) / 10.0; + tool.settings.from = tool.settings.to = tool.anchor; + tool.dragging = true; + return true; +} + +void editor_mode::sweep_drag() +{ + if (m_sweep.dragging) + sweep_drag_at(Global.pCamera.Pos + GfxRenderer->Mouse_Position()); +} + +void editor_mode::sweep_drag_at(glm::dvec3 const &Point) +{ + auto &tool{m_sweep}; + if (false == tool.dragging) + return; + double lateral; + for (int i = 0; i < 4; ++i) + { + auto const raw{sweep_station(Point, lateral)}; + if (raw >= tool.curve_length - 0.5 && sweep_extend(true, Point)) + continue; + if (raw <= 0.5 && sweep_extend(false, Point)) + continue; + break; + } + auto const station{std::round(sweep_station(Point, lateral) * 10.0) / 10.0}; + tool.settings.from = std::min(tool.anchor, station); + tool.settings.to = std::max(tool.anchor, station); +} + +void editor_mode::sweep_release() +{ + auto &tool{m_sweep}; + if (false == tool.dragging) + return; + tool.dragging = false; + // a click lays one copy as long as the original from the place clicked, a drag the stretch dragged over + if (tool.settings.to - tool.settings.from < 1.0) + { + tool.length_mode = 0; + tool.settings.from = tool.anchor; + } + else + { + tool.length_mode = 1; + tool.length = tool.settings.to - tool.settings.from; + } + if (tool.edited == nullptr) + { + sweep_create(); + return; + } + // the model being edited is laid again along the line, over the stretch marked + auto state{sweep_definition()}; + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Other; + snap.node_name = tool.edited->name(); + snap.sweeps.emplace_back(tool.edited, tool.edited->definition()); + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + tool.edited->define(state); + tool.edited->mark_dirty(); + if (auto *track{selected_track()}) + sweeps_captured(*track); + tool.status = format(STR_C("%s laid along %.1f m, Ctrl+Z takes it back"), tool.model, tool.edited->end() - tool.edited->start()); +} + +void editor_mode::sweeps_captured(TTrack const &Track) +{ + for (auto *sweep : simulation::Sweeps.sequence()) + { + if (sweep == nullptr || sweep->m_editorremoved) + continue; + auto const &pieces{sweep->definition().pieces}; + for (std::size_t i = 0; i < pieces.size(); ++i) + for (int path = 0; path < static_cast(Track.m_paths.size()); ++path) + { + bool const along{same_piece(pieces[i], Track.m_paths[path])}; + bool const against{false == along && same_piece(pieces[i], reversed(Track.m_paths[path]))}; + if (false == along && false == against) + continue; + if (std::none_of(m_sweep_links.begin(), m_sweep_links.end(), [&](sweep_link const &Link) { return Link.sweep == sweep && Link.piece == i; })) + m_sweep_links.push_back({sweep, i, const_cast(&Track), path, against}); + } + } +} + +void editor_mode::sweeps_committed(std::vector const &Tracks) +{ + std::vector changed; + std::vector> states; + for (auto const &link : m_sweep_links) + { + if (link.track == nullptr || link.track->m_editorremoved || link.sweep->m_editorremoved || std::find(Tracks.begin(), Tracks.end(), link.track) == Tracks.end() || + link.path >= static_cast(link.track->m_paths.size())) + continue; + auto entry{std::find_if(states.begin(), states.end(), [&](auto const &State) { return State.first == link.sweep; })}; + if (entry == states.end()) + { + states.emplace_back(link.sweep, link.sweep->definition()); + entry = std::prev(states.end()); + } + if (link.piece >= entry->second.pieces.size()) + continue; + auto const &path{link.track->m_paths[link.path]}; + entry->second.pieces[link.piece] = link.reversed ? reversed(path) : path; + } + for (auto &entry : states) + { + if (entry.second == entry.first->definition()) + continue; + entry.first->define(entry.second); + entry.first->mark_dirty(); + } +} + +void editor_mode::sweeps_split(TTrack &Original, TTrack &Created) +{ + if (Created.m_paths.empty()) + return; + std::vector added; + for (auto &link : m_sweep_links) + { + if (link.track != &Original || link.path != 0) + continue; + auto state{link.sweep->definition()}; + if (link.piece >= state.pieces.size()) + continue; + // the second part goes after the first one along the curve, or ahead of it on a curve laid against the path + auto const at{link.piece + (link.reversed ? 0 : 1)}; + state.pieces.insert(state.pieces.begin() + at, link.reversed ? reversed(Created.m_paths.front()) : Created.m_paths.front()); + for (auto &other : m_sweep_links) + if (other.sweep == link.sweep && other.piece >= at && &other != &link) + ++other.piece; + if (link.reversed) + ++link.piece; + added.push_back({link.sweep, at, &Created, 0, link.reversed}); + link.sweep->define(state); + } + m_sweep_links.insert(m_sweep_links.end(), added.begin(), added.end()); +} + +void editor_mode::render_sweep_ui() +{ + auto &tool{m_sweep}; + auto *track{selected_track()}; + sweep_curve(); + sweep_scan_models(); + ImGui::TextDisabled("%s", STR_C("A model bent to follow the track, or repeated along it: platforms, walls, fences, barriers, lamps")); + + // models along curves by this line + if (track != nullptr) + { + auto const near{sweeps_near(*track)}; + if (false == near.empty()) + { + ImGui::TextDisabled("%s", STR_C("By this track:")); + for (auto *sweep : near) + { + ImGui::PushID(sweep); + auto const &definition{sweep->definition()}; + if (ImGui::Selectable(format("%s %s, %.0f m", sweep->name().c_str(), definition.model.c_str(), sweep->end() - sweep->start()).c_str(), tool.edited == sweep, 0, ImVec2(ImGui::GetContentRegionAvail().x - 60.0f, 0.0f))) + sweep_edit(*sweep); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Delete"))) + sweep_delete(*sweep); + ImGui::PopID(); + } + if (tool.edited != nullptr && ImGui::SmallButton(STR_C("New one"))) + tool.edited = nullptr; + ImGui::Separator(); + } + } + bool changed{false}; + auto const edited = [&](bool const Changed) { changed |= Changed; }; + + ImGui::SetNextItemWidth(-1.0f); + if (ImGui::InputTextWithHint("##sweepmodel", STR_C("model, e.g. peronowe/1k96w200m"), tool.model, sizeof(tool.model))) + changed = true; + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputTextWithHint("##sweepsearch", STR_C("search the models"), tool.search, sizeof(tool.search)); + std::string const text{lower(tool.search)}; + if (false == text.empty()) + { + ImGui::BeginChild("##sweepmodels", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 7.0f + 6.0f), true); + std::size_t listed{0}; + for (auto const &model : tool.models) + { + if (lower(model).find(text) == std::string::npos) + continue; + if (++listed > kSweepModelList) + break; + if (ImGui::Selectable(model.c_str(), model == tool.model)) + { + std::snprintf(tool.model, sizeof(tool.model), "%s", model.c_str()); + changed = true; + } + } + ImGui::EndChild(); + } + // a template takes its parameters, the ones which place it are zeros: it's laid out in its own space + std::string const model{tool.model}; + bool const istemplate{model.size() > 4 && lower(model.substr(model.size() - 4)) == ".inc"}; + if (istemplate && model != tool.parameters_for) + { + tool.parameters_for = model; + include_info info; + std::string error; + editor_includes::load(model, info, error); + editor_includes::suggest(model, info); + auto const count{editor_includes::parameter_count(model)}; + tool.parameters.assign(static_cast(count), {}); + tool.parameter_labels.assign(static_cast(count), {}); + tool.parameter_placement.assign(static_cast(count), false); + tool.parameter_choices.assign(static_cast(count), {}); + // a parameter which makes a part of the name of a texture gets the variants of that texture to choose from + { + std::ifstream file(Global.asCurrentSceneryPath + model); + std::stringstream content; + content << file.rdbuf(); + std::string word; + while (content >> word) + { + auto const open{word.find("(p")}; + auto const close{open != std::string::npos ? word.find(')', open) : std::string::npos}; + if (close == std::string::npos || word.find('/') == std::string::npos || word.find('/') > open) + continue; + auto const id{std::atoi(word.substr(open + 2, close - open - 2).c_str())}; + if (id < 1 || id > count || false == tool.parameter_choices[id - 1].empty()) + continue; + auto const prefix{lower(word.substr(0, open))}; + auto suffix{word.substr(close + 1)}; + suffix = suffix.substr(0, suffix.find(':')); + auto const slash{prefix.rfind('/')}; + auto const folder{std::string{"textures/"} + prefix.substr(0, slash)}; + auto const start{prefix.substr(slash + 1)}; + std::set variants; + std::error_code error; + for (auto const &entry : std::filesystem::directory_iterator(folder, error)) + { + auto const stem{lower(entry.path().stem().string())}; + if (stem.size() > start.size() + suffix.size() && stem.compare(0, start.size(), start) == 0 && (suffix.empty() || stem.compare(stem.size() - suffix.size(), suffix.size(), lower(suffix)) == 0)) + variants.insert(stem.substr(start.size(), stem.size() - start.size() - suffix.size())); + } + tool.parameter_choices[id - 1].assign(variants.begin(), variants.end()); + } + } + for (int id = 1; id <= count; ++id) + { + auto const ¶meter{info.parameter(id)}; + auto const placement{parameter.role.rfind("pos.", 0) == 0 || parameter.role.rfind("rot.", 0) == 0}; + auto const value{placement ? std::string{"0"} : parameter.value.empty() ? std::string{"none"} : parameter.value}; + std::snprintf(tool.parameters[id - 1].data(), tool.parameters[id - 1].size(), "%s", value.c_str()); + tool.parameter_labels[id - 1] = parameter.label.empty() ? "p" + std::to_string(id) + (parameter.role != "free" ? " (" + parameter.role + ")" : std::string{}) : parameter.label; + tool.parameter_placement[id - 1] = placement; + } + tool.measured.clear(); + changed = true; + } + else if (false == istemplate && false == tool.parameters.empty()) + { + tool.parameters.clear(); + tool.parameters_for.clear(); + } + if (false == tool.parameters.empty() && ImGui::TreeNodeEx(STR_C("Parameters of the template"), ImGuiTreeNodeFlags_DefaultOpen)) + { + for (std::size_t i = 0; i < tool.parameters.size(); ++i) + { + ImGui::PushID(static_cast(i)); + ImGui::SetNextItemWidth(160.0f); + auto const label{i < tool.parameter_labels.size() ? tool.parameter_labels[i] : "p" + std::to_string(i + 1)}; + auto const *choices{i < tool.parameter_choices.size() && false == tool.parameter_choices[i].empty() ? &tool.parameter_choices[i] : nullptr}; + if (choices != nullptr && ImGui::BeginCombo(label.c_str(), tool.parameters[i].data())) + { + for (auto const &choice : *choices) + if (ImGui::Selectable(choice.c_str(), choice == tool.parameters[i].data())) + { + std::snprintf(tool.parameters[i].data(), tool.parameters[i].size(), "%s", choice.c_str()); + tool.measured.clear(); + changed = true; + } + ImGui::EndCombo(); + } + else if (choices == nullptr) + { + ImGui::InputText(label.c_str(), tool.parameters[i].data(), tool.parameters[i].size()); + if (ImGui::IsItemDeactivatedAfterEdit()) + { + tool.measured.clear(); + changed = true; + } + } + ImGui::PopID(); + } + ImGui::TextDisabled("%s", STR_C("The location and the rotation are zeros, the template is laid out along the track")); + ImGui::TreePop(); + } + sweep_measure(); + if (tool.model_length > 0.0) + ImGui::TextDisabled(STR_C("The model is %.2f m long along the track"), tool.model_length); + else if (tool.model[0] != '\0') + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR_C("The model has no triangles to lay, or it can't be loaded")); + + auto &settings{tool.settings}; + int mode{settings.bend ? 0 : 1}; + edited(ImGui::RadioButton(STR_C("Bent along the track"), &mode, 0)); + ImGui::SameLine(); + edited(ImGui::RadioButton(STR_C("Repeated"), &mode, 1)); + settings.bend = mode == 0; + if (false == settings.bend) + { + ImGui::SameLine(); + ImGui::SetNextItemWidth(80.0f); + edited(ImGui::InputDouble(STR_C("every (m, 0: its length)"), &settings.step, 0.0, 0.0, "%.2f")); + settings.step = std::max(0.0, settings.step); + } + ImGui::TextDisabled("%s", STR_C("Platform edge:")); + for (auto const height : {0.55, 0.76, 0.96}) + { + ImGui::SameLine(); + if (ImGui::SmallButton(format("%.2f m##platform", height).c_str())) + { + settings.lateral = std::copysign(1.725, settings.lateral == 0.0 ? 1.0 : settings.lateral); + settings.side_anchor = 1; + settings.height = height; + settings.height_anchor = 3; + settings.face = true; + settings.widen = true; + changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The edge of the platform 1.725 m from the axis of the track, its top this high over the rail top")); + } + ImGui::TextDisabled(STR_C("%.2f m %s of the axis, %.2f m over the rail top"), std::abs(settings.lateral), settings.lateral >= 0.0 ? STR_C("right") : STR_C("left"), settings.height); + ImGui::TextWrapped("%s", STR_C("Press beside the track where the model begins and drag along to where it ends; the side of the press is the side of the model. " + "Past the end of the path it goes on along the joined ones. A click lays one copy.")); + + if (tool.edited != nullptr && changed) + sweep_apply(); + if (tool.edited == nullptr && false == tool.curve.empty() && tool.model_length > 0.0 && ImGui::Button(STR_C("Lay it along the selected path"))) + { + tool.length_mode = 2; + settings.from = 0.0; + settings.to = -1.0; + sweep_create(); + } + if (tool.edited != nullptr && false == tool.curve.empty() && ImGui::Button(STR_C("Lay it again along the selected line"))) + { + auto state{sweep_definition()}; + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Other; + snap.node_name = tool.edited->name(); + snap.sweeps.emplace_back(tool.edited, tool.edited->definition()); + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + tool.edited->define(state); + tool.edited->mark_dirty(); + if (track != nullptr) + sweeps_captured(*track); + } + + changed = false; + if (ImGui::CollapsingHeader(STR_C("More settings"))) + { + ImGui::SetNextItemWidth(200.0f); + edited(ImGui::InputText(STR_C("Skin"), tool.skin, sizeof(tool.skin), ImGuiInputTextFlags_EnterReturnsTrue)); + edited(ImGui::IsItemDeactivatedAfterEdit()); + ImGui::PushItemWidth(90.0f); + edited(ImGui::InputDouble(STR_C("Sideways (m, + right)"), &settings.lateral, 0.0, 0.0, "%.3f")); + ImGui::SameLine(); + ImGui::SetNextItemWidth(150.0f); + char const *sides[] = {STR_C("model origin"), STR_C("side facing the track"), STR_C("far side"), STR_C("middle")}; + edited(ImGui::Combo("##sweepsideanchor", &settings.side_anchor, sides, IM_ARRAYSIZE(sides))); + edited(ImGui::InputDouble(STR_C("Up (m, from the rail top)"), &settings.height, 0.0, 0.0, "%.3f")); + ImGui::SameLine(); + ImGui::SetNextItemWidth(150.0f); + char const *heights[] = {STR_C("model origin"), STR_C("bottom"), STR_C("top"), STR_C("top of the edge by the track")}; + edited(ImGui::Combo("##sweepheightanchor", &settings.height_anchor, heights, IM_ARRAYSIZE(heights))); + if (settings.bend) + { + edited(ImGui::InputDouble(STR_C("Copy length (m, 0: the model's)"), &settings.step, 0.0, 0.0, "%.2f")); + settings.step = std::max(0.0, settings.step); + } + ImGui::PopItemWidth(); + char const *axes[] = {"auto", "z", "x"}; + ImGui::SetNextItemWidth(70.0f); + if (ImGui::Combo(STR_C("Axis of the model along the track"), &tool.axis, axes, IM_ARRAYSIZE(axes))) + { + tool.measured.clear(); + changed = true; + } + edited(ImGui::Checkbox(STR_C("Edge to the track"), &settings.face)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The model is turned so its -x side faces the track, the way the platforms and the walls of the scenery are made")); + ImGui::SameLine(); + edited(ImGui::Checkbox(STR_C("Turned around"), &settings.flip)); + ImGui::SameLine(); + edited(ImGui::Checkbox(STR_C("Mirrored"), &settings.mirror)); + ImGui::SameLine(); + edited(ImGui::Checkbox(STR_C("Leans with the cant"), &settings.tilt)); + edited(ImGui::Checkbox(STR_C("Set back by the widening of the structure gauge in the curves"), &settings.widen)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The offset grows by the widening of the gauge below the platforms: 3.75/R in the curves of R >= 250 m,\n" + "more in the sharper ones, and on the inner side of a canted curve by the cant times the height of the edge / 1.5;\n" + "it begins 20 m (inner side) and 26 m (outer side) ahead of the curve, as in the gauge check")); + ImGui::TextDisabled("%s", STR_C("Length")); + ImGui::SameLine(); + edited(ImGui::RadioButton(STR_C("as the original"), &tool.length_mode, 0)); + ImGui::SameLine(); + edited(ImGui::RadioButton(STR_C("given"), &tool.length_mode, 1)); + ImGui::SameLine(); + edited(ImGui::RadioButton(STR_C("the whole stretch"), &tool.length_mode, 2)); + ImGui::PushItemWidth(90.0f); + edited(ImGui::InputDouble(STR_C("From (m)"), &settings.from, 0.0, 0.0, "%.1f")); + ImGui::SameLine(); + if (tool.length_mode == 1) + edited(ImGui::InputDouble(STR_C("Length (m)"), &tool.length, 0.0, 0.0, "%.1f")); + else if (tool.length_mode == 2) + edited(ImGui::InputDouble(STR_C("To (m, -1: the end)"), &settings.to, 0.0, 0.0, "%.1f")); + ImGui::PopItemWidth(); + settings.from = std::max(0.0, settings.from); + if (tool.model_length > 0.0) + { + auto const state{sweep_definition()}; + auto const end{state.to < 0.0 ? tool.curve_length : std::min(state.to, tool.curve_length)}; + auto const stretch{std::max(0.0, end - state.from)}; + auto const own{settings.step > 0.01 ? settings.step : tool.model_length}; + auto const copies{settings.bend ? std::max(1, static_cast(std::round(stretch / own))) : static_cast(std::floor(stretch / own + 1e-6)) + 1}; + if (settings.bend) + ImGui::TextDisabled(STR_C("%.1f m: %d copies of %.2f m (the original %.2f m)"), stretch, copies, stretch / copies, tool.model_length); + else + ImGui::TextDisabled(STR_C("%.1f m: %d copies every %.2f m"), stretch, copies, own); + } + if (tool.edited != nullptr && changed) + sweep_apply(); + } + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} + +void editor_mode::draw_sweep_overlay() const +{ + auto const &tool{m_sweep}; + if (false == tool.open || tool.outline.size() < 2) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList()}; + auto const &settings{tool.settings}; + auto const from{std::max(0.0, settings.from)}; + auto const to{settings.to < 0.0 ? tool.curve_length : std::min(settings.to, tool.curve_length)}; + glm::dvec3 previous{0.0}; + bool started{false}; + for (std::size_t i = 1; i < tool.outline.size(); ++i) + { + if (tool.stations[i] < from || tool.stations[i - 1] > to) + continue; + auto const direction{plan_of(tool.outline[i] - tool.outline[i - 1])}; + if (glm::length(direction) < 1e-9) + continue; + auto const unit{glm::normalize(direction)}; + glm::dvec3 const left{-unit.y, 0.0, unit.x}; + auto const point{tool.outline[i] + left * settings.lateral + glm::dvec3{0.0, settings.height + kRailTop, 0.0}}; + if (started) + projection.line(drawlist, previous, point, IM_COL32(255, 160, 40, 230), 3.0f); + else + { + ImVec2 screen; + if (projection.project(point, screen)) + drawlist->AddCircleFilled(screen, 5.0f, IM_COL32(255, 160, 40, 230)); + } + previous = point; + started = true; + } + ImVec2 screen; + if (started && projection.project(previous, screen)) + drawlist->AddCircle(screen, 6.0f, IM_COL32(255, 160, 40, 230), 12, 2.0f); +} diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index eec9f2ae..2e78c5ec 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -14,6 +14,7 @@ http: #include "application/editorprojection.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" #include "utilities/Globals.h" #include "rendering/renderer.h" @@ -29,6 +30,7 @@ http: #include #include #include +#include #include #include #include @@ -43,6 +45,7 @@ using geometry::plan_distance; using geometry::plan_of; using geometry::signed_angle; using geometry::turned; +using geometry::arc_pieces; std::string describe(editor_track::straight const &Line) { @@ -64,6 +67,8 @@ editor_track::point_kind const kPointKinds[] = {editor_track::point_kind::start, float const kHandleRadius{10.0f}; double const kLaySnapRadius{5.0}; +float const kSnapPixels{16.0f}; +float const kDragPixels{5.0f}; double const kSwitchReach{6.0}; // a press farther from the selected track selects another one instead of placing the switch double const kLineReach{2000.0}; // how far each way the Curve tab follows the line double const kJointRange{400.0}; // the ends of the paths are drawn for the tracks this close to the camera @@ -77,37 +82,18 @@ void item_tooltip(char const *Text) } // circular curve from a point along a direction, cut into pieces of 90 degrees at most. Side: 1 to the left, -1 to the right -std::vector arc_pieces(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, double const Radius, double const Angle, int const Side) +void compound_from(editor_track::curve const &Curve, alignment::vertex &Vertex) { - std::vector result; - double const side{static_cast(Side)}; - glm::dvec2 const centre{plan_of(Point) + glm::dvec2{-Direction.y, Direction.x} * (side * Radius)}; - auto const count{std::max(1, static_cast(std::ceil(Angle / glm::radians(90.0) - 1e-9)))}; - auto const step{Angle / count}; - auto const handle{4.0 / 3.0 * std::tan(step / 4.0) * Radius}; - glm::dvec2 const radial{plan_of(Point) - centre}; - for (int i = 0; i < count; ++i) - { - auto const a0{side * step * i}; - auto const a1{side * step * (i + 1)}; - auto const p0{centre + turned(radial, a0)}; - auto const p3{centre + turned(radial, a1)}; - auto const d0{turned(Direction, a0)}; - auto const d3{turned(Direction, a1)}; - auto const y0{Point.y + Grade * Radius * step * i}; - auto const y3{Point.y + Grade * Radius * step * (i + 1)}; - segment_data path; - path.points[segment_data::point::start] = {p0.x, y0, p0.y}; - path.points[segment_data::point::end] = {p3.x, y3, p3.y}; - path.points[segment_data::point::control1] = {d0.x * handle, Grade * handle, d0.y * handle}; - path.points[segment_data::point::control2] = {-d3.x * handle, -Grade * handle, -d3.y * handle}; - path.radius = static_cast(Radius); - result.push_back(path); - } - return result; + if (false == Curve.compound || Curve.arcs.size() < 2) + return; + Vertex.compound = true; + Vertex.radius = std::round(Curve.arcs.front().radius); + Vertex.share = Curve.arcs.front().turn; + Vertex.arcs.clear(); + for (std::size_t i = 1; i < Curve.arcs.size(); ++i) + Vertex.arcs.push_back({std::round(Curve.arcs[i].radius), std::round(Curve.arcs[i].transition), Curve.arcs[i].turn}); } - } TTrack *editor_mode::selected_track() const @@ -158,11 +144,11 @@ void editor_mode::select_track(scene::basic_node *Node) } } -bool editor_mode::pick_track_handle() +editor_track::point_ref editor_mode::track_handle_hit() const { auto *track = selected_track(); - if (track == nullptr || false == editor_track::is_supported(*track) || m_route_tab) - return false; + if (track == nullptr || false == editor_track::is_supported(*track) || m_track_tab != track_tab::path) + return {}; screen_projection const projection; float best = kHandleRadius * kHandleRadius; @@ -180,10 +166,7 @@ bool editor_mode::pick_track_handle() } } } - if (false == hit.valid()) - return false; - m_track_point = hit; - return true; + return hit; } void editor_mode::draw_track_overlay() const @@ -261,8 +244,6 @@ void editor_mode::render_track_gizmo() gizmo_frame const frame; - ImGuizmo::OPERATION const operation = (false == pointmode && m_gizmo_op == gizmo_operation::rotate) ? ImGuizmo::ROTATE_Y : ImGuizmo::TRANSLATE; - if (false == m_track_gizmo_using) { auto const anchor = pointmode ? editor_track::point_position(*track, m_track_point) : editor_track::pivot(*track); @@ -270,7 +251,7 @@ void editor_mode::render_track_gizmo() } glm::mat4 delta(1.0f); - frame.manipulate(operation, m_track_gizmo, operation == ImGuizmo::ROTATE_Y ? 5.0f : m_gizmo_snap, &delta); + frame.manipulate(ImGuizmo::TRANSLATE, m_track_gizmo, m_gizmo_snap, &delta); if (ImGuizmo::IsUsing()) { @@ -307,17 +288,7 @@ void editor_mode::render_track_gizmo() push_track_snapshot(std::move(states)); } - if (operation == ImGuizmo::ROTATE_Y) - { - double const angle = std::atan2(delta[2][0], delta[0][0]); - if (std::abs(angle) > 1e-9) - { - editor_track::rotate_y(*track, m_track_pivot, angle); - editor_track::follow(*track, m_track_joints); - m_track_dirty = true; - } - } - else if (pointmode) + if (pointmode) { glm::dvec3 const position = frame.camera + glm::dvec3(m_track_gizmo[3]); editor_track::move_point(*track, m_track_point, position); @@ -327,7 +298,9 @@ void editor_mode::render_track_gizmo() 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 = editor_track::find_snap_target(*track, position, m_track_snap_radius, exclude); + screen_projection const projection; + ImVec2 screen; + m_track_snap = projection.project(position, screen) ? snap_free_end(position, {screen.x, screen.y}, track, track->iCategoryFlag & 15, exclude) : editor_track::snap_target{}; } } else @@ -495,16 +468,19 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve profile_restore(); } -std::array const &editor_mode::track_modes() +std::array const &editor_mode::track_modes() { - static std::array const modes{{ + static std::array const modes{{ {"Select", "Esc", track_tab::path, "LMB on a path selects it: its points, control vectors and parameters"}, {"Lay track", "L", track_tab::lay, "Lays new track through the clicked points, on the ground or from a free end of a track"}, {"Switch", "T", track_tab::turnout, "Puts switches into the selected straight or curve, edits the geometry of the selected switch"}, + {"Turntable", "U", track_tab::turntable, "Places a turntable, leads tracks out of it and fits the tracks around to it"}, {"Straight", "G", track_tab::straights, "The whole straight through the selected path: drag its ends or the middle, break it, shift it"}, {"Curve", "C", track_tab::route, "The line through the path, switch to switch: vertices with the radii, transitions and cant of the curves"}, + {"Objects", "B", track_tab::lineside, "Along the track: platforms and other models, hectometre posts, fouling point markers, a parallel track, a vehicle to drive"}, {"Profile", "P", track_tab::profile, "Vertical profile (grade line) along the line"}, {"Speed", "V", track_tab::speed, "Speed limits of the paths against the speed their geometry allows"}, + {"Joints", "J", track_tab::joints, "Ends of the paths which almost meet, steps, kinks, jumps of the cant and of the grade at the joints"}, {"Infra", "I", track_tab::infra, "Infrastructure along the track, bound to follow its changes"}, }}; return modes; @@ -512,28 +488,15 @@ std::array const &editor_mode::track_modes() void editor_mode::render_track_menu() { - if (false == ImGui::BeginMenu(STR_C("Tracks"))) - return; - if (ImGui::MenuItem(STR_C("Track editor"), nullptr, m_track_window_open)) + if (ImGui::MenuItem(STR_C("Tracks"), nullptr, m_track_window_open)) { if (m_track_window_open) m_track_window_open = false; else show_track_tab(m_track_tab); } - ImGui::Separator(); - for (auto const &mode : track_modes()) - { - if (ImGui::MenuItem(STR_C(mode.label), mode.key, m_track_window_open && m_track_tab == mode.tab)) - show_track_tab(mode.tab); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C(mode.tooltip)); - } - ImGui::Separator(); - ImGui::MenuItem(STR_C("Structure gauge"), nullptr, &m_gauge.open); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Checks which models enter the structure gauge of the tracks and the clearance over the roads (skrajnia budowli)")); - ImGui::EndMenu(); + ImGui::SetTooltip("%s", STR_C("Track editor: the window with its modes opens or closes")); } void editor_mode::show_track_tab(track_tab const Tab) @@ -576,6 +539,10 @@ void editor_mode::show_track_tab(track_tab const Tab) if (track != nullptr && m_profile.route.spans.empty()) profile_open_run(*track); break; + case track_tab::turntable: + scan_turntables(); + turntable_read(track); + break; default: break; } @@ -593,6 +560,11 @@ void editor_mode::arm_switch() } tool.status.clear(); tool.armed = tool.templates.empty() ? -1 : std::clamp(tool.last, 0, static_cast(tool.templates.size()) - 1); + if (tool.armed >= 0 && tool.templates[tool.armed].double_slip) + { + auto const ordinary{std::find_if(tool.templates.begin(), tool.templates.end(), [](editor_track::switch_template const &Template) { return false == Template.double_slip; })}; + tool.armed = ordinary != tool.templates.end() ? static_cast(ordinary - tool.templates.begin()) : -1; + } } bool editor_mode::track_shortcut(int const Key) @@ -611,7 +583,7 @@ bool editor_mode::track_shortcut(int const Key) bool editor_mode::track_busy() const { auto const &straights{m_straights}; - return (m_lay.active && false == m_lay.points.empty()) || straights.dragging || m_extend.active || m_switch.placing || m_track_point.valid() || m_route.vertex >= 0 || m_route.grip >= 0 || false == straights.detour.empty() || straights.tool != 0 || straights.handle >= 0; + return (m_lay.active && false == m_lay.points.empty()) || m_turntable.placing || straights.dragging || m_extend.active || m_point_drag.active || m_handle_drag.active || m_switch.placing || m_track_point.valid() || m_route.vertex >= 0 || m_route.grip >= 0 || false == straights.detour.empty() || straights.tool != 0 || straights.handle >= 0; } std::string editor_mode::track_mode_name() const @@ -620,10 +592,12 @@ std::string editor_mode::track_mode_name() const return STR_C("Select: LMB on a path"); switch (m_track_tab) { - case track_tab::lay: return STR_C("Laying track: LMB adds the points"); + case track_tab::lay: return m_extend.active ? STR_C("Laying track: new path from the free end") : STR_C("Laying track: LMB adds the points"); case track_tab::turnout: if (m_switch.placing) return STR_C("Switch: drag to set its direction and side"); + if (m_switch.armed < 0) + return selected_track() != nullptr && selected_track()->eType == tt_Switch ? STR_C("Switch: the selected one. Another: LMB on a track, then its type") : STR_C("Switch: choose its type from the list"); return STR_C("Switch: press and drag on the selected track"); case track_tab::straights: if (false == m_straights.detour.empty()) @@ -636,8 +610,17 @@ std::string editor_mode::track_mode_name() const case track_tab::route: return STR_C("Curve: drag the vertices and the radius grips"); case track_tab::profile: return STR_C("Vertical profile: edit the grade line in the profile window"); case track_tab::speed: return STR_C("Speed check: LMB on a row shows the path"); + case track_tab::joints: return STR_C("Joints: LMB on a row shows the place"); + case track_tab::lineside: + if (m_lineside_tab == 0) + return m_sweep.dragging ? STR_C("Model along the track: release where it ends") : STR_C("Model along the track: LMB by the track where it starts, drag to the end"); + return STR_C("Objects along the track"); case track_tab::infra: return STR_C("Infrastructure along the track"); - default: return m_extend.active ? STR_C("New path from the free end") : selected_track() != nullptr ? STR_C("Select: drag the points of the path") : STR_C("Select: LMB on a path"); + case track_tab::turntable: + if (m_turntable.placing) + return STR_C("Turntable: drag to turn the bridge, release to place it"); + return m_turntable.table != nullptr ? STR_C("Turntable: LMB around it leads a track out, on a free end fits that track") : STR_C("Turntable: LMB places a new one"); + default: return m_point_drag.moved ? STR_C("Select: moving the point") : selected_track() != nullptr ? STR_C("Select: drag the points of the path") : STR_C("Select: LMB on a path"); } } @@ -647,6 +630,9 @@ void editor_mode::render_track_inspector() m_profile.open = m_track_window_open && m_track_tab == track_tab::profile; m_speed.open = m_track_window_open && m_track_tab == track_tab::speed; m_infra.open = m_track_window_open && m_track_tab == track_tab::infra; + m_joints.open = m_track_window_open && m_track_tab == track_tab::joints; + if (false == m_track_window_open || m_track_tab != track_tab::lineside) + m_sweep.open = m_hekto.open = m_fouling.open = m_parallel.open = m_vehicle.open = false; ui()->set_track(m_track_window_open); if (false == m_track_window_open) { @@ -691,8 +677,13 @@ void editor_mode::render_track_inspector() break; case track_tab::profile: render_profile_body(); break; case track_tab::speed: render_speed_body(); break; + case track_tab::joints: render_joints_body(); break; + case track_tab::lineside: render_lineside_ui(); break; case track_tab::infra: render_infra_body(); break; + case track_tab::turntable: render_turntable_ui(); break; default: + render_track_set_ui(); + render_track_spread_ui(); if (track != nullptr) render_path_ui(); else @@ -718,16 +709,28 @@ void editor_mode::render_track_modes(TTrack *Track) for (int i = From; i < To; ++i) { auto const &mode{modes[i]}; - ImGui::SameLine(i == From ? 72.0f : 0.0f); auto const label{std::string{STR_C(mode.label)} + "###trackmode" + std::to_string(i)}; + // the buttons which don't fit go on to the next line, under the first one + auto const width{ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x + ImGui::CalcTextSize(STR_C(mode.label)).x}; + if (i == From) + ImGui::SameLine(72.0f); + else if (ImGui::GetItemRectMax().x + ImGui::GetStyle().ItemSpacing.x + width > ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x) + ImGui::SetCursorPosX(72.0f); + else + ImGui::SameLine(); if (ImGui::RadioButton(label.c_str(), m_track_tab == mode.tab)) show_track_tab(mode.tab); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s [%s]", STR_C(mode.tooltip), mode.key); } }; - row("Edit", 0, 5); - row("Analysis", 5, static_cast(modes.size())); + row("Edit", 0, 6); + row("Analysis", 6, static_cast(modes.size())); + ImGui::SameLine(); + ImGui::Checkbox(STR_C("Structure gauge"), &m_gauge.open); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Checks which models enter the structure gauge of the tracks and the clearance over the roads (skrajnia budowli)")); + render_track_search(); ImGui::Spacing(); render_track_guide(); if (Track != nullptr && m_track_tab != track_tab::lay && false == track_analysis_tab()) @@ -781,6 +784,105 @@ void editor_mode::render_track_guide() ImGui::Dummy(ImVec2(0.0f, 0.0f)); } +void editor_mode::render_track_name(TTrack &Track) +{ + auto &edit{m_track_name}; + auto const current{Track.name() == "none" ? std::string{} : Track.name()}; + auto const sync = [&]() { + edit.track = &Track; + edit.synced = current; + std::snprintf(edit.text, sizeof(edit.text), "%s", current.c_str()); + edit.uses = editor_track::uses_of_name(Track); + }; + if (edit.track != &Track) + { + sync(); + edit.status.clear(); + } + else if (edit.synced != current && edit.synced == edit.text) + sync(); + + std::string why; + if (false == scene::Layers.editable(&Track, &why)) + { + ImGui::Text(STR_C("Name: %s"), current.empty() ? STR_C("(no name)") : current.c_str()); + ImGui::SameLine(); + ImGui::TextDisabled(STR_C("(can't be changed: %s)"), why.c_str()); + return; + } + auto const apply = [&]() { + std::string name{edit.text}; + std::string reason; + if (false == editor_track::name_valid(Track, name, reason)) + return; + auto const before{editor_track::capture(Track)}; + editor_track::rename(Track, name); + push_track_snapshot({{&Track, before}}); + edit.status = format(STR_C("Renamed from %s"), current.empty() ? STR_C("(no name)") : current.c_str()); + sync(); + }; + ImGui::SetNextItemWidth(220.f); + if (ImGui::InputTextWithHint(STR_C("Name##trackname"), STR_C("(no name)"), edit.text, sizeof(edit.text), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CharsNoBlank)) + apply(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Enter renames the path, Esc while typing takes the text back.\nThe events, memory cells and the map of the game find the path by its name")); + std::string typed{edit.text}; + if (typed == edit.synced) + { + if (false == edit.status.empty()) + { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.f), "%s", edit.status.c_str()); + } + return; + } + std::string reason; + TTrack *owner{nullptr}; + auto const valid{editor_track::name_valid(Track, typed, reason, &owner)}; + ImGui::SameLine(); + if (valid) + { + auto const warned{false == edit.uses.events.empty() || false == edit.uses.loose.empty()}; + if (ImGui::SmallButton(warned ? STR_C("Rename anyway") : STR_C("Rename"))) + apply(); + } + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Keep the old"))) + { + std::snprintf(edit.text, sizeof(edit.text), "%s", edit.synced.c_str()); + return; + } + if (false == valid) + { + ImGui::TextColored(ImVec4(1.f, 0.45f, 0.35f, 1.f), "%s", STR_C(reason.c_str())); + if (owner != nullptr) + { + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Look at it"))) + focus_track(*owner, 0, owner->location()); + } + return; + } + if (typed != edit.text) + ImGui::TextDisabled(STR_C("Saved as %s: the scenery keeps the names in lower case"), typed.c_str()); + if (edit.uses.cells > 0 || edit.uses.includes > 0) + ImGui::TextDisabled(STR_C("The new name goes to %d memory cell(s) and %d include(s), the drives of a switch among them"), edit.uses.cells, edit.uses.includes); + auto const listed = [](std::vector const &Names) { + std::string result; + for (std::size_t i = 0; i < Names.size() && i < 4; ++i) + result += (i > 0 ? ", " : "") + Names[i]; + if (Names.size() > 4) + result += format(" (+%d)", static_cast(Names.size() - 4)); + return result; + }; + if (false == edit.uses.events.empty()) + ImGui::TextColored(ImVec4(1.f, 0.75f, 0.3f, 1.f), STR_C("These events aim at the path by the old name and the editor doesn't write them: %s"), listed(edit.uses.events).c_str()); + if (false == edit.uses.loose.empty()) + ImGui::TextColored(ImVec4(1.f, 0.75f, 0.3f, 1.f), STR_C("These events belong to the path by the old name: %s"), listed(edit.uses.loose).c_str()); + if (false == edit.uses.events.empty() || false == edit.uses.loose.empty()) + ImGui::TextDisabled("%s", STR_C("They work until the scenery is loaded again; then they need the new name in their files")); +} + void editor_mode::render_turnout_ui() { auto *track{selected_track()}; @@ -805,6 +907,18 @@ void editor_mode::render_turnout_ui() glm::dvec2 const tangent{glm::normalize(glm::dvec2{-control.x, -control.z})}; auto const angle{std::atan2(std::abs(tangent.x * axis.y - tangent.y * axis.x), glm::dot(tangent, axis))}; ImGui::Text(STR_C("Length %.3f m, diverging %s, end offset %.3f m"), length, offset > 0.0 ? STR_C("left") : STR_C("right"), std::abs(offset)); + render_track_name(*track); + if ((simulation::Events.FindEvent(track->name() + "+") != nullptr && simulation::Events.FindEvent(track->name() + "-") != nullptr) || (m_track_name.track == track && m_track_name.uses.includes > 0)) + ImGui::TextDisabled(STR_C("Drive: events %s+ and %s- switch it, also in the map"), track->name().c_str(), track->name().c_str()); + else + { + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", STR_C("No drive: the switch can't be switched during the game")); + render_switch_drive_choice(); + if (m_switch.drive >= 0 && ImGui::Button(STR_C("Put the drive by the switch"))) + place_switch_drive(*track); + } + if (false == m_switch.status.empty()) + ImGui::TextWrapped("%s", m_switch.status.c_str()); if (angle > 1e-6) ImGui::Text(STR_C("Angle 1:%.2f (%.4f deg), R in the entry %.0f m"), 1.0 / std::tan(angle), glm::degrees(angle), diverging.radius); @@ -1249,6 +1363,7 @@ void editor_mode::render_path_ui() ImGui::TextDisabled(STR_C("Editing of this path type isn't supported")); return; } + render_track_name(*track); std::string reason; bool const geometry = editor_track::can_edit_geometry(*track, reason); if (false == geometry) @@ -1271,8 +1386,6 @@ void editor_mode::render_path_ui() } }; - if (geometry) - render_extend_ui(); if (ImGui::CollapsingHeader(STR_C("Geometry"), ImGuiTreeNodeFlags_DefaultOpen)) { for (int i = 0; i < static_cast(track->m_paths.size()); ++i) @@ -1486,13 +1599,7 @@ alignment::vertex editor_mode::route_vertex_of(editor_track::curve const &Curve) if (Curve.cant > 0.0) vertex.cant = std::round(Curve.cant); vertex.reverse_turn = std::abs(Curve.turn) > glm::pi(); - if (Curve.compound) - { - vertex.compound = true; - vertex.radius2 = std::round(Curve.radius2); - vertex.transition_middle = std::round(Curve.transition_middle); - vertex.split = Curve.split; - } + compound_from(Curve, vertex); return vertex; } @@ -1796,7 +1903,7 @@ glm::dvec3 editor_mode::route_vertex_position(int const Vertex) const return {position.x, height, position.y}; } -bool editor_mode::pick_route_vertex() +bool editor_mode::route_hit(int &Vertex, int &Grip) const { if (false == route_active()) return false; @@ -1821,11 +1928,9 @@ bool editor_mode::pick_route_vertex() hit = -1; } } - if (hit < 0 && griphit < 0) - return false; - m_route.vertex = hit; - m_route.grip = griphit; - return true; + Vertex = hit; + Grip = griphit; + return hit >= 0 || griphit >= 0; } void editor_mode::draw_route_overlay() const @@ -1888,7 +1993,13 @@ void editor_mode::draw_route_overlay() const 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 const radius{report != nullptr && report->radius2 > 0.0 ? format("R %.0f / %.0f", report->radius, report->radius2) : format("R %.0f", report != nullptr ? report->radius : design.vertices[i].radius)}; + 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()); @@ -1909,11 +2020,24 @@ void editor_mode::render_route_gizmo() gizmo_frame const frame; auto const position{grip ? alignment::evaluate(route.result, route.result.vertex_chainages[route.grip]).position : route_vertex_position(route.vertex)}; - if (false == m_route_gizmo_using) - m_route_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(position - frame.camera)); - frame.manipulate(ImGuizmo::TRANSLATE, m_route_gizmo, m_gizmo_snap); + glm::dvec3 moved{position}; + bool held{false}; + if (m_handle_drag.active) + { + held = handle_drag_step(moved); + if (false == held && false == m_route_gizmo_using) + return; + } + else + { + if (false == m_route_gizmo_using) + m_route_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(position - frame.camera)); + frame.manipulate(ImGuizmo::TRANSLATE, m_route_gizmo, m_gizmo_snap); + held = ImGuizmo::IsUsing(); + moved = frame.camera + glm::dvec3(m_route_gizmo[3]); + } - if (false == ImGuizmo::IsUsing()) + if (false == held) { if (m_route_gizmo_using) { @@ -1923,7 +2047,6 @@ void editor_mode::render_route_gizmo() return; } m_route_gizmo_using = true; - glm::dvec3 const moved{frame.camera + glm::dvec3(m_route_gizmo[3])}; if (grip) { auto &vertex{route.design.vertices[route.grip]}; @@ -2043,20 +2166,58 @@ bool editor_mode::render_route_vertex(int const Index) ImGui::TextDisabled(STR_C("Curve %d of the S-curve"), Index + 1); ImGui::PushItemWidth(110.0f); changed |= ImGui::DragScalar(vertex.compound ? STR_C("Radius R1 (m)") : STR_C("Radius R (m)"), ImGuiDataType_Double, &vertex.radius, 5.0f, nullptr, nullptr, "%.0f"); - changed |= ImGui::Checkbox(STR_C("Compound curve (two radii)"), &vertex.compound); - item_tooltip("Curve made of two arcs of different radii, joined with a transition"); + 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) { - changed |= ImGui::InputDouble(STR_C("Radius R2 (m)"), &vertex.radius2, 0.0, 0.0, "%.1f"); - changed |= ImGui::InputDouble(STR_C("Transition R1-R2 (m)"), &vertex.transition_middle, 0.0, 0.0, "%.1f"); - float split{static_cast(vertex.split)}; - if (ImGui::SliderFloat(STR_C("Share of R1"), &split, 0.0f, 1.0f, "%.2f")) + 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) { - vertex.split = split; + 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; } - vertex.radius2 = std::max(1.0, vertex.radius2); - vertex.transition_middle = std::max(0.0, vertex.transition_middle); + 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(); } changed |= ImGui::DragScalar(STR_C("Transition in (m)"), ImGuiDataType_Double, &vertex.transition_in, 1.0f, nullptr, nullptr, "%.0f"); changed |= ImGui::DragScalar(STR_C("Transition out (m)"), ImGuiDataType_Double, &vertex.transition_out, 1.0f, nullptr, nullptr, "%.0f"); @@ -2515,10 +2676,10 @@ bool editor_mode::straights_active() return ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::straights && false == current_straight().tracks.empty(); } -bool editor_mode::pick_straight_handle() +int editor_mode::straight_handle_hit() { if (false == straights_active()) - return false; + return -1; auto const &line{current_straight()}; glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5}; screen_projection const projection; @@ -2532,10 +2693,7 @@ bool editor_mode::pick_straight_handle() hit = i; } } - if (hit < 0) - return false; - m_straights.handle = hit; - return true; + return hit; } void editor_mode::straight_refresh() @@ -2587,8 +2745,9 @@ void editor_mode::render_straight_gizmo() auto &state{m_straights}; if (state.tool_mouse) { - glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; + glm::dvec3 const ground{cursor_ground()}; state.tool_handle = {ground.x, state.tool_handle.y, ground.z}; + typed_turn(); return; } if (state.tool_placed && m_gizmo_enabled) @@ -2607,11 +2766,24 @@ void editor_mode::render_straight_gizmo() glm::dvec3 const anchors[] = {line.start, line.end, (line.start + line.end) * 0.5}; auto const anchor{anchors[state.handle]}; - if (false == state.dragging) - state.gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(anchor - frame.camera)); - frame.manipulate(ImGuizmo::TRANSLATE, state.gizmo, m_gizmo_snap); + glm::dvec3 handled{anchor}; + bool held{false}; + if (m_handle_drag.active) + { + held = handle_drag_step(handled); + if (false == held && false == state.dragging) + return; + } + else + { + if (false == state.dragging) + state.gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(anchor - frame.camera)); + frame.manipulate(ImGuizmo::TRANSLATE, state.gizmo, m_gizmo_snap); + held = ImGuizmo::IsUsing(); + handled = frame.camera + glm::dvec3(state.gizmo[3]); + } - if (ImGuizmo::IsUsing()) + if (held) { if (false == state.dragging) { @@ -2626,7 +2798,7 @@ void editor_mode::render_straight_gizmo() { lines.push_back(line); } - state.fitting = lines.size() == 1 && start_curve_fit(line); + state.fitting = lines.size() == 1 && start_curve_fit(line, state.handle); auto const tracks{state.fitting ? std::vector{} : editor_track::straight_affected(lines)}; if (false == tracks_editable(tracks, state.status)) { @@ -2646,11 +2818,13 @@ void editor_mode::render_straight_gizmo() } } auto const &grabbed{state.drag_line}; - glm::dvec3 moved{frame.camera + glm::dvec3(state.gizmo[3])}; + glm::dvec3 moved{handled}; std::function transform; if (state.handle == 2) { - auto const offset{snap_straight_offset(grabbed, moved - (grabbed.start + grabbed.end) * 0.5)}; + auto offset{snap_straight_offset(grabbed, moved - (grabbed.start + grabbed.end) * 0.5)}; + if (auto const values{typed_values()}; false == values.empty()) + offset = glm::dvec3{-grabbed.direction.y, 0.0, grabbed.direction.x} * values[0]; transform = [offset](glm::dvec3 const &Point) { return Point + offset; }; } else @@ -2658,6 +2832,15 @@ void editor_mode::render_straight_gizmo() auto const pivot{state.handle == 0 ? grabbed.end : grabbed.start}; auto const original{state.handle == 0 ? grabbed.start : grabbed.end}; moved = snap_straight_direction(pivot, moved); + if (auto const values{typed_values()}; false == values.empty() && values[0] > 0.1) + { + glm::dvec2 heading{moved.x - pivot.x, moved.z - pivot.z}; + if (glm::length(heading) < 1e-6) + heading = {original.x - pivot.x, original.z - pivot.z}; + heading = glm::normalize(heading) * values[0]; + moved.x = pivot.x + heading.x; + moved.z = pivot.z + heading.y; + } glm::dvec2 const before{original.x - pivot.x, original.z - pivot.z}; glm::dvec2 const after{moved.x - pivot.x, moved.z - pivot.z}; auto const lengthbefore{glm::length(before)}; @@ -2732,6 +2915,13 @@ void editor_mode::cancel_track_tools() straight_refresh(); } m_extend.active = false; + m_turntable.placing = false; + m_handle_drag = {}; + if (m_point_drag.active) + { + m_track_snap = {}; + point_drag_finish(); + } if (m_switch.placing) { m_switch.placing = false; @@ -2981,7 +3171,7 @@ bool editor_mode::place_straight_tool() auto const &line{current_straight()}; if (line.tracks.empty()) return false; - glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; + glm::dvec3 const ground{cursor_ground()}; auto const along{std::clamp(line.along(ground), 0.0, line.length)}; glm::dvec3 const axis{line.direction.x, line.grade, line.direction.y}; state.tool_line = line; @@ -3007,6 +3197,7 @@ void editor_mode::render_straight_tool_gizmo() state.tool_dragging = true; auto const moved{frame.camera + glm::dvec3(state.tool_gizmo[3])}; state.tool_handle = {moved.x, state.tool_handle.y, moved.z}; + typed_turn(); } else if (state.tool_dragging) { @@ -3135,9 +3326,13 @@ std::vector editor_mode::track_key_hints(bool const All) if (m_lay.active) { if (m_lay.points.empty()) - hints = {{"LMB", "start of the new track, on the ground or at a free end"}, {"Esc", "stop laying"}}; + hints = {{"LMB", "start of the new track, on the ground or at a free end"}, {"Drag from a free end", "straight along it, to another free end: a curve joining them"}, {"Esc", "stop laying"}}; else - hints = {{"LMB", "next point, a curve goes at it"}, {"LMB on a free end", "join and lay"}, {"Enter", "lay"}, {"Backspace", "take back the point"}, {"Esc", "cancel"}}; + { + hints = {{"LMB", "next point, a curve goes at it"}, {"LMB on a free end", "join and lay"}, {"Enter", "lay up to the cursor"}, {"Backspace", "take back the point"}, {"Esc", "cancel"}}; + if (glm::dvec2 heading; lay_heading(heading)) + hints.insert(hints.begin() + 1, {"Shift", "straight on in the direction of the track"}); + } } else if (false == m_straights.detour.empty()) { @@ -3148,7 +3343,7 @@ std::vector editor_mode::track_key_hints(bool const All) else if (m_extend.active && m_extend_freehand) hints = {{"Drag", "curve through the cursor, along the end: straight"}, {"Ctrl", "straight"}, {"Release", "build"}, {"Esc", "cancel"}}; else if (m_extend.active) - hints = {{"Drag", "straight along the end, to a free end: a curve joining it"}, {"Release", "build"}, {"Esc", "cancel"}}; + hints = {{"Drag", "straight along the end, to a free end: a curve joining it"}, {"Shift", "straight, joins nothing"}, {"Release", "build"}, {"Esc", "cancel"}}; else if (m_switch.placing) hints = {{"Drag along", "direction of the switch"}, {"Drag sideways", "side of the diverging track"}, {"Esc", "cancel"}}; else if (m_track_tab == track_tab::lay) @@ -3163,6 +3358,8 @@ std::vector editor_mode::track_key_hints(bool const All) gizmo = true; } } + else if (m_track_tab == track_tab::turnout && m_switch.armed < 0) + hints = {{"List below", "choose the type of the switch to put in"}, {"LMB on a track", "choose the straight or the curve"}, {"Esc", "back to Select"}}; else if (m_track_tab == track_tab::turnout) hints = {{"1. List below", "choose the type of the switch"}, {"2. Press+drag", "on the selected track where the switch starts: along sets the direction, sideways the side"}, {"LMB elsewhere", "choose another track"}}; else if (m_track_tab == track_tab::profile && m_profile.route.spans.empty()) @@ -3171,6 +3368,12 @@ std::vector editor_mode::track_key_hints(bool const All) hints = {{"Drag a point", "in the profile window: changes the grade"}, {"Double click", "on the line: adds a point"}, {"Apply", "writes the heights into the tracks"}}; else if (m_track_window_open && m_track_tab == track_tab::speed) hints = {{"LMB on a row", "show the path"}}; + else if (m_track_window_open && m_track_tab == track_tab::lineside && m_lineside_tab == 0) + hints = {{"LMB by the track", "where it starts, the side of the track it goes on"}, {"Drag", "to where it ends, a click: one as long as the original"}, {"Release", "lays it"}, {"LMB away from the track", "selects another track"}}; + else if (m_track_window_open && m_track_tab == track_tab::lineside) + hints = {{"LMB on a track", "the line the objects go along"}}; + else if (m_track_window_open && m_track_tab == track_tab::joints) + hints = {{"LMB on a row", "show the place"}, {"Fix", "joins the ends, aligns them"}}; else if (m_track_window_open && m_track_tab == track_tab::infra) hints = {{"LMB", "select a path"}}; else if (m_track_tab == track_tab::straights && m_straights.tool == 1) @@ -3189,17 +3392,21 @@ std::vector editor_mode::track_key_hints(bool const All) else if (m_track_tab == track_tab::route) hints = {{"Drag yellow", "moves a vertex, the curve follows"}, {"Drag green", "changes the radius"}, {"Apply", "writes the design into the tracks"}, {"Esc", "release the vertex"}}; else if (selected == nullptr) - hints = {{"LMB on a track", "selects it"}, {"L", "lay a new track"}}; + hints = {{"LMB on a track", "selects it"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"RMB on a track", "what can be done with it"}, {"L", "lay a new track"}}; + else if (m_point_drag.moved) + hints = {{"Drag", "moves the point, level"}, {"Over a free end", "the end joins it"}, {"Release", "done"}, {"Esc", "done"}}; else if (m_track_point.valid()) { - hints = {{"Gizmo", "moves the point"}, {"Esc", "releases the point"}}; + hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", "moves the point along the axes"}, {"Esc", "releases the point"}}; gizmo = true; } else { - hints = {{"LMB on a handle", "picks a point: an end or a control vector"}, {"Gizmo", "moves the whole track, W turns it"}, {"Drag the red ring", "leads a new track out of a free end, or use Continue below"}, {"Del", "deletes the track"}}; + hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", "moves the whole track"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"L", "new track, also from a free end"}, {"Esc", "deselects"}, {"Del", "deletes the track"}}; gizmo = true; } + if (auto const *meaning{typed_meaning()}; meaning != nullptr) + hints.insert(hints.begin() + std::min(1, hints.size()), {"0-9", meaning}); if (All) { if (gizmo && m_gizmo_snap > 0.0f) @@ -3207,7 +3414,8 @@ std::vector editor_mode::track_key_hints(bool const All) hints.push_back({"K", "split the path under the cursor"}); hints.push_back({"O", "top view"}); hints.push_back({"Ctrl+Z", "undo"}); - hints.push_back({"L T G C P V I", "modes: lay, switch, straight, curve, profile, speed, infra"}); + hints.push_back({"Ctrl+Y", "redo"}); + hints.push_back({"L T G C B P V J I", "modes: lay, switch, straight, curve, objects, profile, speed, joints, infra"}); } for (auto &hint : hints) { @@ -3307,8 +3515,7 @@ std::string editor_mode::track_readout() const text += "\njoins " + name(m_track_snap.track); return text; } - if (m_gizmo_op != gizmo_operation::rotate) - return format("moved %.3f m", glm::distance(editor_track::pivot(*track), m_track_pivot)); + return format("moved %.3f m", glm::distance(editor_track::pivot(*track), m_track_pivot)); } return {}; } @@ -3368,7 +3575,9 @@ void editor_mode::draw_track_hints() } } - auto const readout{track_readout()}; + auto readout{track_readout()}; + if (auto const *meaning{typed_meaning()}; meaning != nullptr && false == m_typed.empty()) + readout += (readout.empty() ? "" : "\n") + std::string{"= "} + m_typed + "_ " + STR(meaning); if (readout.empty() || ImGui::GetIO().WantCaptureMouse) return; auto *foreground{ImGui::GetForegroundDrawList()}; @@ -3383,20 +3592,37 @@ void editor_mode::draw_track_hints() void editor_mode::update_build_tools() { - glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; + 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; if (m_extend.active) - m_extend.mouse = ground; + { + screen_projection const projection; + glm::dvec3 level; + m_extend.mouse = projection.on_level(io.MousePos, m_extend.point.y, level) && plan_distance(level, m_extend.point) < static_cast(kMaxPlacementDistance) ? level : ground; + typed_extend(); + m_extend.snap = m_typed.empty() && false == io.KeyShift && 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_switch.placing) - m_switch.mouse = ground; + 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; - lay.mouse = ground; - lay.mouse_snap = editor_track::find_free_end(nullptr, kRailCategory, ground, kLaySnapRadius, {}); + auto mouse{ground}; + glm::dvec2 heading; + bool const straight{io.KeyShift && lay_heading(heading)}; + if (straight) + { + auto const &last{lay.points.back()}; + auto const along{std::max(0.0, glm::dot(glm::dvec2{ground.x - last.x, ground.z - last.z}, heading))}; + mouse = {last.x + heading.x * along, ground.y, last.z + heading.y * along}; + } + lay.mouse = typed_lay(mouse); + lay.mouse_snap = straight ? editor_track::snap_target{} : m_typed.empty() ? snap_free_end(lay.mouse, cursor, nullptr, kRailCategory, {}) : editor_track::find_free_end(nullptr, kRailCategory, lay.mouse, kLaySnapRadius, {}); if (lay.points.empty()) return; auto points{lay.points}; @@ -3406,8 +3632,8 @@ void editor_mode::update_build_tools() else { end = {}; - if (plan_distance(ground, points.back()) > 1.0) - points.push_back(ground); + 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); } @@ -3454,30 +3680,20 @@ bool editor_mode::chosen_free_end(extend_tool &Tool) return false; } -bool editor_mode::start_extend() +bool editor_mode::start_extend(editor_track::snap_target const &End) { - auto *track{selected_track()}; - if (m_track_tab != track_tab::path && m_track_tab != track_tab::straights && m_track_tab != track_tab::turnout) - return false; + auto *track{End.track}; if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || false == editor_track::is_supported(*track)) return false; - screen_projection const projection; - for (int i = 0; i < static_cast(track->m_paths.size()); ++i) - { - for (auto const atend : {false, true}) - { - editor_track::point_ref const point{i, atend ? editor_track::point_kind::end : editor_track::point_kind::start}; - if (editor_track::is_connected(*track, point) || projection.mouse_distance2(editor_track::point_position(*track, point)) > kHandleRadius * kHandleRadius * 1.5f) - continue; - extend_tool tool; - if (false == free_end(*track, i, atend, tool)) - return false; - m_extend = tool; - m_extend.active = true; - return true; - } - } - 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; + return true; } bool editor_mode::extend_snap(editor_track::snap_target &Target) const @@ -3485,10 +3701,225 @@ 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 = editor_track::find_snap_target(*tool.track, tool.mouse, 5.0, {tool.track}); + 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 +{ + glm::dvec3 const ground{cursor_ground()}; + screen_projection const projection; + glm::dvec3 level; + if (false == projection.on_level(ImGui::GetIO().MousePos, Height, level) || 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()}; + auto const &display{ImGui::GetIO().DisplaySize}; + auto const reach{Global.EditorOrtho ? std::max(kJointRange, static_cast(Global.EditorOrthoExtent) * 2.0) : kJointRange}; + 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) && 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)); + } + screen_projection const projection; + glm::dvec3 position; + if (false == projection.on_level(io.MousePos, drag.height, position) || 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); + m_track_drag.clear(); + m_track_drag_points.clear(); + m_track_joints.clear(); +} + std::vector editor_mode::extend_pieces() const { std::vector result; @@ -3529,6 +3960,25 @@ std::vector editor_mode::extend_pieces() const auto const shape{alignment::compute(design)}; return shape.valid ? alignment::pieces(shape, design, 0) : result; } + if (false == ImGui::GetIO().KeyShift && 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 (std::abs(turn) > 1e-4) + { + int const side{turn > 0.0 ? 1 : -1}; + auto const radius{std::abs(diverging.radius) > 1.0f ? static_cast(std::abs(diverging.radius)) : curve.plan_length() / std::abs(turn)}; + 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; + return arc_pieces(tool.point, direction, tool.grade, radius, std::min(angle, glm::pi()), side); + } + } glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; auto const chord{glm::length(offset)}; if (chord < 0.5) @@ -3537,7 +3987,7 @@ std::vector editor_mode::extend_pieces() const auto const along{glm::dot(offset, direction)}; auto const lateral{direction.x * offset.y - direction.y * offset.x}; auto const height = [&](double const Distance) { return tool.point.y + tool.grade * Distance; }; - if (false == m_extend_freehand || ImGui::GetIO().KeyCtrl || std::abs(lateral) < 0.005 * chord + 0.05) + if (false == m_extend_freehand || ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift || std::abs(lateral) < 0.005 * chord + 0.05) { if (along < 0.5) return result; @@ -3553,6 +4003,20 @@ std::vector editor_mode::extend_pieces() const 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_typed.empty()) + { + 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; + } crossover_plan plan; editor_track::snap_target target; if (false == extend_snap(target) && plan_crossover(plan)) @@ -3645,7 +4109,7 @@ void editor_mode::render_extend_ui() if (ImGui::Button(STR_C("Curve right##extend"), ImVec2(100.0f, 0.0f))) extend_add(-1); ImGui::Checkbox(STR_C("Freehand"), &m_extend_freehand); - item_tooltip("Dragging the red ring of a free end draws a curve through the cursor; off: it draws a straight along the end"); + item_tooltip("Dragging from a free end draws a curve through the cursor; off: it draws a straight along the end"); } std::vector editor_mode::lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const @@ -3723,7 +4187,7 @@ void editor_mode::lay_click() { auto &tool{m_lay}; tool.status.clear(); - auto const snap{editor_track::find_free_end(nullptr, kRailCategory, tool.mouse, kLaySnapRadius, {})}; + 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 @@ -3741,6 +4205,7 @@ void editor_mode::lay_click() if (plan_distance(point, tool.points.back()) < 1.0) return; tool.points.push_back(point); + m_typed.clear(); } void editor_mode::lay_finish(editor_track::snap_target const &End) @@ -3777,6 +4242,47 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) straight_refresh(); } +bool editor_mode::lay_heading(glm::dvec2 &Heading) const +{ + auto const &tool{m_lay}; + glm::dvec2 direction{0.0}; + if (tool.points.size() >= 2) + direction = plan_of(tool.points.back() - tool.points[tool.points.size() - 2]); + else if (tool.points.size() == 1 && tool.start.track != nullptr) + direction = -plan_of(tool.start.direction); + if (glm::length(direction) < 1e-6) + return false; + Heading = glm::normalize(direction); + return true; +} + +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}; @@ -3788,6 +4294,12 @@ void editor_mode::lay_cancel() void editor_mode::render_lay_ui() { auto &tool{m_lay}; + if (auto *track{selected_track()}; track != nullptr) + { + std::string reason; + if (editor_track::can_edit_geometry(*track, reason)) + render_extend_ui(); + } ImGui::PushItemWidth(120.0f); ImGui::DragScalar(STR_C("Curve radius (m)"), ImGuiDataType_Double, &tool.radius, 5.0f, nullptr, nullptr, "%.0f"); item_tooltip("Drag to change, Ctrl+click to type the value"); @@ -3867,9 +4379,9 @@ bool editor_mode::start_switch_placement() return false; // with no plain track selected, or a press away from it, the click selects auto *selected{selected_track()}; - if (selected == nullptr || selected->eType != tt_Normal) + if (selected == nullptr || selected->eType != tt_Normal || selected->m_paths.empty()) return false; - glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; + glm::dvec3 const ground{cursor_level(selected->m_paths.front().points[segment_data::point::start].y)}; tool.curved = false; tool.status.clear(); if (current_straight().tracks.empty()) @@ -3952,10 +4464,158 @@ void editor_mode::insert_switch(editor_track::straight const &Line, double const std::vector> states; std::vector created; std::vector removed; - if (false == place_switch_on_straight(Line, m_switch.templates[m_switch.armed], nullptr, Along, Direction, Side, true, states, created, removed)) + if (false == place_switch_on_straight(Line, m_switch.templates[m_switch.armed], nullptr, Along, Direction, Side, true, states, created, removed, switch_name_for_new())) return; + auto const placed{created}; push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); + switch_placed(placed); +} + +void editor_mode::switch_placed(std::vector const &Created) +{ + auto const found{std::find_if(Created.begin(), Created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; })}; + if (found == Created.end()) + return; + auto const slip{std::count_if(Created.begin(), Created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; }) > 1}; + m_switch.armed = -1; + m_switch.placing = false; + m_track_point = {}; + m_node = *found; + ui()->set_node(m_node); + if (slip) + return; + if (m_switch.drive >= 0) + place_switch_drive(**found); + std::string name{m_switch.name}; + auto digits{name.size()}; + while (digits > 0 && std::isdigit(static_cast(name[digits - 1]))) + --digits; + if (digits < name.size() && name.size() - digits < 9) + name = name.substr(0, digits) + std::to_string(std::stoi(name.substr(digits)) + 1); + else + name.clear(); + std::snprintf(m_switch.name, sizeof(m_switch.name), "%s", name.c_str()); +} + +void editor_mode::scan_switch_drives() +{ + auto &tool{m_switch}; + if (tool.drives_scanned) + return; + tool.drives_scanned = true; + tool.drives.clear(); + if (false == EditorIncludes.scanned()) + EditorIncludes.scan(); + for (auto const &entry : EditorIncludes.entries()) + { + if (entry.category != "switch drive") + continue; + include_info info; + std::string error; + if (false == editor_includes::load(entry.file, info, error) || editor_includes::parameter_with_role(info, "track") == 0) + continue; + auto const side = [](std::string const &Side) { return Side == "left" ? 0 : Side == "right" ? 1 : -1; }; + auto const hand{side(info.switch_hand)}; + auto const drive{side(info.switch_drive)}; + if (hand < 0 || drive < 0) + continue; + auto const name{info.name.empty() ? entry.file : info.name}; + auto family{std::find_if(tool.drives.begin(), tool.drives.end(), [&](switch_tool::drive_family const &Family) { return Family.name == name; })}; + if (family == tool.drives.end()) + { + tool.drives.push_back({name, {}}); + family = std::prev(tool.drives.end()); + } + family->files[hand * 2 + drive] = entry.file; + } + std::sort(tool.drives.begin(), tool.drives.end(), [](switch_tool::drive_family const &A, switch_tool::drive_family const &B) { return A.name < B.name; }); + tool.drive = tool.drives.empty() ? -1 : 0; +} + +std::string editor_mode::switch_name_for_new() const +{ + if (m_switch.name[0] != '\0') + return m_switch.name; + for (int i = 1;; ++i) + { + auto const name{"z" + std::to_string(i)}; + if (simulation::Paths.find(name) == nullptr && simulation::Events.FindEvent(name + "+") == nullptr) + return name; + } +} + +bool editor_mode::place_switch_drive(TTrack &Switch) +{ + auto &tool{m_switch}; + if (tool.drive < 0 || tool.drive >= static_cast(tool.drives.size()) || Switch.eType != tt_Switch || Switch.m_paths.size() < 2) + return false; + auto const &main{Switch.m_paths[0]}; + auto const &diverging{Switch.m_paths[1]}; + auto const start{main.points[segment_data::point::start]}; + auto const &control{main.points[segment_data::point::control1]}; + auto heading{plan_of(control != glm::dvec3{} ? control : main.points[segment_data::point::end] - start)}; + if (glm::length(heading) < 1e-9) + return false; + heading = glm::normalize(heading); + glm::dvec2 const right{-heading.y, heading.x}; + bool const rightswitch{glm::dot(plan_of(diverging.points[segment_data::point::end] - start), right) > 0.0}; + bool const driveright{(tool.drive_side == 0) == rightswitch}; + auto const &family{tool.drives[tool.drive]}; + template_item item; + item.file = family.files[(rightswitch ? 2 : 0) + (driveright ? 1 : 0)]; + if (item.file.empty()) + { + tool.status = format(STR_C("The drive %s has no variant for a %s switch with the drive on the %s"), family.name.c_str(), rightswitch ? STR_C("right") : STR_C("left"), driveright ? STR_C("right") : STR_C("left")); + return false; + } + item.location = start; + auto yaw{glm::degrees(std::atan2(heading.x, heading.y))}; + item.yaw = yaw < 0.0 ? yaw + 360.0 : yaw; + item.described = true; + item.track = Switch.name(); + std::string error; + if (place_templates({item}, error) == 0) + { + tool.status = STR_C("The drive couldn't be placed: ") + error; + return false; + } + tool.status = format(STR_C("Switch %s with the drive %s: events %s+ and %s- switch it, also in the map"), Switch.name().c_str(), item.file.c_str(), Switch.name().c_str(), Switch.name().c_str()); + return true; +} + +bool editor_mode::render_switch_drive_choice() +{ + auto &tool{m_switch}; + scan_switch_drives(); + if (tool.drives.empty()) + { + ImGui::TextDisabled("%s", STR_C("No switch drives: no template is described with category: switch drive")); + return false; + } + auto const label = [&](int const Index) { return Index < 0 ? std::string{STR_C("no drive")} : tool.drives[Index].name; }; + ImGui::PushItemWidth(200.0f); + bool changed{false}; + if (ImGui::BeginCombo(STR_C("Drive"), label(tool.drive).c_str())) + { + for (int i = -1; i < static_cast(tool.drives.size()); ++i) + if (ImGui::Selectable(label(i).c_str(), tool.drive == i)) + { + tool.drive = i; + changed = true; + } + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + item_tooltip("Templates described with category: switch drive, the side of the switch and of the drive, and a parameter with the role track for the name of the switch.\nIts events + and - switch it with the keys by the switch and in the map during the game"); + if (tool.drive >= 0) + { + ImGui::SameLine(); + changed |= ImGui::RadioButton(STR_C("diverging side"), &tool.drive_side, 0); + ImGui::SameLine(); + changed |= ImGui::RadioButton(STR_C("other side"), &tool.drive_side, 1); + } + return changed; } bool editor_mode::cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack **Style) @@ -4046,7 +4706,7 @@ bool editor_mode::cut_straight(editor_track::straight const &Line, double const return true; } -bool editor_mode::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) +bool editor_mode::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) { auto const &shape{Shape}; if (shape.length > Line.length - 0.02) @@ -4090,7 +4750,7 @@ bool editor_mode::place_switch_on_straight(editor_track::straight const &Line, e auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; }; glm::dvec2 const direction{Line.direction * static_cast(Direction)}; auto const paths{editor_track::place_switch(shape, point(origin), direction, Side, Line.grade * Direction)}; - auto *track{editor_track::create_switch(shape, paths, Style != nullptr ? *Style : *first)}; + auto *track{editor_track::create_switch(shape, paths, Style != nullptr ? *Style : *first, Name)}; if (track != nullptr) { editor_track::commit({track}); @@ -4134,6 +4794,8 @@ void editor_mode::draw_build_overlay() const } if (m_extend.active) { + if (m_extend.track != nullptr) + draw_free_ends(m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}); crossover_plan plan; editor_track::snap_target target; if (extend_snap(target)) @@ -4177,24 +4839,8 @@ void editor_mode::draw_build_overlay() const drawlist->AddCircle(screen, 14.0f, IM_COL32(255, 60, 255, 255), 20, 3.0f); if (lay.start.track != nullptr && projection.project(lay.start.position, screen)) drawlist->AddCircle(screen, 10.0f, IM_COL32(255, 60, 255, 255), 20, 2.0f); - if (lay.points.empty()) - { - // free ends nearby, which the new track can start from - for (auto *section : simulation::Region->sections(lay.mouse, 300.f)) - for (auto const &cell : section->m_cells) - for (auto *path : cell.m_directories.paths) - { - if ((path->iCategoryFlag & 15) != kRailCategory || false == editor_track::is_supported(*path)) - continue; - for (int i = 0; i < static_cast(path->m_paths.size()); ++i) - for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end}) - { - editor_track::point_ref const point{i, kind}; - if (false == editor_track::is_connected(*path, point) && projection.project(editor_track::point_position(*path, point), screen)) - drawlist->AddCircle(screen, 7.0f, IM_COL32(255, 60, 255, 160), 12, 1.5f); - } - } - } + if (false == m_extend.active) + draw_free_ends(nullptr, kRailCategory, {}); } if (false == m_straights.detour.empty()) { @@ -4208,37 +4854,45 @@ void editor_mode::draw_build_overlay() const drawlist->AddCircleFilled(screen, 6.0f, overlay_color::invalid); } } - auto const *track{selected_track()}; - if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || m_extend.active) - return; - for (int i = 0; i < static_cast(track->m_paths.size()); ++i) - { - for (auto const atend : {false, true}) - { - editor_track::point_ref const point{i, atend ? editor_track::point_kind::end : editor_track::point_kind::start}; - if (editor_track::is_connected(*track, point)) - continue; - ImVec2 screen; - if (projection.project(editor_track::point_position(*track, point), screen)) - drawlist->AddCircle(screen, 10.0f, IM_COL32(240, 60, 60, 255), 16, 3.0f); - } - } + if ((m_point_drag.moved || (m_track_gizmo_using && m_track_point.valid())) && editor_track::is_end(m_track_point.kind) && m_track_drag_points.empty()) + if (auto const *track{selected_track()}; track != nullptr) + draw_free_ends(track, track->iCategoryFlag & 15, std::vector(m_track_drag.begin(), m_track_drag.end())); } void editor_mode::render_switch_ui() { auto &tool{m_switch}; + auto const list = [&](bool const Slips) { + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + { + auto const &entry{tool.templates[i]}; + if (entry.double_slip != Slips) + continue; + auto label{entry.source != nullptr ? STR("Scenery: ") + entry.label + " (" + std::to_string(entry.count) + "x)" : "PLK: " + entry.label}; + label += "##switch" + std::to_string(i); + if (ImGui::Selectable(label.c_str(), tool.armed == i)) + tool.armed = tool.last = i; + } + }; ImGui::TextDisabled("%s", STR_C("Switch template")); ImGui::BeginChild("##switchtemplates", ImVec2(0.0f, 240.0f), true); - for (int i = 0; i < static_cast(tool.templates.size()); ++i) - { - auto const &entry{tool.templates[i]}; - auto label{entry.source != nullptr ? STR("Scenery: ") + entry.label + " (" + std::to_string(entry.count) + "x)" : "PLK: " + entry.label}; - label += "##switch" + std::to_string(i); - if (ImGui::Selectable(label.c_str(), tool.armed == i)) - tool.armed = tool.last = i; - } + list(false); ImGui::EndChild(); + if (std::any_of(tool.templates.begin(), tool.templates.end(), [](editor_track::switch_template const &Template) { return Template.double_slip; })) + { + if (tool.armed >= 0 && tool.armed < static_cast(tool.templates.size()) && tool.templates[tool.armed].double_slip) + ImGui::SetNextItemOpen(true); + if (ImGui::CollapsingHeader(STR_C("Double slip switches"))) + { + ImGui::TextDisabled("%s", STR_C("Only at a crossing of two straights")); + list(true); + } + } + ImGui::PushItemWidth(120.0f); + ImGui::InputTextWithHint(STR_C("Name##switchname"), switch_name_for_new().c_str(), tool.name, sizeof(tool.name)); + ImGui::PopItemWidth(); + item_tooltip("Name of the next switch, its drive events are named after it; empty: the next free z1, z2...; a number at the end goes up with each switch"); + render_switch_drive_choice(); if (false == tool.status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", tool.status.c_str()); if (ImGui::SmallButton(STR_C("Collect again from the scenery"))) @@ -4280,7 +4934,7 @@ void editor_mode::add_detour_point() return; state.detour_line = line; } - state.detour.push_back(Global.pCamera.Pos + GfxRenderer->Mouse_Position()); + state.detour.push_back(cursor_ground()); if (state.detour.size() >= 4) { apply_detour(); @@ -4398,20 +5052,31 @@ void editor_mode::apply_detour() straight_refresh(); } -bool editor_mode::start_curve_fit(editor_track::straight const &Line) +bool editor_mode::start_curve_fit(editor_track::straight const &Line, int const Handle) { auto &state{m_straights}; auto *before{editor_track::outside_neighbour(Line, false)}; auto *after{editor_track::outside_neighbour(Line, true)}; - if (before == nullptr || after == nullptr) + auto const curve_at = [&](TTrack *Neighbour, editor_track::curve &Curve) { + Curve = {}; + return Neighbour != nullptr && Neighbour->eType == tt_Normal && editor_track::find_curve(*Neighbour, state.tolerance, m_route.design.norms.gauge, Curve) && Curve.reversals == 0 && std::abs(Curve.turn) > 1e-4; + }; + state.fit_has_before = curve_at(before, state.fit_before); + state.fit_has_after = curve_at(after, state.fit_after); + if (false == state.fit_has_before && false == state.fit_has_after) return false; - if (false == editor_track::find_curve(*before, state.tolerance, m_route.design.norms.gauge, state.fit_before) || false == editor_track::find_curve(*after, state.tolerance, m_route.design.norms.gauge, state.fit_after)) - return false; - if (state.fit_before.reversals != 0 || state.fit_after.reversals != 0) + auto const side_follows = [&](bool const Curve, TTrack const *Neighbour, bool const Pivot) { return Curve || Neighbour == nullptr || Pivot; }; + if (false == side_follows(state.fit_has_before, before, Handle == 1) || false == side_follows(state.fit_has_after, after, Handle == 0)) return false; + auto const own_end = [&](glm::dvec3 const &Point) -> TTrack * { + for (auto *track : Line.tracks) + if (editor_track::touches(*track, Point)) + return track; + return Line.tracks.empty() ? nullptr : Line.tracks.front(); + }; auto const other_end = [](editor_track::curve const &Curve, glm::dvec3 const &Joint) { return editor_track::touches(*Curve.from, Joint) ? Curve.to : Curve.from; }; - m_route.from = other_end(state.fit_before, Line.start); - m_route.to = other_end(state.fit_after, Line.end); + m_route.from = state.fit_has_before ? other_end(state.fit_before, Line.start) : own_end(Line.start); + m_route.to = state.fit_has_after ? other_end(state.fit_after, Line.end) : own_end(Line.end); route_reset(); if (m_route.chain.tracks.empty()) return false; @@ -4440,26 +5105,34 @@ void editor_mode::update_curve_fit(glm::dvec3 const &Start, glm::dvec3 const &En vertex.transition_out = std::round(Curve.transition_out); if (Curve.cant > 0.0) vertex.cant = std::round(Curve.cant); - if (Curve.compound) - { - vertex.compound = true; - vertex.radius2 = std::round(Curve.radius2); - vertex.transition_middle = std::round(Curve.transition_middle); - vertex.split = Curve.split; - } + compound_from(Curve, vertex); vertex.offset = std::max(0.1, Offset); return vertex; }; - auto before{make(state.fit_before, first)}; - auto after{make(state.fit_after, last)}; - design.vertices = {before, after}; + design.vertices.clear(); + if (state.fit_has_before) + design.vertices.push_back(make(state.fit_before, first)); + else + { + design.start = Start; + design.start_direction = direction; + design.start_reserve = 0.0; + } + if (state.fit_has_after) + design.vertices.push_back(make(state.fit_after, last)); + else + { + design.end = End; + design.end_direction = direction; + design.end_reserve = 0.0; + } route_update(); } bool editor_mode::plan_crossover(crossover_plan &Plan) const { auto const &tool{m_extend}; - if (false == tool.active || tool.track == nullptr || tool.track->eType != tt_Switch || tool.track->m_paths.size() < 2 || tool.path != 1 || false == tool.atend) + if (false == tool.active || tool.track == nullptr || tool.track->eType != tt_Switch || tool.track->m_paths.size() < 2 || tool.path != 1 || false == tool.atend || ImGui::GetIO().KeyShift) return false; auto const &own{*tool.track}; auto const &main{own.m_paths[0]}; @@ -4737,6 +5410,7 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv created.insert(created.end(), parts.begin(), parts.end()); push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); + switch_placed(parts); return true; } @@ -5051,7 +5725,7 @@ void editor_mode::insert_curved_switch(int const Direction, int const Side) } } auto const paths{curved_switch_paths(shape, tool.frame, station, Direction, Side)}; - auto *track{editor_track::create_switch(shape, paths, *tool.frame_tracks.front())}; + auto *track{editor_track::create_switch(shape, paths, *tool.frame_tracks.front(), switch_name_for_new())}; if (track != nullptr) { editor_track::commit({track}); @@ -5060,6 +5734,8 @@ void editor_mode::insert_curved_switch(int const Direction, int const Side) } else tool.status = STR_C("The switch couldn't be created from this template"); + auto const placed{created}; push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); + switch_placed(placed); } diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp new file mode 100644 index 00000000..d7472c61 --- /dev/null +++ b/application/editormodetrackux.cpp @@ -0,0 +1,1359 @@ +/* +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/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "utilities/Globals.h" +#include "rendering/renderer.h" +#include "world/Track.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include "imgui/ImGuizmo.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::bezier; + +float const kHoverPixels{12.0f}; +double const kHoverReachMin{15.0}; +double const kHoverReachMax{250.0}; +double const kHoverLabelDelay{0.35}; // s the cursor rests on a path before its data shows up +double const kFocusDistance{25.0}; +auto const kContextClick{std::chrono::milliseconds(350)}; +float const kContextTurn{0.02f}; // rad the camera may turn during a click of the right button +std::size_t const kSearchLimit{200}; +int const kRailCategory{1}; + +bool viewport_click() +{ + return false == ImGui::GetIO().MouseDownOwned[0]; +} + +bool rail(TTrack const &Track) +{ + return (Track.iCategoryFlag & 15) == kRailCategory && false == Track.m_editorremoved && Track.visible(); +} + +std::string name_of(TTrack const &Track) +{ + return Track.name().empty() ? std::string{"(noname)"} : Track.name(); +} + +std::string lower(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + return Text; +} + +char const *type_of(TTrack const &Track) +{ + switch (Track.eType) + { + case tt_Normal: return "normal"; + case tt_Switch: return "switch"; + case tt_Cross: return "cross"; + case tt_Table: return "turntable"; + case tt_Tributary: return "tributary"; + default: return "unknown"; + } +} + +double segment_distance2(glm::vec2 const &Point, glm::vec2 const &A, glm::vec2 const &B, double &T) +{ + auto const ab{B - A}; + auto const length2{glm::dot(ab, ab)}; + T = length2 > 1e-6f ? std::clamp(static_cast(glm::dot(Point - A, ab) / length2), 0.0, 1.0) : 0.0; + auto const closest{A + ab * static_cast(T)}; + return glm::dot(Point - closest, Point - closest); +} + +} // namespace + +editor_mode::track_hover editor_mode::track_under_cursor() const +{ + track_hover result; + auto const &io{ImGui::GetIO()}; + glm::dvec3 const camera{Global.pCamera.Pos}; + glm::dvec3 const ground{cursor_ground()}; + auto const reach{std::clamp(glm::distance(camera, ground) * 0.1, kHoverReachMin, kHoverReachMax)}; + screen_projection const projection; + glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; + double best{kHoverPixels * kHoverPixels}; + for (auto *section : simulation::Region->sections(ground, static_cast(reach))) + { + for (auto const &cell : section->m_cells) + { + for (auto *track : cell.m_directories.paths) + { + if (track == nullptr || false == rail(*track)) + continue; + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + bezier const curve{track->m_paths[i]}; + auto const count{std::clamp(static_cast(curve.plan_length() / 3.0), 6, 64)}; + auto previous{curve.point(0.0)}; + ImVec2 from; + bool visible{projection.project(previous, from)}; + for (int k = 1; k <= count; ++k) + { + auto const next{curve.point(static_cast(k) / count)}; + ImVec2 to; + bool const shown{projection.project(next, to)}; + if (visible && shown) + { + double t; + auto const distance{segment_distance2(mouse, {from.x, from.y}, {to.x, to.y}, t)}; + auto const point{glm::mix(previous, next, t)}; + if (distance < best && glm::distance(point, camera) <= static_cast(kMaxPlacementDistance)) + { + best = distance; + result.track = track; + result.path = i; + result.point = point; + } + } + previous = next; + from = to; + visible = shown; + } + } + } + } + } + return result; +} + +void editor_mode::update_track_hover() +{ + if (typed_meaning() == nullptr) + m_typed.clear(); + bool const blocked{ImGui::GetIO().WantCaptureMouse || mouseHold || m_track_set_using || m_track_box.active || m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS || m_track_gizmo_using || m_route_gizmo_using || m_extend.active || m_switch.placing || + m_straights.dragging || m_lay.active}; + if (blocked) + { + m_hover = {}; + return; + } + auto found{track_under_cursor()}; + found.since = found.track == m_hover.track ? m_hover.since : ImGui::GetTime(); + m_hover = found; +} + +void editor_mode::draw_track_hover() const +{ + using kind = track_intent::kind; + auto const *track{m_hover.track}; + if (track == nullptr || (m_intent.what != kind::select && m_intent.what != kind::box && m_intent.what != kind::straight_set) || m_intent.track != track) + return; + screen_projection const projection; + if (track != selected_track()) + { + auto *drawlist{ImGui::GetBackgroundDrawList()}; + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + bezier const curve{track->m_paths[i]}; + auto const count{std::clamp(static_cast(curve.plan_length() / 3.0), 6, 64)}; + auto const colour{i == m_hover.path ? IM_COL32(255, 255, 255, 200) : IM_COL32(255, 255, 255, 110)}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= count; ++k) + { + auto const next{curve.point(static_cast(k) / count)}; + projection.line(drawlist, previous, next, colour, i == m_hover.path ? 3.5f : 2.0f); + previous = next; + } + } + } +} + +bool editor_mode::track_gesture_on() const +{ + return mouseHold || m_track_set_using || m_track_box.active || m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS || m_track_gizmo_using || m_route_gizmo_using || m_extend.active || m_switch.placing || m_straights.dragging || m_straights.tool_mouse || + m_point_drag.active || m_handle_drag.active || m_sweep.dragging || m_turntable.placing || ImGuizmo::IsUsing(); +} + +editor_mode::track_intent editor_mode::track_intent_at(int const Mods) +{ + using kind = track_intent::kind; + track_intent intent; + if (ImGui::GetIO().WantCaptureMouse) + return intent; + glm::dvec3 const ground{cursor_ground()}; + intent.position = ground; + bool const shift{(Mods & GLFW_MOD_SHIFT) != 0}; + bool const control{(Mods & GLFW_MOD_CONTROL) != 0}; + bool const alt{(Mods & GLFW_MOD_ALT) != 0}; + auto const make = [&](kind const What, char const *Action) { + intent.what = What; + intent.action = Action != nullptr ? STR(Action) : std::string{}; + return intent; + }; + auto *selected{selected_track()}; + + if (m_lay.active) + { + auto const &lay{m_lay}; + if (lay.mouse_snap.track != nullptr && (lay.points.empty() || glm::distance(lay.mouse_snap.position, lay.points.front()) > 1.0)) + { + intent.snap = lay.mouse_snap; + intent.track = lay.mouse_snap.track; + intent.position = lay.mouse_snap.position; + auto const diverging{lay.mouse_snap.track->eType == tt_Switch && lay.mouse_snap.point == editor_track::point_ref{1, editor_track::point_kind::end}}; + if (lay.points.empty() && diverging) + return make(kind::lay_end, "Click: start from this free end, drag: the curve of the switch goes on, Shift: straight"); + return make(kind::lay_end, lay.points.empty() ? "Click: start from this free end, drag: lead a track out of it" : "Click: join this end and lay"); + } + intent.position = lay.mouse; + return make(kind::lay, lay.points.empty() ? "Click: start of the new track" : "Click: next point"); + } + if (m_track_tab == track_tab::turntable) + return turntable_intent(ground, shift); + if (m_track_tab == track_tab::lineside && sweep_grabs(ground)) + return make(kind::sweep, "Press: where the model starts, drag to its end"); + if (m_track_tab == track_tab::path) + { + if (shift && control && m_hover.track != nullptr && rail(*m_hover.track)) + { + intent.track = m_hover.track; + intent.position = m_hover.point; + auto const &preview{track_spread_preview(*m_hover.track)}; + intent.action = format(in_track_set(m_hover.track) ? STR_C("Click: take out the %zu joined paths, %.0f m") : STR_C("Click: add the %zu joined paths, %.0f m"), preview.size(), m_spread.length); + intent.what = kind::spread; + return intent; + } + if (shift && false == gizmo_frame::over()) + { + intent.track = m_hover.track; + return make(kind::box, m_hover.track == nullptr ? "Drag: the tracks in a box join the selection" : in_track_set(m_hover.track) ? "Click: take out of the selection, drag: a box" : "Click: add to the selection, drag: a box"); + } + if (auto const hit{track_handle_hit()}; hit.valid() && selected != nullptr) + { + std::string reason; + if (editor_track::can_edit_geometry(*selected, reason)) + { + intent.track = selected; + intent.point = hit; + intent.position = editor_track::point_position(*selected, hit); + if (false == editor_track::is_end(hit.kind)) + return make(kind::point, "Drag: the control vector, the curve follows"); + if (editor_track::is_connected(*selected, hit)) + return make(kind::point, "Drag: the joint, the neighbours follow"); + if (control) + return make(kind::point, "Drag: the end, dropped on a free end it joins it"); + intent.snap.track = selected; + intent.snap.point = hit; + intent.snap.position = intent.position; + auto const diverging{selected->eType == tt_Switch && hit == editor_track::point_ref{1, editor_track::point_kind::end}}; + return make(kind::extend_end, diverging ? "Drag: the curve of the switch goes on, Shift: straight; Ctrl+drag: move the end" : "Drag: a new track out of this end, as in laying; Ctrl+drag: move the end"); + } + } + } + if (m_track_tab == track_tab::straights) + { + auto const &line{current_straight()}; + if (alt) + { + intent.track = m_hover.track; + return m_hover.track != nullptr ? make(kind::straight_set, "Click: move this straight together with the others") : intent; + } + if (control && false == line.tracks.empty()) + return make(kind::straight_break, "Press: break the straight here, drag to turn the rest"); + if (shift && false == line.tracks.empty()) + return make(kind::detour, "Click: next point of the shift around an obstacle"); + } + if (m_track_tab == track_tab::straights && m_straights.tool == 0) + { + if (auto const hit{straight_handle_hit()}; hit >= 0) + { + auto const &line{current_straight()}; + glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5}; + intent.index = hit; + intent.position = handles[hit]; + return make(kind::straight_handle, hit == 2 ? "Drag: shift the straight sideways" : "Drag: move this end, the curves at it follow"); + } + } + if (int vertex, grip; route_hit(vertex, grip)) + { + intent.index = vertex >= 0 ? vertex : grip; + intent.position = vertex >= 0 ? route_vertex_position(vertex) : alignment::evaluate(m_route.result, m_route.result.vertex_chainages[grip]).position; + return make(vertex >= 0 ? kind::route_vertex : kind::route_grip, vertex >= 0 ? "Drag: move this vertex, the curve follows" : "Drag: the radius of this curve"); + } + if (gizmo_frame::over()) + return make(kind::gizmo, nullptr); + if (m_track_tab == track_tab::straights) + { + auto const &line{current_straight()}; + if (m_straights.tool != 0 && false == line.tracks.empty()) + { + intent.position = line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * std::clamp(line.along(ground), 0.0, line.length); + return make(kind::straight_tool, m_straights.tool == 1 ? "Click: break the straight here" : "Click: start of the shift"); + } + } + if (selected != nullptr && false == selected->m_paths.empty()) + { + glm::dvec3 point; + if (switch_reach(cursor_level(selected->m_paths.front().points[segment_data::point::start].y), point)) + { + intent.track = selected; + intent.position = point; + return make(kind::switch_at, "Press and drag: a switch here, along sets its direction, sideways its side"); + } + } + if (m_hover.track != nullptr) + { + intent.track = m_hover.track; + intent.path = m_hover.path; + intent.position = m_hover.point; + switch (m_track_tab) + { + case track_tab::straights: return make(kind::select, "Click: the whole straight through this track"); + case track_tab::route: return make(kind::select, "Click: the curve of this track with its straights"); + case track_tab::turnout: return make(kind::select, m_hover.track->eType == tt_Switch ? "Click: edit this switch" : m_switch.armed < 0 ? "Click: choose this track, then the type of the switch" : "Click: put a switch into this track"); + case track_tab::profile: return make(kind::select, "Click: the grade line through this track"); + case track_tab::lineside: return make(kind::select, "Click: the line the objects go along"); + default: return make(kind::select, "Click: select"); + } + } + return intent; +} + +void editor_mode::update_track_intent() +{ + using kind = track_intent::kind; + if (track_gesture_on()) + { + m_intent = {}; + return; + } + auto const &io{ImGui::GetIO()}; + m_intent = track_intent_at((io.KeyShift ? GLFW_MOD_SHIFT : 0) | (io.KeyCtrl ? GLFW_MOD_CONTROL : 0) | (io.KeyAlt ? GLFW_MOD_ALT : 0)); + switch (m_intent.what) + { + case kind::none: + case kind::gizmo: break; + case kind::point: + case kind::straight_handle: + case kind::route_vertex: + case kind::route_grip: ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); break; + default: ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); break; + } +} + +void editor_mode::track_press(track_intent const &Intent) +{ + using kind = track_intent::kind; + switch (Intent.what) + { + case kind::lay_end: + if (m_lay.points.empty() && start_extend(Intent.snap)) + return; + [[fallthrough]]; + case kind::lay: + GfxRenderer->Pick_Node_Callback([this](scene::basic_node * /*node*/) { + if (viewport_click()) + lay_click(); + }); + return; + case kind::sweep: sweep_press(); return; + case kind::box: start_track_box(); return; + case kind::spread: + if (Intent.track != nullptr) + track_spread_apply(*Intent.track); + return; + case kind::point: point_drag_start(Intent.point); return; + case kind::gizmo: return; + case kind::straight_set: + { + auto *hovered{Intent.track}; + GfxRenderer->Pick_Node_Callback([this, hovered](scene::basic_node *node) { + auto *track{hovered != nullptr ? hovered : dynamic_cast(node)}; + if (viewport_click() && track != nullptr) + toggle_straight_set(*track); + }); + return; + } + case kind::straight_break: start_straight_gesture(1); return; + case kind::detour: add_detour_point(); return; + case kind::straight_tool: place_straight_tool(); return; + case kind::straight_handle: + m_straights.handle = Intent.index; + handle_drag_start(Intent.position); + return; + case kind::route_vertex: + m_route.vertex = Intent.index; + m_route.grip = -1; + handle_drag_start(Intent.position); + return; + case kind::route_grip: + m_route.vertex = -1; + m_route.grip = Intent.index; + handle_drag_start(Intent.position); + return; + case kind::switch_at: start_switch_placement(); return; + case kind::extend_end: + if (false == start_extend(Intent.snap)) + point_drag_start(Intent.snap.point); + return; + case kind::turntable_place: + case kind::turntable_exit: + case kind::turntable_fit: turntable_press(Intent); return; + case kind::select: + case kind::none: + { + auto *hovered{Intent.track}; + GfxRenderer->Pick_Node_Callback([this, hovered](scene::basic_node *node) { + if (false == viewport_click()) + return; + auto *track{hovered != nullptr ? hovered : dynamic_cast(node)}; + if (m_track_tab == track_tab::path) + { + m_track_set.clear(); + if (track == nullptr) + { + m_node = nullptr; + m_track_point = {}; + ui()->set_node(nullptr); + return; + } + } + select_track(track != nullptr ? track : node); + }); + return; + } + } +} + +void editor_mode::draw_track_intent() const +{ + using kind = track_intent::kind; + auto const &intent{m_intent}; + auto const &io{ImGui::GetIO()}; + if (intent.what == kind::none || intent.what == kind::gizmo || io.WantCaptureMouse) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList()}; + ImVec2 at; + if (projection.project(intent.position, at)) + { + switch (intent.what) + { + case kind::point: + case kind::straight_handle: + case kind::route_vertex: + case kind::route_grip: drawlist->AddCircle(at, 13.0f, overlay_color::marked, 24, 2.5f); break; + case kind::switch_at: + case kind::straight_tool: + drawlist->AddCircleFilled(at, 4.0f, overlay_color::grip); + drawlist->AddCircle(at, 11.0f, overlay_color::grip, 20, 2.0f); + break; + case kind::lay_end: + case kind::extend_end: + case kind::turntable_fit: drawlist->AddCircleFilled(at, 5.0f, IM_COL32(255, 60, 255, 255)); break; + case kind::turntable_exit: drawlist->AddCircle(at, 11.0f, overlay_color::grip, 20, 2.0f); break; + case kind::lay: drawlist->AddCircle(at, 6.0f, IM_COL32(255, 255, 255, 200), 12, 1.5f); break; + default: break; + } + } + if (intent.action.empty() || false == track_readout().empty()) + return; + std::string details; + auto const *track{m_hover.track}; + if (intent.what == kind::select && track != nullptr && track == intent.track && ImGui::GetTime() - m_hover.since >= kHoverLabelDelay) + { + auto const &path{track->m_paths[std::min(m_hover.path, track->m_paths.size() - 1)]}; + details = name_of(*track); + details += "\n" + std::string{STR_C(type_of(*track))}; + if (track->eType == tt_Switch) + details += m_hover.path == 0 ? STR(" (main)") : STR(" (branch)"); + details += format(" L %.1f m", bezier{path}.plan_length()); + details += path.radius != 0.0f ? format(" R %.0f m", std::abs(path.radius)) : std::string{" "} + STR_C("straight"); + auto const velocity{editor_track::velocity(*track)}; + details += velocity > 0.0 ? format(" V %.0f km/h", velocity) : std::string{}; + } + auto *foreground{ImGui::GetForegroundDrawList()}; + auto const actionsize{ImGui::CalcTextSize(intent.action.c_str())}; + auto const detailsize{details.empty() ? ImVec2(0.0f, 0.0f) : ImGui::CalcTextSize(details.c_str())}; + ImVec2 const size{std::max(actionsize.x, detailsize.x), actionsize.y + (details.empty() ? 0.0f : detailsize.y + 4.0f)}; + ImVec2 position{io.MousePos.x + 18.0f, io.MousePos.y + 18.0f}; + position.x = std::min(position.x, io.DisplaySize.x - size.x - 10.0f); + position.y = std::min(position.y, io.DisplaySize.y - size.y - 10.0f); + foreground->AddRectFilled(ImVec2(position.x - 6.0f, position.y - 4.0f), ImVec2(position.x + size.x + 6.0f, position.y + size.y + 4.0f), IM_COL32(0, 0, 0, 170), 4.0f); + foreground->AddText(position, overlay_color::marked, intent.action.c_str()); + if (false == details.empty()) + foreground->AddText(ImVec2(position.x, position.y + actionsize.y + 4.0f), IM_COL32(255, 255, 255, 235), details.c_str()); +} + +void editor_mode::track_context_press() +{ + auto &context{m_track_context}; + auto const &io{ImGui::GetIO()}; + context.armed = m_hover.track != nullptr; + context.target = m_hover; + context.pressed = std::chrono::steady_clock::now(); + context.angle = Camera.Angle; + context.mouse = {io.MousePos.x, io.MousePos.y}; +} + +void editor_mode::track_context_release() +{ + auto &context{m_track_context}; + if (false == context.armed) + return; + context.armed = false; + if (std::chrono::steady_clock::now() - context.pressed < kContextClick && glm::length(Camera.Angle - context.angle) < kContextTurn) + context.pending = true; +} + +void editor_mode::split_track_at(TTrack &Track, glm::dvec3 const &Point) +{ + auto const parameter{editor_track::nearest_parameter(Track, Point)}; + auto const before{editor_track::capture(Track)}; + if (auto *created{editor_track::split_path(Track, parameter)}) + { + push_track_snapshot({{&Track, before}}, {created}); + straight_refresh(); + } +} + +void editor_mode::focus_track(TTrack &Track, int const Path, glm::dvec3 const &Point) +{ + auto const &path{Track.m_paths[std::min(std::max(Path, 0), Track.m_paths.size() - 1)]}; + bezier const curve{path}; + glm::dvec3 look{curve.first(std::clamp(editor_track::nearest_parameter(Track, Point), 0.0, 1.0))}; + look.y = 0.0; + look = glm::length(look) > 1e-6 ? glm::normalize(look) : glm::dvec3(0.0, 0.0, 1.0); + if (glm::dot(Camera.Pos - Point, look) > 0.0) + look = -look; + look = glm::normalize(look - glm::dvec3(0.0, 0.25, 0.0)); + m_focus_start_pos = Camera.Pos; + m_focus_start_angle = Camera.Angle; + m_focus_target_pos = Point - look * kFocusDistance; + 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; +} + +void editor_mode::render_track_context() +{ + auto &context{m_track_context}; + auto *track{context.target.track}; + if (context.pending) + { + context.pending = false; + if (track != nullptr && false == track->m_editorremoved) + { + select_track(track); + ImGui::OpenPopup("##trackcontext"); + ImGui::SetNextWindowPos(ImVec2(context.mouse.x, context.mouse.y)); + } + } + if (false == ImGui::BeginPopup("##trackcontext")) + return; + if (track == nullptr || track->m_editorremoved) + { + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + return; + } + auto const point{context.target.point}; + bool const normal{track->eType == tt_Normal && editor_track::is_supported(*track)}; + ImGui::TextDisabled("%s (%s, %.1f m)", name_of(*track).c_str(), STR_C(type_of(*track)), track->Length()); + ImGui::Separator(); + if (ImGui::MenuItem(STR_C("Split here"), "K", false, normal)) + split_track_at(*track, point); + ImGui::Separator(); + if (ImGui::MenuItem(STR_C("Switch"), "T", false, normal || track->eType == tt_Switch)) + show_track_tab(track_tab::turnout); + if (ImGui::MenuItem(STR_C("Whole straight"), "G", false, normal && editor_track::is_straight(*track, m_straights.tolerance))) + show_track_tab(track_tab::straights); + if (ImGui::MenuItem(STR_C("Curve"), "C", false, normal)) + show_track_tab(track_tab::route); + if (ImGui::MenuItem(STR_C("Parallel track"), nullptr, false, normal)) + { + show_track_tab(track_tab::lineside); + m_parallel.expand = true; + } + if (ImGui::MenuItem(STR_C("Model along the track"))) + { + show_track_tab(track_tab::lineside); + m_sweep.expand = true; + } + if (ImGui::MenuItem(STR_C("Place a vehicle here"))) + { + show_track_tab(track_tab::lineside); + vehicle_start(*track, point); + } + if (ImGui::MenuItem(STR_C("Hectometre posts from here"))) + { + show_track_tab(track_tab::lineside); + hekto_start(*track, point); + } + if (ImGui::MenuItem(STR_C("Fouling point markers"), nullptr, false, track->eType == tt_Switch)) + { + show_track_tab(track_tab::lineside); + m_fouling.scope = 0; + m_fouling.dirty = true; + m_fouling.expand = true; + } + if (ImGui::MenuItem(STR_C("Vertical profile of the line"), "P")) + { + show_track_tab(track_tab::profile); + profile_open_run(*track); + } + if (ImGui::MenuItem(STR_C("Speed check of the line"), "V")) + { + m_speed.scope = 0; + show_track_tab(track_tab::speed); + speed_start(); + } + ImGui::Separator(); + if (ImGui::MenuItem(in_track_set(track) ? STR_C("Take out of the selection") : STR_C("Add to the selection"), "Shift+LMB")) + track_set_toggle(*track); + if (ImGui::MenuItem(STR_C("Look at it"))) + focus_track(*track, context.target.path, point); + if (ImGui::MenuItem(STR_C("Copy the name"), nullptr, false, false == track->name().empty())) + ImGui::SetClipboardText(track->name().c_str()); + ImGui::Separator(); + if (ImGui::MenuItem(STR_C("Delete"), "Del")) + delete_selected_track(); + ImGui::EndPopup(); +} + +void editor_mode::render_track_search() +{ + auto &search{m_track_search}; + if (search.focus) + { + ImGui::SetKeyboardFocusHere(); + search.focus = false; + } + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputTextWithHint("##findtrack", STR_C("Find a track by its name (Ctrl+F)"), search.text, sizeof(search.text)); + std::string const text{lower(search.text)}; + if (text.empty()) + { + search.searched.clear(); + search.found.clear(); + return; + } + if (text != search.searched) + { + search.searched = text; + search.found.clear(); + search.total = 0; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == rail(*track) || lower(track->name()).find(text) == std::string::npos) + continue; + ++search.total; + if (search.found.size() < kSearchLimit) + search.found.push_back(track); + } + } + if (search.found.empty()) + { + ImGui::TextDisabled("%s", STR_C("No track of that name")); + return; + } + if (search.total > search.found.size()) + ImGui::TextDisabled(STR_C("%zu found, the first %zu listed"), search.total, search.found.size()); + auto const rows{std::min(search.found.size(), 8)}; + ImGui::BeginChild("##foundtracks", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * static_cast(rows) + 6.0f), true); + for (std::size_t i = 0; i < search.found.size(); ++i) + { + auto *track{search.found[i]}; + if (track->m_editorremoved) + continue; + auto const label{format("%s##found%zu", name_of(*track).c_str(), i)}; + if (ImGui::Selectable(label.c_str(), track == selected_track())) + { + m_node = track; + m_track_point = {}; + ui()->set_node(m_node); + focus_track(*track, 0, bezier{track->m_paths.front()}.point(0.5)); + } + ImGui::SameLine(ImGui::GetWindowContentRegionMax().x * 0.6f); + ImGui::TextDisabled("%s, %.1f m", STR_C(type_of(*track)), track->Length()); + } + ImGui::EndChild(); +} + +char const *editor_mode::typed_meaning() const +{ + auto const &straights{m_straights}; + if (m_extend.active) + return m_extend_freehand && false == ImGui::GetIO().KeyCtrl ? "type the radius, a space, the length" : "type the length"; + if (straights.dragging) + return straights.handle == 2 ? "type the shift, + to the right" : "type the length"; + if (straights.tool == 1 && (straights.tool_mouse || straights.tool_dragging)) + return "type the turn in degrees, + to the right"; + if (m_lay.active && false == m_lay.points.empty()) + return "type the length of the next leg, Enter adds the point"; + return nullptr; +} + +std::vector editor_mode::typed_values() const +{ + std::vector values; + std::string text{m_typed}; + std::replace(text.begin(), text.end(), ',', '.'); + std::size_t at{0}; + while (at < text.size()) + { + auto const end{text.find(' ', at)}; + auto const word{text.substr(at, end == std::string::npos ? std::string::npos : end - at)}; + if (false == word.empty() && word != "-" && word != ".") + values.push_back(std::strtod(word.c_str(), nullptr)); + if (end == std::string::npos) + break; + at = end + 1; + } + return values; +} + +bool editor_mode::typed_key(int const Key) +{ + if (typed_meaning() == nullptr) + { + m_typed.clear(); + return false; + } + char character{'\0'}; + if (Key >= GLFW_KEY_0 && Key <= GLFW_KEY_9) + character = static_cast('0' + (Key - GLFW_KEY_0)); + else if (Key >= GLFW_KEY_KP_0 && Key <= GLFW_KEY_KP_9) + character = static_cast('0' + (Key - GLFW_KEY_KP_0)); + else if (Key == GLFW_KEY_PERIOD || Key == GLFW_KEY_COMMA || Key == GLFW_KEY_KP_DECIMAL) + character = '.'; + else if (Key == GLFW_KEY_MINUS || Key == GLFW_KEY_KP_SUBTRACT) + character = '-'; + else if (Key == GLFW_KEY_SPACE) + character = ' '; + else if (Key == GLFW_KEY_BACKSPACE && false == m_typed.empty()) + { + m_typed.pop_back(); + return true; + } + else + return false; + auto const start{m_typed.empty() || m_typed.back() == ' '}; + if ((character == '-' && false == start) || (character == ' ' && start) || (character == '.' && m_typed.substr(m_typed.rfind(' ') == std::string::npos ? 0 : m_typed.rfind(' ')).find('.') != std::string::npos)) + return true; + if (m_typed.size() < 24) + m_typed += character; + return true; +} + +void editor_mode::typed_extend() +{ + auto const values{typed_values()}; + if (values.empty()) + return; + auto &tool{m_extend}; + auto const &direction{tool.direction}; + if (false == m_extend_freehand || ImGui::GetIO().KeyCtrl) + { + auto const length{std::max(0.0, values[0])}; + tool.mouse = {tool.point.x + direction.x * length, tool.mouse.y, tool.point.z + direction.y * length}; + return; + } + auto const radius{std::abs(values[0])}; + if (radius < 1.0) + return; + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + double const side{geometry::cross(direction, offset) >= 0.0 ? 1.0 : -1.0}; + glm::dvec2 const point{tool.point.x, tool.point.z}; + glm::dvec2 const centre{point + glm::dvec2{-direction.y, direction.x} * (side * radius)}; + auto const radial{point - centre}; + auto angle{values.size() > 1 ? std::abs(values[1]) / radius : side * geometry::signed_angle(radial, glm::dvec2{tool.mouse.x, tool.mouse.z} - centre)}; + if (angle < 0.0) + angle += 2.0 * glm::pi(); + angle = std::clamp(angle, 1e-3, 2.0 * glm::pi() - 0.1); + auto const end{centre + geometry::turned(radial, side * angle)}; + tool.mouse = {end.x, tool.mouse.y, end.y}; +} + +glm::dvec3 editor_mode::typed_lay(glm::dvec3 const &Mouse) const +{ + auto const values{typed_values()}; + if (values.empty() || m_lay.points.empty() || values[0] <= 0.0) + return Mouse; + auto const &last{m_lay.points.back()}; + glm::dvec2 heading{Mouse.x - last.x, Mouse.z - last.z}; + if (glm::length(heading) < 1e-6) + return Mouse; + heading = glm::normalize(heading) * values[0]; + return {last.x + heading.x, Mouse.y, last.z + heading.y}; +} + +void editor_mode::typed_turn() +{ + auto &state{m_straights}; + auto const values{typed_values()}; + if (values.empty() || state.tool != 1) + return; + auto const reach{std::max(1.0, glm::length(glm::dvec2{state.tool_handle.x - state.tool_point.x, state.tool_handle.z - state.tool_point.z}))}; + auto const heading{geometry::turned(state.tool_line.direction, glm::radians(values[0])) * reach}; + state.tool_handle = {state.tool_point.x + heading.x, state.tool_handle.y, state.tool_point.z + heading.y}; +} + +bool editor_mode::in_track_set(TTrack const *Track) const +{ + return std::find(m_track_set.begin(), m_track_set.end(), Track) != m_track_set.end(); +} + +void editor_mode::track_set_prune() +{ + m_track_set.erase(std::remove_if(m_track_set.begin(), m_track_set.end(), [](TTrack const *Track) { return Track == nullptr || Track->m_editorremoved; }), m_track_set.end()); +} + +void editor_mode::track_set_toggle(TTrack &Track) +{ + track_set_prune(); + if (m_track_set.empty()) + if (auto *selected{selected_track()}; selected != nullptr && selected != &Track) + m_track_set.push_back(selected); + if (auto const found{std::find(m_track_set.begin(), m_track_set.end(), &Track)}; found != m_track_set.end()) + m_track_set.erase(found); + else + m_track_set.push_back(&Track); + m_node = m_track_set.empty() ? nullptr : m_track_set.back(); + m_track_point = {}; + ui()->set_node(m_node); +} + +void editor_mode::start_track_box() +{ + auto const &io{ImGui::GetIO()}; + m_track_box.active = true; + m_track_box.from = m_track_box.to = {io.MousePos.x, io.MousePos.y}; + m_track_box.hovered = m_hover.track; +} + +void editor_mode::finish_track_box() +{ + auto &box{m_track_box}; + if (false == box.active) + return; + box.active = false; + auto const &io{ImGui::GetIO()}; + box.to = {io.MousePos.x, io.MousePos.y}; + if (glm::length(box.to - box.from) < 6.0f) + { + if (box.hovered != nullptr && false == box.hovered->m_editorremoved) + track_set_toggle(*box.hovered); + return; + } + glm::vec2 const low{std::min(box.from.x, box.to.x), std::min(box.from.y, box.to.y)}; + glm::vec2 const high{std::max(box.from.x, box.to.x), std::max(box.from.y, box.to.y)}; + screen_projection const projection; + glm::dvec3 const camera{Global.pCamera.Pos}; + track_set_prune(); + if (m_track_set.empty()) + if (auto *selected{selected_track()}; selected != nullptr) + m_track_set.push_back(selected); + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == rail(*track) || in_track_set(track) || glm::distance(track->location(), camera) > static_cast(Global.BaseDrawRange)) + continue; + bool inside{false}; + for (auto const &path : track->m_paths) + { + bezier const curve{path}; + for (int k = 0; k <= 8 && false == inside; ++k) + { + ImVec2 screen; + if (projection.project(curve.point(k / 8.0), screen)) + inside = screen.x >= low.x && screen.x <= high.x && screen.y >= low.y && screen.y <= high.y; + } + if (inside) + break; + } + if (inside) + m_track_set.push_back(track); + } + if (false == m_track_set.empty()) + { + m_node = m_track_set.back(); + m_track_point = {}; + ui()->set_node(m_node); + } +} + +std::vector editor_mode::track_spread_from(TTrack &Start) const +{ + auto const &rules{m_spread}; + auto const bed = [](TTrack const &Track) { return Track.eType == tt_Switch ? (Track.SwitchExtension ? Track.SwitchExtension->m_material3 : null_handle) : Track.m_material2; }; + auto const startspeed{editor_track::velocity(Start)}; + auto const alike = [&](TTrack const &Track) { + if (rules.same_rails && Track.m_material1 != Start.m_material1) + return false; + if (rules.same_bed && bed(Track) != bed(Start)) + return false; + if (rules.same_speed && std::abs(editor_track::velocity(Track) - startspeed) > 0.5) + return false; + return true; + }; + auto const exits = [&](TTrack const &Track) { + std::vector result; + if (Track.m_paths.empty()) + return result; + auto const &main{Track.m_paths.front()}; + result = {main.points[segment_data::point::start], main.points[segment_data::point::end]}; + if (Track.eType == tt_Switch && rules.stop == 2) + for (std::size_t i = 1; i < Track.m_paths.size(); ++i) + result.push_back(Track.m_paths[i].points[segment_data::point::end]); + return result; + }; + std::size_t constexpr limit{20000}; + std::vector result; + std::unordered_map reached{{&Start, 0.0}}; + std::unordered_set done; + using entry = std::pair; + std::priority_queue, std::greater> queue; + queue.push({0.0, &Start}); + while (false == queue.empty() && result.size() < limit) + { + auto const [distance, track] = queue.top(); + queue.pop(); + if (false == done.insert(track).second) + continue; + result.push_back(track); + auto const further{distance + track->Length()}; + if (rules.reach > 0.f && further > static_cast(rules.reach)) + continue; + for (auto const &end : exits(*track)) + for (auto const &[other, point] : editor_track::connected_points(*track, end)) + { + if (other == nullptr || done.count(other) > 0 || false == rail(*other) || other->eType == tt_Table || false == alike(*other)) + continue; + if (other->eType == tt_Switch && (rules.stop == 0 || (rules.stop == 1 && point.path != 0))) + continue; + auto const known{reached.find(other)}; + if (known != reached.end() && known->second <= further) + continue; + reached[other] = further; + queue.push({further, other}); + } + } + return result; +} + +std::vector const &editor_mode::track_spread_preview(TTrack &Start) const +{ + auto &spread{m_spread}; + auto const signature{spread.stop | (spread.same_rails ? 4 : 0) | (spread.same_bed ? 8 : 0) | (spread.same_speed ? 16 : 0) | (static_cast(spread.reach) << 5)}; + if (spread.from != &Start || spread.signature != signature) + { + spread.from = &Start; + spread.signature = signature; + spread.preview = track_spread_from(Start); + spread.length = 0.0; + for (auto const *track : spread.preview) + spread.length += track->Length(); + } + return spread.preview; +} + +void editor_mode::track_spread_apply(TTrack &Start) +{ + track_set_prune(); + if (m_track_set.empty()) + if (auto *selected{selected_track()}; selected != nullptr && selected != &Start) + m_track_set.push_back(selected); + auto const taken{track_spread_from(Start)}; + if (in_track_set(&Start)) + { + std::unordered_set const out(taken.begin(), taken.end()); + m_track_set.erase(std::remove_if(m_track_set.begin(), m_track_set.end(), [&](TTrack const *Track) { return out.count(Track) > 0; }), m_track_set.end()); + } + else + for (auto *track : taken) + if (false == in_track_set(track)) + m_track_set.push_back(track); + m_spread.from = nullptr; + m_node = m_track_set.empty() ? nullptr : m_track_set.back(); + m_track_point = {}; + ui()->set_node(m_node); +} + +void editor_mode::track_set_grow() +{ + track_set_prune(); + auto const reach{m_spread.reach}; + std::vector added; + for (auto *track : m_track_set) + { + m_spread.reach = std::max(0.01f, static_cast(track->Length()) - 0.01f); + for (auto *other : track_spread_from(*track)) + if (false == in_track_set(other) && std::find(added.begin(), added.end(), other) == added.end()) + added.push_back(other); + } + m_spread.reach = reach; + m_spread.from = nullptr; + m_track_set.insert(m_track_set.end(), added.begin(), added.end()); +} + +void editor_mode::render_track_spread_ui() +{ + auto &spread{m_spread}; + if (false == ImGui::CollapsingHeader(STR_C("Spread along the joins: Ctrl+Shift+LMB on a path"))) + return; + char const *const stops[] = {STR_C("Stop before the switches"), STR_C("Through the switches, along their main track"), STR_C("Through everything joined")}; + for (int i = 0; i < IM_ARRAYSIZE(stops); ++i) + ImGui::RadioButton(stops[i], &spread.stop, i); + ImGui::TextUnformatted(STR_C("Only to the paths like the clicked one:")); + ImGui::SameLine(); + ImGui::Checkbox(STR_C("rails"), &spread.same_rails); + ImGui::SameLine(); + ImGui::Checkbox(STR_C("trackbed"), &spread.same_bed); + ImGui::SameLine(); + ImGui::Checkbox(STR_C("speed"), &spread.same_speed); + ImGui::SetNextItemWidth(110.f); + ImGui::InputFloat(STR_C("Reach along the track, m (0: no limit)"), &spread.reach, 50.f, 500.f, "%.0f"); + spread.reach = std::clamp(spread.reach, 0.f, 100000.f); + ImGui::TextDisabled("%s", STR_C("Holding Ctrl+Shift shows what the click takes; on a path of the set it takes them out")); +} + +void editor_mode::draw_track_spread() const +{ + if (m_intent.what != track_intent::kind::spread || m_spread.from == nullptr) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList()}; + auto const removing{in_track_set(m_spread.from)}; + auto const colour{removing ? IM_COL32(255, 90, 70, 220) : IM_COL32(90, 220, 255, 220)}; + for (auto const *track : m_spread.preview) + { + if (track->m_editorremoved) + continue; + for (auto const &path : track->m_paths) + { + bezier const curve{path}; + auto const count{std::clamp(static_cast(curve.plan_length() / 4.0), 4, 48)}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= count; ++k) + { + auto const next{curve.point(static_cast(k) / count)}; + projection.line(drawlist, previous, next, colour, 2.5f); + previous = next; + } + } + } +} + +void editor_mode::draw_track_set() const +{ + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList()}; + if (m_track_box.active) + { + auto const &io{ImGui::GetIO()}; + ImVec2 const from{m_track_box.from.x, m_track_box.from.y}; + drawlist->AddRectFilled(from, io.MousePos, IM_COL32(255, 160, 40, 40)); + drawlist->AddRect(from, io.MousePos, IM_COL32(255, 160, 40, 220), 0.0f, 0, 1.5f); + } + if (m_track_set.size() < 2) + return; + for (auto const *track : m_track_set) + { + if (track->m_editorremoved) + continue; + for (auto const &path : track->m_paths) + { + bezier const curve{path}; + auto const count{std::clamp(static_cast(curve.plan_length() / 3.0), 6, 64)}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= count; ++k) + { + auto const next{curve.point(static_cast(k) / count)}; + projection.line(drawlist, previous, next, IM_COL32(255, 160, 40, 230), 3.0f); + previous = next; + } + } + } +} + +void editor_mode::track_set_apply(std::function const &Change, bool const Rebuild, char const *What) +{ + track_set_prune(); + std::vector> states; + std::vector changed; + std::size_t locked{0}; + for (auto *track : m_track_set) + { + if (false == scene::Layers.editable(track)) + { + ++locked; + continue; + } + auto const before{editor_track::capture(*track)}; + if (false == Change(*track)) + continue; + states.emplace_back(track, before); + changed.push_back(track); + } + if (changed.empty()) + return; + push_track_snapshot(std::move(states)); + for (auto *track : changed) + { + if (Rebuild) + editor_track::commit_parameters(*track); + else + track->mark_dirty(); + } + auto status{format(STR_C("%s set on %zu paths, Ctrl+Z takes it back"), STR_C(What), changed.size())}; + if (locked > 0) + status += format(STR_C(", %zu in the files which can't be changed are left as they are"), locked); + ui()->set_status(status, false); +} + +void editor_mode::track_set_delete() +{ + track_set_prune(); + std::vector removed; + std::size_t kept{0}; + for (auto *track : m_track_set) + { + if (false == track->Dynamics.empty() || false == scene::Layers.editable(track)) + { + ++kept; + continue; + } + editor_track::retire(*track); + removed.push_back(track); + if (std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) != m_route.chain.tracks.end()) + m_route = {}; + } + push_track_snapshot({}, {}, removed); + m_track_set.clear(); + m_node = nullptr; + ui()->set_node(nullptr); + m_track_point = {}; + straight_refresh(); + auto status{format(STR_C("%zu paths deleted, Ctrl+Z brings them back"), removed.size())}; + if (kept > 0) + status += format(STR_C(", %zu with vehicles on them or in the files which can't be changed are left"), kept); + ui()->set_status(status, false); +} + +void editor_mode::render_track_set_ui() +{ + track_set_prune(); + if (m_track_set.size() < 2) + return; + auto &fields{m_track_set_fields}; + double length{0.0}; + for (auto const *track : m_track_set) + length += track->Length(); + ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.2f, 1.0f), STR_C("%zu paths selected, %.1f m"), m_track_set.size(), length); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Clear"))) + { + m_track_set.clear(); + return; + } + ImGui::TextDisabled("%s", STR_C("The gizmo moves them together; Shift+LMB adds or takes out a path")); + if (ImGui::SmallButton(STR_C("Grow by the neighbours"))) + track_set_grow(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Adds the paths joined to the set, by the rules of the spread")); + auto const button_width{90.0f}; + ImGui::PushItemWidth(-button_width - ImGui::GetStyle().ItemSpacing.x); + ImGui::InputDouble("##setvelocity", &fields.velocity, 0.0, 0.0, "%.0f"); + ImGui::SameLine(); + if (ImGui::Button(format("%s##setv", STR_C("Velocity")).c_str(), ImVec2(button_width, 0.0f))) + { + auto const velocity{fields.velocity}; + track_set_apply([velocity](TTrack &Track) { editor_track::velocity(Track, velocity); return true; }, false, "Velocity"); + } + ImGui::InputTextWithHint("##setrail", STR_C("texture of the rails"), fields.rail, sizeof(fields.rail)); + ImGui::SameLine(); + if (ImGui::Button(format("%s##setr", STR_C("Rails")).c_str(), ImVec2(button_width, 0.0f)) && fields.rail[0] != '\0') + { + auto const material{editor_track::fetch_material(fields.rail)}; + track_set_apply([material](TTrack &Track) { + Track.m_material1 = material; + if (Track.eType == tt_Switch) + Track.m_material2 = material; + return true; + }, true, "Rails"); + } + ImGui::InputTextWithHint("##settrackbed", STR_C("texture of the trackbed"), fields.trackbed, sizeof(fields.trackbed)); + ImGui::SameLine(); + if (ImGui::Button(format("%s##setb", STR_C("Trackbed")).c_str(), ImVec2(button_width, 0.0f)) && fields.trackbed[0] != '\0') + { + auto const material{editor_track::fetch_material(fields.trackbed)}; + track_set_apply([material](TTrack &Track) { + if (Track.eType == tt_Switch) + { + if (Track.SwitchExtension == nullptr) + return false; + Track.SwitchExtension->m_material3 = material; + } + else + Track.m_material2 = material; + return true; + }, true, "Trackbed"); + } + ImGui::InputTextWithHint("##setcircuits", STR_C("track circuits, separated by spaces"), fields.circuits, sizeof(fields.circuits)); + ImGui::SameLine(); + if (ImGui::Button(format("%s##setc", STR_C("Circuits")).c_str(), ImVec2(button_width, 0.0f))) + { + std::vector names; + std::string const text{fields.circuits}; + std::size_t at{0}; + while (at < text.size()) + { + auto const end{text.find(' ', at)}; + auto const word{text.substr(at, end == std::string::npos ? std::string::npos : end - at)}; + if (false == word.empty()) + names.push_back(word); + if (end == std::string::npos) + break; + at = end + 1; + } + track_set_apply([names](TTrack &Track) { editor_track::isolated(Track, names); return true; }, false, "Circuits"); + } + char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"}; + ImGui::Combo("##setenvironment", &fields.environment, environments, IM_ARRAYSIZE(environments)); + ImGui::SameLine(); + if (ImGui::Button(format("%s##sete", STR_C("Environment")).c_str(), ImVec2(button_width, 0.0f))) + { + auto const environment{static_cast(fields.environment)}; + track_set_apply([environment](TTrack &Track) { Track.eEnvironment = environment; return true; }, false, "Environment"); + } + ImGui::PopItemWidth(); + if (ImGui::Button(format(STR_C("Delete %zu paths"), m_track_set.size()).c_str())) + track_set_delete(); + ImGui::Separator(); +} + +void editor_mode::render_track_set_gizmo() +{ + track_set_prune(); + if (m_track_set.size() < 2) + { + m_track_set_using = false; + return; + } + gizmo_frame const frame; + if (false == m_track_set_using) + { + glm::dvec3 centre{0.0}; + for (auto const *track : m_track_set) + centre += editor_track::pivot(*track); + m_track_set_pivot = centre / static_cast(m_track_set.size()); + m_track_set_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(m_track_set_pivot - frame.camera)); + } + glm::mat4 delta(1.0f); + frame.manipulate(ImGuizmo::TRANSLATE, m_track_set_gizmo, m_gizmo_snap, &delta); + if (ImGuizmo::IsUsing()) + { + if (false == m_track_set_using) + { + std::string reason; + if (false == tracks_editable(m_track_set, reason)) + { + ui()->set_status(reason, true); + return; + } + m_track_set_using = true; + m_track_drag = m_track_set; + m_track_set_joints.clear(); + for (auto *track : m_track_set) + { + auto joints{editor_track::joints(*track)}; + joints.erase(std::remove_if(joints.begin(), joints.end(), [&](editor_track::joint const &Joint) { return in_track_set(Joint.other); }), joints.end()); + for (auto const &joint : joints) + if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end()) + m_track_drag.push_back(joint.other); + m_track_set_joints.emplace_back(track, std::move(joints)); + } + std::vector> states; + for (auto *track : m_track_drag) + states.emplace_back(track, editor_track::capture(*track)); + push_track_snapshot(std::move(states)); + } + glm::dvec3 const offset{delta[3]}; + if (glm::length(offset) > 1e-9) + { + for (auto &entry : m_track_set_joints) + { + editor_track::translate(*entry.first, offset); + editor_track::follow(*entry.first, entry.second); + } + m_track_dirty = true; + } + commit_track_drag(false); + } + else if (m_track_set_using) + { + commit_track_drag(true); + m_track_set_using = false; + m_track_drag.clear(); + m_track_set_joints.clear(); + straight_refresh(); + } +} + +void editor_mode::render_lineside_ui() +{ + struct tab + { + char const *label; + bool *open; + bool *expand; + std::function render; + }; + std::array const tabs{{ + {"Platforms and models", &m_sweep.open, &m_sweep.expand, [this]() { render_sweep_ui(); }}, + {"Hectometre posts", &m_hekto.open, &m_hekto.expand, [this]() { render_hekto_ui(); }}, + {"Fouling points", &m_fouling.open, &m_fouling.expand, [this]() { render_fouling_ui(); }}, + {"Parallel track", &m_parallel.open, &m_parallel.expand, [this]() { render_parallel_ui(); }}, + {"Vehicle", &m_vehicle.open, &m_vehicle.expand, [this]() { render_vehicle_ui(); }}, + }}; + if (selected_track() == nullptr) + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("LMB on a track: the line the objects go along")); + if (false == ImGui::BeginTabBar("##lineside")) + return; + for (int i = 0; i < static_cast(tabs.size()); ++i) + { + auto const &entry{tabs[i]}; + auto const wanted{*entry.expand}; + *entry.expand = false; + *entry.open = ImGui::BeginTabItem(STR_C(entry.label), nullptr, wanted ? ImGuiTabItemFlags_SetSelected : 0); + if (false == *entry.open) + continue; + m_lineside_tab = i; + entry.render(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); +} diff --git a/application/editormodeturntable.cpp b/application/editormodeturntable.cpp new file mode 100644 index 00000000..f9a64c29 --- /dev/null +++ b/application/editormodeturntable.cpp @@ -0,0 +1,1025 @@ +/* +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/editorprojection.h" +#include "application/editoruilayer.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" +#include "model/AnimModel.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/translation.h" +#include "utilities/utilities.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::arc_pieces; +using geometry::plan_distance; +using geometry::plan_of; +using geometry::signed_angle; + +double constexpr kRimTolerance{0.05}; +double constexpr kMinimumRadius{80.0}; +float constexpr kDragPixels{5.0f}; + +glm::dvec2 heading_vector(double const Heading) +{ + return {std::sin(glm::radians(Heading)), std::cos(glm::radians(Heading))}; +} + +double heading_of(glm::dvec2 const &Direction) +{ + return glm::degrees(std::atan2(Direction.x, Direction.y)); +} + +double circular(double Angle) +{ + Angle = std::fmod(Angle, 360.0); + if (Angle <= -180.0) + Angle += 360.0; + if (Angle > 180.0) + Angle -= 360.0; + return Angle; +} + +double line_angle(double const Angle) +{ + auto result{circular(Angle)}; + if (result >= 90.0) + result -= 180.0; + if (result < -90.0) + result += 180.0; + return result; +} + +bool numeric(std::string const &Text, double &Value) +{ + char *end{nullptr}; + Value = std::strtod(Text.c_str(), &end); + return false == Text.empty() && *end == '\0'; +} + +std::string normalized(std::string File) +{ + std::replace(File.begin(), File.end(), '\\', '/'); + return ToLower(File); +} + +std::vector fixed_angles(std::string const &File) +{ + std::vector result; + std::ifstream input{Global.asCurrentSceneryPath + File, std::ios_base::binary}; + if (false == input.is_open()) + return result; + std::string const content{std::istreambuf_iterator(input), std::istreambuf_iterator()}; + static std::regex const pattern{R"(event\s+\S*tor(\d+)rot\s+animation\s+\S+\s+\S+\s+rotate\s+\S+\s+\S+\s+\S+\s+(\S+))", std::regex::icase}; + for (auto match{std::sregex_iterator(content.begin(), content.end(), pattern)}; match != std::sregex_iterator(); ++match) + { + double angle; + if (numeric((*match)[2].str(), angle) && std::none_of(result.begin(), result.end(), [&](double const Angle) { return std::abs(line_angle(Angle - angle)) < 0.01; })) + result.push_back(angle); + } + return result; +} + +glm::dvec3 centre_of(TTrack const &Table) +{ + auto const &path{Table.m_paths.front()}; + return glm::mix(path.points[segment_data::point::start], path.points[segment_data::point::end], 0.5); +} + +double length_of(TTrack const &Table) +{ + auto const &path{Table.m_paths.front()}; + return plan_distance(path.points[segment_data::point::start], path.points[segment_data::point::end]); +} + +struct rim_end +{ + double heading{0.0}; + TTrack *track{nullptr}; +}; + +std::vector rim_ends(TTrack const *Table, glm::dvec3 const &Centre, double const Rim) +{ + std::vector result; + for (auto *section : simulation::Region->sections(Centre, static_cast(Rim + 10.0))) + for (auto const &cell : section->m_cells) + for (auto *other : cell.m_directories.paths) + { + if (other == Table || other->m_editorremoved || (other->iCategoryFlag & 15) != 1 || other->eType == tt_Table) + continue; + for (auto const &path : other->m_paths) + for (auto const &point : {path.points[segment_data::point::start], path.points[segment_data::point::end]}) + { + if (std::abs(plan_distance(point, Centre) - Rim) > kRimTolerance || std::abs(point.y - Centre.y) > 0.5) + continue; + auto const heading{heading_of(plan_of(point - Centre))}; + if (std::none_of(result.begin(), result.end(), [&](rim_end const &End) { return End.track == other && std::abs(circular(End.heading - heading)) < 0.01; })) + result.push_back({heading, other}); + } + } + return result; +} + +void draw_circle(screen_projection const &Projection, ImDrawList *Drawlist, glm::dvec3 const &Centre, double const Radius, ImU32 const Colour, float const Thickness) +{ + int const count{48}; + glm::dvec3 previous{Centre.x, Centre.y, Centre.z + Radius}; + for (int i = 1; i <= count; ++i) + { + auto const angle{glm::two_pi() * i / count}; + glm::dvec3 const next{Centre.x + Radius * std::sin(angle), Centre.y, Centre.z + Radius * std::cos(angle)}; + Projection.line(Drawlist, previous, next, Colour, Thickness); + previous = next; + } +} + +void draw_pieces(screen_projection const &Projection, ImDrawList *Drawlist, std::vector const &Pieces, ImU32 const Colour, float const Thickness) +{ + for (auto const &piece : Pieces) + { + geometry::bezier const curve{piece}; + auto const count{std::clamp(static_cast(curve.plan_length() / 2.0), 2, 48)}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= count; ++k) + { + auto const next{curve.point(static_cast(k) / count)}; + Projection.line(Drawlist, previous, next, Colour, Thickness); + previous = next; + } + } +} + +void label(screen_projection const &Projection, glm::dvec3 const &Point, std::string const &Text) +{ + ImVec2 at; + if (false == Projection.project(Point, at)) + return; + auto *drawlist{ImGui::GetForegroundDrawList()}; + auto const size{ImGui::CalcTextSize(Text.c_str())}; + drawlist->AddRectFilled(ImVec2(at.x + 8.f, at.y - 4.f), ImVec2(at.x + 16.f + size.x, at.y + size.y + 4.f), IM_COL32(0, 0, 0, 170), 4.f); + drawlist->AddText(ImVec2(at.x + 12.f, at.y), IM_COL32(255, 255, 255, 235), Text.c_str()); +} + +} // namespace + +void editor_mode::scan_turntables() +{ + auto &tool{m_turntable}; + if (tool.scanned) + return; + tool.scanned = true; + tool.templates.clear(); + if (false == EditorIncludes.scanned()) + EditorIncludes.scan(); + for (auto const &entry : EditorIncludes.entries()) + { + if (entry.category != "turntable") + continue; + include_info info; + std::string error; + if (false == editor_includes::load(entry.file, info, error) || info.turntable_length <= 0.0) + continue; + turntable_template item; + item.file = entry.file; + item.name = info.name.empty() ? entry.file : info.name; + item.length = info.turntable_length; + item.track = editor_includes::parameter_with_role(info, "track"); + item.yaw = editor_includes::parameter_with_role(info, "rot.y"); + if (info.turntable_angles > 0 && info.turntable_positions > 0) + { + item.angles = info.turntable_angles; + item.positions = info.turntable_positions; + } + else + item.fixed = fixed_angles(entry.file); + if (item.track == 0 || item.yaw == 0 || (item.angles == 0 && item.fixed.empty())) + continue; + tool.templates.push_back(std::move(item)); + } + std::stable_sort(tool.templates.begin(), tool.templates.end(), [](turntable_template const &A, turntable_template const &B) { return (A.angles > 0) != (B.angles > 0) ? A.angles > 0 : A.name < B.name; }); + tool.chosen = tool.templates.empty() ? -1 : 0; +} + +std::string editor_mode::turntable_name_for_new() const +{ + if (m_turntable.name[0] != '\0') + return ToLower(m_turntable.name); + for (int i = 1;; ++i) + { + auto const name{"obr" + std::to_string(i)}; + if (simulation::Paths.find(name) == nullptr && simulation::Instances.find(name) == nullptr) + return name; + } +} + +bool editor_mode::turntable_read(TTrack *Table) +{ + auto &tool{m_turntable}; + if (Table == nullptr || Table->eType != tt_Table || Table->m_editorremoved || Table->m_paths.empty()) + { + tool.table = nullptr; + tool.include = 0; + tool.kind = -1; + tool.exits.clear(); + tool.fits.clear(); + return false; + } + if (tool.table != Table) + { + tool.fits.clear(); + tool.hovered = -1; + tool.status.clear(); + tool.error.clear(); + } + scan_turntables(); + tool.table = Table; + tool.length = length_of(*Table); + auto const &path{Table->m_paths.front()}; + tool.table_yaw = heading_of(plan_of(path.points[segment_data::point::start] - centre_of(*Table))); + tool.include = 0; + tool.kind = -1; + tool.exits.clear(); + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count() && tool.include == 0; ++handle) + { + if (false == scene::Layers.tracked(handle)) + continue; + auto const &included{scene::Layers.instance(handle)}; + if (included.removed || included.dead || included.file == nullptr) + continue; + auto const file{normalized(*included.file)}; + auto const kind{std::find_if(tool.templates.begin(), tool.templates.end(), [&](turntable_template const &Template) { return normalized(Template.file) == file; })}; + if (kind == tool.templates.end()) + continue; + std::string target; + std::vector values; + if (false == editor_includes::parse_directive(scene::Layers.directive(handle), target, values) || kind->track > static_cast(values.size()) || ToLower(values[kind->track - 1]) != Table->name()) + continue; + tool.include = handle; + tool.kind = static_cast(kind - tool.templates.begin()); + if (double yaw; kind->yaw <= static_cast(values.size()) && numeric(values[kind->yaw - 1], yaw)) + tool.table_yaw = yaw; + if (kind->angles > 0) + { + for (int i = 0; i < kind->positions && kind->angles - 1 + i < static_cast(values.size()); ++i) + if (double angle; numeric(values[kind->angles - 1 + i], angle) && std::none_of(tool.exits.begin(), tool.exits.end(), [&](double const Exit) { return std::abs(line_angle(Exit - angle)) < 0.01; })) + tool.exits.push_back(angle); + } + else + tool.exits = kind->fixed; + } + return true; +} + +glm::dvec3 editor_mode::turntable_centre() const +{ + return m_turntable.table != nullptr ? centre_of(*m_turntable.table) : glm::dvec3{0.0}; +} + +std::vector editor_mode::turntable_lines() const +{ + std::vector result; + for (auto const angle : m_turntable.exits) + result.push_back(m_turntable.table_yaw + angle); + return result; +} + +double editor_mode::turntable_snap(double const Heading) const +{ + auto const &tool{m_turntable}; + auto const fixed{tool.kind >= 0 && tool.templates[tool.kind].angles == 0}; + std::optional nearest; + for (auto const line : turntable_lines()) + for (auto const heading : {line, line + 180.0}) + if (false == nearest.has_value() || std::abs(circular(heading - Heading)) < std::abs(circular(*nearest - Heading))) + nearest = heading; + if (nearest.has_value() && (fixed || std::abs(circular(*nearest - Heading)) < 1.0)) + return circular(*nearest); + auto const step{tool.step > 0.f ? static_cast(tool.step) : 0.1}; + return circular(tool.table_yaw + std::round(circular(Heading - tool.table_yaw) / step) * step); +} + +bool editor_mode::turntable_exit_at(glm::dvec3 const &Ground, double &Heading, double &Length) const +{ + auto const &tool{m_turntable}; + if (tool.table == nullptr) + return false; + auto const centre{turntable_centre()}; + auto const rim{tool.length * 0.5}; + auto const distance{plan_distance(Ground, centre)}; + if (distance < rim + 1.0 || distance > rim + tool.reach) + return false; + Heading = turntable_snap(heading_of(plan_of(Ground - centre))); + Length = std::max(5.0, std::round((distance - rim) * 2.0) * 0.5); + for (auto const &end : rim_ends(tool.table, centre, rim)) + if (std::abs(circular(end.heading - Heading)) < 0.05) + return false; + return true; +} + +bool editor_mode::turntable_fit_end(editor_track::snap_target const &End, turntable_fit &Fit) const +{ + auto const &tool{m_turntable}; + Fit = {}; + Fit.end = End; + auto const centre{turntable_centre()}; + auto const rim{tool.length * 0.5}; + glm::dvec2 const c{plan_of(centre)}; + auto const into{plan_of(End.direction)}; + if (tool.table == nullptr || glm::length(into) < 1e-6) + return false; + glm::dvec2 const t{glm::normalize(into)}; + auto target{End.position}; + glm::dvec2 e{plan_of(target)}; + struct shape + { + glm::dvec2 radial{0.0}; + double radius{0.0}; + double first{0.0}; + double turn{0.0}; + double second{0.0}; + }; + auto const connect = [&](glm::dvec2 const &Radial, double const Radius) -> std::optional { + auto const rimpoint{c + Radial * rim}; + auto const delta{signed_angle(Radial, t)}; + auto const offset{e - rimpoint}; + if (std::abs(delta) < 1e-5) + { + auto const along{glm::dot(offset, Radial)}; + if (along < 0.5 || std::abs(geometry::cross(Radial, offset)) > 0.02) + return std::nullopt; + return shape{Radial, 0.0, along, 0.0, 0.0}; + } + if (std::abs(delta) > glm::radians(90.0)) + return std::nullopt; + auto const a{geometry::cross(t, offset) / geometry::cross(t, Radial)}; + auto const s{geometry::cross(Radial, offset) / geometry::cross(Radial, t)}; + auto const tangent{Radius * std::tan(std::abs(delta) * 0.5)}; + if (a - tangent < -1e-3 || s - tangent < -1e-3) + return std::nullopt; + return shape{Radial, Radius, std::max(0.0, a - tangent), delta, std::max(0.0, s - tangent)}; + }; + std::optional best; + std::string issue; + auto const better = [&](shape const &Candidate) { + if (false == best.has_value()) + return true; + auto const straight = [](shape const &Shape) { return Shape.radius == 0.0; }; + if (straight(Candidate) != straight(*best)) + return straight(Candidate); + return Candidate.radius > best->radius + 1e-6; + }; + auto const solve = [&]() { + auto const fixed{tool.kind >= 0 && tool.templates[tool.kind].angles == 0}; + if (fixed) + { + for (auto const line : turntable_lines()) + for (auto const heading : {line, line + 180.0}) + { + auto const radial{heading_vector(heading)}; + for (auto radius{static_cast(tool.radius)}; radius >= kMinimumRadius; radius *= 0.9) + if (auto const candidate{connect(radial, radius)}) + { + if (better(*candidate)) + best = candidate; + break; + } + } + if (false == best.has_value()) + issue = STR("It doesn't line up with any position of the turntable"); + } + else if (std::abs(geometry::cross(t, c - e)) < 0.02 && glm::dot(t, e - c) > rim + 0.5) + { + best = connect(t, 0.0); + } + else + { + for (auto radius{static_cast(tool.radius)}; radius >= kMinimumRadius && false == best.has_value(); radius *= 0.9) + { + auto const gap = [&](double const S) { + auto const vertex{e - t * S}; + auto const radial{vertex - c}; + if (glm::length(radial) < 1e-6) + return -1.0; + auto const delta{signed_angle(glm::normalize(radial), t)}; + return S - radius * std::tan(std::abs(delta) * 0.5); + }; + auto const limit{plan_distance(End.position, centre) + 400.0}; + double low{0.0}; + std::optional high; + for (double s = 0.25; s <= limit; s += 0.25) + { + if (gap(s) >= 0.0) + { + high = s; + break; + } + low = s; + } + if (false == high.has_value()) + continue; + auto upper{*high}; + for (int i = 0; i < 50; ++i) + { + auto const middle{(low + upper) * 0.5}; + (gap(middle) >= 0.0 ? upper : low) = middle; + } + auto const vertex{e - t * upper}; + if (glm::length(vertex - c) <= rim) + continue; + best = connect(glm::normalize(vertex - c), radius); + } + if (false == best.has_value()) + issue = format(STR_C("Too close to the turntable for a curve of %.0f m or more"), kMinimumRadius); + } + }; + solve(); + auto *own{End.track}; + if (false == best.has_value() && own != nullptr && own->eType == tt_Normal && own->m_paths.size() == 1 && own->Dynamics.empty()) + { + auto const &path{own->m_paths.front()}; + auto const far{End.point.kind == editor_track::point_kind::start ? path.points[segment_data::point::end] : path.points[segment_data::point::start]}; + auto const straight{std::abs(geometry::cross(t, plan_of(far - End.position))) < 0.01 && glm::dot(t, plan_of(far - End.position)) > 1.0}; + std::string reason; + if (straight && editor_track::can_edit_geometry(*own, reason)) + { + auto const first{issue}; + target = far; + e = plan_of(target); + solve(); + if (best.has_value()) + Fit.replaces = own; + else + { + target = End.position; + e = plan_of(target); + issue = first; + } + } + } + if (false == best.has_value()) + { + Fit.issue = issue.empty() ? STR("It points away from the turntable") : issue; + return false; + } + auto const &shape{*best}; + Fit.heading = heading_of(shape.radial); + Fit.radius = shape.radius; + Fit.length = shape.first + shape.radius * std::abs(shape.turn) + shape.second; + auto const grade{Fit.length > 1e-6 ? (target.y - centre.y) / Fit.length : 0.0}; + glm::dvec3 point{c.x + shape.radial.x * rim, centre.y, c.y + shape.radial.y * rim}; + auto const straight = [&](glm::dvec2 const &Direction, double const Length) { + segment_data piece; + piece.points[segment_data::point::start] = point; + point = {point.x + Direction.x * Length, point.y + grade * Length, point.z + Direction.y * Length}; + piece.points[segment_data::point::end] = point; + Fit.pieces.push_back(piece); + }; + if (shape.first > 0.05) + straight(shape.radial, shape.first); + if (shape.radius > 0.0) + { + auto const arc{arc_pieces(point, shape.radial, grade, shape.radius, std::abs(shape.turn), shape.turn > 0.0 ? 1 : -1)}; + Fit.pieces.insert(Fit.pieces.end(), arc.begin(), arc.end()); + point = arc.back().points[segment_data::point::end]; + } + if (shape.second > 0.05) + straight(t, shape.second); + if (false == Fit.pieces.empty()) + Fit.pieces.back().points[segment_data::point::end] = target; + return false == Fit.pieces.empty(); +} + +void editor_mode::turntable_find_fits() +{ + auto &tool{m_turntable}; + tool.fits.clear(); + tool.hovered = -1; + tool.error.clear(); + if (tool.table == nullptr) + return; + auto const centre{turntable_centre()}; + auto const rim{tool.length * 0.5}; + for (auto const &end : editor_track::free_ends(nullptr, 1, centre, rim + tool.reach, {tool.table})) + { + if (std::abs(plan_distance(end.position, centre) - rim) <= kRimTolerance) + continue; + turntable_fit fit; + turntable_fit_end(end, fit); + fit.chosen = fit.issue.empty(); + tool.fits.push_back(std::move(fit)); + } + std::sort(tool.fits.begin(), tool.fits.end(), [](turntable_fit const &A, turntable_fit const &B) { return A.end.distance < B.end.distance; }); + tool.status = tool.fits.empty() ? format(STR_C("No free ends within %.0f m of the rim"), tool.reach) : std::string{}; +} + +void editor_mode::turntable_build(std::vector const &Fits) +{ + auto &tool{m_turntable}; + if (tool.table == nullptr || Fits.empty()) + return; + std::vector created; + std::vector removed; + for (auto const *fit : Fits) + { + if (false == fit->issue.empty() || fit->end.track == nullptr || fit->end.track->m_editorremoved) + continue; + for (auto const &piece : fit->pieces) + created.push_back(editor_track::create_path(*fit->end.track, piece)); + if (fit->replaces != nullptr && false == fit->replaces->m_editorremoved) + { + editor_track::retire(*fit->replaces); + removed.push_back(fit->replaces); + } + } + if (created.empty()) + return; + editor_track::commit(created); + push_track_snapshot({}, created, removed); + turntable_store_exits(); + tool.status = format(STR_C("%d track(s) led to the turntable"), static_cast(Fits.size())); +} + +void editor_mode::turntable_store_exits() +{ + auto &tool{m_turntable}; + if (tool.table == nullptr || tool.include == 0 || tool.kind < 0) + { + turntable_read(tool.table); + return; + } + auto const &kind{tool.templates[tool.kind]}; + if (kind.angles == 0) + { + turntable_read(tool.table); + return; + } + std::vector lines; + for (auto const &end : rim_ends(tool.table, turntable_centre(), tool.length * 0.5)) + { + auto const angle{line_angle(end.heading - tool.table_yaw)}; + if (std::none_of(lines.begin(), lines.end(), [&](double const Line) { return std::abs(line_angle(Line - angle)) < 0.01; })) + lines.push_back(angle); + } + std::sort(lines.begin(), lines.end()); + if (static_cast(lines.size()) > kind.positions) + tool.error = format(STR_C("The turntable has %d positions, %d track lines lead to it: the rest won't be reached"), kind.positions, static_cast(lines.size())); + std::string target; + std::vector values; + if (false == editor_includes::parse_directive(scene::Layers.directive(tool.include), target, values)) + return; + for (int i = 0; i < kind.positions && kind.angles - 1 + i < static_cast(values.size()); ++i) + values[kind.angles - 1 + i] = editor_includes::number(i < static_cast(lines.size()) ? lines[i] : lines.empty() ? 0.0 : lines.front()); + auto const directive{editor_includes::compose_directive(target, values)}; + auto before{scene::Layers.directive(tool.include)}; + if (directive != before) + { + EditorSnapshot snap; + snap.node_name = target; + snap.directives.emplace_back(tool.include, std::move(before)); + set_include_directive(tool.include, directive); + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + } + turntable_read(tool.table); +} + +void editor_mode::turntable_place(glm::dvec3 const &Centre, double const Yaw) +{ + auto &tool{m_turntable}; + scan_turntables(); + tool.error.clear(); + if (tool.chosen < 0 || tool.chosen >= static_cast(tool.templates.size())) + return; + auto const &kind{tool.templates[tool.chosen]}; + auto const name{turntable_name_for_new()}; + if (simulation::Paths.find(name) != nullptr) + { + tool.error = format(STR_C("The name %s is taken by another path"), name.c_str()); + return; + } + auto const rim{kind.length * 0.5}; + auto const direction{heading_vector(Yaw)}; + segment_data path; + path.points[segment_data::point::start] = {Centre.x + direction.x * rim, Centre.y, Centre.z + direction.y * rim}; + path.points[segment_data::point::end] = {Centre.x - direction.x * rim, Centre.y, Centre.z - direction.y * rim}; + auto *table{editor_track::create_turntable(path, tool.snap.track, name)}; + editor_track::commit({table}); + push_track_snapshot({}, {table}); + template_item item; + item.file = kind.file; + item.location = Centre; + item.yaw = Yaw; + item.described = true; + item.track = table->name(); + std::string error; + if (place_templates({item}, error) == 0) + tool.error = error; + tool.name[0] = '\0'; + m_node = table; + m_track_point = {}; + ui()->set_node(m_node); + turntable_read(table); + tool.status = format(STR_C("Turntable %s: click around it for the tracks out of it, or fit the tracks around. It turns after the scenery is saved and loaded again"), table->name().c_str()); +} + +void editor_mode::turntable_press(track_intent const &Intent) +{ + using kind = track_intent::kind; + auto &tool{m_turntable}; + auto const &io{ImGui::GetIO()}; + switch (Intent.what) + { + case kind::turntable_place: + tool.placing = true; + tool.pressed = {io.MousePos.x, io.MousePos.y}; + tool.snap = Intent.snap; + tool.centre = Intent.position; + return; + case kind::turntable_exit: + { + if (tool.table == nullptr) + return; + auto const centre{turntable_centre()}; + auto const rim{tool.length * 0.5}; + auto const direction{heading_vector(tool.heading)}; + segment_data piece; + piece.points[segment_data::point::start] = {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}; + piece.points[segment_data::point::end] = {centre.x + direction.x * (rim + tool.exit_length), centre.y, centre.z + direction.y * (rim + tool.exit_length)}; + auto const ends{rim_ends(tool.table, centre, rim)}; + auto *track{ends.empty() ? editor_track::create_path(m_lay.style, piece) : editor_track::create_path(*ends.front().track, piece)}; + editor_track::commit({track}); + push_track_snapshot({}, {track}); + turntable_store_exits(); + tool.status = format(STR_C("Track out of the turntable at %.1f deg, %.1f m"), circular(tool.heading - tool.table_yaw), tool.exit_length); + return; + } + case kind::turntable_fit: + { + turntable_fit fit; + if (turntable_fit_end(Intent.snap, fit)) + turntable_build({&fit}); + else + tool.error = fit.issue; + return; + } + default: return; + } +} + +void editor_mode::turntable_finish_placement() +{ + auto &tool{m_turntable}; + if (false == tool.placing) + return; + tool.placing = false; + scan_turntables(); + if (tool.chosen < 0) + return; + auto const rim{tool.templates[tool.chosen].length * 0.5}; + auto const &io{ImGui::GetIO()}; + auto const dragged{glm::length(glm::vec2{io.MousePos.x, io.MousePos.y} - tool.pressed) >= kDragPixels}; + if (tool.snap.track != nullptr && false == dragged) + { + auto const into{glm::normalize(plan_of(tool.snap.direction))}; + glm::dvec3 const centre{tool.snap.position.x - into.x * rim, tool.snap.position.y, tool.snap.position.z - into.y * rim}; + tool.yaw = heading_of(into); + turntable_place(centre, tool.yaw); + return; + } + tool.snap = {}; + if (dragged) + { + auto const cursor{cursor_level(tool.centre.y)}; + if (plan_distance(cursor, tool.centre) > 0.5) + tool.yaw = std::round(heading_of(plan_of(cursor - tool.centre)) * 10.0) / 10.0; + } + turntable_place(tool.centre, tool.yaw); +} + +editor_mode::track_intent editor_mode::turntable_intent(glm::dvec3 const &Ground, bool const Shift) +{ + using kind = track_intent::kind; + auto &tool{m_turntable}; + track_intent intent; + intent.position = Ground; + auto const make = [&](kind const What, std::string const &Action) { + intent.what = What; + intent.action = Action; + return intent; + }; + auto const &io{ImGui::GetIO()}; + glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; + if (tool.table != selected_track()) + turntable_read(selected_track()); + if (tool.table != nullptr) + { + auto const centre{turntable_centre()}; + auto const rim{tool.length * 0.5}; + auto const end{snap_free_end(Ground, mouse, nullptr, 1, {tool.table})}; + if (end.track != nullptr && std::abs(plan_distance(end.position, centre) - rim) > kRimTolerance && plan_distance(end.position, centre) <= rim + tool.reach) + { + intent.snap = end; + intent.track = end.track; + intent.position = end.position; + return make(kind::turntable_fit, STR("Click: lead this track to the turntable")); + } + double heading, length; + if (turntable_exit_at(cursor_level(centre.y), heading, length)) + { + tool.heading = heading; + tool.exit_length = length; + auto const direction{heading_vector(heading)}; + intent.position = {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}; + return make(kind::turntable_exit, format(STR_C("Click: a track out of the turntable, %.1f deg, %.1f m"), circular(heading - tool.table_yaw), length)); + } + } + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->eType != tt_Table || track->m_editorremoved || track->m_paths.empty()) + continue; + if (plan_distance(centre_of(*track), Ground) <= length_of(*track) * 0.5) + { + intent.track = track; + return make(kind::select, track == tool.table ? std::string{} : STR("Click: this turntable")); + } + } + scan_turntables(); + if (tool.chosen < 0) + return intent; + if (false == Shift) + { + auto const end{snap_free_end(Ground, mouse, nullptr, 1, {})}; + if (end.track != nullptr) + { + intent.snap = end; + intent.position = end.position; + return make(kind::turntable_place, STR("Click: a turntable at the end of this track")); + } + } + return make(kind::turntable_place, STR("Press: the centre of a new turntable, drag: the direction of its bridge")); +} + +void editor_mode::render_turntable_ui() +{ + auto &tool{m_turntable}; + scan_turntables(); + auto *selected{selected_track()}; + if (tool.table != selected) + turntable_read(selected); + ImVec4 const warncolour{1.f, 0.75f, 0.3f, 1.f}; + + if (tool.table != nullptr) + { + auto &table{*tool.table}; + ImGui::Text(STR_C("Turntable %s, bridge %.1f m"), table.name().c_str(), tool.length); + if (tool.kind >= 0) + { + auto const &kind{tool.templates[tool.kind]}; + ImGui::TextDisabled("%s", kind.name.c_str()); + ImGui::SameLine(); + ImGui::TextDisabled(kind.angles > 0 ? STR_C("(positions set by the tracks around, up to %d)") : STR_C("(%d positions fixed by the template)"), kind.angles > 0 ? kind.positions : static_cast(kind.fixed.size())); + } + else + ImGui::TextColored(warncolour, "%s", STR_C("No turntable template takes this path as its track: the positions aren't known")); + render_track_name(table); + + auto const centre{turntable_centre()}; + auto const ends{rim_ends(&table, centre, tool.length * 0.5)}; + if (false == tool.exits.empty() && ImGui::TreeNodeEx(format(STR_C("Positions (%d)"), static_cast(tool.exits.size())).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + { + for (std::size_t i = 0; i < tool.exits.size(); ++i) + { + auto const heading{tool.table_yaw + tool.exits[i]}; + auto const occupied = [&](double const Heading) { return std::any_of(ends.begin(), ends.end(), [&](rim_end const &End) { return std::abs(circular(End.heading - Heading)) < 0.05; }); }; + auto const front{occupied(heading)}; + auto const back{occupied(heading + 180.0)}; + ImGui::Text("%2d %7.2f deg", static_cast(i + 1), tool.exits[i]); + ImGui::SameLine(); + if (front || back) + ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.f), "%s%s%s", front ? STR_C("track") : "", front && back ? " + " : "", back ? STR_C("track on the other end") : ""); + else + ImGui::TextDisabled("%s", STR_C("no track")); + } + ImGui::TreePop(); + } + + ImGui::Separator(); + ImGui::TextUnformatted(STR_C("Tracks around")); + ImGui::PushItemWidth(110.f); + ImGui::InputFloat(STR_C("Reach from the rim, m"), &tool.reach, 10.f, 50.f, "%.0f"); + tool.reach = std::clamp(tool.reach, 5.f, 500.f); + ImGui::InputFloat(STR_C("Radius of the curves, m"), &tool.radius, 10.f, 50.f, "%.0f"); + tool.radius = std::clamp(tool.radius, static_cast(kMinimumRadius), 2000.f); + if (tool.kind < 0 || tool.templates[tool.kind].angles > 0) + { + ImGui::InputFloat(STR_C("Angle step of a new track, deg (0: free)"), &tool.step, 0.5f, 2.5f, "%.2f"); + tool.step = std::clamp(tool.step, 0.f, 45.f); + } + ImGui::PopItemWidth(); + ImGui::TextDisabled("%s", STR_C("Click around the turntable: a track out of it, as long as the cursor is far.\nClick a free end near it: that track is led to the rim, straight or with a curve")); + if (ImGui::Button(STR_C("Find the free ends around"))) + turntable_find_fits(); + if (false == tool.fits.empty()) + { + int chosen{0}; + for (auto const &fit : tool.fits) + chosen += fit.chosen && fit.issue.empty() ? 1 : 0; + ImGui::SameLine(); + if (ImGui::Button(format(STR_C("Lead the chosen to the turntable (%d)"), chosen).c_str())) + { + std::vector fits; + for (auto const &fit : tool.fits) + if (fit.chosen && fit.issue.empty()) + fits.push_back(&fit); + turntable_build(fits); + turntable_find_fits(); + } + tool.hovered = -1; + ImGui::BeginChild("##turntablefits", ImVec2(0.f, 180.f), true); + for (int i = 0; i < static_cast(tool.fits.size()); ++i) + { + auto &fit{tool.fits[i]}; + ImGui::PushID(i); + if (fit.issue.empty()) + ImGui::Checkbox("##chosen", &fit.chosen); + else + ImGui::TextDisabled(" - "); + ImGui::SameLine(); + auto const name{fit.end.track->name().empty() ? std::string{"(noname)"} : fit.end.track->name()}; + if (ImGui::Selectable(name.c_str(), false, ImGuiSelectableFlags_None, ImVec2(ImGui::CalcTextSize(name.c_str()).x, 0.f))) + focus_track(*fit.end.track, fit.end.point.path, fit.end.position); + if (ImGui::IsItemHovered()) + tool.hovered = i; + ImGui::SameLine(); + if (false == fit.issue.empty()) + ImGui::TextColored(warncolour, "%s", fit.issue.c_str()); + else if (fit.radius > 0.0) + ImGui::TextDisabled(STR_C("%.1f deg, curve R %.0f m, %.1f m"), circular(fit.heading - tool.table_yaw), fit.radius, fit.length); + else + ImGui::TextDisabled(STR_C("%.1f deg, straight %.1f m"), circular(fit.heading - tool.table_yaw), fit.length); + if (fit.replaces != nullptr) + { + ImGui::SameLine(); + ImGui::TextDisabled(STR_C("in place of its last straight %s"), fit.replaces->name().c_str()); + } + ImGui::PopID(); + } + ImGui::EndChild(); + } + } + else + { + if (selected != nullptr && selected->eType == tt_Table) + ImGui::TextColored(warncolour, "%s", STR_C("This turntable can't be read")); + ImGui::TextUnformatted(STR_C("New turntable")); + if (tool.templates.empty()) + ImGui::TextColored(warncolour, "%s", STR_C("No turntables: no template is described with category: turntable and its turntable: {length}")); + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + { + auto const &kind{tool.templates[i]}; + auto const text{kind.angles > 0 ? format(STR_C("%s %.0f m, up to %d positions set by the tracks"), kind.name.c_str(), kind.length, kind.positions) + : format(STR_C("%s %.0f m, %d fixed positions"), kind.name.c_str(), kind.length, static_cast(kind.fixed.size()))}; + if (ImGui::Selectable((text + "##turntable" + std::to_string(i)).c_str(), tool.chosen == i)) + tool.chosen = i; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", kind.file.c_str()); + } + ImGui::SetNextItemWidth(160.f); + ImGui::InputTextWithHint(STR_C("Name##turntablename"), turntable_name_for_new().c_str(), tool.name, sizeof(tool.name), ImGuiInputTextFlags_CharsNoBlank); + ImGui::TextDisabled("%s", STR_C("Press where the centre goes and drag to turn the bridge.\nClick a free end of a track: the turntable lines up with it, at its end. Shift: no lining up")); + } + if (false == tool.error.empty()) + ImGui::TextColored(ImVec4(1.f, 0.45f, 0.35f, 1.f), "%s", tool.error.c_str()); + else if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} + +void editor_mode::draw_turntable_overlay() const +{ + using kind = track_intent::kind; + auto const &tool{m_turntable}; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList()}; + auto const camera{glm::dvec3{Global.pCamera.Pos}}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->eType != tt_Table || track->m_editorremoved || track->m_paths.empty() || track == tool.table) + continue; + auto const centre{centre_of(*track)}; + if (glm::distance(centre, camera) < 1500.0) + draw_circle(projection, drawlist, centre, length_of(*track) * 0.5, IM_COL32(255, 255, 255, 90), 1.5f); + } + if (tool.table != nullptr) + { + auto const centre{turntable_centre()}; + auto const rim{tool.length * 0.5}; + draw_circle(projection, drawlist, centre, rim, overlay_color::selected, 2.5f); + auto const ends{rim_ends(tool.table, centre, rim)}; + for (auto const line : turntable_lines()) + for (auto const heading : {line, line + 180.0}) + { + auto const direction{heading_vector(heading)}; + auto const occupied{std::any_of(ends.begin(), ends.end(), [&](rim_end const &End) { return std::abs(circular(End.heading - heading)) < 0.05; })}; + glm::dvec3 const from{centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}; + glm::dvec3 const to{centre.x + direction.x * (rim + 6.0), centre.y, centre.z + direction.y * (rim + 6.0)}; + projection.line(drawlist, from, to, occupied ? IM_COL32(90, 230, 110, 230) : IM_COL32(255, 255, 255, 150), occupied ? 2.5f : 1.5f); + } + for (int i = 0; i < static_cast(tool.fits.size()); ++i) + { + auto const &fit{tool.fits[i]}; + auto const colour{false == fit.issue.empty() ? IM_COL32(255, 150, 60, 200) : fit.chosen ? IM_COL32(90, 230, 110, 230) : IM_COL32(200, 200, 200, 140)}; + draw_pieces(projection, drawlist, fit.pieces, colour, i == tool.hovered ? 4.f : 2.f); + ImVec2 at; + if (projection.project(fit.end.position, at)) + drawlist->AddCircle(at, i == tool.hovered ? 10.f : 6.f, colour, 16, 2.f); + } + } + auto const &intent{m_intent}; + switch (intent.what) + { + case kind::turntable_exit: + { + auto const centre{turntable_centre()}; + auto const direction{heading_vector(tool.heading)}; + auto const rim{tool.length * 0.5}; + glm::dvec3 const to{centre.x + direction.x * (rim + tool.exit_length), centre.y, centre.z + direction.y * (rim + tool.exit_length)}; + projection.line(drawlist, intent.position, to, overlay_color::grip, 3.f); + break; + } + case kind::turntable_fit: + { + turntable_fit fit; + auto const fits{turntable_fit_end(intent.snap, fit)}; + draw_pieces(projection, drawlist, fit.pieces, fits ? overlay_color::grip : overlay_color::invalid, 3.f); + if (false == fits) + label(projection, intent.position, fit.issue); + break; + } + case kind::turntable_place: + { + if (tool.chosen < 0 || tool.chosen >= static_cast(tool.templates.size())) + break; + auto const rim{tool.templates[tool.chosen].length * 0.5}; + auto centre{intent.position}; + auto yaw{tool.yaw}; + if (intent.snap.track != nullptr) + { + auto const into{glm::normalize(plan_of(intent.snap.direction))}; + centre = {intent.snap.position.x - into.x * rim, intent.snap.position.y, intent.snap.position.z - into.y * rim}; + yaw = heading_of(into); + } + auto const direction{heading_vector(yaw)}; + draw_circle(projection, drawlist, centre, rim, IM_COL32(255, 255, 255, 170), 1.5f); + projection.line(drawlist, {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}, {centre.x - direction.x * rim, centre.y, centre.z - direction.y * rim}, IM_COL32(255, 255, 255, 170), 2.f); + break; + } + default: break; + } + if (tool.placing && tool.chosen >= 0 && tool.chosen < static_cast(tool.templates.size())) + { + auto const rim{tool.templates[tool.chosen].length * 0.5}; + auto const cursor{cursor_level(tool.centre.y)}; + auto const &io{ImGui::GetIO()}; + auto const dragged{glm::length(glm::vec2{io.MousePos.x, io.MousePos.y} - tool.pressed) >= kDragPixels}; + auto centre{tool.centre}; + auto yaw{tool.yaw}; + if (tool.snap.track != nullptr && false == dragged) + { + auto const into{glm::normalize(plan_of(tool.snap.direction))}; + centre = {tool.snap.position.x - into.x * rim, tool.snap.position.y, tool.snap.position.z - into.y * rim}; + yaw = heading_of(into); + } + else if (dragged && plan_distance(cursor, centre) > 0.5) + yaw = std::round(heading_of(plan_of(cursor - centre)) * 10.0) / 10.0; + auto const direction{heading_vector(yaw)}; + draw_circle(projection, drawlist, centre, rim, overlay_color::marked, 2.5f); + projection.line(drawlist, {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}, {centre.x - direction.x * rim, centre.y, centre.z - direction.y * rim}, overlay_color::marked, 3.f); + if (dragged) + label(projection, cursor, format("%.1f deg", yaw)); + } +} diff --git a/application/editormodevehicle.cpp b/application/editormodevehicle.cpp new file mode 100644 index 00000000..f2c39b1f --- /dev/null +++ b/application/editormodevehicle.cpp @@ -0,0 +1,315 @@ +/* +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/editorprojection.h" +#include "application/application.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "launcher/textures_scanner.h" + +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "vehicle/DynObj.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include +#include + +namespace +{ + +using geometry::bezier; + +std::size_t const kVehicleList{300}; +double const kHalfVehicle{8.0}; // m, the vehicle is placed about centred on the point + +std::string lower(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + return Text; +} + +std::string skin_label(ui::vehicle_desc const &Vehicle, ui::skin_set const &Skin) +{ + auto label{Skin.meta != nullptr && false == Skin.meta->name.empty() ? Skin.meta->name : Skin.skin}; + return label + " (" + Vehicle.path.generic_string() + ")"; +} + +int path_under(TTrack const &Track, glm::dvec3 const &Point) +{ + int result{0}; + auto best{std::numeric_limits::max()}; + for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) + { + bezier const curve{Track.m_paths[i]}; + for (int k = 0; k <= 64; ++k) + { + auto const distance{geometry::plan_distance(curve.point(k / 64.0), Point)}; + if (distance < best) + { + best = distance; + result = i; + } + } + } + return result; +} + +double nearest_on(TTrack const &Track, int const Path, glm::dvec3 const &Point) +{ + bezier const curve{Track.m_paths[Path]}; + auto parameter{0.0}; + auto best{std::numeric_limits::max()}; + for (int k = 0; k <= 256; ++k) + { + auto const distance{geometry::plan_distance(curve.point(k / 256.0), Point)}; + if (distance < best) + { + best = distance; + parameter = k / 256.0; + } + } + return parameter; +} + +glm::dvec2 heading_at(TTrack const &Track, int const Path, glm::dvec3 const &Point) +{ + auto const tangent{bezier{Track.m_paths[Path]}.first(nearest_on(Track, Path, Point))}; + glm::dvec2 const planar{tangent.x, tangent.z}; + return glm::length(planar) > 1e-9 ? glm::normalize(planar) : glm::dvec2{0.0, 1.0}; +} + +// distance along the path of the track from its start to the point nearest to the one given +double distance_along(TTrack const &Track, int const Path, glm::dvec3 const &Point) +{ + bezier const curve{Track.m_paths[Path]}; + auto const parameter{nearest_on(Track, Path, Point)}; + int const count{64}; + double length{0.0}, total{0.0}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= count; ++k) + { + auto const t{static_cast(k) / count}; + auto const next{curve.point(t)}; + auto const step{glm::distance(previous, next)}; + total += step; + if (t <= parameter) + length += step; + else if (t - 1.0 / count < parameter) + length += step * (parameter - (t - 1.0 / count)) * count; + previous = next; + } + return std::clamp(length * Track.Length() / std::max(total, 1e-6), 0.0, Track.Length()); +} + +} // namespace + +void editor_mode::vehicle_start(TTrack &Track, glm::dvec3 const &Point) +{ + m_vehicle.track = &Track; + m_vehicle.point = Point; + m_vehicle.expand = true; + m_vehicle.status.clear(); + auto const heading{heading_at(Track, Track.eType == tt_Switch ? path_under(Track, Point) : 0, Point)}; + glm::dvec2 const away{Point.x - Global.pCamera.Pos.x, Point.z - Global.pCamera.Pos.z}; + m_vehicle.facing = glm::dot(heading, away) >= 0.0 ? 1 : -1; +} + +bool editor_mode::vehicle_place() +{ + auto &tool{m_vehicle}; + if (tool.track == nullptr || tool.track->m_editorremoved) + return false; + auto train{tool.consist}; + if (train.empty()) + { + if (tool.vehicle == nullptr || tool.skin == nullptr) + return false; + train.push_back({tool.vehicle, tool.skin, false}); + } + auto const path{tool.track->eType == tt_Switch ? path_under(*tool.track, tool.point) : 0}; + if (tool.track->eType == tt_Switch && tool.track->GetSwitchState() != path) + tool.track->Switch(path); + auto const along{distance_along(*tool.track, path, tool.point)}; + auto const against{tool.facing < 0}; + std::vector names; + std::string nodes; + std::string head; + for (std::size_t i = 0; i < train.size(); ++i) + { + auto const &item{train[i]}; + auto const stem{item.vehicle->path.stem().generic_string()}; + std::string name; + for (int index = 1; index < 10000; ++index) + { + name = lower("editor_" + stem + "_" + std::to_string(index)); + if (simulation::Vehicles.find(name) == nullptr && std::find(names.begin(), names.end(), name) == names.end()) + break; + } + names.push_back(name); + auto const leads{i == 0}; + if (leads) + head = name; + nodes += "node -1 0 " + name + " dynamic " + item.vehicle->path.parent_path().generic_string() + " " + item.skin->skin + " " + stem + " " + (item.turned ? "-1" : "0") + " " + (leads && tool.driver == 0 ? "headdriver" : "nobody") + " 3 0 enddynamic\n"; + } + auto const offset{against ? along - kHalfVehicle : along + kHalfVehicle}; + auto const vehicles{simulation::State.insert_trainset(head, tool.track, offset, nodes, against)}; + if (vehicles.empty()) + { + tool.status = STR_C("The vehicle couldn't be placed, the log says why"); + return false; + } + tool.placed = head; + tool.status = format(STR_C("%s and %d vehicle(s) behind it stand on %s. It isn't saved with the scenery"), head.c_str(), static_cast(vehicles.size()) - 1, tool.track->name().empty() ? "(noname)" : tool.track->name().c_str()); + WriteLog("Editor: " + tool.status, logtype::generic); + return true; +} + +void editor_mode::render_vehicle_ui() +{ + auto &tool{m_vehicle}; + auto *selected{selected_track()}; + if (selected != nullptr && selected != tool.track) + vehicle_start(*selected, bezier{selected->m_paths.front()}.point(0.5)); + if (tool.track == nullptr) + return; + if (tool.bank == nullptr) + { + tool.bank = std::make_shared(); + tool.bank->scan_textures(); + } + ImGui::TextDisabled("%s", STR_C("A vehicle to drive, placed where RMB on the track was clicked, or in the middle of the path.\nIt isn't written to the scenery files")); + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputTextWithHint("##vehiclesearch", STR_C("search: name, series, skin"), tool.search, sizeof(tool.search)); + ImGui::Checkbox(STR_C("Only the ones with a cab"), &tool.controllable_only); + std::string const text{lower(tool.search)}; + std::vector, std::shared_ptr>> listed; + std::size_t total{0}; + for (auto const &[path, vehicle] : tool.bank->vehicles) + { + if (vehicle == nullptr || (tool.controllable_only && false == vehicle->controllable)) + continue; + for (auto const &skin : vehicle->matching_skinsets) + { + if (skin == nullptr) + continue; + if (false == text.empty() && lower(skin_label(*vehicle, *skin)).find(text) == std::string::npos && (skin->meta == nullptr || skin->meta->search_lowered.find(text) == std::string::npos)) + continue; + ++total; + if (listed.size() < kVehicleList) + listed.emplace_back(vehicle, skin); + } + } + ImGui::BeginChild("##vehicles", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 8.0f + 6.0f), true); + for (std::size_t i = 0; i < listed.size(); ++i) + { + auto const &[vehicle, skin] = listed[i]; + if (ImGui::Selectable(format("%s##vehicle%zu", skin_label(*vehicle, *skin).c_str(), i).c_str(), tool.skin == skin)) + { + tool.vehicle = vehicle; + tool.skin = skin; + } + } + ImGui::EndChild(); + if (total > listed.size()) + ImGui::TextDisabled(STR_C("%zu found, the first %zu listed"), total, listed.size()); + if (tool.vehicle != nullptr && tool.skin != nullptr && ImGui::Button(STR_C("Add to the train"))) + tool.consist.push_back({tool.vehicle, tool.skin, false}); + if (false == tool.consist.empty()) + { + ImGui::SameLine(); + ImGui::TextDisabled(STR_C("Train: %d vehicle(s), the first one leads"), static_cast(tool.consist.size())); + std::optional remove; + std::optional raise; + for (std::size_t i = 0; i < tool.consist.size(); ++i) + { + auto &item{tool.consist[i]}; + auto const vehicle{item.skin->vehicle.lock()}; + ImGui::PushID(static_cast(i)); + ImGui::Checkbox("##turned", &item.turned); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Turned around in the train")); + ImGui::SameLine(); + ImGui::Text("%2d. %s", static_cast(i + 1), vehicle != nullptr ? skin_label(*vehicle, *item.skin).c_str() : item.skin->skin.c_str()); + ImGui::SameLine(); + if (i > 0 && ImGui::SmallButton(STR_C("up"))) + raise = i; + ImGui::SameLine(); + if (ImGui::SmallButton("x")) + remove = i; + ImGui::PopID(); + } + if (raise) + std::swap(tool.consist[*raise], tool.consist[*raise - 1]); + if (remove) + tool.consist.erase(tool.consist.begin() + static_cast(*remove)); + if (ImGui::SmallButton(STR_C("Empty the train"))) + tool.consist.clear(); + } + if (ImGui::Button(STR_C("Turn the train around"))) + tool.facing = -tool.facing; + ImGui::SameLine(); + ImGui::TextDisabled("%s", STR_C("the head goes where the arrow at the marker points")); + ImGui::RadioButton(STR_C("With a driver, to drive"), &tool.driver, 0); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("Nobody, parked"), &tool.driver, 1); + if (tool.consist.empty() && (tool.vehicle == nullptr || tool.skin == nullptr)) + ImGui::TextDisabled("%s", STR_C("Pick a vehicle from the list")); + else + { + if (ImGui::Button(STR_C("Place"))) + vehicle_place(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Place and drive")) && vehicle_place()) + { + Global.editor_enter_vehicle = tool.placed; + tool.leave = true; + } + } + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} + +void editor_mode::draw_vehicle_marker() const +{ + auto const &tool{m_vehicle}; + if (false == tool.open || tool.track == nullptr) + return; + screen_projection const projection; + ImVec2 screen; + if (false == projection.project(tool.point, screen)) + return; + auto *drawlist{ImGui::GetBackgroundDrawList()}; + auto const colour{IM_COL32(255, 220, 60, 235)}; + drawlist->AddCircle(screen, 9.0f, colour, 16, 2.5f); + if (tool.track->m_editorremoved || tool.track->m_paths.empty()) + return; + auto const heading{heading_at(*tool.track, tool.track->eType == tt_Switch ? path_under(*tool.track, tool.point) : 0, tool.point) * static_cast(tool.facing)}; + glm::dvec3 const tip{tool.point.x + heading.x * 8.0, tool.point.y, tool.point.z + heading.y * 8.0}; + ImVec2 end; + if (false == projection.project(tip, end)) + return; + drawlist->AddLine(screen, end, colour, 3.0f); + auto const dx{end.x - screen.x}; + auto const dy{end.y - screen.y}; + auto const size{std::sqrt(dx * dx + dy * dy)}; + if (size < 1.0f) + return; + ImVec2 const unit{dx / size, dy / size}; + ImVec2 const side{-unit.y, unit.x}; + drawlist->AddTriangleFilled(ImVec2(end.x + unit.x * 10.0f, end.y + unit.y * 10.0f), ImVec2(end.x + side.x * 7.0f, end.y + side.y * 7.0f), ImVec2(end.x - side.x * 7.0f, end.y - side.y * 7.0f), colour); +} diff --git a/application/editorprojection.h b/application/editorprojection.h index 31825b0b..75aa20ea 100644 --- a/application/editorprojection.h +++ b/application/editorprojection.h @@ -76,6 +76,31 @@ class screen_projection auto const &mouse{ImGui::GetIO().MousePos}; return (position.x - mouse.x) * (position.x - mouse.x) + (position.y - mouse.y) * (position.y - mouse.y); } + // world space ray through the point of the screen + void ray(ImVec2 const &Screen, glm::dvec3 &Origin, glm::dvec3 &Direction) const + { + auto const inverse{glm::inverse(m_viewprojection)}; + glm::vec2 const ndc{m_size.x > 0.0f ? Screen.x / m_size.x * 2.0f - 1.0f : 0.0f, m_size.y > 0.0f ? 1.0f - Screen.y / m_size.y * 2.0f : 0.0f}; + auto nearpoint{inverse * glm::vec4(ndc, -1.0f, 1.0f)}; + auto farpoint{inverse * glm::vec4(ndc, 1.0f, 1.0f)}; + nearpoint /= nearpoint.w; + farpoint /= farpoint.w; + Origin = m_camera + glm::dvec3(glm::vec3(nearpoint)); + Direction = glm::normalize(glm::dvec3(glm::vec3(farpoint - nearpoint))); + } + // where the ray through the point of the screen crosses the level of the height; false if it runs along it or away + bool on_level(ImVec2 const &Screen, double const Height, glm::dvec3 &Point) const + { + glm::dvec3 origin, direction; + ray(Screen, origin, direction); + if (std::abs(direction.y) < 1e-6) + return false; + auto const distance{(Height - origin.y) / direction.y}; + if (distance < 0.0) + return false; + Point = origin + direction * distance; + return true; + } private: static constexpr float kNear{0.1f}; @@ -87,6 +112,9 @@ class screen_projection // starts a gizmo frame over the whole display, the view and the projection are those of the current camera struct gizmo_frame { + static inline int drawn{-1}; + // the cursor is over a gizmo shown in this frame, ImGuizmo alone tells it of the last one shown even when it's gone + static bool over() { return ImGui::GetFrameCount() - drawn <= 1 && ImGuizmo::IsOver(); } glm::mat4 view; glm::mat4 projection; glm::dvec3 camera; @@ -106,6 +134,7 @@ struct gizmo_frame { glm::vec3 snapvalue(Snap); float const *snap = Global.ctrlState && Snap > 0.0f ? &snapvalue.x : nullptr; + drawn = ImGui::GetFrameCount(); return ImGuizmo::Manipulate(&view[0][0], &projection[0][0], Operation, ImGuizmo::WORLD, &Gizmo[0][0], Delta != nullptr ? &(*Delta)[0][0] : nullptr, snap); } }; diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index 8015499f..d757c35c 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -109,6 +109,10 @@ void editor_ui::render_menu_contents() { if (ImGui::BeginMenu(STR_C("File"))) { + if (ImGui::MenuItem(STR_C("New scenery...")) && m_newscenery) + m_newscenery(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The editor starts again with the wizard of a new scenery: its name and its centre on the map")); // changes go to the scenery files only if the scenery was loaded with its sources tracked auto const editsession{false == scene::Layers.empty()}; if (ImGui::MenuItem(STR_C("Save"), "Ctrl+S", false, editsession) && m_save) diff --git a/application/editoruilayer.h b/application/editoruilayer.h index f4e9679a..6e37e4e3 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -68,6 +68,7 @@ class editor_ui : public ui_layer } // actions of the file menu, performed by the editor mode void set_file_actions(std::function Save, std::function Export); + void set_new_scenery(std::function New) { m_newscenery = std::move(New); } // shows outcome of an operation in the layers window void set_status(std::string const &Status, bool const Error = false); @@ -84,6 +85,7 @@ class editor_ui : public ui_layer includes_panel m_includespanel{"Include database", false}; std::function m_save; std::function m_export; + std::function m_newscenery; brush_object_list m_brushobjects; bool m_insertrandom{false}; // insert mode picks a random template from m_insertset model_set_ref m_insertset; diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 5bc319f3..cf894d63 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -97,18 +97,34 @@ struct compound_curve double shortened{1.0}; }; -compound_curve build_compound(double const Deflection, double const Radius1, double const Radius2, double const Lengthin, double const Lengthmiddle, double const Lengthout, double const Split) +compound_curve build_compound(double const Deflection, std::vector const &Arcs, double const Lengthin, double const Lengthout) { compound_curve result; - auto const k1{1.0 / Radius1}; - auto const k2{1.0 / Radius2}; - auto const transitions{(k1 * Lengthin + (k1 + k2) * Lengthmiddle + k2 * Lengthout) * 0.5}; + if (Arcs.empty()) + return result; + std::vector curvatures; + for (auto const &arc : Arcs) + curvatures.push_back(1.0 / std::max(1.0, arc.radius)); + auto transitions{curvatures.front() * Lengthin + curvatures.back() * Lengthout}; + for (std::size_t i = 1; i < Arcs.size(); ++i) + transitions += (curvatures[i - 1] + curvatures[i]) * std::max(0.0, Arcs[i].transition); + transitions *= 0.5; if (transitions > Deflection) result.shortened = Deflection / transitions * (1.0 - 1e-9); auto const f{result.shortened}; auto const arcs{std::max(0.0, Deflection - transitions * f)}; - auto const split{std::clamp(Split, 0.0, 1.0)}; - std::array const segments[] = {{0.0, k1, Lengthin * f}, {k1, k1, split * arcs / k1}, {k1, k2, Lengthmiddle * f}, {k2, k2, (1.0 - split) * arcs / k2}, {k2, 0.0, Lengthout * f}}; + double total{0.0}; + for (auto const &arc : Arcs) + total += std::max(0.0, arc.share); + std::vector> segments{{0.0, curvatures.front(), Lengthin * f}}; + for (std::size_t i = 0; i < Arcs.size(); ++i) + { + if (i > 0) + segments.push_back({curvatures[i - 1], curvatures[i], std::max(0.0, Arcs[i].transition) * f}); + auto const share{total > 0.0 ? std::max(0.0, Arcs[i].share) / total : 1.0 / static_cast(Arcs.size())}; + segments.push_back({curvatures[i], curvatures[i], share * arcs / curvatures[i]}); + } + segments.push_back({curvatures.back(), 0.0, Lengthout * f}); double x{0.0}, y{0.0}, heading{0.0}; for (auto const &segment : segments) { @@ -209,8 +225,11 @@ std::size_t element_pieces(element const &Element, transition_shape const Shape, case element_kind::spiral: { auto const turn{(Element.curvature_start + Element.curvature_end) * 0.5 * Element.length}; - auto const count{static_cast(std::max({1.0, std::ceil(turn / (Arcangle * kPi / 180.0) - 1e-9), std::ceil(Element.length / 20.0 - 1e-9)}))}; - return Element.curvature_start != Element.curvature_end ? std::max(count, std::max(1, Transitions)) : count; + auto const arcs{std::max(1.0, std::ceil(turn / (Arcangle * kPi / 180.0) - 1e-9))}; + if (Element.curvature_start == Element.curvature_end) + return static_cast(arcs); + auto const count{static_cast(std::max(arcs, std::ceil(Element.length / 20.0 - 1e-9)))}; + return std::max(count, std::max(1, Transitions)); } default: return 1; @@ -360,10 +379,17 @@ result fit_between(design const &Design) double tangent_out{0.0}; double deflection{0.0}; bool compound{false}; - double radius2{0.0}; - double transition_middle{0.0}; + std::vector arcs; std::function tangents; }; + auto const scaled = [](std::vector Arcs, double const Scale) { + for (auto &arc : Arcs) + { + arc.radius *= Scale; + arc.transition *= Scale; + } + return Arcs; + }; std::vector fits(count); for (std::size_t k = 0; k < count; ++k) { @@ -386,16 +412,17 @@ result fit_between(design const &Design) r.errors.emplace_back(format("Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex", k + 1)); return r; } - if (vertex.compound) + if (vertex.compound && false == vertex.arcs.empty()) { fit.compound = true; - fit.radius2 = std::max(1.0, vertex.radius2); - fit.transition_middle = std::max(0.0, vertex.transition_middle); + fit.arcs = {{fit.radius, 0.0, vertex.share}}; + for (auto const &arc : vertex.arcs) + fit.arcs.push_back({std::max(1.0, arc.radius), std::max(0.0, arc.transition), arc.share}); fit.deflection = deflection; - fit.tangents = [&, k, directionin, directionout, side](double const Radius, double &Tangentin, double &Tangentout) { + fit.tangents = [&, k, directionin, directionout, side, scaled](double const Radius, double &Tangentin, double &Tangentout) { auto const &own{fits[k]}; auto const scale{Radius / own.radius}; - auto const shape{build_compound(own.deflection, Radius, own.radius2 * scale, own.transition_in * scale, own.transition_middle * scale, own.transition_out * scale, Design.vertices[k].split)}; + auto const shape{build_compound(own.deflection, scaled(own.arcs, scale), own.transition_in * scale, own.transition_out * scale)}; auto const normalin{perpendicular(directionin) * static_cast(side)}; auto const end{directionin * shape.end.x + normalin * shape.end.y}; auto const along{cross(directionout, end) / cross(directionout, directionin)}; @@ -485,9 +512,8 @@ result fit_between(design const &Design) else { fit.radius *= scales[k]; - fit.radius2 *= scales[k]; + fit.arcs = scaled(fit.arcs, scales[k]); fit.transition_in *= scales[k]; - fit.transition_middle *= scales[k]; fit.transition_out *= scales[k]; } r.warnings.emplace_back(format("Vertex %zu: R reduced from %.1f to %.1f m, transitions to %.1f / %.1f m, to fit between the ends", k + 1, Design.vertices[k].radius, fit.radius, fit.transition_in, fit.transition_out)); @@ -525,7 +551,7 @@ result fit_between(design const &Design) } if (fit.compound) { - auto const shape{build_compound(deflection, fit.radius, fit.radius2, fit.transition_in, fit.transition_middle, fit.transition_out, vertex.split)}; + auto const shape{build_compound(deflection, fit.arcs, fit.transition_in, fit.transition_out)}; if (shape.shortened < 1.0) r.warnings.emplace_back(format("Vertex %zu: transition curves shortened to fit the deflection angle", k + 1)); auto const normalin{perpendicular(directionin) * static_cast(side)}; @@ -541,7 +567,10 @@ result fit_between(design const &Design) auto const startchainage{chainage}; glm::dvec2 origin{curvestart}; glm::dvec2 heading{directionin}; - auto const peak{std::max(1.0 / fit.radius, 1.0 / fit.radius2)}; + auto sharpest{fit.radius}; + for (auto const &arc : fit.arcs) + sharpest = std::min(sharpest, arc.radius); + auto const peak{1.0 / sharpest}; for (auto const &segment : shape.segments) { element piece; @@ -568,14 +597,14 @@ result fit_between(design const &Design) curve_report report; report.vertex = static_cast(k); report.radius = fit.radius; - report.radius2 = fit.radius2; + for (auto const &arc : fit.arcs) + report.radii.push_back(arc.radius); report.transition_in = fit.transition_in * shape.shortened; report.transition_out = fit.transition_out * shape.shortened; report.deflection = deflection; report.arc_length = chainage - startchainage; report.tangent_in = glm::distance(position, curvestart); report.tangent_out = glm::distance(position, cursor); - auto const sharpest{std::min(fit.radius, fit.radius2)}; report.unbalanced = unbalanced_acceleration(Design.speed, sharpest, std::max(0.0, vertex.cant), Design.norms); report.recommended = recommend(Design.speed, sharpest, Design.norms); r.curves.push_back(report); diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index c57b386f..732bd454 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -37,6 +37,13 @@ struct limits double tangent_min_time{0.0}; }; +struct compound_arc +{ + double radius{600.0}; + double transition{0.0}; + double share{1.0}; +}; + struct vertex { glm::dvec2 position{0.0}; @@ -47,9 +54,8 @@ struct vertex double cant{0.0}; bool reverse_turn{false}; bool compound{false}; - double radius2{600.0}; - double transition_middle{0.0}; - double split{0.5}; + double share{1.0}; + std::vector arcs; }; struct design @@ -116,7 +122,7 @@ struct curve_report { int vertex{-1}; double radius{0.0}; - double radius2{0.0}; + std::vector radii; double transition_in{0.0}; double transition_out{0.0}; double deflection{0.0}; diff --git a/editor/editorGeometry.hpp b/editor/editorGeometry.hpp index 5b1db3e1..2253f43f 100644 --- a/editor/editorGeometry.hpp +++ b/editor/editorGeometry.hpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include #include "world/Segment.h" @@ -56,6 +57,36 @@ inline double grade_of(glm::dvec3 const &Direction) return Direction.y / std::max(1e-9, std::hypot(Direction.x, Direction.z)); } +inline std::vector arc_pieces(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, double const Radius, double const Angle, int const Side) +{ + std::vector result; + double const side{static_cast(Side)}; + glm::dvec2 const centre{plan_of(Point) + glm::dvec2{-Direction.y, Direction.x} * (side * Radius)}; + auto const count{std::max(1, static_cast(std::ceil(Angle / glm::radians(90.0) - 1e-9)))}; + auto const step{Angle / count}; + auto const handle{4.0 / 3.0 * std::tan(step / 4.0) * Radius}; + glm::dvec2 const radial{plan_of(Point) - centre}; + for (int i = 0; i < count; ++i) + { + auto const a0{side * step * i}; + auto const a1{side * step * (i + 1)}; + auto const p0{centre + turned(radial, a0)}; + auto const p3{centre + turned(radial, a1)}; + auto const d0{turned(Direction, a0)}; + auto const d3{turned(Direction, a1)}; + auto const y0{Point.y + Grade * Radius * step * i}; + auto const y3{Point.y + Grade * Radius * step * (i + 1)}; + segment_data path; + path.points[segment_data::point::start] = {p0.x, y0, p0.y}; + path.points[segment_data::point::end] = {p3.x, y3, p3.y}; + path.points[segment_data::point::control1] = {d0.x * handle, Grade * handle, d0.y * handle}; + path.points[segment_data::point::control2] = {-d3.x * handle, -Grade * handle, -d3.y * handle}; + path.radius = static_cast(Radius); + result.push_back(path); + } + return result; +} + // path as a cubic Bezier curve; a path without control points is a straight with evenly spread parameter struct bezier { diff --git a/editor/editorIncludeInfo.cpp b/editor/editorIncludeInfo.cpp index 46116f9e..8f7fe005 100644 --- a/editor/editorIncludeInfo.cpp +++ b/editor/editorIncludeInfo.cpp @@ -246,6 +246,17 @@ bool parse(template_text const &Text, include_info &Info, std::string &Error) Info.name = document["name"].as(""); Info.category = document["category"].as(""); Info.description = document["description"].as(""); + if (auto const drive{document["switch"]}; drive.IsDefined() && drive.IsMap()) + { + Info.switch_hand = drive["hand"].as(""); + Info.switch_drive = drive["drive"].as(""); + } + if (auto const table{document["turntable"]}; table.IsDefined() && table.IsMap()) + { + Info.turntable_length = table["length"].as(0.0); + Info.turntable_angles = table["angles"].as(0); + Info.turntable_positions = table["positions"].as(0); + } if (auto const parameters{document["params"]}; parameters.IsDefined() && parameters.IsSequence()) { for (auto const &entry : parameters) @@ -633,7 +644,7 @@ std::string compose_directive(std::string const &File, std::vector return text + " end"; } -std::string directive(std::string const &File, include_info const &Info, int const Parameters, glm::dvec3 const &Location, std::optional Yaw) +std::string directive(std::string const &File, include_info const &Info, int const Parameters, glm::dvec3 const &Location, std::optional Yaw, std::string const &Track) { std::vector values; for (auto id = 1; id <= Parameters; ++id) @@ -661,6 +672,10 @@ std::string directive(std::string const &File, include_info const &Info, int con { value = "0"; } + else if (role == "track" && false == Track.empty()) + { + value = Track; + } else if (role == "name") { // names have to be unique in the scenery. the default value, or the name of the template, makes them recognizable diff --git a/editor/editorIncludeInfo.hpp b/editor/editorIncludeInfo.hpp index 90241406..9f8ef370 100644 --- a/editor/editorIncludeInfo.hpp +++ b/editor/editorIncludeInfo.hpp @@ -34,6 +34,11 @@ struct include_info std::string name; std::string category; std::string description; + std::string switch_hand; // a switch drive: side of the diverging track, left or right, looking from the start of the switch + std::string switch_drive; // and the side the drive stands on + double turntable_length{0.0}; + int turntable_angles{0}; + int turntable_positions{0}; std::vector parameters; // description of specified parameter, created if there's none @@ -66,7 +71,7 @@ bool complete(include_info const &Info, int Parameters, std::string *Issue = nul // builds include directive which places specified template. Location and Yaw are the placement to pass through // the parameters with matching roles, in the coordinates of the place the directive goes to; without Yaw the // rotation parameters receive their default values -std::string directive(std::string const &File, include_info const &Info, int Parameters, glm::dvec3 const &Location, std::optional Yaw); +std::string directive(std::string const &File, include_info const &Info, int Parameters, glm::dvec3 const &Location, std::optional Yaw, std::string const &Track = {}); // splits include directive into the name of the included file and the values of the parameters, both as they're // written. returns: false if the text isn't a complete include directive diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index d4dcb9c4..a3d4854c 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -17,6 +17,9 @@ http: #include "scene/scene.h" #include "scene/scenelayers.h" #include "simulation/simulation.h" +#include "editor/editorIncludeInfo.hpp" +#include "world/Event.h" +#include "world/MemCell.h" #include "rendering/renderer.h" #include "utilities/Globals.h" #include "utilities/utilities.h" @@ -157,6 +160,7 @@ editor_track::state editor_track::capture(TTrack const &Track) result.sleeper_ballast = Track.m_sleeper_ballast_z; for (int i = 0; i < 6; ++i) result.events[i] = events(const_cast(Track), i); + result.name = Track.name(); return result; } @@ -257,6 +261,131 @@ void editor_track::apply(TTrack &Track, state const &State) Track.m_events = std::any_of(State.events.begin(), State.events.end(), [](auto const &Events) { return std::any_of(Events.begin(), Events.end(), [](auto const &Event) { return Event.second != nullptr; }); }); + if (State.name != Track.name()) + rename(Track, State.name); +} + +namespace +{ + +std::string lower_name(std::string Name) +{ + for (auto &character : Name) + if (character >= 'A' && character <= 'Z') + character = static_cast(character - 'A' + 'a'); + return Name; +} + +int track_parameter(scene::instance_handle const Instance, std::string &File, std::vector &Values) +{ + if (false == scene::Layers.tracked(Instance)) + return -1; + auto const &included{scene::Layers.instance(Instance)}; + if (included.removed || included.dead || included.file == nullptr) + return -1; + static std::map descriptions; + auto found{descriptions.find(*included.file)}; + if (found == descriptions.end()) + { + include_info info; + std::string error; + editor_includes::load(*included.file, info, error); + found = descriptions.emplace(*included.file, std::move(info)).first; + } + auto const index{editor_includes::parameter_with_role(found->second, "track") - 1}; + if (index < 0 || false == editor_includes::parse_directive(scene::Layers.directive(Instance), File, Values) || index >= static_cast(Values.size())) + return -1; + return index; +} + +} // namespace + +bool editor_track::name_valid(TTrack const &Track, std::string &Name, std::string &Reason, TTrack **Owner) +{ + Name = lower_name(Name); + if (Name.empty() || Name == "none") + { + Reason = "The path needs a name"; + return false; + } + if (Name.find_first_of(" \t\r\n\"';") != std::string::npos || Name.find("//") != std::string::npos) + { + Reason = "The name can't hold blanks, quotes, semicolons or //"; + return false; + } + auto *owner{simulation::Paths.find(Name)}; + if (owner != nullptr && owner != &Track && false == owner->m_editorremoved) + { + if (Owner != nullptr) + *Owner = owner; + Reason = "Another path has this name"; + return false; + } + Reason.clear(); + return true; +} + +editor_track::name_uses editor_track::uses_of_name(TTrack const &Track) +{ + name_uses result; + auto const &name{Track.name()}; + if (name.empty() || name == "none") + return result; + for (auto const *cell : simulation::Memory.sequence()) + result.cells += (cell != nullptr && cell->Track == &Track) ? 1 : 0; + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + std::string file; + std::vector values; + auto const index{track_parameter(handle, file, values)}; + result.includes += (index >= 0 && lower_name(values[index]) == name) ? 1 : 0; + } + static std::array const kinds{"event0", "event1", "event2", "eventall0", "eventall1", "eventall2"}; + for (auto const *event : simulation::Events.sequence()) + { + if (event == nullptr) + continue; + auto const &eventname{event->name()}; + if (std::any_of(kinds.begin(), kinds.end(), [&](char const *Kind) { return eventname == name + ':' + Kind; })) + { + result.loose.push_back(eventname); + continue; + } + if (result.includes > 0 && eventname.compare(0, name.size(), name) == 0) + continue; + auto const targets{event->target_nodes()}; + if (std::find(targets.begin(), targets.end(), &Track) != targets.end()) + result.events.push_back(eventname); + } + return result; +} + +void editor_track::rename(TTrack &Track, std::string const &Name) +{ + auto const old{Track.name()}; + if (old == Name) + return; + simulation::Paths.rename(&Track, Name); + Track.m_name = Name; + Track.mark_dirty(); + for (auto *cell : simulation::Memory.sequence()) + if (cell != nullptr && cell->Track == &Track) + { + cell->asTrackName = Name; + cell->mark_dirty(); + } + if (old.empty() || old == "none") + return; + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + std::string file; + std::vector values; + auto const index{track_parameter(handle, file, values)}; + if (index < 0 || lower_name(values[index]) != old) + continue; + values[index] = Name; + scene::Layers.modify(handle, editor_includes::compose_directive(file, values)); + } } bool editor_track::is_supported(TTrack const &Track) @@ -550,9 +679,13 @@ editor_track::snap_target editor_track::find_snap_target(TTrack const &Track, gl editor_track::snap_target editor_track::find_free_end(TTrack const *Self, int const Category, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude) { - snap_target result; - result.distance = std::numeric_limits::max(); + auto const found{free_ends(Self, Category, Position, Radius, Exclude)}; + return found.empty() ? snap_target{} : found.front(); +} +std::vector editor_track::free_ends(TTrack const *Self, int const Category, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude) +{ + std::vector result; auto const excluded = [&](TTrack const *Other) { return std::find(Exclude.begin(), Exclude.end(), Other) != Exclude.end(); }; auto const sections{simulation::Region->sections(Position, static_cast(Radius))}; for (auto *section : sections) @@ -561,7 +694,7 @@ editor_track::snap_target editor_track::find_free_end(TTrack const *Self, int co { for (auto *other : cell.m_directories.paths) { - if (other == Self || excluded(other) || (other->iCategoryFlag & 15) != Category || false == is_supported(*other)) + if (other == Self || excluded(other) || other->m_editorremoved || (other->iCategoryFlag & 15) != Category || false == is_supported(*other)) continue; for (int i = 0; i < static_cast(other->m_paths.size()); ++i) { @@ -570,7 +703,9 @@ editor_track::snap_target editor_track::find_free_end(TTrack const *Self, int co { auto const &point{path.points[point_index(kind)]}; auto const distance{glm::distance(point, Position)}; - if (distance > Radius || distance >= result.distance) + if (distance > Radius) + continue; + if (std::any_of(result.begin(), result.end(), [&](snap_target const &Found) { return Found.track == other && Found.point == point_ref{i, kind}; })) continue; auto const connections{connected_points(*other, point)}; if (std::any_of(connections.begin(), connections.end(), [&](auto const &Connection) { return false == excluded(Connection.first); })) @@ -590,16 +725,19 @@ editor_track::snap_target editor_track::find_free_end(TTrack const *Self, int co if (glm::length(direction) < 1e-6) continue; - result.track = other; - result.point = point_ref{i, kind}; - result.position = point; - result.direction = glm::normalize(direction); - result.distance = distance; + snap_target target; + target.track = other; + target.point = point_ref{i, kind}; + target.position = point; + target.direction = glm::normalize(direction); + target.distance = distance; + result.push_back(target); } } } } } + std::sort(result.begin(), result.end(), [](snap_target const &A, snap_target const &B) { return A.distance < B.distance; }); return result; } @@ -1341,42 +1479,31 @@ bool editor_track::analyse_curve(std::vector> const &R if (Curve.transition_in + Curve.transition_out >= std::accumulate(lengths.begin(), lengths.end(), 0.0)) Curve.transition_in = Curve.transition_out = 0.0; Curve.radius = smallest; - std::vector> arcs; + std::vector arcs; + double between{0.0}; for (std::size_t i = 0; i < run.size(); ++i) { if (varying[i] || curvatures[i] <= 0.0) + { + if (false == arcs.empty()) + between += lengths[i]; continue; + } auto const radius{1.0 / curvatures[i]}; - if (false == arcs.empty() && std::abs(arcs.back().first - radius) < 0.05 * radius) - arcs.back().second += lengths[i]; + if (false == arcs.empty() && std::abs(arcs.back().radius - radius) < 0.05 * radius) + arcs.back().turn += (lengths[i] + between) / radius; else - arcs.emplace_back(radius, lengths[i]); + arcs.push_back({radius, lengths[i] / radius, arcs.empty() ? 0.0 : between}); + between = 0.0; } if (arcs.size() >= 2 && Curve.reversals == 0) { - auto const &first{arcs.front()}; - auto const &last{arcs.back()}; - if (std::abs(first.first - last.first) > 0.1 * std::min(first.first, last.first)) + auto const [low, high]{std::minmax_element(arcs.begin(), arcs.end(), [](curve::arc_part const &A, curve::arc_part const &B) { return A.radius < B.radius; })}; + if (high->radius - low->radius > 0.1 * low->radius) { Curve.compound = true; - Curve.radius = first.first; - Curve.radius2 = last.first; - auto const turnfirst{first.second / first.first}; - auto const turnlast{last.second / last.first}; - Curve.split = turnfirst / std::max(1e-9, turnfirst + turnlast); - double middle{0.0}; - bool inside{false}; - for (std::size_t i = 0; i < run.size(); ++i) - { - if (false == varying[i]) - { - inside = true; - continue; - } - if (inside && i + 1 < run.size() && std::any_of(varying.begin() + i + 1, varying.end(), [](bool const Varying) { return false == Varying; })) - middle += lengths[i]; - } - Curve.transition_middle = middle; + Curve.radius = arcs.front().radius; + Curve.arcs = arcs; } } Curve.from = run.front().first; @@ -2131,14 +2258,6 @@ std::vector editor_track::standard_switch_templat {"Rz 1:26,5 R2500", 2500.0, 26.5, 0.0, 0.0}, }; std::vector result; - for (auto const &definition : {std::pair{190.0, "1:9"}, {300.0, "1:9"}, {500.0, "1:12"}}) - { - switch_template slip; - slip.double_slip = true; - slip.radius = definition.first; - slip.label = std::string{"Rkpd "} + definition.second + " R" + std::to_string(static_cast(definition.first)) + " double slip (click at a crossing of two straights)"; - result.push_back(slip); - } for (auto const &definition : definitions) { auto const angle{std::atan(1.0 / definition.ratio)}; @@ -2162,6 +2281,14 @@ std::vector editor_track::standard_switch_templat entry.label = std::string{definition.name} + " a " + std::to_string(a).substr(0, std::to_string(a).find('.') + 4) + " b " + std::to_string(b).substr(0, std::to_string(b).find('.') + 4) + (estimated ? " (b est.)" : ""); result.push_back(entry); } + for (auto const &slipping : {std::pair{190.0, "1:9"}, {300.0, "1:9"}, {500.0, "1:12"}}) + { + switch_template slip; + slip.double_slip = true; + slip.radius = slipping.first; + slip.label = std::string{"Rkpd "} + slipping.second + " R" + std::to_string(static_cast(slipping.first)) + " double slip (click at a crossing of two straights)"; + result.push_back(slip); + } return result; } @@ -2210,6 +2337,22 @@ material_handle ballast_of(TTrack const &Style) } // namespace +TTrack *editor_track::create_turntable(segment_data const &Path, TTrack const *Style, std::string const &Name) +{ + std::ostringstream text; + text.precision(std::numeric_limits::digits10); + auto const length{glm::distance(Path.points[segment_data::point::start], Path.points[segment_data::point::end])}; + if (Style != nullptr && (Style->iCategoryFlag & 15) == 1 && Style->m_visible) + write_header(text, "turn", length, Style->fSoundDistance, *Style, null_handle); + else + text << "turn " << length << " 1.435 0.15 25 20 0 flat vis rail_screw_used1 4 none 0.2 0.5 1.3 "; + write_points(text, Path); + text << "endtrack\n"; + auto *track{load_text(text.str(), Style, Name)}; + track->m_paths = {Path}; + return track; +} + TTrack *editor_track::create_switch(switch_template const &Template, std::vector const &Paths, TTrack const &Style, std::string const &Name) { if (Paths.size() < 2) diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 6ed9d70b..43d42762 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -62,10 +62,22 @@ class editor_track glm::vec3 sleeper_offset{0.f}; float sleeper_ballast{0.f}; std::array>, 6> events; + std::string name; }; static state capture(TTrack const &Track); static void apply(TTrack &Track, state const &State); + static bool name_valid(TTrack const &Track, std::string &Name, std::string &Reason, TTrack **Owner = nullptr); + struct name_uses + { + int cells{0}; + int includes{0}; + std::vector events; + std::vector loose; + }; + static name_uses uses_of_name(TTrack const &Track); + static void rename(TTrack &Track, std::string const &Name); + // lists of the events of the path, in the order of event_keyword(): event0, event1, event2, eventall0, eventall1, eventall2 static std::vector> &events(TTrack &Track, int const Index); static char const *event_keyword(int const Index); @@ -116,6 +128,8 @@ class editor_track static snap_target find_snap_target(TTrack const &Track, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude); // nearest end of a path of the category which nothing else joins; Self may be null static snap_target find_free_end(TTrack const *Self, int const Category, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude); + // all of them within the radius, nearest first + static std::vector free_ends(TTrack const *Self, int const Category, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude); static void snap_point(TTrack &Track, point_ref const &Point, snap_target const &Target, bool const Aligntangent); struct chain @@ -183,9 +197,13 @@ class editor_track double transition_out{0.0}; double cant{0.0}; bool compound{false}; - double radius2{0.0}; - double transition_middle{0.0}; - double split{0.5}; + struct arc_part + { + double radius{0.0}; + double turn{0.0}; + double transition{0.0}; + }; + std::vector arcs; }; static bool find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve); // curve made of the given paths, each with the flag of being run from its start @@ -292,6 +310,7 @@ class editor_track static glm::dvec3 point_at(TTrack const &Track, double const T); static std::vector place_switch(switch_template const &Template, glm::dvec3 const &Origin, glm::dvec2 const &Direction, int const Side, double const Grade); static TTrack *create_switch(switch_template const &Template, std::vector const &Paths, TTrack const &Style, std::string const &Name = {}); + static TTrack *create_turntable(segment_data const &Path, TTrack const *Style, std::string const &Name); static void move_straights(std::vector const &Lines, std::vector> const &Ends); static std::vector relay(chain const &Chain, std::vector const &Pieces); static void retire(TTrack &Track); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index fc7b7c14..6f4ad28b 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -419,6 +419,9 @@ msgstr "widok z góry" msgid "undo" msgstr "cofnij" +msgid "redo" +msgstr "ponów" + msgid "lay, switch, straight, curve, profile, speed, infra" msgstr "układanie, rozjazd, prosta, łuk, profil, prędkość, infrastruktura" @@ -1854,9 +1857,6 @@ msgstr "Modeli wchodzących w skrajnię: %zu%s" msgid " on the map" msgstr " na mapie" -msgid "none" -msgstr "brak" - msgid "Outline of %s (right half)" msgstr "Obrys %s (prawa połowa)" @@ -2023,3 +2023,1438 @@ msgstr "Rzeźbienie..." msgid "Opens the terrain tab of the editor settings: raising, lowering and smoothing with a brush" msgstr "Otwiera zakładkę terenu w ustawieniach edytora: podnoszenie, obniżanie i wygładzanie pędzlem" + +# scenery editor: track editor, selection, joints, parallel track, hectometre posts, fouling points + +msgid " (branch)" +msgstr " (zwrotny)" + +msgid " (main)" +msgstr " (zasadniczy)" + +msgid "%s set on %zu paths, Ctrl+Z takes it back" +msgstr "%s ustawiono na %zu odcinkach, Ctrl+Z cofa" + +msgid "%zu fouling point markers moved to their places, Ctrl+Z takes it back" +msgstr "Przestawiono %zu wskaźników ukresu na ich miejsca, Ctrl+Z cofa" + +msgid "%zu fouling point markers placed, Ctrl+Z takes them back" +msgstr "Postawiono %zu wskaźników ukresu, Ctrl+Z je usuwa" + +msgid "%zu found, the first %zu listed" +msgstr "Znaleziono %zu, pokazano pierwsze %zu" + +msgid "%zu hectometre posts moved to their places, Ctrl+Z takes it back" +msgstr "Przestawiono %zu słupków hektometrowych na ich miejsca, Ctrl+Z cofa" + +msgid "%zu hectometre posts placed, Ctrl+Z takes them back" +msgstr "Postawiono %zu słupków hektometrowych, Ctrl+Z je usuwa" + +msgid "%zu joints fixed, Ctrl+Z takes them back" +msgstr "Naprawiono %zu styków, Ctrl+Z cofa" + +msgid "%zu paths checked: %d gaps, %d steps, %d kinks, %d cant jumps, %d grade breaks" +msgstr "Sprawdzono %zu odcinków: %d szczelin, %d uskoków, %d załamań, %d skoków przechyłki, %d załamań pochylenia" + +msgid "%zu paths deleted, Ctrl+Z brings them back" +msgstr "Usunięto %zu odcinków, Ctrl+Z je przywraca" + +msgid "%zu paths selected, %.1f m" +msgstr "Zaznaczono %zu odcinków, %.1f m" + +msgid "%zu switches, %zu fouling points: %d without a marker, %d markers standing wrong" +msgstr "%zu rozjazdów, %zu ukresów: %d bez wskaźnika, %d wskaźników stoi źle" + +msgid "(of the direction of the line, see the preview)" +msgstr "(względem kierunku linii, patrz podgląd)" + +msgid ", %d free ends" +msgstr ", %d wolnych końców" + +msgid ", %zu in the files which can't be changed are left as they are" +msgstr ", %zu w plikach, których nie można zmieniać, zostaje bez zmian" + +msgid ", %zu with vehicles on them or in the files which can't be changed are left" +msgstr ", %zu z pojazdami albo w plikach, których nie można zmieniać, zostaje" + +msgid "3.5 m on the straight track; more on the curves, where the cars lean out" +msgstr "3,5 m na prostej; na łukach więcej, bo wagony się wychylają" + +msgid "A free end closer than this to the end of another path is a gap: the trains don't go across it.\nThe simulation joins the ends no more than 2 cm apart" +msgstr "Wolny koniec bliżej niż tyle od końca innego odcinka to szczelina: pociągi przez nią nie przejadą.\nSymulacja łączy końce oddalone najwyżej o 2 cm" + +msgid "A marker off by more (m) is wrong" +msgstr "Wskaźnik dalej niż tyle (m) stoi źle" + +msgid "A post standing off by more (m) is wrong" +msgstr "Słupek przesunięty o więcej (m) stoi źle" + +msgid "A switch has no line of its own: select a plain path" +msgstr "Rozjazd nie ma własnej linii: zaznacz zwykły odcinek" + +msgid "Add to the selection" +msgstr "Dodaj do zaznaczenia" + +msgid "Along the line each way (m)" +msgstr "Wzdłuż linii w każdą stronę (m)" + +msgid "Along the line of the selected path" +msgstr "Wzdłuż linii zaznaczonego odcinka" + +msgid "Around the camera" +msgstr "Wokół kamery" + +msgid "Around the camera (m)" +msgstr "Wokół kamery (m)" + +msgid "Build %zu paths, %.1f m" +msgstr "Zbuduj %zu odcinków, %.1f m" + +msgid "Cant jump (mm)" +msgstr "Skok przechyłki (mm)" + +msgid "Circuits" +msgstr "Obwody" + +msgid "Copy the name" +msgstr "Kopiuj nazwę" + +msgid "Delete" +msgstr "Usuń" + +msgid "Delete %zu paths" +msgstr "Usuń %zu odcinków" + +msgid "Ends of the paths which almost meet, steps, kinks, jumps of the cant and of the grade at the joints" +msgstr "Końce odcinków, które prawie się stykają, uskoki, załamania, skoki przechyłki i pochylenia na stykach" + +msgid "Ends of the paths which almost meet, steps, kinks,\njumps of the cant and of the grade at the joints" +msgstr "Końce odcinków, które prawie się stykają, uskoki, załamania,\nskoki przechyłki i pochylenia na stykach" + +msgid "Environment" +msgstr "Otoczenie" + +msgid "Find a track by its name (Ctrl+F)" +msgstr "Znajdź tor po nazwie (Ctrl+F)" + +msgid "Fix" +msgstr "Napraw" + +msgid "Fix %zu listed" +msgstr "Napraw %zu z listy" + +msgid "Fix this one" +msgstr "Napraw ten" + +msgid "Fouling point markers" +msgstr "Wskaźniki ukresu (W17)" + +msgid "Free ends too" +msgstr "Także wolne końce" + +msgid "From the axis (m)" +msgstr "Od osi toru (m)" + +msgid "Gap up to (m)" +msgstr "Szczelina do (m)" + +msgid "Gaps: the free end goes to the other end, along it. Steps: the end goes to the other one.\nKinks and grade breaks: the end is turned in line with the other path. Cant: the cant of the other path.\nThe plain path moves, a switch stays as it is. One Ctrl+Z takes it all back" +msgstr "Szczeliny: wolny koniec idzie do drugiego końca, w jego kierunku. Uskoki: koniec idzie do drugiego.\nZałamania kierunku i pochylenia: koniec obraca się w linię z drugim odcinkiem. Przechyłka: taka jak na drugim odcinku.\nRusza się zwykły odcinek, rozjazd zostaje. Jedno Ctrl+Z cofa wszystko" + +msgid "Grade break (per mille)" +msgstr "Załamanie pochylenia (‰)" + +msgid "Hectometre posts" +msgstr "Słupki hektometrowe" + +msgid "Hectometre posts from here" +msgstr "Słupki hektometrowe od tego miejsca" + +msgid "Joints" +msgstr "Styki" + +msgid "Joints: LMB on a row shows the place" +msgstr "Styki: LPM na wierszu pokazuje miejsce" + +msgid "Kilometrage read from %zu posts standing along the line" +msgstr "Kilometraż odczytany z %zu słupków stojących przy linii" + +msgid "Kilometrage read from the post %s, check which way it grows" +msgstr "Kilometraż odczytany ze słupka %s, sprawdź, w którą stronę rośnie" + +msgid "Kilometre at the point (km)" +msgstr "Kilometr w tym miejscu (km)" + +msgid "Kink (deg)" +msgstr "Załamanie (st.)" + +msgid "Left" +msgstr "Lewa" + +msgid "Line %.0f m, km %s - %s: %d posts to place, %zu standing, %d of them wrong" +msgstr "Linia %.0f m, km %s - %s: %d słupków do postawienia, %zu stoi, %d z nich źle" + +msgid "Look at it" +msgstr "Pokaż" + +msgid "Marker" +msgstr "Wskaźnik" + +msgid "Move %d wrong markers to their places" +msgstr "Przestaw %d źle stojących wskaźników" + +msgid "Move %d wrong posts to their places" +msgstr "Przestaw %d źle stojących słupków" + +msgid "No line through this path" +msgstr "Przez ten odcinek nie biegnie żadna linia" + +msgid "No track of that name" +msgstr "Nie ma toru o takiej nazwie" + +msgid "Nothing could be fixed: the paths are switches or in the files which can't be changed" +msgstr "Nic nie dało się naprawić: to rozjazdy albo odcinki w plikach, których nie można zmieniać" + +msgid "Parallel track" +msgstr "Tor równoległy" + +msgid "Parallel track of %zu paths, %.1f m, %.2f m away; Ctrl+Z takes it back" +msgstr "Tor równoległy z %zu odcinków, %.1f m, w rozstawie %.2f m; Ctrl+Z cofa" + +msgid "Place %d markers" +msgstr "Postaw %d wskaźników" + +msgid "Place %d posts" +msgstr "Postaw %d słupków" + +msgid "Posts" +msgstr "Słupki" + +msgid "RMB on a track" +msgstr "PPM na torze" + +msgid "Rails" +msgstr "Szyny" + +msgid "Read from the posts" +msgstr "Odczytaj ze słupków" + +msgid "Right" +msgstr "Prawa" + +msgid "Second track alongside, curves included: radius R minus or plus the spacing,\nthe same transitions and cant, heights on the plane of the cant" +msgstr "Drugi tor obok, także na łukach: promień R minus albo plus rozstaw,\nte same krzywe przejściowe i przechyłka, wysokości na płaszczyźnie przechyłki" + +msgid "Select a path of the line, or RMB on it: Hectometre posts" +msgstr "Zaznacz odcinek linii albo PPM na nim: Słupki hektometrowe" + +msgid "Select a switch, or check around the camera" +msgstr "Zaznacz rozjazd albo sprawdź wokół kamery" + +msgid "Shift+LMB" +msgstr "Shift+LPM" + +msgid "Side, looking where it grows" +msgstr "Strona, patrząc w kierunku wzrostu" + +msgid "Spacing (m)" +msgstr "Rozstaw (m)" + +msgid "Spacing of the axes (m)" +msgstr "Odległość osi torów (m)" + +msgid "Speed check of the line" +msgstr "Sprawdź prędkość linii" + +msgid "Split here" +msgstr "Podziel tutaj" + +msgid "Step (m)" +msgstr "Uskok (m)" + +msgid "Take out of the selection" +msgstr "Usuń z zaznaczenia" + +msgid "Templates can be placed only in a scenery opened for editing (-edit)" +msgstr "Szablony można stawiać tylko w scenerii otwartej do edycji (-edit)" + +msgid "The active layer can't take them" +msgstr "Aktywna warstwa ich nie przyjmie" + +msgid "The active layer can't take them: " +msgstr "Aktywna warstwa ich nie przyjmie: " + +msgid "The file of the active layer would receive them with a rotation or a scale in effect, pick another layer" +msgstr "Plik aktywnej warstwy dostałby je z obrotem albo skalą, wybierz inną warstwę" + +msgid "The gizmo moves them together, W turns them; Shift+LMB adds or takes out a path" +msgstr "Gizmo przesuwa je razem, W obraca; Shift+LPM dodaje albo usuwa odcinek" + +msgid "The kilometrage grows" +msgstr "Kilometraż rośnie" + +msgid "The line, up to the switches" +msgstr "Linia, do rozjazdów" + +msgid "The numbers of the %zu posts along the line don't go by 100 m, the kilometrage wasn't read from them" +msgstr "Numery %zu słupków przy linii nie idą co 100 m, kilometrażu z nich nie odczytano" + +msgid "The scenery isn't opened for editing, the new track won't be saved" +msgstr "Sceneria nie jest otwarta do edycji, nowy tor nie zostanie zapisany" + +msgid "The selected switch" +msgstr "Zaznaczony rozjazd" + +msgid "The spacing is larger than the radius of %s" +msgstr "Rozstaw jest większy niż promień odcinka %s" + +msgid "This path" +msgstr "Ten odcinek" + +msgid "Trackbed" +msgstr "Podsypka" + +msgid "Turned by (deg)" +msgstr "Obrót (st.)" + +msgid "Velocity" +msgstr "Prędkość" + +msgid "Vertical profile of the line" +msgstr "Profil podłużny linii" + +msgid "W17 %.1f m off" +msgstr "W17 o %.1f m za daleko" + +msgid "W17 where the axes of the tracks meeting at a switch are the spacing apart,\nhalf way between them" +msgstr "W17 tam, gdzie osie torów zbiegających się na rozjeździe są o rozstaw od siebie,\nw połowie międzytorza" + +msgid "Whole straight" +msgstr "Cała prosta" + +msgid "adds a track to the selection, drag: a box" +msgstr "dodaje tor do zaznaczenia, przeciągnięcie: prostokąt" + +msgid "against it" +msgstr "przeciwnie" + +msgid "along the line" +msgstr "zgodnie z linią" + +msgid "cant jump" +msgstr "skok przechyłki" + +msgid "fixed" +msgstr "naprawiono" + +msgid "free end" +msgstr "wolny koniec" + +msgid "gap" +msgstr "szczelina" + +msgid "grade break" +msgstr "załamanie pochylenia" + +msgid "joins the ends, aligns them" +msgstr "łączy końce, wyrównuje je" + +msgid "kink" +msgstr "załamanie" + +msgid "km %s stands %+.1f m off##%u" +msgstr "km %s stoi %+.1f m od miejsca##%u" + +msgid "km %s, %+.1f m off" +msgstr "km %s, %+.1f m od miejsca" + +msgid "modes: lay, switch, straight, curve, profile, speed, joints, infra" +msgstr "tryby: układanie, rozjazd, prosta, łuk, profil, prędkość, styki, infrastruktura" + +msgid "show the place" +msgstr "pokazuje miejsce" + +msgid "step" +msgstr "uskok" + +msgid "texture of the rails" +msgstr "tekstura szyn" + +msgid "texture of the trackbed" +msgstr "tekstura podsypki" + +msgid "there's no active layer" +msgstr "nie ma aktywnej warstwy" + +msgid "track circuits, separated by spaces" +msgstr "obwody torowe, oddzielone spacjami" + +msgid "type the length" +msgstr "wpisz długość" + +msgid "type the length of the next leg, Enter adds the point" +msgstr "wpisz długość następnego odcinka, Enter dodaje punkt" + +msgid "type the radius, a space, the length" +msgstr "wpisz promień, spację, długość" + +msgid "type the shift, + to the left" +msgstr "wpisz przesunięcie, + w lewo" + +msgid "type the turn in degrees, + to the left" +msgstr "wpisz obrót w stopniach, + w lewo" + +msgid "what can be done with it" +msgstr "co można z nim zrobić" + +msgid "Vehicle on the track" +msgstr "Pojazd na torze" + +msgid "A vehicle to drive, placed where RMB on the track was clicked, or in the middle of the path.\nIt isn't written to the scenery files" +msgstr "Pojazd do jazdy, stawiany tam, gdzie kliknięto PPM na torze, albo w połowie odcinka.\nNie jest zapisywany do plików scenerii" + +msgid "search: name, series, skin" +msgstr "szukaj: nazwa, seria, malowanie" + +msgid "Only the ones with a cab" +msgstr "Tylko z kabiną" + +msgid "With a driver, to drive" +msgstr "Z maszynistą, do jazdy" + +msgid "Nobody, parked" +msgstr "Bez obsady, odstawiony" + +msgid "Pick a vehicle from the list" +msgstr "Wybierz pojazd z listy" + +msgid "Place" +msgstr "Postaw" + +msgid "Place and drive" +msgstr "Postaw i jedź" + +msgid "The vehicle couldn't be placed, the log says why" +msgstr "Nie udało się postawić pojazdu, przyczyna jest w logu" + +msgid "%s stands on %s. It isn't saved with the scenery" +msgstr "%s stoi na %s. Nie zapisuje się razem ze scenerią" + +msgid "Place a vehicle here" +msgstr "Postaw tu pojazd" + +msgid "Map" +msgstr "Mapa" + +msgid "Orthophoto" +msgstr "Ortofotomapa" + +msgid "Orthophoto settings..." +msgstr "Ustawienia ortofotomapy..." + +msgid "Orthophoto##window" +msgstr "Ortofotomapa##window" + +msgid "Aerial imagery of geoportal.gov.pl under the scenery, laid out by the origin of the scenery" +msgstr "Zdjęcia lotnicze z geoportal.gov.pl pod scenerią, ułożone według punktu odniesienia scenerii" + +msgid "in the scenery file: Y (easting) %.0f m, X (northing) %.0f m" +msgstr "w pliku scenerii: Y (wschód) %.0f m, X (północ) %.0f m" + +msgid "Use it" +msgstr "Użyj" + +msgid "Model along the track" +msgstr "Model wzdłuż toru" + +msgid "A model bent to follow the track, or repeated along it: platforms, walls, fences, barriers, lamps" +msgstr "Model wygięty wzdłuż toru albo powtarzany przy nim: perony, mury, płoty, bariery, latarnie" + +msgid "By this track:" +msgstr "Przy tym torze:" + +msgid "New one" +msgstr "Nowy" + +msgid "model, e.g. peronowe/1k96w200m" +msgstr "model, np. peronowe/1k96w200m" + +msgid "search the models" +msgstr "szukaj modeli" + +msgid "Skin" +msgstr "Skóra" + +msgid "The model is %.2f m long along the track" +msgstr "Model ma %.2f m długości wzdłuż toru" + +msgid "The model has no triangles to lay, or it can't be loaded" +msgstr "Model nie ma trójkątów do ułożenia albo nie da się go wczytać" + +msgid "Bent along the track" +msgstr "Wygięty wzdłuż toru" + +msgid "Repeated" +msgstr "Powtarzany" + +msgid "Copy length (m, 0: the model's)" +msgstr "Długość kopii (m, 0: jak model)" + +msgid "Step (m, 0: the model's length)" +msgstr "Krok (m, 0: długość modelu)" + +msgid "Sideways (m, + left)" +msgstr "W bok (m, + w lewo)" + +msgid "Up (m)" +msgstr "W górę (m)" + +msgid "Axis of the model along the track" +msgstr "Oś modelu wzdłuż toru" + +msgid "Turned around" +msgstr "Odwrócony" + +msgid "Mirrored" +msgstr "Lustrzany" + +msgid "Leans with the cant" +msgstr "Przechyla się z przechyłką" + +msgid "Along" +msgstr "Wzdłuż" + +msgid "this path" +msgstr "tego odcinka" + +msgid "the line to the switches" +msgstr "linii do rozjazdów" + +msgid "through the switches" +msgstr "przez rozjazdy" + +msgid "From (m)" +msgstr "Od (m)" + +msgid "To (m, -1: the end)" +msgstr "Do (m, -1: koniec)" + +msgid "LMB where it starts, the distance from the track sets the offset; drag to where it ends" +msgstr "LPM tam, gdzie się zaczyna (odległość od toru ustawia przesunięcie), przeciągnij do końca" + +msgid "Mark it in the view" +msgstr "Zaznacz w widoku" + +msgid "line %.0f m" +msgstr "linia %.0f m" + +msgid "Lay it" +msgstr "Ułóż" + +msgid "Lay it again along the selected line" +msgstr "Ułóż ponownie wzdłuż zaznaczonej linii" + +msgid "%s laid along %.1f m, Ctrl+Z takes it back" +msgstr "%s ułożony na %.1f m, Ctrl+Z cofa" + +msgid "The scenery isn't opened for editing, it won't be saved" +msgstr "Sceneria nie jest otwarta do edycji, to nie zostanie zapisane" + +msgid "type the shift, + to the right" +msgstr "wpisz przesunięcie, + w prawo" + +msgid "type the turn in degrees, + to the right" +msgstr "wpisz obrót w stopniach, + w prawo" + +msgid "Sideways (m, + right)" +msgstr "W bok (m, + w prawo)" + +msgid "model origin" +msgstr "początek modelu" + +msgid "side facing the track" +msgstr "strona od toru" + +msgid "far side" +msgstr "dalsza strona" + +msgid "middle" +msgstr "środek" + +msgid "Up (m, from the rail top)" +msgstr "W górę (m, od główki szyny)" + +msgid "bottom" +msgstr "spód" + +msgid "top" +msgstr "wierzch" + +msgid "Platform edge:" +msgstr "Krawędź peronu:" + +msgid "The edge of the platform 1.725 m from the axis of the track, its top this high over the rail top" +msgstr "Krawędź peronu 1,725 m od osi toru, wierzch tyle nad główką szyny" + +msgid "Parameters of the template" +msgstr "Parametry szablonu" + +msgid "The location and the rotation are zeros, the template is laid out along the track" +msgstr "Położenie i obrót są zerami, szablon układa się wzdłuż toru" + +msgid "top of the edge by the track" +msgstr "wierzch krawędzi od toru" + +msgid "Edge to the track" +msgstr "Krawędź do toru" + +msgid "The model is turned so its -x side faces the track, the way the platforms and the walls of the scenery are made" +msgstr "Model jest obrócony stroną -x do toru, tak jak są zrobione perony i mury w scenerii" + +msgid "Length" +msgstr "Długość" + +msgid "as the original" +msgstr "jak oryginał" + +msgid "given" +msgstr "podana" + +msgid "the whole stretch" +msgstr "cały zakres" + +msgid "%.1f m: %d copies of %.2f m (the original %.2f m)" +msgstr "%.1f m: %d kopii po %.2f m (oryginał %.2f m)" + +msgid "%.1f m: %d copies every %.2f m" +msgstr "%.1f m: %d kopii co %.2f m" + +msgid "The line ends before that, it's cut there" +msgstr "Linia kończy się wcześniej, tam zostanie ucięte" + +msgid "Set back by the widening of the structure gauge in the curves" +msgstr "Odsunięty o poszerzenie skrajni na łukach" + +msgid "The offset grows by the widening of the gauge below the platforms: 3.75/R in the curves of R >= 250 m,\nmore in the sharper ones, and on the inner side of a canted curve by the cant times the height of the edge / 1.5;\nit begins 20 m (inner side) and 26 m (outer side) ahead of the curve, as in the gauge check" +msgstr "Przesunięcie rośnie o poszerzenie skrajni poniżej peronów: 3,75/R na łukach R >= 250 m,\nwięcej na ciaśniejszych, a po wewnętrznej stronie łuku z przechyłką o przechyłkę razy wysokość krawędzi / 1,5;\nzaczyna się 20 m (strona wewnętrzna) i 26 m (zewnętrzna) przed łukiem, jak w sprawdzaniu skrajni" + +msgid "On the ground" +msgstr "Na gruncie" + +msgid "Over the rail heads" +msgstr "Nad główką szyny" + +msgid "by (m)" +msgstr "o (m)" + +msgid "A marker higher or lower by more (m) is wrong" +msgstr "Wskaźnik wyżej lub niżej o więcej (m) stoi źle" + +msgid "W17 %.1f m off, %+.2f m high" +msgstr "W17 %.1f m od miejsca, %+.2f m wysokości" + +msgid "alternately" +msgstr "naprzemiennie" + +msgid "The even hectometres on the right, the odd ones on the left" +msgstr "Parzyste hektometry po prawej, nieparzyste po lewej" + +msgid "Objects" +msgstr "Obiekty" + +msgid "Along the track: platforms and other models, hectometre posts, fouling point markers, a parallel track, a vehicle to drive" +msgstr "Przy torze: perony i inne modele, słupki hektometrowe, wskaźniki ukresu, tor równoległy, pojazd do jazdy" + +msgid "Model along the track: release where it ends" +msgstr "Model wzdłuż toru: puść tam, gdzie się kończy" + +msgid "Model along the track: LMB by the track where it starts, drag to the end" +msgstr "Model wzdłuż toru: LPM przy torze tam, gdzie się zaczyna, przeciągnij do końca" + +msgid "Objects along the track" +msgstr "Obiekty przy torze" + +msgid "LMB by the track" +msgstr "LPM przy torze" + +msgid "where it starts, the side of the track it goes on" +msgstr "tam, gdzie się zaczyna, po tej stronie toru" + +msgid "to where it ends, a click: one as long as the original" +msgstr "do końca; samo kliknięcie: jeden jak oryginał" + +msgid "lays it" +msgstr "układa" + +msgid "LMB away from the track" +msgstr "LPM z dala od toru" + +msgid "selects another track" +msgstr "zaznacza inny tor" + +msgid "the line the objects go along" +msgstr "linia, wzdłuż której idą obiekty" + +msgid "modes: lay, switch, straight, curve, objects, profile, speed, joints, infra" +msgstr "tryby: układanie, rozjazd, prosta, łuk, obiekty, profil, prędkość, styki, infrastruktura" + +msgid "Platforms and models" +msgstr "Perony i modele" + +msgid "Fouling points" +msgstr "Ukresy W17" + +msgid "Vehicle" +msgstr "Pojazd" + +msgid "LMB on a track: the line the objects go along" +msgstr "LPM na torze: linia, wzdłuż której idą obiekty" + +msgid "%zu duplicate hectometre posts removed, Ctrl+Z brings them back" +msgstr "Usunięto %zu zdublowanych słupków hektometrowych, Ctrl+Z je przywraca" + +msgid "Remove %d duplicate posts" +msgstr "Usuń %d zdublowanych słupków" + +msgid "Posts with a number another post nearer to its place already has" +msgstr "Słupki z numerem, który ma już inny słupek stojący bliżej swojego miejsca" + +msgid "km %s duplicate##%u" +msgstr "km %s zdublowany##%u" + +msgid "km %s stands %.1f m from its place##%u" +msgstr "km %s stoi %.1f m od swojego miejsca##%u" + +msgid "km %s duplicate" +msgstr "km %s zdublowany" + +msgid "km %s, %.1f m from its place" +msgstr "km %s, %.1f m od swojego miejsca" + +# scenery editor: what a click does + +msgid "Click: start from this free end, drag: lead a track out of it" +msgstr "Klik: zacznij od tego wolnego końca, przeciągnij: wyprowadź z niego tor" + +msgid "Click: join this end and lay" +msgstr "Klik: dołącz do tego końca i ułóż" + +msgid "Click: start of the new track" +msgstr "Klik: początek nowego toru" + +msgid "Click: next point" +msgstr "Klik: następny punkt" + +msgid "Press: where the model starts, drag to its end" +msgstr "Wciśnij: początek modelu, przeciągnij do jego końca" + +msgid "Drag: the tracks in a box join the selection" +msgstr "Przeciągnij: tory w ramce dołączą do zaznaczenia" + +msgid "Click: take out of the selection, drag: a box" +msgstr "Klik: usuń z zaznaczenia, przeciągnij: ramka" + +msgid "Click: add to the selection, drag: a box" +msgstr "Klik: dodaj do zaznaczenia, przeciągnij: ramka" + +msgid "Drag: the control vector, the curve follows" +msgstr "Przeciągnij: wektor kontrolny, krzywa podąża" + +msgid "Drag: the joint, the neighbours follow" +msgstr "Przeciągnij: styk, sąsiednie tory podążają" + +msgid "Drag: the end, dropped on a free end it joins it" +msgstr "Przeciągnij: koniec, upuszczony na wolny koniec łączy się z nim" + +msgid "Click: move this straight together with the others" +msgstr "Klik: przesuwaj tę prostą razem z pozostałymi" + +msgid "Press: break the straight here, drag to turn the rest" +msgstr "Wciśnij: załam prostą tutaj, przeciągnij, by obrócić resztę" + +msgid "Click: next point of the shift around an obstacle" +msgstr "Klik: następny punkt objazdu przeszkody" + +msgid "Click: break the straight here" +msgstr "Klik: załam prostą tutaj" + +msgid "Click: start of the shift" +msgstr "Klik: początek przesunięcia" + +msgid "Click: shift the straight sideways" +msgstr "Klik: przesuń prostą w bok" + +msgid "Click: move this end of the straight" +msgstr "Klik: przesuń ten koniec prostej" + +msgid "Click: move this vertex" +msgstr "Klik: przesuń ten wierzchołek" + +msgid "Click: change the radius of this curve" +msgstr "Klik: zmień promień tego łuku" + +msgid "Press and drag: a switch here, along sets its direction, sideways its side" +msgstr "Wciśnij i przeciągnij: rozjazd tutaj, wzdłuż ustala kierunek, w bok stronę" + +msgid "Click: the whole straight through this track" +msgstr "Klik: cała prosta przez ten tor" + +msgid "Click: the curve of this track with its straights" +msgstr "Klik: łuk tego toru z prostymi" + +msgid "Click: edit this switch" +msgstr "Klik: edytuj ten rozjazd" + +msgid "Click: choose this track, then the type of the switch" +msgstr "Klik: wybierz ten tor, potem typ rozjazdu" + +msgid "Click: put a switch into this track" +msgstr "Klik: wstaw rozjazd w ten tor" + +msgid "Click: the grade line through this track" +msgstr "Klik: niweleta przez ten tor" + +msgid "Click: the line the objects go along" +msgstr "Klik: linia, wzdłuż której idą obiekty" + +msgid "Click: select" +msgstr "Klik: zaznacz" + +msgid "Laying track: new path from the free end" +msgstr "Układanie toru: nowy odcinek z wolnego końca" + +msgid "Select: moving the point" +msgstr "Wybór: przesuwanie punktu" + +msgid "Switch: the selected one. Another: LMB on a track, then its type" +msgstr "Rozjazd: zaznaczony. Kolejny: LPM na torze, potem typ" + +msgid "Switch: choose its type from the list" +msgstr "Rozjazd: wybierz typ z listy" + +msgid "Drag from a free end" +msgstr "Przeciągnij z wolnego końca" + +msgid "straight along it, to another free end: a curve joining them" +msgstr "prosta wzdłuż niego, do innego wolnego końca: łuk łączący" + +msgid "moves the point, level" +msgstr "przesuwa punkt w poziomie" + +msgid "Over a free end" +msgstr "Nad wolnym końcem" + +msgid "the end joins it" +msgstr "koniec się z nim łączy" + +msgid "done" +msgstr "gotowe" + +msgid "moves it, an end dropped on a free end joins it" +msgstr "przesuwa go, koniec upuszczony na wolny koniec łączy się" + +msgid "Gizmo" +msgstr "Gizmo" + +msgid "moves the point along the axes" +msgstr "przesuwa punkt wzdłuż osi" + +msgid "L" +msgstr "L" + +msgid "new track, also from a free end" +msgstr "nowy tor, także z wolnego końca" + +msgid "Esc" +msgstr "Esc" + +msgid "deselects" +msgstr "odznacza" + +msgid "List below" +msgstr "Lista poniżej" + +msgid "choose the type of the switch to put in" +msgstr "wybierz typ rozjazdu do wstawienia" + +msgid "choose the straight or the curve" +msgstr "wybierz prostą albo łuk" + +msgid "Dragging from a free end draws a curve through the cursor; off: it draws a straight along the end" +msgstr "Przeciąganie z wolnego końca rysuje łuk przez kursor; wyłączone: prostą wzdłuż końca" + +msgid "Enter" +msgstr "Enter" + +msgid "lay up to the cursor" +msgstr "ułóż do kursora" + +msgid "Shift" +msgstr "Shift" + +msgid "straight on in the direction of the track" +msgstr "prosto w kierunku toru" + +msgid "straight, joins nothing" +msgstr "prosto, bez łączenia" + +msgid "moves the whole track" +msgstr "przesuwa cały tor" + +msgid "The gizmo moves them together; Shift+LMB adds or takes out a path" +msgstr "Gizmo przesuwa je razem; Shift+LPM dodaje albo usuwa odcinek" + +msgid "Double slip switches" +msgstr "Rozjazdy krzyżowe podwójne" + +msgid "Only at a crossing of two straights" +msgstr "Tylko na skrzyżowaniu dwóch prostych" + +msgid "Drag: shift the straight sideways" +msgstr "Przeciągnij: przesuń prostą w bok" + +msgid "Drag: move this end, the curves at it follow" +msgstr "Przeciągnij: przesuń ten koniec, łuki przy nim podążają" + +msgid "Drag: move this vertex, the curve follows" +msgstr "Przeciągnij: przesuń wierzchołek, łuk podąża" + +msgid "Drag: the radius of this curve" +msgstr "Przeciągnij: promień tego łuku" + +msgid "Compound curve (several radii)" +msgstr "Łuk koszowy (kilka promieni)" + +msgid "Curve made of arcs of different radii, each one its own path, joined with transitions" +msgstr "Łuk z kilku łuków o różnych promieniach, każdy osobnym odcinkiem, połączonych krzywymi przejściowymi" + +msgid "Share of R%d" +msgstr "Udział R%d" + +msgid "Radius R%d (m)" +msgstr "Promień R%d (m)" + +msgid "Transition R%d-R%d (m)" +msgstr "Przejściowa R%d-R%d (m)" + +msgid "Takes this radius out of the curve" +msgstr "Usuwa ten promień z łuku" + +msgid "Add a radius" +msgstr "Dodaj promień" + +msgid "Another arc after the last one, the angle of the curve is shared out by the shares" +msgstr "Kolejny łuk za ostatnim; kąt zwrotu dzielony według udziałów" + +msgid "Formation under the ballast of the track" +msgstr "Korona pod podsypką toru" + +msgid "The terrain goes as deep under the track as its ballast reaches and as wide as its foot,\nread from each path of the route; off: the values given below" +msgstr "Teren schodzi pod torem tak głęboko, jak sięga jego podsypka, i tak szeroko, jak jej podstawa,\nodczytane z każdego odcinka trasy; wyłączone: wartości podane niżej" + +msgid "Ballast %.2f m deep, its foot %.2f m from the axis" +msgstr "Podsypka %.2f m, jej podstawa %.2f m od osi" + +msgid "Formation below the track (m)" +msgstr "Korona poniżej toru (m)" + +msgid "Name##switchname" +msgstr "Nazwa##switchname" + +msgid "Name of the next switch, its drive events are named after it; empty: the next free z1, z2...; a number at the end goes up with each switch" +msgstr "Nazwa następnej zwrotnicy, od niej nazywają się zdarzenia napędu; puste: kolejna wolna z1, z2...; liczba na końcu rośnie z każdą zwrotnicą" + +msgid "No switch drives in scenery/tory/napedy" +msgstr "Brak napędów zwrotnicowych w scenery/tory/napedy" + +msgid "Drive" +msgstr "Napęd" + +msgid "Drive of the switch from scenery/tory/napedy: its events + and - switch it with the keys by the switch and in the map during the game; none: the switch can't be switched" +msgstr "Napęd zwrotnicy ze scenery/tory/napedy: jego zdarzenia + i - przestawiają ją klawiszami przy zwrotnicy i w podglądzie mapy podczas gry; brak: zwrotnicy nie da się przestawić" + +msgid "diverging side" +msgstr "od toru zwrotnego" + +msgid "other side" +msgstr "z drugiej strony" + +msgid "The drive couldn't be placed: " +msgstr "Nie udało się wstawić napędu: " + +msgid "Switch %s with the drive %s: events %s+ and %s- switch it, also in the map" +msgstr "Zwrotnica %s z napędem %s: zdarzenia %s+ i %s- ją przestawiają, także w mapie" + +msgid "Drive: events %s+ and %s- switch it, also in the map" +msgstr "Napęd: zdarzenia %s+ i %s- ją przestawiają, także w mapie" + +msgid "No drive: the switch can't be switched during the game" +msgstr "Brak napędu: zwrotnicy nie da się przestawić podczas gry" + +msgid "Put the drive by the switch" +msgstr "Postaw napęd przy zwrotnicy" + +msgid "no drive" +msgstr "bez napędu" + +msgid "The drive %s has no variant for a %s switch with the drive on the %s" +msgstr "Napęd %s nie ma wariantu dla rozjazdu %s z napędem z %s" + +msgid "No switch drives: no template is described with category: switch drive" +msgstr "Brak napędów: żaden wzorzec nie ma opisu z category: switch drive" + +msgid "Templates described with category: switch drive, the side of the switch and of the drive, and a parameter with the role track for the name of the switch.\nIts events + and - switch it with the keys by the switch and in the map during the game" +msgstr "Wzorce opisane jako category: switch drive, ze stroną rozjazdu i napędu oraz parametrem o roli track na nazwę zwrotnicy.\nIch zdarzenia + i - przestawiają zwrotnicę klawiszami przy niej i w podglądzie mapy podczas gry" + +msgid "Track editor: the window with its modes opens or closes" +msgstr "Edytor torów: otwiera albo zamyka okno z jego trybami" + +msgid "Look along" +msgstr "Szukaj wzdłuż" + +msgid "Route of the vertical profile" +msgstr "Trasa profilu podłużnego" + +msgid "%d read by the paths" +msgstr "%d odczytywanych przez tory" + +msgid "The memory cell of the object is read by a getvalues event of a path, listed by the path, inside a multiple\nevent or written loose as :event. The object is bound to the thread of that path, not to the nearest one" +msgstr "Komórkę pamięci obiektu odczytuje zdarzenie getvalues toru: wpisane w torze, wewnątrz zdarzenia multiple\nalbo dopisane luzem jako :event. Obiekt zostaje przypisany do nitki tego toru, nie do najbliższej" + +msgid "%d of them nearer to another path" +msgstr "w tym %d bliżej innego toru" + +msgid "By the distance alone these would go to the wrong thread. Check them in 3D: the dashed line goes to the nearer path" +msgstr "Po samej odległości trafiłyby do złej nitki. Sprawdź je w 3D: przerywana linia prowadzi do bliższego toru" + +msgid "%d signal(s) not read by any path" +msgstr "%d sygnalizator(ów) bez odczytu przez tor" + +msgid "The signal has a memory cell, but no event of a path reads it: the vehicles won't see it.\nBound to the nearest path" +msgstr "Sygnalizator ma komórkę pamięci, ale żadne zdarzenie toru jej nie odczytuje: pojazdy go nie zobaczą.\nPrzypisany do najbliższego toru" + +msgid "Read by a path" +msgstr "Odczytywane przez tor" + +msgid "Nearer to another path" +msgstr "Bliżej innego toru" + +msgid "Not read" +msgstr "Bez odczytu" + +msgid "\nRead by the path %s: event %s, getvalues of the memory cell %s" +msgstr "\nOdczytywany przez tor %s: zdarzenie %s, getvalues komórki pamięci %s" + +msgid " (written loose, the path takes it by the name)" +msgstr " (dopisane luzem, tor bierze je po nazwie)" + +msgid "\nThe path %s is nearer, but it doesn't read the object" +msgstr "\nTor %s jest bliżej, ale nie odczytuje obiektu" + +msgid "\nNo path reads the memory cell of the signal" +msgstr "\nŻaden tor nie odczytuje komórki pamięci sygnalizatora" + +msgid "\nClick to look at it" +msgstr "\nKliknij, żeby go obejrzeć" + +msgid "read by %s" +msgstr "odczyt: %s" + +msgid "(nearer: %s)" +msgstr "(bliżej: %s)" + +msgid "not read" +msgstr "bez odczytu" + +msgid "; %d left out: they stand by the scope, but the paths which read them are elsewhere" +msgstr "; pominięto %d: stoją przy zakresie, ale tory, które je odczytują, są gdzie indziej" + +msgid "Name: %s" +msgstr "Nazwa: %s" + +msgid "(can't be changed: %s)" +msgstr "(nie można zmienić: %s)" + +msgid "Renamed from %s" +msgstr "Zmieniono z %s" + +msgid "Name##trackname" +msgstr "Nazwa##trackname" + +msgid "Enter renames the path, Esc while typing takes the text back.\nThe events, memory cells and the map of the game find the path by its name" +msgstr "Enter zmienia nazwę toru, Esc w trakcie pisania przywraca tekst.\nZdarzenia, komórki pamięci i mapa w grze znajdują tor po nazwie" + +msgid "Rename anyway" +msgstr "Zmień mimo to" + +msgid "Rename" +msgstr "Zmień nazwę" + +msgid "Keep the old" +msgstr "Zostaw starą" + +msgid "The path needs a name" +msgstr "Tor musi mieć nazwę" + +msgid "The name can't hold blanks, quotes, semicolons or //" +msgstr "Nazwa nie może zawierać spacji, cudzysłowów, średników ani //" + +msgid "Another path has this name" +msgstr "Inny tor ma już tę nazwę" + +msgid "Saved as %s: the scenery keeps the names in lower case" +msgstr "Zapisana jako %s: sceneria trzyma nazwy małymi literami" + +msgid "The new name goes to %d memory cell(s) and %d include(s), the drives of a switch among them" +msgstr "Nowa nazwa trafi do %d komórek pamięci i %d includów, w tym napędów zwrotnicy" + +msgid "These events aim at the path by the old name and the editor doesn't write them: %s" +msgstr "Te zdarzenia wskazują tor starą nazwą, a edytor ich nie zapisuje: %s" + +msgid "These events belong to the path by the old name: %s" +msgstr "Te zdarzenia należą do toru przez starą nazwę: %s" + +msgid "They work until the scenery is loaded again; then they need the new name in their files" +msgstr "Działają do ponownego wczytania scenerii; potem trzeba im wpisać nową nazwę w plikach" + +msgid "(no name)" +msgstr "(bez nazwy)" + +msgid "Turntable" +msgstr "Obrotnica" + +msgid "Places a turntable, leads tracks out of it and fits the tracks around to it" +msgstr "Stawia obrotnicę, wyprowadza z niej tory i dopasowuje do niej tory dookoła" + +msgid "Turntable: drag to turn the bridge, release to place it" +msgstr "Obrotnica: przeciągnij, żeby obrócić most, puść, żeby postawić" + +msgid "Turntable: LMB around it leads a track out, on a free end fits that track" +msgstr "Obrotnica: LPM dookoła wyprowadza tor, na wolnym końcu dopasowuje ten tor" + +msgid "Turntable: LMB places a new one" +msgstr "Obrotnica: LPM stawia nową" + +msgid "It doesn't line up with any position of the turntable" +msgstr "Nie trafia w żadne położenie obrotnicy" + +msgid "Too close to the turntable for a curve of %.0f m or more" +msgstr "Za blisko obrotnicy na łuk o promieniu %.0f m lub większym" + +msgid "It points away from the turntable" +msgstr "Jest skierowany od obrotnicy" + +msgid "No free ends within %.0f m of the rim" +msgstr "Brak wolnych końców w promieniu %.0f m od krawędzi" + +msgid "%d track(s) led to the turntable" +msgstr "Doprowadzono %d tor(ów) do obrotnicy" + +msgid "The turntable has %d positions, %d track lines lead to it: the rest won't be reached" +msgstr "Obrotnica ma %d położeń, a dochodzi do niej %d linii torów: do reszty nie dojedzie" + +msgid "The name %s is taken by another path" +msgstr "Nazwę %s ma już inny tor" + +msgid "Turntable %s: click around it for the tracks out of it, or fit the tracks around. It turns after the scenery is saved and loaded again" +msgstr "Obrotnica %s: klikaj dookoła, żeby wyprowadzić tory, albo dopasuj tory wokół. Obraca się po zapisaniu i ponownym wczytaniu scenerii" + +msgid "Track out of the turntable at %.1f deg, %.1f m" +msgstr "Tor z obrotnicy pod kątem %.1f st., %.1f m" + +msgid "Click: lead this track to the turntable" +msgstr "Kliknij: doprowadź ten tor do obrotnicy" + +msgid "Click: a track out of the turntable, %.1f deg, %.1f m" +msgstr "Kliknij: tor z obrotnicy, %.1f st., %.1f m" + +msgid "Click: this turntable" +msgstr "Kliknij: ta obrotnica" + +msgid "Click: a turntable at the end of this track" +msgstr "Kliknij: obrotnica na końcu tego toru" + +msgid "Press: the centre of a new turntable, drag: the direction of its bridge" +msgstr "Wciśnij: środek nowej obrotnicy, przeciągnij: kierunek jej mostu" + +msgid "Turntable %s, bridge %.1f m" +msgstr "Obrotnica %s, most %.1f m" + +msgid "(positions set by the tracks around, up to %d)" +msgstr "(położenia według torów dookoła, do %d)" + +msgid "(%d positions fixed by the template)" +msgstr "(%d położeń ustalonych we wzorcu)" + +msgid "No turntable template takes this path as its track: the positions aren't known" +msgstr "Żaden wzorzec obrotnicy nie bierze tego toru: położenia nie są znane" + +msgid "Positions (%d)" +msgstr "Położenia (%d)" + +msgid "track" +msgstr "tor" + +msgid "track on the other end" +msgstr "tor na drugim końcu" + +msgid "no track" +msgstr "brak toru" + +msgid "Tracks around" +msgstr "Tory dookoła" + +msgid "Reach from the rim, m" +msgstr "Zasięg od krawędzi, m" + +msgid "Radius of the curves, m" +msgstr "Promień łuków, m" + +msgid "Angle step of a new track, deg (0: free)" +msgstr "Krok kąta nowego toru, st. (0: dowolny)" + +msgid "Click around the turntable: a track out of it, as long as the cursor is far.\nClick a free end near it: that track is led to the rim, straight or with a curve" +msgstr "Kliknij dookoła obrotnicy: tor z niej, tak długi, jak daleko jest kursor.\nKliknij wolny koniec w pobliżu: ten tor zostanie doprowadzony do krawędzi, prosto albo łukiem" + +msgid "Find the free ends around" +msgstr "Znajdź wolne końce dookoła" + +msgid "Lead the chosen to the turntable (%d)" +msgstr "Doprowadź wybrane do obrotnicy (%d)" + +msgid "%.1f deg, curve R %.0f m, %.1f m" +msgstr "%.1f st., łuk R %.0f m, %.1f m" + +msgid "%.1f deg, straight %.1f m" +msgstr "%.1f st., prosta %.1f m" + +msgid "in place of its last straight %s" +msgstr "w miejsce jego ostatniej prostej %s" + +msgid "This turntable can't be read" +msgstr "Nie da się odczytać tej obrotnicy" + +msgid "New turntable" +msgstr "Nowa obrotnica" + +msgid "No turntables: no template is described with category: turntable and its turntable: {length}" +msgstr "Brak obrotnic: żaden wzorzec nie ma opisu category: turntable z turntable: {length}" + +msgid "%s %.0f m, up to %d positions set by the tracks" +msgstr "%s %.0f m, do %d położeń według torów" + +msgid "%s %.0f m, %d fixed positions" +msgstr "%s %.0f m, %d stałych położeń" + +msgid "Name##turntablename" +msgstr "Nazwa##turntablename" + +msgid "Press where the centre goes and drag to turn the bridge.\nClick a free end of a track: the turntable lines up with it, at its end. Shift: no lining up" +msgstr "Wciśnij w miejscu środka i przeciągnij, żeby obrócić most.\nKliknij wolny koniec toru: obrotnica ustawi się w jego osi, na jego końcu. Shift: bez dopasowania" + +msgid "The scenery has its own ground under the route: convert it to editor terrain first" +msgstr "Sceneria ma pod trasą własny teren: najpierw zamień go na teren edytora" + +msgid "; %d chunk(s) of terrain made under the route" +msgstr "; utworzono %d kawałków terenu pod trasą" + +msgid "; %d chunk(s) left out: the scenery has its own ground there, convert it to shape it" +msgstr "; pominięto %d kawałków: sceneria ma tam własny teren, zamień go, żeby go kształtować" + +msgid "Make terrain where the route has none" +msgstr "Utwórz teren tam, gdzie trasa go nie ma" + +msgid "Chunks of editor terrain are made along the route where there's none, from the ground around,\nthen shaped. Where the scenery has its own ground triangles the chunk is left out: convert them instead" +msgstr "Wzdłuż trasy, tam gdzie go brak, powstają kawałki terenu edytora z wysokości otoczenia,\npotem są kształtowane. Tam, gdzie sceneria ma własne trójkąty terenu, kawałek jest pomijany: zamień je zamiast tego" + +msgid "Puts back the terrain as it was before the last shaping, and takes away the terrain it made" +msgstr "Przywraca teren sprzed ostatniego kształtowania i usuwa teren, który przy tym powstał" + +msgid "Click: start from this free end, drag: the curve of the switch goes on, Shift: straight" +msgstr "Kliknij: zacznij od tego wolnego końca, przeciągnij: łuk rozjazdu biegnie dalej, Shift: prosta" + +msgid "%s and %d vehicle(s) behind it stand on %s. It isn't saved with the scenery" +msgstr "%s i %d pojazd(ów) za nim stoją na %s. Nie zapisuje się ze scenerią" + +msgid "Add to the train" +msgstr "Dodaj do składu" + +msgid "Train: %d vehicle(s), the first one leads" +msgstr "Skład: %d pojazd(ów), pierwszy prowadzi" + +msgid "Turned around in the train" +msgstr "Obrócony w składzie" + +msgid "up" +msgstr "w górę" + +msgid "Empty the train" +msgstr "Wyczyść skład" + +msgid "Turn the train around" +msgstr "Obróć skład" + +msgid "the head goes where the arrow at the marker points" +msgstr "czoło jedzie tam, gdzie wskazuje strzałka przy znaczniku" + +msgid "Trainset turned around" +msgstr "Skład odwrócony" + +msgid "The first vehicle stays where the trainset starts and leads the other way,\nthe rest of the trainset stands behind it" +msgstr "Pierwszy pojazd zostaje w miejscu początku składu i prowadzi w drugą stronę,\nreszta składu stoi za nim" + +msgid "Click: take out the %zu joined paths, %.0f m" +msgstr "Kliknij: wyjmij %zu połączonych torów, %.0f m" + +msgid "Click: add the %zu joined paths, %.0f m" +msgstr "Kliknij: dodaj %zu połączonych torów, %.0f m" + +msgid "Spread along the joins: Ctrl+Shift+LMB on a path" +msgstr "Rozlewanie po połączeniach: Ctrl+Shift+LPM na torze" + +msgid "Stop before the switches" +msgstr "Zatrzymaj przed rozjazdami" + +msgid "Through the switches, along their main track" +msgstr "Przez rozjazdy, po ich torze zasadniczym" + +msgid "Through everything joined" +msgstr "Przez wszystko, co połączone" + +msgid "Only to the paths like the clicked one:" +msgstr "Tylko do torów takich jak kliknięty:" + +msgid "rails" +msgstr "szyny" + +msgid "trackbed" +msgstr "podsypka" + +msgid "speed" +msgstr "prędkość" + +msgid "Reach along the track, m (0: no limit)" +msgstr "Zasięg wzdłuż toru, m (0: bez limitu)" + +msgid "Holding Ctrl+Shift shows what the click takes; on a path of the set it takes them out" +msgstr "Trzymając Ctrl+Shift widać, co kliknięcie zabierze; na torze ze zbioru je wyjmuje" + +msgid "Grow by the neighbours" +msgstr "Poszerz o sąsiadów" + +msgid "Adds the paths joined to the set, by the rules of the spread" +msgstr "Dodaje tory połączone ze zbiorem, według zasad rozlewania" + +msgid "New scenery" +msgstr "Nowa sceneria" + +msgid "E %.3f km, N %.3f km (%.5f N, %.5f E)" +msgstr "E %.3f km, N %.3f km (%.5f N, %.5f E)" + +msgid "loading the map..." +msgstr "wczytywanie mapy..." + +msgid "The map service sent no image" +msgstr "Serwis mapy nie odesłał obrazu" + +msgid "No map: " +msgstr "Brak mapy: " + +msgid "Find the place on the map: the wheel zooms, dragging moves the map. A click sets the centre of the scenery, its point 0 0 0." +msgstr "Znajdź miejsce na mapie: kółko przybliża, przeciąganie przesuwa mapę. Kliknięcie ustala środek scenerii, jej punkt 0 0 0." + +msgid "file name, e.g. my_line" +msgstr "nazwa pliku, np. moja_linia" + +msgid "scenery/.scn" +msgstr "scenery/.scn" + +msgid "Lower case letters, digits, _ and - only" +msgstr "Tylko małe litery, cyfry, _ i -" + +msgid "%s exists already" +msgstr "%s już istnieje" + +msgid "title, written in the file" +msgstr "tytuł, wpisany do pliku" + +msgid "Centre of the scenery, PUWG 1992" +msgstr "Środek scenerii, PUWG 1992" + +msgid "E, km##east" +msgstr "E, km##east" + +msgid "N, km##north" +msgstr "N, km##north" + +msgid "latitude, longitude" +msgstr "szerokość, długość geograficzna" + +msgid "Show" +msgstr "Pokaż" + +msgid "Give the latitude and the longitude in degrees, e.g. 52.2297, 21.0122" +msgstr "Podaj szerokość i długość geograficzną w stopniach, np. 52.2297, 21.0122" + +msgid "Degrees, as given by the online maps; the map moves there" +msgstr "Stopnie, jak podają mapy internetowe; mapa się tam przesunie" + +msgid "%.5f N, %.5f E" +msgstr "%.5f N, %.5f E" + +msgid "The scenery keeps the centre as its geographic reference (//$g PUWG1992): the orthophoto of the editor lies under it, in place." +msgstr "Sceneria zapamiętuje środek jako odniesienie geograficzne (//$g PUWG1992): ortofotomapa edytora leży pod nią na swoim miejscu." + +msgid "Give the file a name" +msgstr "Nadaj plikowi nazwę" + +msgid "Click the centre on the map" +msgstr "Kliknij środek na mapie" + +msgid "Create and open in the editor" +msgstr "Utwórz i otwórz w edytorze" + +msgid "The file can't be written" +msgstr "Nie da się zapisać pliku" + +msgid "Map: (c) OpenStreetMap contributors, served by geoportal.gov.pl" +msgstr "Mapa: (c) współtwórcy OpenStreetMap, udostępniana przez geoportal.gov.pl" + +msgid "Along the selected path; dragged past its end the model goes on along the joined paths, at a switch along the branch nearer the cursor" +msgstr "Wzdłuż zaznaczonego toru; przeciągnięty za jego koniec model idzie dalej po połączonych torach, na rozjeździe gałęzią bliższą kursorowi" + +msgid "every (m, 0: its length)" +msgstr "co (m, 0: jego długość)" + +msgid "%.2f m %s of the axis, %.2f m over the rail top" +msgstr "%.2f m %s od osi, %.2f m nad główką szyny" + +msgid "Press beside the track where the model begins and drag along to where it ends; the side of the press is the side of the model. Past the end of the path it goes on along the joined ones. A click lays one copy." +msgstr "Wciśnij obok toru tam, gdzie model się zaczyna, i przeciągnij wzdłuż do jego końca; strona wciśnięcia to strona modelu. Za końcem toru idzie dalej po połączonych. Kliknięcie kładzie jedną kopię." + +msgid "Lay it along the selected path" +msgstr "Połóż wzdłuż zaznaczonego toru" + +msgid "New scenery..." +msgstr "Nowa sceneria..." + +msgid "The editor starts again with the wizard of a new scenery: its name and its centre on the map" +msgstr "Edytor uruchamia się od nowa z kreatorem nowej scenerii: jej nazwa i środek na mapie" + +msgid "New scenery##restart" +msgstr "Nowa sceneria##restart" + +msgid "The editor starts again, with the wizard of a new scenery." +msgstr "Edytor uruchomi się od nowa, z kreatorem nowej scenerii." + +msgid "What isn't saved of this scenery is lost." +msgstr "To, co nie jest zapisane w tej scenerii, przepadnie." + +msgid "Save and start the wizard" +msgstr "Zapisz i otwórz kreator" + +msgid "Start the wizard" +msgstr "Otwórz kreator" + +msgid "The editor couldn't be started again" +msgstr "Nie udało się ponownie uruchomić edytora" From d4271ea7c5fcfed21f4053dcc5af77cf04efc1a6 Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 6 Oct 2026 22:37:00 +0200 Subject: [PATCH 6/6] fix(editor): no variables named near or far, the Windows headers define them away --- application/editormodeinfra.cpp | 6 +++--- application/editormodeprofile.cpp | 10 +++++----- application/editormodesweep.cpp | 16 ++++++++-------- application/editormodeturntable.cpp | 6 +++--- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index 056b6b00..6c85f4b7 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -899,7 +899,7 @@ void editor_mode::infra_recognize() auto const cells{points.size() == 1 ? links.cells_of(Binding) : std::vector{}}; auto const readers{links.readers_of(cells)}; infra::station geometric; - auto const near{index.nearest(points.front(), Reach, geometric)}; + auto const nearby{index.nearest(points.front(), Reach, geometric)}; if (false == readers.empty()) { auto best{-1.0}; @@ -923,11 +923,11 @@ void editor_mode::infra_recognize() } if (candidate.reader == nullptr) { - state.elsewhere += near ? 1 : 0; + state.elsewhere += nearby ? 1 : 0; return; } auto const &thread{thread_of(candidate.reader)}; - if (near && geometric.track != station.track && std::none_of(thread.begin(), thread.end(), [&](track_area const &Area) { return Area.track == geometric.track; })) + if (nearby && geometric.track != station.track && std::none_of(thread.begin(), thread.end(), [&](track_area const &Area) { return Area.track == geometric.track; })) { candidate.nearer = geometric.track; candidate.nearer_foot = infra::frame_at(geometric).point; diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 45643a28..15b42441 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -512,11 +512,11 @@ std::pair editor_mode::profile_generate_ground(std::vector 0) continue; - std::vector near; + std::vector around; for (std::size_t i = 0; i < samples.size(); ++i) if (std::abs(samples[i].position.x - middle.x) <= size * 0.5 + Reach && std::abs(samples[i].position.z - middle.y) <= size * 0.5 + Reach) - near.push_back(i); - if (near.empty()) + around.push_back(i); + if (around.empty()) continue; std::vector points; std::vector nearest; @@ -526,9 +526,9 @@ std::pair editor_mode::profile_generate_ground(std::vector::max()}; - for (auto const i : near) + for (auto const i : around) { auto const d{plan_distance(point, samples[i].position)}; if (d < distance) diff --git a/application/editormodesweep.cpp b/application/editormodesweep.cpp index 38c78dac..a52369a4 100644 --- a/application/editormodesweep.cpp +++ b/application/editormodesweep.cpp @@ -395,19 +395,19 @@ std::vector editor_mode::sweeps_near(TTrack const &Track) const if (sweep == nullptr || sweep->m_editorremoved) continue; auto const &definition{sweep->definition()}; - bool near{false}; + bool close{false}; for (auto const &piece : definition.pieces) { bezier const curve{piece}; - for (int k = 0; k <= 8 && false == near; ++k) + for (int k = 0; k <= 8 && false == close; ++k) { auto const point{curve.point(k / 8.0)}; - near = std::any_of(points.begin(), points.end(), [&](glm::dvec3 const &Other) { return plan_distance(point, Other) < kSweepNear; }); + close = std::any_of(points.begin(), points.end(), [&](glm::dvec3 const &Other) { return plan_distance(point, Other) < kSweepNear; }); } - if (near) + if (close) break; } - if (near) + if (close) result.push_back(sweep); } return result; @@ -628,11 +628,11 @@ void editor_mode::render_sweep_ui() // models along curves by this line if (track != nullptr) { - auto const near{sweeps_near(*track)}; - if (false == near.empty()) + auto const alongside{sweeps_near(*track)}; + if (false == alongside.empty()) { ImGui::TextDisabled("%s", STR_C("By this track:")); - for (auto *sweep : near) + for (auto *sweep : alongside) { ImGui::PushID(sweep); auto const &definition{sweep->definition()}; diff --git a/application/editormodeturntable.cpp b/application/editormodeturntable.cpp index f9a64c29..25cfb987 100644 --- a/application/editormodeturntable.cpp +++ b/application/editormodeturntable.cpp @@ -461,13 +461,13 @@ bool editor_mode::turntable_fit_end(editor_track::snap_target const &End, turnta if (false == best.has_value() && own != nullptr && own->eType == tt_Normal && own->m_paths.size() == 1 && own->Dynamics.empty()) { auto const &path{own->m_paths.front()}; - auto const far{End.point.kind == editor_track::point_kind::start ? path.points[segment_data::point::end] : path.points[segment_data::point::start]}; - auto const straight{std::abs(geometry::cross(t, plan_of(far - End.position))) < 0.01 && glm::dot(t, plan_of(far - End.position)) > 1.0}; + auto const opposite{End.point.kind == editor_track::point_kind::start ? path.points[segment_data::point::end] : path.points[segment_data::point::start]}; + auto const straight{std::abs(geometry::cross(t, plan_of(opposite - End.position))) < 0.01 && glm::dot(t, plan_of(opposite - End.position)) > 1.0}; std::string reason; if (straight && editor_track::can_edit_geometry(*own, reason)) { auto const first{issue}; - target = far; + target = opposite; e = plan_of(target); solve(); if (best.has_value())