From d1dbd405667d3ae9685b9a9d57d121a98a87d104 Mon Sep 17 00:00:00 2001 From: maj00r Date: Mon, 19 Oct 2026 17:10:27 +0200 Subject: [PATCH] feat(editor): the top view of the tracks draws the terrain to the corners of the screen, the ballast optional - In the top view of the tracks work area, the renderer's reach is the half diagonal of the screen. It used to be three extents, which cut wide windows short. - Cells far from the camera no longer draw only their tracks: the terrain shapes are drawn too. Models keep their own ranges. - In that view, the heightmap streamer loads chunks as far as the corners, up to 50 km. The detail comes from the size of a pixel, not from the distance. - The walk over the chunks around the camera is skipped while it would ask for nothing, so a wide reach costs little. - A setting, kept in the editor settings, leaves out the ballast of the railway in the top view, so the ground under the tracks shows. It is in the View menu and next to the extent slider. - Self-test: ortho [noballast|ballast], orthophoto off. --- application/editormode.cpp | 16 ++++++++ application/editormodeselftestscene.cpp | 7 +++- application/editormodeselftestterrain.cpp | 6 +++ editor/editorSettings.cpp | 3 ++ editor/editorSettings.hpp | 4 ++ editor/editorTerrainStreamer.cpp | 45 ++++++++++++++++++++--- editor/editorTerrainStreamer.hpp | 7 ++++ lang/editor_pl.po | 9 +++++ rendering/opengl33renderer.cpp | 28 +++++++++++++- rendering/openglrenderer.cpp | 13 ++++++- utilities/Globals.h | 1 + 11 files changed, 128 insertions(+), 11 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 1e2eadc7..ba91f84e 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -636,6 +636,7 @@ editor_ui *editor_mode::ui() const bool editor_mode::init() { EditorSettings.load(); + Global.EditorOrthoBallast = EditorSettings.top_ballast(); EditorModelSets.load(); Camera.Init({0, 15, 0}, {glm::radians(-30.0), glm::radians(180.0), 0}, nullptr); return m_input.init(); @@ -3516,6 +3517,13 @@ void editor_mode::render_view_menu() toggle_ortho(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("The view straight down without perspective, as on a map; again for the camera as it was")); + if (ImGui::MenuItem(STR_C("Ballast in the top view"), nullptr, &Global.EditorOrthoBallast)) + { + EditorSettings.top_ballast(Global.EditorOrthoBallast); + EditorSettings.save(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Off: the top view draws the rails and sleepers of the tracks without the ballast, so the ground under them shows")); ImGui::MenuItem(STR_C("Show lanes"), nullptr, &m_roadtool.lanes); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("The lanes of the roads drawn as the paths the vehicles take")); @@ -5521,6 +5529,14 @@ void editor_mode::render_gizmo_options() ImGui::SliderFloat(STR_C("Extent (m), wheel / MMB pan"), &Global.EditorOrthoExtent, 5.0f, 50000.0f, "%.0f", ImGuiSliderFlags_Logarithmic); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Wheel zooms. Hold the wheel and drag to pan.")); + ImGui::SameLine(); + if (ImGui::Checkbox(STR_C("Ballast"), &Global.EditorOrthoBallast)) + { + EditorSettings.top_ballast(Global.EditorOrthoBallast); + EditorSettings.save(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Off: the top view draws the rails and sleepers of the tracks without the ballast, so the ground under them shows")); } if (ui()->mode() == nodebank_panel::TRACK && m_track_tab != track_tab::path && m_track_tab != track_tab::turnout && m_track_tab != track_tab::turntable && m_track_tab != track_tab::signals && m_track_tab != track_tab::lineside) diff --git a/application/editormodeselftestscene.cpp b/application/editormodeselftestscene.cpp index 4a8cc66e..b74dc2de 100644 --- a/application/editormodeselftestscene.cpp +++ b/application/editormodeselftestscene.cpp @@ -275,13 +275,16 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm if (command == "ortho") { float extent{0.0f}; - words >> extent; + std::string ballast; + words >> extent >> ballast; choose_work_area(work_area::tracks); if (Global.EditorOrtho == extent < 0.0f) toggle_ortho(); if (extent > 0.0f) Global.EditorOrthoExtent = std::clamp(extent, 5.0f, 50000.0f); - WriteLog(format("SELFTEST ortho extent %.0f", Global.EditorOrthoExtent)); + if (false == ballast.empty()) + Global.EditorOrthoBallast = ballast != "noballast"; + WriteLog(format("SELFTEST ortho extent %.0f ballast %d", Global.EditorOrthoExtent, Global.EditorOrthoBallast ? 1 : 0)); return selftest_result::handled; } if (command == "mapstate") diff --git a/application/editormodeselftestterrain.cpp b/application/editormodeselftestterrain.cpp index 8ee06f02..2b49a7bc 100644 --- a/application/editormodeselftestterrain.cpp +++ b/application/editormodeselftestterrain.cpp @@ -53,6 +53,12 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co if (command == "orthophoto") { auto config{m_orthophoto.settings()}; + if (words.str().find(" off") != std::string::npos) + { + m_orthophoto.enabled(false); + WriteLog("SELFTEST orthophoto off"); + return selftest_result::handled; + } int fit{1}; int inscene{0}; words >> config.north >> config.east >> config.radius >> fit >> inscene; diff --git a/editor/editorSettings.cpp b/editor/editorSettings.cpp index 515d5cdd..4a9b8232 100644 --- a/editor/editorSettings.cpp +++ b/editor/editorSettings.cpp @@ -108,6 +108,8 @@ bool editorSettings::load() m_terrain_offer = (value == "1"); else if (key == "track_tools_all") m_track_tools_more = (value == "1"); + else if (key == "top_ballast") + m_top_ballast = (value == "1"); else if (key.rfind(origin_prefix, 0) == 0) { // northing;easting @@ -154,6 +156,7 @@ bool editorSettings::save() stream << "first_steps " << (m_first_steps ? 1 : 0) << "\n"; stream << "terrain_offer " << (m_terrain_offer ? 1 : 0) << "\n"; stream << "track_tools_all " << (m_track_tools_more ? 1 : 0) << "\n"; + stream << "top_ballast " << (m_top_ballast ? 1 : 0) << "\n"; stream << std::fixed << std::setprecision(3); for (auto const &origin : m_orthophoto_origins) stream << origin.first << " " << origin.second.first << ";" << origin.second.second << "\n"; diff --git a/editor/editorSettings.hpp b/editor/editorSettings.hpp index 84fea1e7..ddee1ab1 100644 --- a/editor/editorSettings.hpp +++ b/editor/editorSettings.hpp @@ -61,6 +61,9 @@ public: void terrain_offer(bool Offer) { m_terrain_offer = Offer; } bool track_tools_more() const { return m_track_tools_more; } void track_tools_more(bool Show) { m_track_tools_more = Show; } + // the ballast of the railway drawn in the top view + bool top_ballast() const { return m_top_ballast; } + void top_ballast(bool Show) { m_top_ballast = Show; } private: movement_scheme m_movement{movement_scheme::wsad}; @@ -74,6 +77,7 @@ private: bool m_first_steps{true}; bool m_terrain_offer{true}; bool m_track_tools_more{false}; + bool m_top_ballast{true}; }; // global editor settings instance diff --git a/editor/editorTerrainStreamer.cpp b/editor/editorTerrainStreamer.cpp index 52fa7995..8f799c46 100644 --- a/editor/editorTerrainStreamer.cpp +++ b/editor/editorTerrainStreamer.cpp @@ -34,6 +34,9 @@ double constexpr upload_budget{0.004}; // seconds of a frame for taking over fin int constexpr materials_per_frame{6}; double constexpr look_ahead{8.0}; // seconds; chunks are loaded around where the camera will be by then double constexpr look_ahead_limit{2000.0}; // metres +double constexpr pixel_distance{1300.0}; // m; a pixel of the top view is as big as one of the 3D view this far away +int constexpr top_radius{200}; // chunks the top view of the tracks loads around its centre at most +double constexpr settled_walk{1.0}; // seconds; the chunks around the camera are looked over this often when nothing is coming double constexpr region_half{scene::EU07_REGIONSIDESECTIONCOUNT * scene::EU07_SECTIONSIZE * 0.5 - heightmap::chunk_size}; double constexpr kPi{3.14159265358979323846}; @@ -135,6 +138,7 @@ void terrain_streamer::close() m_packs.clear(); m_loading.clear(); m_removed.clear(); + m_settled = false; m_palette.clear(); m_recording = 0; m_change = change(); @@ -159,6 +163,7 @@ void terrain_streamer::reset() m_packs.clear(); m_loading.clear(); m_removed.clear(); + m_settled = false; m_palette.clear(); clear_water(false); m_recording = 0; @@ -506,7 +511,7 @@ int terrain_streamer::wanted_stride(editor_terrain const &Chunk, glm::dvec3 cons // distance from the camera to the box of the chunk glm::dvec3 const offset{std::max({Chunk.m_low.x - Camera.x, 0.0, Camera.x - Chunk.m_high.x}), std::max({Chunk.m_low.y - Camera.y, 0.0, Camera.y - Chunk.m_high.y}), std::max({Chunk.m_low.z - Camera.z, 0.0, Camera.z - Chunk.m_high.z})}; - auto const distance{glm::length(offset)}; + auto const distance{m_pixel > 0.0 ? m_pixel * pixel_distance : glm::length(offset)}; auto const spacing{Chunk.data().spacing()}; auto const limit{heightmap::max_stride(Chunk.cells())}; auto const stride_for = [&](double const Distance) { @@ -586,12 +591,19 @@ void terrain_streamer::update(glm::dvec3 const &Camera) m_results.pop_front(); } process(done); + m_settled = false; } + // the top view of the tracks wants the terrain as far as the corners of the screen, as fine as its pixels need + auto const top{Global.EditorOrtho && Global.editor_tracks}; + auto const aspect{Global.window_size.x / std::max(1.0, static_cast(Global.window_size.y))}; + auto const reach{top ? Global.EditorOrthoExtent * std::sqrt(1.0 + aspect * aspect) : 0.0}; + m_pixel = top ? 2.0 * Global.EditorOrthoExtent / std::max(1, Global.window_size.y) : 0.0; + // chunks wanted around the camera and around where it's heading - auto const range{std::max(m_radius * heightmap::chunk_size, static_cast(Global.BaseDrawRange) * std::max(1.0, static_cast(Global.fDistanceFactor))) + heightmap::chunk_size * 0.5}; - auto const radius{std::min(64, static_cast(std::ceil(range / heightmap::chunk_size)))}; - if (m_ringradius != radius) + auto const range{std::max({m_radius * heightmap::chunk_size, static_cast(Global.BaseDrawRange) * std::max(1.0, static_cast(Global.fDistanceFactor)), reach}) + heightmap::chunk_size * 0.5}; + auto const radius{std::min(top ? top_radius : 64, static_cast(std::ceil(range / heightmap::chunk_size)))}; + if (m_ringradius < radius) { m_ring.clear(); for (int dz = -radius; dz <= radius; ++dz) @@ -606,12 +618,31 @@ void terrain_streamer::update(glm::dvec3 const &Camera) std::vector centres{here}; if (there != here) centres.push_back(there); + // a walk which asked for nothing isn't made again until something comes or the camera moves to another chunk + if (m_settled && (here != m_walkhere || there != m_walkthere || radius != m_walkradius || now - m_walktime > settled_walk)) + m_settled = false; + if (false == m_settled) + { + m_walkhere = here; + m_walkthere = there; + m_walkradius = radius; + m_walktime = now; + m_settled = m_loading.empty(); + } + else + centres.clear(); + auto const walked{(radius + 1) * (radius + 1)}; for (auto const ¢re : centres) { for (auto const &offset : m_ring) { - if (m_loading.size() >= max_loading) + if (offset.first * offset.first + offset.second * offset.second > walked) break; + if (m_loading.size() >= max_loading) + { + m_settled = false; + break; + } chunk_key const key{centre.first + offset.first, centre.second + offset.second}; if (m_chunks.count(key) > 0 || m_loading.count(key) > 0 || m_removed.count(key) > 0) continue; @@ -627,8 +658,11 @@ void terrain_streamer::update(glm::dvec3 const &Camera) task.generation = pack.generation; task.priority = distance; submit(std::move(task)); + m_settled = false; continue; } + if (pack.status == pack_state::state::loading) + m_settled = false; if (pack.status != pack_state::state::ready || false == pack.file->has(key)) continue; job task; @@ -640,6 +674,7 @@ void terrain_streamer::update(glm::dvec3 const &Camera) task.priority = distance; m_loading.insert(key); submit(std::move(task)); + m_settled = false; } } diff --git a/editor/editorTerrainStreamer.hpp b/editor/editorTerrainStreamer.hpp index ab76ca81..beff9746 100644 --- a/editor/editorTerrainStreamer.hpp +++ b/editor/editorTerrainStreamer.hpp @@ -295,6 +295,13 @@ class terrain_streamer bool m_hascamera{false}; std::vector m_ring; // offsets of the chunks around a point, the nearest first int m_ringradius{-1}; + double m_pixel{0.0}; // metres a pixel of the top view spans; none in the 3D view + // the last walk over the chunks around the camera, and whether it found nothing more to ask for + bool m_settled{false}; + chunk_key m_walkhere{0, 0}; + chunk_key m_walkthere{0, 0}; + int m_walkradius{0}; + double m_walktime{0.0}; // shared with the workers, guarded by m_mutex std::mutex m_mutex; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 4d64ab93..8f3408bf 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -9889,3 +9889,12 @@ msgid_plural "\nThe terrain followed the tracks: %zu formations laid anew" msgstr[0] "\nTeren poszedł za torami: %zu torowisko ułożone od nowa" msgstr[1] "\nTeren poszedł za torami: %zu torowiska ułożone od nowa" msgstr[2] "\nTeren poszedł za torami: %zu torowisk ułożonych od nowa" + +msgid "Ballast in the top view" +msgstr "Podsypka w widoku z góry" + +msgid "Ballast" +msgstr "Podsypka" + +msgid "Off: the top view draws the rails and sleepers of the tracks without the ballast, so the ground under them shows" +msgstr "Wyłączone: widok z góry rysuje szyny i podkłady bez podsypki, widać teren pod torami" diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 7f2704da..b77731cf 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -31,6 +31,14 @@ int constexpr EU07_REFLECTIONFIDELITYOFFSET { 250 }; // artificial increase of r namespace { +// the top view of the editor shows everything this close to the camera, the corners of the screen included +float +editor_ortho_reach() +{ + auto const aspect{Global.window_size.x / std::max(1.f, static_cast(Global.window_size.y))}; + return Global.EditorOrthoExtent * std::sqrt(1.f + aspect * aspect) + 500.f; +} + float editor_track_view_range(float const Range, bool const Apply) { @@ -38,14 +46,22 @@ editor_track_view_range(float const Range, bool const Apply) return Range; float extra = 50000.0f; if (Global.EditorOrtho) - extra = Global.EditorOrthoExtent * 3.0f; + extra = editor_ortho_reach(); return Range > extra ? Range : extra; } +// the top view of the editor may leave the ballast of the railway out, for the ground under the tracks to show +bool +editor_ballast_hidden(TTrack const *Track, bool const Colour) +{ + return Colour && EditorModeFlag && Global.EditorOrtho && false == Global.EditorOrthoBallast && (Track->iCategoryFlag & 15) == 1; +} + +// the top view draws the scenery as far as the tracks; its models keep their own ranges bool editor_far_tracks_only(bool const Apply, scene::basic_cell const *Cell, glm::dvec3 const &Camera, double const SceneryLimit) { - if (false == Apply) + if (false == Apply || Global.EditorOrtho) return false; double nearest = glm::length(Cell->m_area.center - Camera) - static_cast(Cell->m_area.radius); if (nearest < 0.0) @@ -4519,6 +4535,10 @@ void opengl33_renderer::Render(scene::basic_cell::path_sequence::const_iterator { continue; } + if (track->eType == tt_Normal && editor_ballast_hidden(track, m_renderpass.draw_mode == rendermode::color)) + { + continue; + } switch (m_renderpass.draw_mode) { @@ -4582,6 +4602,10 @@ void opengl33_renderer::Render(scene::basic_cell::path_sequence::const_iterator { continue; } + if (editor_ballast_hidden(track, m_renderpass.draw_mode == rendermode::color)) + { + continue; + } switch (m_renderpass.draw_mode) { diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index d66e7bf6..c4351b9e 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -33,6 +33,14 @@ int constexpr EU07_REFLECTIONFIDELITYOFFSET { 250 }; // artificial increase of r namespace { +// the top view of the editor shows everything this close to the camera, the corners of the screen included +float +editor_ortho_reach() +{ + auto const aspect{ Global.window_size.x / std::max( 1.f, static_cast( Global.window_size.y ) ) }; + return Global.EditorOrthoExtent * std::sqrt( 1.f + aspect * aspect ) + 500.f; +} + float editor_track_view_range(float const Range, bool const Apply) { @@ -40,14 +48,15 @@ editor_track_view_range(float const Range, bool const Apply) return Range; float extra = 50000.0f; if( Global.EditorOrtho ) - extra = Global.EditorOrthoExtent * 3.0f; + extra = editor_ortho_reach(); return Range > extra ? Range : extra; } +// the top view draws the scenery as far as the tracks; its models keep their own ranges bool editor_far_tracks_only(bool const Apply, scene::basic_cell const *Cell, glm::dvec3 const &Camera, double const SceneryLimit) { - if( false == Apply ) + if( false == Apply || Global.EditorOrtho ) return false; double nearest = glm::length( Cell->m_area.center - Camera ) - static_cast( Cell->m_area.radius ); if( nearest < 0.0 ) diff --git a/utilities/Globals.h b/utilities/Globals.h index b2ccd941..c63197aa 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -174,6 +174,7 @@ struct global_settings { float FieldOfView{ 45.f }; // vertical field of view for the camera. TODO: move it to the renderer bool EditorOrtho{ false }; float EditorOrthoExtent{ 150.f }; + bool EditorOrthoBallast{ true }; // the ballast of the railway drawn in the top view of the editor GLint iMaxTextureSize{ 4096 }; // maksymalny rozmiar tekstury GLint iMaxCabTextureSize{ 4096 }; // largest allowed texture in vehicle cab int iMultisampling{ 2 }; // tryb antyaliasingu: 0=brak,1=2px,2=4px,3=8px,4=16px