From 8903d15b945aea8a03d82e445a09d4dc5112d2d8 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 08:41:50 +0200 Subject: [PATCH 001/145] fix(editor): a track dragged around no longer piles copies of its geometry up in the graphics memory The geometry of a track made anew, ten times a second while it's dragged, only forgot the chunks it had: the bank kept them all, so the buffer of the section grew by the whole track and its neighbours each time and was sent to the gpu in full again. Ten rebuilds of a test scenery took the bank from 2 to 29 MB; a long session would run the card out of memory. The old chunks are erased from the bank now, but for those of a moving nose, laid again in place, and a moved traction wire drops its old one too. A chunk swapped for one without the extra vertex data kept the flag of having it and the upload read from an empty array; the buffer is made again when that changes. --- application/editormodeinfra.cpp | 1 + rendering/geometrybank.cpp | 22 ++++++++++++++++++++++ rendering/geometrybank.h | 2 ++ rendering/opengl33geometrybank.cpp | 3 ++- rendering/opengl33renderer.cpp | 5 +++++ rendering/opengl33renderer.h | 1 + rendering/openglrenderer.cpp | 5 +++++ rendering/openglrenderer.h | 1 + rendering/renderer.h | 1 + world/Track.cpp | 15 +++++++++++++++ 10 files changed, 55 insertions(+), 1 deletion(-) diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index 1840ffe7..db6e4f3b 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -292,6 +292,7 @@ void move_traction(TTraction *Traction, std::array const &Points) Traction->location(glm::mix(Points[1], Points[0], 0.5)); Traction->m_area.radius = -1.f; simulation::Region->insert_and_register(Traction); + GfxRenderer->Erase(Traction->m_geometry); Traction->m_geometry = gfx::geometry_handle{}; auto §ion{simulation::Region->section(Traction->location())}; if (section.m_geometrycreated) diff --git a/rendering/geometrybank.cpp b/rendering/geometrybank.cpp index f801c450..2a1ff5ee 100644 --- a/rendering/geometrybank.cpp +++ b/rendering/geometrybank.cpp @@ -318,6 +318,20 @@ geometry_bank::append( gfx::vertex_array &Vertices, userdata_array &Userdata, gf return replace( Vertices, Userdata, Geometry, gfx::geometry_bank::chunk( Geometry ).vertices.size() ); } +void +geometry_bank::erase( gfx::geometry_handle const &Geometry ) { + + if( ( Geometry.chunk == 0 ) || ( Geometry.chunk > m_chunks.size() ) ) { return; } + + auto &chunk = gfx::geometry_bank::chunk( Geometry ); + if( chunk.vertices.empty() && chunk.indices.empty() ) { return; } + + gfx::vertex_array().swap( chunk.vertices ); + gfx::index_array().swap( chunk.indices ); + gfx::userdata_array().swap( chunk.userdata ); + replace_( Geometry ); +} + // draws geometry stored in specified chunk std::size_t geometry_bank::draw( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams ) { @@ -425,6 +439,14 @@ geometrybank_manager::append( gfx::vertex_array &Vertices, userdata_array &Userd return bank( Geometry ).first->append( Vertices, Userdata, Geometry ); } + +void +geometrybank_manager::erase( gfx::geometry_handle const &Geometry ) { + + if( ( Geometry.bank == 0 ) || ( Geometry.bank > m_geometrybanks.size() ) ) { return; } + + bank( Geometry ).first->erase( Geometry ); +} // draws geometry stored in specified chunk void geometrybank_manager::draw( gfx::geometry_handle const &Geometry, unsigned int const Streams ) { diff --git a/rendering/geometrybank.h b/rendering/geometrybank.h index 47e7afc7..537c79ba 100644 --- a/rendering/geometrybank.h +++ b/rendering/geometrybank.h @@ -117,6 +117,7 @@ public: auto replace( gfx::vertex_array &Vertices, gfx::userdata_array& Userdata, gfx::geometry_handle const &Geometry, std::size_t const Offset = 0 ) -> bool; // adds supplied vertex data at the end of specified chunk auto append( gfx::vertex_array &Vertices, gfx::userdata_array& Userdata, gfx::geometry_handle const &Geometry ) -> bool; + void erase( gfx::geometry_handle const &Geometry ); // draws geometry stored in specified chunk auto draw( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) -> std::size_t; // draws geometry stored in specified chunk N times via glDrawElementsInstanced* @@ -222,6 +223,7 @@ public: auto replace( gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, std::size_t const Offset = 0 ) -> bool; // adds supplied vertex data at the end of specified chunk auto append( gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry ) -> bool; + void erase( gfx::geometry_handle const &Geometry ); // draws geometry stored in specified chunk void draw( gfx::geometry_handle const &Geometry, unsigned int const Streams = basic_streams ); // draws geometry stored in specified chunk InstanceCount times via GPU instancing. diff --git a/rendering/opengl33geometrybank.cpp b/rendering/opengl33geometrybank.cpp index 4713ec69..978a1b75 100644 --- a/rendering/opengl33geometrybank.cpp +++ b/rendering/opengl33geometrybank.cpp @@ -33,7 +33,8 @@ opengl33_vaogeometrybank::replace_( gfx::geometry_handle const &Geometry ) { auto &chunkrecord = m_chunkrecords[ Geometry.chunk - 1 ]; chunkrecord.is_good = false; // if the overall length of the chunk didn't change we can get away with reusing the old buffer... - if( geometry_bank::chunk( Geometry ).vertices.size() != chunkrecord.vertex_count ) { + if( ( geometry_bank::chunk( Geometry ).vertices.size() != chunkrecord.vertex_count ) + || ( geometry_bank::chunk( Geometry ).userdata.empty() == chunkrecord.has_userdata ) ) { // ...but otherwise we'll need to allocate a new one // TBD: we could keep and reuse the old buffer also if the new chunk is smaller than the old one, // but it'd require some extra tracking and work to keep all chunks up to date; also wasting vram; may be not worth it? diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index b77731cf..b1f9e56d 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -2217,6 +2217,11 @@ bool opengl33_renderer::Append(gfx::vertex_array &Vertices, gfx::userdata_array return m_geometry.append(Vertices, Userdata, Geometry); } +void opengl33_renderer::Erase(gfx::geometry_handle const &Geometry) +{ + m_geometry.erase(Geometry); +} + // provides direct access to index data of specfied chunk gfx::index_array const & opengl33_renderer::Indices(gfx::geometry_handle const &Geometry) const { diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index a7bd46d6..d7adfb9e 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -67,6 +67,7 @@ class opengl33_renderer : public gfx_renderer { bool Replace(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type, const std::size_t Offset = 0) override; // adds supplied vertex data at the end of specified chunk bool Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) override; + void Erase( gfx::geometry_handle const &Geometry ) override; // provides direct access to index data of specfied chunk gfx::index_array const & Indices( gfx::geometry_handle const &Geometry ) const override; diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index c4351b9e..d20626e3 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -1767,6 +1767,11 @@ bool opengl_renderer::Append(gfx::vertex_array &Vertices, gfx::userdata_array &U return m_geometry.append( Vertices, Userdata, Geometry ); } +void opengl_renderer::Erase( gfx::geometry_handle const &Geometry ) +{ + m_geometry.erase( Geometry ); +} + // provides direct access to vertex data of specfied chunk gfx::index_array const & opengl_renderer::Indices( gfx::geometry_handle const &Geometry ) const { diff --git a/rendering/openglrenderer.h b/rendering/openglrenderer.h index 78fffa40..da3faad0 100644 --- a/rendering/openglrenderer.h +++ b/rendering/openglrenderer.h @@ -65,6 +65,7 @@ public: bool Replace(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type, const std::size_t Offset = 0) override; // adds supplied vertex data at the end of specified chunk bool Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) override; + void Erase( gfx::geometry_handle const &Geometry ) override; // provides direct access to index data of specfied chunk gfx::index_array const & Indices( gfx::geometry_handle const &Geometry ) const override; diff --git a/rendering/renderer.h b/rendering/renderer.h index 649dd795..2c302a75 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -59,6 +59,7 @@ public: virtual auto Replace(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type, const std::size_t Offset = 0) -> bool = 0; // adds supplied vertex data at the end of specified chunk virtual auto Append(gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometry_handle const &Geometry, int const Type) -> bool = 0; + virtual void Erase( gfx::geometry_handle const &Geometry ) {} // provides direct access to index data of specfied chunk virtual auto Indices( gfx::geometry_handle const &Geometry ) const->gfx::index_array const & = 0; // provides direct access to vertex data of specfied chunk diff --git a/world/Track.cpp b/world/Track.cpp index ba24fa9d..ae071220 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -1471,12 +1471,27 @@ glm::vec3 TTrack::get_nearest_point(const glm::dvec3 &point) const void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { track_rails::forget(); + auto const erase = [ this ]( gfx::geometry_handle const &Chunk ) { + if( SwitchExtension + && std::any_of( SwitchExtension->m_nosechunks.begin(), SwitchExtension->m_nosechunks.end(), [ & ]( auto const &Pooled ) { return Pooled.handle == Chunk; } ) ) { + return; + } + GfxRenderer->Erase( Chunk ); + }; + for( auto const *geometry : { &Geometry1, &Geometry2, &GeometryCut1, &GeometryCut2 } ) { + std::for_each( geometry->begin(), geometry->end(), erase ); + } + for( auto const &tread : GeometryTreads ) { + std::for_each( tread.geometry.begin(), tread.geometry.end(), erase ); + std::for_each( tread.cuts.begin(), tread.cuts.end(), erase ); + } Geometry1.clear(); Geometry2.clear(); GeometryCut1.clear(); GeometryCut2.clear(); GeometryTreads.clear(); if( SwitchExtension ) { + erase( SwitchExtension->Geometry3 ); SwitchExtension->Geometry3 = gfx::geometry_handle{}; } m_sleeper_local_transforms.clear(); From 9696b5f7956c3fb5d577c6e3a1c2c11c250aad31 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 08:41:59 +0200 Subject: [PATCH 002/145] fix: closing the simulator no longer makes a destroyed window current The python task queue was stopped after the windows were destroyed, and its worker, leaving, released the context of its own window, which was gone by then: a use after free on every exit. The queue is stopped first. --- application/application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/application.cpp b/application/application.cpp index 3c0acc54..012cd4e3 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -717,11 +717,11 @@ void eu07_application::exit() ui_layer::shutdown(); + m_taskqueue.exit(); for (auto *window : m_windows) { glfwDestroyWindow(window); } - m_taskqueue.exit(); glfwPollEvents(); // TODO: This fixes a segfault on Wayland when closing. Remove after updating glfw to 3.5. glfwTerminate(); From 4512ed4e0b3f3c648a25158be5e110b1dce5cfff Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 08:42:29 +0200 Subject: [PATCH 003/145] fix(editor): guard the places where the editor could crash outright Found with an address and undefined behaviour sanitizer build, random monkey sessions on the test sceneries and broken inputs: - A deleted model left its pointer in the pick list of the renderer, the bound node of the panels, the bindings of the infrastructure and the selection: the next pick or panel frame read freed memory. The renderer forgets it, and one erase_model does the cleaning for all the places which deleted a model by hand. - The plan band of a route read the vertex before the first one in the frame a drag was let go. - A scenery which can't be opened goes back to the launcher with the reason in the log, a track of an unknown type or a tributary loads as a normal track or a river with a message, and a spline with no finite length or step gets a sane number of segments. - Typed numbers which aren't (NaN, inf, ends 0 m or 1000 km apart, a vector of 1e30, a diverging track part of 5 km, a curve threshold of 0, a zero history size, radius norms below zero) are refused or clamped, where they made the geometry run away or divide by zero. - Folders which can't be read and file names which aren't valid in the locale no longer throw out of the scans of sceneries, textures, sweeps, roads and signal plates; the scans skip them. - The launcher: Ctrl+Del with no trainset selected, a dropped vehicle whose skin is gone and a text with a percent sign. - A terrain mesh made for another resolution of its chunk isn't put into the chunk, and a slot is reused only for a mesh of the same size. - Vehicles on a track changed by the editor are moved over a copy of the list they leave. - The table of the keyboard shortcuts gives its stretched column a weight, else ImGui divides 0 by 0 in the first frame of the window. --- application/editormode.cpp | 85 ++++++++++++++--------- application/editormode.h | 2 + application/editormodeconnectioncurve.cpp | 4 +- application/editormodeinfra.cpp | 16 +++++ application/editormodeprofile.cpp | 2 + application/editormoderoute.cpp | 6 +- application/editormodesignal.cpp | 13 +++- application/editormodestraights.cpp | 10 +++ application/editormodesweep.cpp | 42 +++++++---- application/editormodetextures.cpp | 20 ++++-- application/editormodetrack.cpp | 13 ++-- application/editormodetrackprops.cpp | 31 +++++---- application/editormodetrackux.cpp | 2 +- application/editoruilayer.h | 4 ++ application/scenarioloadermode.cpp | 12 +++- editor/editorAlignment.cpp | 2 +- editor/editorNmt.cpp | 2 +- editor/editorProfile.cpp | 2 +- editor/editorRoad.cpp | 13 +++- editor/editorSwitchDesign.cpp | 10 +-- editor/editorSwitchStore.cpp | 10 ++- editor/editorTerrainStreamer.cpp | 7 +- editor/editorTerrainStreamer.hpp | 1 + editor/editorTrack.cpp | 5 +- lang/editor_pl.po | 6 ++ launcher/scenery_list.cpp | 28 ++++---- launcher/scenery_scanner.cpp | 16 +++-- launcher/textures_scanner.cpp | 9 +-- rendering/opengl33renderer.cpp | 7 ++ rendering/opengl33renderer.h | 2 + rendering/openglrenderer.cpp | 9 +++ rendering/openglrenderer.h | 2 + rendering/renderer.h | 1 + simulation/simulation.cpp | 4 ++ world/Segment.cpp | 9 ++- world/Track.cpp | 15 +++- 36 files changed, 311 insertions(+), 111 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index cd665ef9..0b2ee811 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -200,6 +200,12 @@ namespace triangle_grid(std::vector Triangles, glm::dvec2 const &Min, glm::dvec2 const &Max) : m_triangles(std::move(Triangles)), m_min(Min) { glm::dvec2 const extent = glm::max(Max - Min, glm::dvec2(1.0)); + if (false == std::isfinite(extent.x) || false == std::isfinite(extent.y)) + { + m_triangles.clear(); + m_cells.resize(1); + return; + } // ~8 m cells, coarser for big areas so large terrain triangles don't flood the grid m_cellsize = std::max(8.0, std::max(extent.x, extent.y) / 128.0); m_columns = static_cast(std::ceil(extent.x / m_cellsize)) + 1; @@ -241,11 +247,13 @@ namespace private: int column(double const X) const { - return static_cast(std::clamp(std::floor((X - m_min.x) / m_cellsize), 0.0, static_cast(m_columns - 1))); + double const cell = std::floor((X - m_min.x) / m_cellsize); + return cell > 0.0 ? static_cast(std::min(cell, static_cast(m_columns - 1))) : 0; } int row(double const Z) const { - return static_cast(std::clamp(std::floor((Z - m_min.y) / m_cellsize), 0.0, static_cast(m_rows - 1))); + double const cell = std::floor((Z - m_min.y) / m_cellsize); + return cell > 0.0 ? static_cast(std::min(cell, static_cast(m_rows - 1))) : 0; } std::vector m_triangles; @@ -862,6 +870,10 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node) if (!node) return; + if (ui()->node() == node) + ui()->set_node(nullptr); + infra_forget(node); + for (auto &s : m_history) { if (s.node_ptr == node) @@ -1076,9 +1088,7 @@ void editor_mode::undo_one() if (auto *model = dynamic_cast(target)) { - nullify_history_pointers(model); - remove_from_hierarchy(model); - simulation::State.delete_model(model); + erase_model(model); m_node = nullptr; ui()->set_node(nullptr); } @@ -1204,9 +1214,7 @@ void editor_mode::redo_one() { if (auto *model = dynamic_cast(target)) { - nullify_history_pointers(model); - remove_from_hierarchy(model); - simulation::State.delete_model(model); + erase_model(model); m_node = nullptr; ui()->set_node(nullptr); } @@ -3061,7 +3069,10 @@ void editor_mode::set_include_directive(scene::instance_handle const Instance, s simulation::State.rebuild_include(Instance, retired); // the pick of the renderer is resolved a few frames after it's requested, and can still deliver one of these for (auto *model : retired) + { + nullify_history_pointers(model); m_retired.emplace_back(model, 120); + } } void editor_mode::apply_include() @@ -3332,12 +3343,10 @@ void editor_mode::capture_terrain() std::string as_text; model->export_as_text(as_text); push_snapshot(model, EditorSnapshot::Action::Delete, as_text); - nullify_history_pointers(model); - remove_from_hierarchy(model); m_node = nullptr; m_dragging = false; ui()->set_node(nullptr); - simulation::State.delete_model(model); + erase_model(model); } // the ground under a point the cursor landed on. the orthophoto laid over the ground catches the cursor like any @@ -3453,13 +3462,21 @@ void editor_mode::read_georeference() std::error_code error; if (false == std::filesystem::exists(file, error)) { - for (auto const &entry : std::filesystem::directory_iterator(Global.asCurrentSceneryPath, error)) + for (std::filesystem::directory_iterator entry{Global.asCurrentSceneryPath, error}, end; entry != end; entry.increment(error)) { - auto name{entry.path().filename().string()}; + std::string name; + try + { + name = entry->path().filename().string(); + } + catch (std::exception const &) + { + continue; + } 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(); + file = entry->path(); break; } } @@ -3696,10 +3713,19 @@ void editor_mode::render_open_scenery_popup() m_openscenery_list.clear(); m_openscenery_choice.clear(); std::error_code error; - for (auto const &entry : std::filesystem::directory_iterator(Global.asCurrentSceneryPath, error)) + for (std::filesystem::directory_iterator entry{Global.asCurrentSceneryPath, error}, end; entry != end; entry.increment(error)) { - auto const name{entry.path().filename().string()}; - if (entry.is_regular_file(error) && name.size() > 4 && lower(name.substr(name.size() - 4)) == ".scn" && name.front() != '$') + std::string name; + try + { + name = entry->path().filename().string(); + } + catch (std::exception const &) + { + continue; + } + std::error_code status; + if (entry->is_regular_file(status) && name.size() > 4 && lower(name.substr(name.size() - 4)) == ".scn" && name.front() != '$') m_openscenery_list.emplace_back(name); } std::sort(m_openscenery_list.begin(), m_openscenery_list.end(), [&](std::string const &A, std::string const &B) { return lower(A) < lower(B); }); @@ -3807,9 +3833,7 @@ void editor_mode::undo_last_area_fill() m_node = nullptr; ui()->set_node(nullptr); } - nullify_history_pointers(model); - remove_from_hierarchy(model); - simulation::State.delete_model(model); + erase_model(model); } m_fill_status = format(STR_C("Removed %zu objects of the last fill"), created.size()); } @@ -4230,9 +4254,7 @@ void editor_mode::shape_array() copies.pop_back(); if (model == nullptr) continue; - nullify_history_pointers(model); - remove_from_hierarchy(model); - simulation::State.delete_model(model); + erase_model(model); } std::string definition; @@ -4317,9 +4339,7 @@ void editor_mode::restore_array_snapshot(EditorSnapshot Snapshot, std::vectorset_node(nullptr); } - nullify_history_pointers(model); - remove_from_hierarchy(model); - simulation::State.delete_model(model); + erase_model(model); } else { @@ -5488,7 +5508,7 @@ void editor_mode::render_shortcuts_window() if (false == ImGui::BeginTable(entry.title, 2, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) continue; ImGui::TableSetupColumn("##keys", ImGuiTableColumnFlags_WidthFixed, 250.0f * Global.ui_scale); - ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthStretch, 1.0f); for (auto const &item : entry.shortcuts) { if (false == matches(item)) @@ -6123,15 +6143,17 @@ void editor_mode::delete_model(TAnimModel *model, bool const Joined) m_history.back().joined = Joined; WriteLog(debug, logtype::generic); - // clear history pointers referencing this model before actually deleting it + erase_model(model); +} + +void editor_mode::erase_model(TAnimModel *model) +{ nullify_history_pointers(model); remove_from_hierarchy(model); - if (m_node == model) { m_node = nullptr; m_dragging = false; - ui()->set_node(nullptr); } simulation::State.delete_model(model); } @@ -6561,6 +6583,7 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const auto *const previous{m_node}; m_node = nullptr; select_include(0); + ui()->set_node(additive ? previous : nullptr); // in a scenery opened for editing selection has its limits. nodes of locked layers are off limits. // nodes defined by a scenery template can't be rewritten one by one on save, so a click on one @@ -6722,7 +6745,7 @@ void editor_mode::render_change_history(){ int maxsize = m_max_history_size; if (ImGui::InputInt(STR_C("Max history size"), &maxsize)) { - m_max_history_size = std::max(0, maxsize); + m_max_history_size = std::max(1, maxsize); if ((int)m_history.size() > m_max_history_size && m_max_history_size >= 0) { auto remove_count = (int)m_history.size() - m_max_history_size; diff --git a/application/editormode.h b/application/editormode.h index 4ef8dac9..05f2ec4e 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -639,6 +639,7 @@ class editor_mode : public application_mode, private editor_track::observer // hierarchy management void add_to_hierarchy(scene::basic_node *node); void remove_from_hierarchy(scene::basic_node *node); + void erase_model(TAnimModel *model); scene::basic_node* find_in_hierarchy(const std::string &uuid_str); scene::basic_node* find_node_by_any(scene::basic_node *node_ptr, const std::string &uuid_str, const std::string &name); @@ -2496,6 +2497,7 @@ class editor_mode : public application_mode, private editor_track::observer bool m_infra_suspended{false}; void infra_load(); void infra_store(); + void infra_forget(scene::basic_node const *Node); std::vector infra_scope() const; void infra_recognize(); void infra_bind_chosen(); diff --git a/application/editormodeconnectioncurve.cpp b/application/editormodeconnectioncurve.cpp index 20e81fdb..d1900c06 100644 --- a/application/editormodeconnectioncurve.cpp +++ b/application/editormodeconnectioncurve.cpp @@ -333,7 +333,7 @@ void editor_mode::insert_curved_connection() // the top of the ramps where the switch on the outer track ends: no short paths left between the two if (c1 > c2) std::swap(c1, c2); - auto const ramp{std::abs(lift) / (kRampShare * m_profile.line.norms.curve_threshold / 1000.0)}; + auto const ramp{std::abs(lift) / (std::max(0.5, kRampShare * m_profile.line.norms.curve_threshold) / 1000.0)}; flatfrom = editor_track::sampled_position(samples, c1); flatto = editor_track::sampled_position(samples, c2); ramplength = ramp; @@ -545,7 +545,7 @@ void editor_mode::insert_curved_connection() } tool.status = format(STR_C("Crossover laid along the curve, switches %s, the tracks %.2f m apart"), names.c_str(), std::abs(offset)); if (std::abs(lift) > 0.002) - tool.status += format(STR_C("\nThe outer track went up %.3f m onto the plane of the cant of the inner one, with ramps of %.1f per mille"), std::abs(lift), kRampShare * m_profile.line.norms.curve_threshold); + tool.status += format(STR_C("\nThe outer track went up %.3f m onto the plane of the cant of the inner one, with ramps of %.1f per mille"), std::abs(lift), std::max(0.5, kRampShare * m_profile.line.norms.curve_threshold)); tool.status += "\n" + connection_readout(plan); ui()->set_status(tool.status, false); } diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index db6e4f3b..2644d07c 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -97,6 +97,11 @@ class live_nodes { return Node != nullptr && m_nodes.count(Node) > 0; } + void forget(scene::basic_node const *Node) + { + m_nodes.erase(Node); + m_valid = false; + } private: std::unordered_set m_nodes; @@ -778,6 +783,17 @@ void editor_mode::infra_load() logtype::generic); } +void editor_mode::infra_forget(scene::basic_node const *Node) +{ + Live.forget(Node); + auto const held = [Node](infra::binding const &Binding) { return Binding.type != infra::kind::include && Binding.node == Node; }; + m_bindings.erase(std::remove_if(m_bindings.begin(), m_bindings.end(), held), m_bindings.end()); + m_infra_buffer.erase(std::remove_if(m_infra_buffer.begin(), m_infra_buffer.end(), [&](infra::object_state const &State) { return held(State.saved); }), m_infra_buffer.end()); + for (auto &candidate : m_infra.candidates) + if (held(candidate.binding)) + candidate.binding.node = nullptr; +} + void editor_mode::infra_store() { if (false == m_bindings_loaded || scene::Layers.empty()) diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index c49b12b7..cc076c1e 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -1912,6 +1912,8 @@ bool editor_mode::render_profile_parameters() ImGui::EndTable(); } norms.grade_switch_max = std::max(0.0, norms.grade_switch_max); + norms.radius_factor = std::max(0.0, norms.radius_factor); + norms.radius_min = std::max(10.0, norms.radius_min); if (ImGui::SmallButton(STR_C("Automatic R at all points"))) { for (auto &point : line.points) diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index a176a4b8..fc46f264 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -279,7 +279,7 @@ void editor_mode::render_route_stored() ImGui::EndTable(); } auto const &records{alignment_store::records()}; - if (route.departed < 0 || route.departed >= static_cast(records.size())) + if (route.departed < 0 || route.departed >= static_cast(records.size()) || false == scene::Layers.valid(records[route.departed].layer)) return; auto const &layer{scene::Layers.layer(records[route.departed].layer).name}; ImGui::PushTextWrapPos(0.0f); @@ -404,7 +404,7 @@ int editor_mode::digitize_parts(editor_track::chain const &Chain, std::string *R bool editor_mode::digitize_chain(editor_track::chain const &Chain, std::string *Reason, TTrack **Culprit) { - if (Chain.tracks.empty()) + if (Chain.tracks.empty() || scene::Layers.empty()) return false; auto const name = [](TTrack const *Track) { return Track == nullptr || Track->name().empty() ? std::string{STR_C("(no name)")} : Track->name(); }; auto const nearest = [&](glm::dvec2 const &Point) { @@ -1423,7 +1423,7 @@ bool editor_mode::render_route_band() if (false == dragging) list->AddLine(ImVec2(mouse.x, plot_top), ImVec2(mouse.x, plot_bottom), IM_COL32(255, 255, 255, 60)); } - if (dragging && band.vertex < static_cast(design.vertices.size())) + if (dragging && band.vertex >= 0 && band.vertex < static_cast(design.vertices.size())) { auto const &vertex{design.vertices[band.vertex]}; auto const text{band.part == 1 ? format("R %.0f m", vertex.radius) : format("%.0f m", band.part == 2 ? vertex.transition_in : vertex.transition_out)}; diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 1fb56425..0156c914 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -156,7 +156,15 @@ std::vector const &plate_textures() std::error_code error; for (std::filesystem::directory_iterator file{"textures/tabl", error}, end; file != end; file.increment(error)) { - auto const stem{lower(file->path().stem().string())}; + std::string stem; + try + { + stem = lower(file->path().stem().string()); + } + catch (std::exception const &) + { + continue; + } if (std::find(plates.begin(), plates.end(), stem) == plates.end()) plates.push_back(stem); } @@ -578,8 +586,9 @@ void editor_mode::signal_release() } tool.status = found->read.empty() ? format(STR_C("Signal %s moved, Ctrl+Z takes it back"), found->name.c_str()) : format(STR_C("Signal %s moved, read by %s %s of %s, Ctrl+Z takes it back"), found->name.c_str(), editor_track::event_keyword(spot.list), found->read.c_str(), spot.track->name().empty() ? STR_C("(no name)") : spot.track->name().c_str()); + auto const instance{found->include.instance}; signal_refresh(true); - signal_select(found->include.instance); + signal_select(instance); } std::string editor_mode::signal_variant(signal_template const &Template, signal_spot const &Spot) const diff --git a/application/editormodestraights.cpp b/application/editormodestraights.cpp index 992bc0a4..2a22da38 100644 --- a/application/editormodestraights.cpp +++ b/application/editormodestraights.cpp @@ -347,6 +347,8 @@ void editor_mode::render_straights_ui() for (int i = 0; i < static_cast(state.found.size()); ++i) { auto const &line{state.found[i]}; + if (line.tracks.empty()) + continue; if (state.filter[0] != '\0' && std::none_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return Track->name().find(state.filter) != std::string::npos; })) continue; auto const label{std::to_string(i + 1) + ". " + describe(line) + "##straight" + std::to_string(i)}; @@ -562,6 +564,14 @@ void editor_mode::straights_apply(std::vector const &Lin { auto &state{m_straights}; state.status.clear(); + auto const finite = [](glm::dvec3 const &Point) { return std::isfinite(Point.x) && std::isfinite(Point.y) && std::isfinite(Point.z); }; + if (std::any_of(Ends.begin(), Ends.end(), [&](std::pair const &End) { + return false == finite(End.first) || false == finite(End.second) || false == (glm::distance(End.first, End.second) >= 0.1 && glm::distance(End.first, End.second) <= 100000.0); + })) + { + state.status = STR_C("The ends have to be numbers, from 0.1 m to 100 km apart"); + return; + } auto const tracks{editor_track::straight_affected(Lines)}; if (false == tracks_editable(tracks, state.status)) return; diff --git a/application/editormodesweep.cpp b/application/editormodesweep.cpp index f34f6c01..5fe783a6 100644 --- a/application/editormodesweep.cpp +++ b/application/editormodesweep.cpp @@ -144,14 +144,26 @@ void editor_mode::sweep_scan_models() tool.scanned = true; changed = true; std::error_code error; - for (auto const &entry : std::filesystem::recursive_directory_iterator("models", std::filesystem::directory_options::skip_permission_denied, error)) + for (std::filesystem::recursive_directory_iterator entry{"models", std::filesystem::directory_options::skip_permission_denied, error}, end; entry != end; entry.increment(error)) { - if (false == entry.is_regular_file(error)) + std::error_code status; + if (false == entry->is_regular_file(status)) continue; - auto const extension{lower(entry.path().extension().string())}; - if (extension != ".e3d" && extension != ".t3d") + std::string extension; + std::string name; + try + { + extension = lower(entry->path().extension().string()); + if (extension != ".e3d" && extension != ".t3d") + continue; + name = std::filesystem::relative(entry->path(), "models", status).generic_string(); + } + catch (std::exception const &) + { + continue; + } + if (name.size() < extension.size()) continue; - auto name{std::filesystem::relative(entry.path(), "models", error).generic_string()}; name.erase(name.size() - extension.size()); tool.models.push_back(name); } @@ -799,9 +811,17 @@ void editor_mode::render_sweep_ui() auto const start{prefix.substr(slash + 1)}; std::set variants; std::error_code error; - for (auto const &entry : std::filesystem::directory_iterator(folder, error)) + for (std::filesystem::directory_iterator entry{folder, error}, end; entry != end; entry.increment(error)) { - auto const stem{lower(entry.path().stem().string())}; + std::string stem; + try + { + stem = lower(entry->path().stem().string()); + } + catch (std::exception const &) + { + continue; + } 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())); } @@ -1405,12 +1425,10 @@ void editor_mode::bend_selection() snap.rotation = model->Angles(); snap.scale = model->Scale(); snap.uuid = model->uuid; - nullify_history_pointers(model); - remove_from_hierarchy(model); m_node = nullptr; m_dragging = false; ui()->set_node(nullptr); - simulation::State.delete_model(model); + erase_model(model); } else { @@ -1463,9 +1481,7 @@ void editor_mode::restore_bend(EditorSnapshot &Snapshot, bool const Undo) } else if (auto *model{dynamic_cast(find_node_by_any(Snapshot.node_ptr, Snapshot.uuid.to_string(), Snapshot.node_name))}) { - nullify_history_pointers(model); - remove_from_hierarchy(model); - simulation::State.delete_model(model); + erase_model(model); Snapshot.node_ptr = nullptr; m_node = nullptr; ui()->set_node(nullptr); diff --git a/application/editormodetextures.cpp b/application/editormodetextures.cpp index f18dff0a..c80e5032 100644 --- a/application/editormodetextures.cpp +++ b/application/editormodetextures.cpp @@ -121,16 +121,28 @@ void editor_mode::render_texture_browser() for (std::filesystem::directory_iterator entry{std::string{paths::textures} + browser.folder, error}, end; false == static_cast(error) && entry != end; entry.increment(error)) { auto const path{entry->path()}; - if (entry->is_directory(error)) + std::string name; + std::string extension; + std::string stem; + try + { + name = path.filename().string(); + extension = lowercase(path.extension().string()); + stem = path.stem().string(); + } + catch (std::exception const &) { - browser.folders.push_back(path.filename().string()); continue; } - auto const extension{lowercase(path.extension().string())}; + std::error_code status; + if (entry->is_directory(status)) + { + browser.folders.push_back(name); + continue; + } auto const material{extension == ".mat"}; if (false == material && false == texture_file(extension)) continue; - auto const stem{path.stem().string()}; auto &item{items[stem]}; item.name = browser.folder + stem; item.label = stem; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index a41f544c..73ade052 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1345,6 +1345,7 @@ void editor_mode::render_path_ui() } }; + auto const finite = [](glm::dvec3 const &Point) { return std::isfinite(Point.x) && std::isfinite(Point.y) && std::isfinite(Point.z); }; if (ImGui::CollapsingHeader(track->eType == tt_Switch ? STR_C("Geometry of the paths") : STR_C("Geometry"))) { for (int i = 0; i < static_cast(track->m_paths.size()); ++i) @@ -1381,7 +1382,7 @@ void editor_mode::render_path_ui() if (editor_track::is_end(kind)) { auto position = path.points[kind == editor_track::point_kind::start ? segment_data::point::start : segment_data::point::end]; - if (ImGui::DragScalarN("x, y, z##point", ImGuiDataType_Double, glm::value_ptr(position), 3, 0.01f, nullptr, nullptr, "%.3f")) + if (ImGui::DragScalarN("x, y, z##point", ImGuiDataType_Double, glm::value_ptr(position), 3, 0.01f, nullptr, nullptr, "%.3f") && finite(position)) editor_track::move_point(*track, m_track_point, position); item_tooltip("Drag to move, Ctrl+click to type the value"); finish_edit(rebuild::geometry); @@ -1389,7 +1390,9 @@ void editor_mode::render_path_ui() else { auto &vector{path.points[kind == editor_track::point_kind::control1 ? segment_data::point::control1 : segment_data::point::control2]}; - ImGui::DragScalarN(STR_C("vector##point"), ImGuiDataType_Double, glm::value_ptr(vector), 3, 0.01f, nullptr, nullptr, "%.3f"); + auto typed{vector}; + if (ImGui::DragScalarN(STR_C("vector##point"), ImGuiDataType_Double, glm::value_ptr(typed), 3, 0.01f, nullptr, nullptr, "%.3f") && finite(typed) && glm::length(typed) < 1e5) + vector = typed; item_tooltip("Control vector, relative to its end; zero for a straight"); finish_edit(rebuild::geometry); } @@ -1399,11 +1402,13 @@ void editor_mode::render_path_ui() if (begin_fields("##cantradius", 0.3f)) { field_row("Roll (deg)"); - ImGui::DragFloat2("##roll", path.rolls.data(), 0.01f, -15.0f, 15.0f, "%.2f"); + if (auto rolls{path.rolls}; ImGui::DragFloat2("##roll", rolls.data(), 0.01f, -15.0f, 15.0f, "%.2f") && std::isfinite(rolls[0]) && std::isfinite(rolls[1])) + path.rolls = rolls; item_tooltip("Cant at the start and at the end as the roll angle of the track"); finish_edit(rebuild::geometry); field_row("Radius"); - ImGui::InputFloat("##radius", &path.radius, 0.0f, 0.0f, "%.1f"); + if (auto radius{path.radius}; ImGui::InputFloat("##radius", &radius, 0.0f, 0.0f, "%.1f") && std::isfinite(radius)) + path.radius = radius; item_tooltip("Radius stored with the path in the scenery; the shape itself comes from the control vectors"); finish_edit(rebuild::geometry); ImGui::EndTable(); diff --git a/application/editormodetrackprops.cpp b/application/editormodetrackprops.cpp index 574c2cda..4d507023 100644 --- a/application/editormodetrackprops.cpp +++ b/application/editormodetrackprops.cpp @@ -875,21 +875,28 @@ void editor_mode::track_textures_index() for (std::filesystem::recursive_directory_iterator entry{root, options, error}, end; false == static_cast(error) && entry != end; entry.increment(error)) { std::error_code ignored; - if (entry->is_directory(ignored)) + try + { + if (entry->is_directory(ignored)) + { + // the normal maps go with their textures, not on a track by themselves + if (lowered(entry->path().filename().string()) == "normals") + entry.disable_recursion_pending(); + continue; + } + auto const extension{lowered(entry->path().extension().string())}; + auto const material{extension == ".mat"}; + if (false == material && false == texture_extension(extension)) + continue; + auto path{entry->path().lexically_relative(root)}; + path.replace_extension(); + auto &known{found[path.generic_string()]}; + known = known || material; + } + catch (std::exception const &) { - // the normal maps go with their textures, not on a track by themselves - if (lowered(entry->path().filename().string()) == "normals") - entry.disable_recursion_pending(); continue; } - auto const extension{lowered(entry->path().extension().string())}; - auto const material{extension == ".mat"}; - if (false == material && false == texture_extension(extension)) - continue; - auto path{entry->path().lexically_relative(root)}; - path.replace_extension(); - auto &known{found[path.generic_string()]}; - known = known || material; } m_track_texture_index.clear(); m_track_texture_index.reserve(found.size()); diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 96d5d9ac..9518c5ca 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -962,7 +962,7 @@ bool editor_mode::typed_key(int const Key) 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) + if (values.empty() || m_lay.points.empty() || false == (values[0] > 0.0 && values[0] <= 50000.0)) return Mouse; auto const &last{m_lay.points.back()}; glm::dvec2 heading{Mouse.x - last.x, Mouse.z - last.z}; diff --git a/application/editoruilayer.h b/application/editoruilayer.h index f405bad5..b6c8a934 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -32,6 +32,10 @@ class editor_ui : public ui_layer // updates state of UI elements void update() override; void set_node(scene::basic_node *Node); + scene::basic_node *node() const + { + return m_node; + } // include of a scenery template to show in the node properties in place of a node; nullptr if there's none void set_include(include_selection *Include); void add_node_template(const std::string &desc); diff --git a/application/scenarioloadermode.cpp b/application/scenarioloadermode.cpp index 8409e762..7afe0a44 100644 --- a/application/scenarioloadermode.cpp +++ b/application/scenarioloadermode.cpp @@ -49,7 +49,17 @@ bool scenarioloader_mode::update() { WriteLog( "\nLoading scenario \"" + Global.SceneryFile + "\"..." ); timestart = std::chrono::system_clock::now(); - state = simulation::State.deserialize_begin(Global.SceneryFile); + try { + state = simulation::State.deserialize_begin(Global.SceneryFile); + } + catch (std::exception const &e) { + ErrorLog("Bad init: scenario \"" + Global.SceneryFile + "\" couldn't be opened (" + e.what() + ")"); + state.reset(); + Global.SceneryFile.clear(); + Application.pop_mode(); + Application.push_mode(eu07_application::mode::launcher); + return true; + } } try { diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index ce4db650..2b0d9d59 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -276,7 +276,7 @@ recommendation recommend(double const Speed, double const Radius, limits const & result.cant_equilibrium = factor * Speed * Speed / Radius; result.cant_min = std::max(0.0, result.cant_equilibrium - allowance); result.cant = std::min(Limits.cant_max, ceil_to(std::max(result.cant_min, result.cant_equilibrium * 2.0 / 3.0), 5.0)); - result.transition = transition_length(Speed, Radius, result.cant, Limits); + result.transition = std::min(5000.0, transition_length(Speed, Radius, result.cant, Limits)); return result; } diff --git a/editor/editorNmt.cpp b/editor/editorNmt.cpp index 64f12308..ce29f607 100644 --- a/editor/editorNmt.cpp +++ b/editor/editorNmt.cpp @@ -117,7 +117,7 @@ bool read_geotiff(std::vector const &Data, Square &Out) }; auto const width{static_cast(number(256, 0))}; auto const height{static_cast(number(257, 0))}; - if (width < 2 || height < 2 || number(258, 0) != 32 || number(339, 0) != 3 || std::max(1u, number(259, 0)) != 1 || false == entries.contains(273) || false == entries.contains(33550) || + if (width < 2 || height < 2 || width > 8192 || height > 8192 || number(258, 0) != 32 || number(339, 0) != 3 || std::max(1u, number(259, 0)) != 1 || false == entries.contains(273) || false == entries.contains(33550) || false == entries.contains(33922)) return false; auto const rows{std::max(1u, number(278, 0))}; diff --git a/editor/editorProfile.cpp b/editor/editorProfile.cpp index 6e610958..b838d13d 100644 --- a/editor/editorProfile.cpp +++ b/editor/editorProfile.cpp @@ -56,7 +56,7 @@ double grade_after(line const &Line, std::size_t const Index) double required_radius(line const &Line) { - return std::max(Line.norms.radius_min, Line.norms.radius_factor * Line.speed * Line.speed); + return std::max({10.0, Line.norms.radius_min, Line.norms.radius_factor * Line.speed * Line.speed}); } void assign_radii(line &Line) diff --git a/editor/editorRoad.cpp b/editor/editorRoad.cpp index 136f607e..b32a68b6 100644 --- a/editor/editorRoad.cpp +++ b/editor/editorRoad.cpp @@ -2069,11 +2069,20 @@ std::vector const &editor_road::materials() { if (error || false == file->is_regular_file(error)) continue; - auto extension{file->path().extension().string()}; + std::string extension; + std::string name; + try + { + extension = file->path().extension().string(); + name = file->path().stem().string(); + } + catch (std::exception const &) + { + continue; + } std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (extension != ".mat" && extension != ".dds" && extension != ".tga" && extension != ".png" && extension != ".bmp" && extension != ".jpg" && extension != ".ktx" && extension != ".tex") continue; - auto name{file->path().stem().string()}; std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); // names with spaces can't be put in a scenery file if (name.empty() || name.find(' ') != std::string::npos) diff --git a/editor/editorSwitchDesign.cpp b/editor/editorSwitchDesign.cpp index d4ef0a4f..16fb37b2 100644 --- a/editor/editorSwitchDesign.cpp +++ b/editor/editorSwitchDesign.cpp @@ -160,10 +160,12 @@ std::string checked(std::vector const &Parts) return "The diverging track has no parts"; for (auto const &part : Parts) { - if (part.length < 0.05) + if (false == (part.length >= 0.05)) return "A part of the diverging track is shorter than 5 cm"; + if (part.length > 1000.0) + return "A part of the diverging track is longer than 1000 m"; for (auto const radius : {part.radius1, part.radius2}) - if (radius != 0.0 && std::abs(radius) < kMinRadius) + if (std::isnan(radius) || (radius != 0.0 && std::abs(radius) < kMinRadius)) return "A radius of the diverging track is under 10 m"; } return {}; @@ -362,9 +364,9 @@ shape build(design const &Design, std::string &Error) bool const mirrored{Design.type == "Rps"}; if (Error.empty() && twofold && false == mirrored && false == Design.parts2.empty()) Error = checked(Design.parts2); - if (Error.empty() && (Design.lead < 0.0 || Design.lead > 30.0)) + if (Error.empty() && false == (Design.lead >= 0.0 && Design.lead <= 30.0)) Error = "The pre-blade stretch has to be between 0 and 30 m"; - if (Error.empty() && (Design.entry < 0.0 || Design.entry > 5.0)) + if (Error.empty() && false == (Design.entry >= 0.0 && Design.entry <= 5.0)) Error = "The angle of the blade has to be between 0 and 5 degrees"; if (false == Error.empty()) return result; diff --git a/editor/editorSwitchStore.cpp b/editor/editorSwitchStore.cpp index 0851a87f..02a04a3b 100644 --- a/editor/editorSwitchStore.cpp +++ b/editor/editorSwitchStore.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include #include @@ -167,7 +168,7 @@ void load() if (entry.kind != "switch" && entry.kind != "slip" && entry.kind != "crossing" && entry.kind != "double" && entry.kind != "connection") continue; line >> entry.name >> entry.type >> entry.rail >> entry.radius >> entry.ratio >> hand >> entry.a >> entry.b >> entry.main_radius >> keyword >> count; - if (line.fail() || keyword != "parts" || count > 64) + if (line.fail() || keyword != "parts" || count == 0 || count > 64) continue; entry.hand = hand.empty() ? '-' : hand.front(); entry.parts.resize(count); @@ -202,7 +203,12 @@ void load() entry.direction = plan_of(ahead - entry.start); entry.direction = glm::length(entry.direction) > 1e-12 ? glm::normalize(entry.direction) : glm::dvec2{0.0, 1.0}; if (km != "-") - entry.km = std::stod(km) * 1000.0; + { + char *end{nullptr}; + auto const value{std::strtod(km.c_str(), &end)}; + if (end != km.c_str() && *end == '\0' && std::isfinite(value)) + entry.km = value * 1000.0; + } g_records.push_back(std::move(entry)); } } diff --git a/editor/editorTerrainStreamer.cpp b/editor/editorTerrainStreamer.cpp index 8f799c46..91067226 100644 --- a/editor/editorTerrainStreamer.cpp +++ b/editor/editorTerrainStreamer.cpp @@ -260,6 +260,7 @@ void terrain_streamer::worker() } case job::kind::mesh: { + done.cells = task.data->cells; auto const &neighbours{task.neighbours}; heightmap::build_mesh(*task.data, task.stride, task.origin, [&neighbours](double const X, double const Z, double &Height) { @@ -332,7 +333,7 @@ void terrain_streamer::process(result &Result) break; auto &chunk{*lookup->second}; chunk.m_building = false; - if (Result.version < chunk.m_builtversion) + if (Result.version < chunk.m_builtversion || Result.cells != chunk.cells()) break; upload(chunk, Result.mesh, Result.stride); chunk.m_builtversion = Result.version; @@ -386,7 +387,9 @@ void terrain_streamer::upload(editor_terrain &Chunk, heightmap::mesh &Mesh, int gfx::userdata_array nouserdata; // the mesh goes to a geometry slot made for its grid; a slot of the same kind is refilled in place - auto fits = [&](geometry_slot const &Slot) { return Slot.cells == cells && Slot.stride == Stride; }; + auto fits = [&](geometry_slot const &Slot) { + return Slot.cells == cells && Slot.stride == Stride && GfxRenderer->Indices(Slot.geometry).size() == Mesh.indices.size() && GfxRenderer->Vertices(Slot.geometry).size() == Mesh.vertices.size(); + }; if (Chunk.m_slot < 0 || false == fits(m_slots[Chunk.m_slot])) { if (Chunk.m_slot >= 0) diff --git a/editor/editorTerrainStreamer.hpp b/editor/editorTerrainStreamer.hpp index beff9746..c5c5d381 100644 --- a/editor/editorTerrainStreamer.hpp +++ b/editor/editorTerrainStreamer.hpp @@ -209,6 +209,7 @@ class terrain_streamer std::shared_ptr data; heightmap::mesh mesh; int stride{1}; + int cells{0}; std::uint64_t version{0}; std::string error; }; diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 4f30765c..a2c6595a 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -472,8 +472,11 @@ void editor_track::commit(std::vector const &Tracks) for (auto *track : Tracks) track->mark_dirty(); for (auto *track : affected) - for (auto *vehicle : track->Dynamics) + { + auto const vehicles{track->Dynamics}; + for (auto *vehicle : vehicles) vehicle->Move(0.000001); + } if (s_observer != nullptr) s_observer->tracks_committed(Tracks); } diff --git a/lang/editor_pl.po b/lang/editor_pl.po index ec07525b..bd6fef78 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5118,6 +5118,9 @@ msgstr "Tor zwrotny nie ma części" msgid "A part of the diverging track is shorter than 5 cm" msgstr "Część toru zwrotnego jest krótsza niż 5 cm" +msgid "A part of the diverging track is longer than 1000 m" +msgstr "Część toru zwrotnego jest dłuższa niż 1000 m" + msgid "A radius of the diverging track is under 10 m" msgstr "Promień toru zwrotnego jest mniejszy niż 10 m" @@ -9949,3 +9952,6 @@ msgstr "Dobudowuj teren przy układaniu" msgid "Where a track is laid past the edge of the terrain, the terrain model is made around the cursor as it goes" msgstr "Gdy tor wychodzi poza teren, NMT dobudowuje się wokół kursora na bieżąco" + +msgid "The ends have to be numbers, from 0.1 m to 100 km apart" +msgstr "Końce muszą być liczbami, od 0,1 m do 100 km od siebie" diff --git a/launcher/scenery_list.cpp b/launcher/scenery_list.cpp index d95c962b..195b0e2c 100644 --- a/launcher/scenery_list.cpp +++ b/launcher/scenery_list.cpp @@ -23,7 +23,7 @@ bool ui::scenerylist_panel::on_key(int key, int action) if (it == keyboard.keytonamemap.end()) return false; - if (Global.ctrlState && it->second == "delete") + if (Global.ctrlState && it->second == "delete" && selected_trainset != nullptr) purge_selected_trainset(); return true; @@ -117,7 +117,7 @@ void ui::scenerylist_panel::draw_launch_box() { ImGui::NextColumn(); - ImGui::TextWrapped(selected_trainset->description.c_str()); + ImGui::TextWrapped("%s", selected_trainset->description.c_str()); ImGui::Checkbox(STR_C("Trainset turned around"), &selected_trainset->reversed); if (ImGui::IsItemHovered()) @@ -283,7 +283,8 @@ void ui::scenerylist_panel::draw_trainset(trainset_desc &trainset) draw_droptarget(trainset, position++); ImGui::SameLine(15.0f); - for (auto &dyn_desc : trainset.vehicles) { + for (std::size_t index = 0; index < trainset.vehicles.size(); ++index) { + auto &dyn_desc = trainset.vehicles[index]; deferred_image *mini = nullptr; if (dyn_desc.skin && dyn_desc.skin->mini.get() != deferred_image::invalid_id) @@ -345,16 +346,19 @@ void ui::scenerylist_panel::draw_droptarget(trainset_desc &trainset, int positio if (payload) { skin_set *ptr = *reinterpret_cast(payload->Data); std::shared_ptr skin; - for (auto &s : ptr->vehicle.lock()->matching_skinsets) - if (s.get() == ptr) - skin = s; + if (auto const vehicle = ptr->vehicle.lock()) + for (auto &s : vehicle->matching_skinsets) + if (s.get() == ptr) + skin = s; - trainset.vehicles.emplace(trainset.vehicles.begin() + position); - dynamic_desc &desc = trainset.vehicles[position]; + if (skin) { + trainset.vehicles.emplace(trainset.vehicles.begin() + position); + dynamic_desc &desc = trainset.vehicles[position]; - desc.name = skin->skin + "_" + std::to_string((int)LocalRandom(0.0, 10000000.0)); - desc.vehicle = skin->vehicle.lock(); - desc.skin = skin; + desc.name = skin->skin + "_" + std::to_string((int)LocalRandom(0.0, 10000000.0)); + desc.vehicle = skin->vehicle.lock(); + desc.skin = skin; + } } payload = ImGui::AcceptDragDropPayload("vehicle_set"); @@ -368,7 +372,7 @@ void ui::scenerylist_panel::draw_droptarget(trainset_desc &trainset, int positio if (moved->position < position) offset = -1; } else { - desc_copy.name = desc_copy.skin->skin + "_" + std::to_string((int)LocalRandom(0.0, 10000000.0)); + desc_copy.name = (desc_copy.skin ? desc_copy.skin->skin : desc_copy.name) + "_" + std::to_string((int)LocalRandom(0.0, 10000000.0)); } trainset.vehicles.insert(trainset.vehicles.begin() + position + offset, desc_copy); diff --git a/launcher/scenery_scanner.cpp b/launcher/scenery_scanner.cpp index bcd18ac9..5f6a6d2f 100644 --- a/launcher/scenery_scanner.cpp +++ b/launcher/scenery_scanner.cpp @@ -10,13 +10,21 @@ scenery_scanner::scenery_scanner(ui::vehicles_bank &bank) void scenery_scanner::scan() { - for (auto &f : std::filesystem::directory_iterator("scenery")) { - std::filesystem::path path(std::filesystem::relative(f.path(), "scenery/")); + std::error_code error; + for (std::filesystem::directory_iterator f{"scenery", std::filesystem::directory_options::skip_permission_denied, error}, end; f != end; f.increment(error)) { + std::filesystem::path path(std::filesystem::relative(f->path(), "scenery/", error)); + std::string name; + try { + name = path.string(); + } + catch (std::exception const &) { + continue; + } - if (*path.filename().string().begin() == '$') + if (name.empty() || path.filename().string().front() == '$') continue; - if (path.string().ends_with(".scn")) + if (name.ends_with(".scn")) scan_scn(path); } diff --git a/launcher/textures_scanner.cpp b/launcher/textures_scanner.cpp index 524a23ca..741f978c 100644 --- a/launcher/textures_scanner.cpp +++ b/launcher/textures_scanner.cpp @@ -4,12 +4,13 @@ void ui::vehicles_bank::scan_textures() { - for (auto &f : std::filesystem::recursive_directory_iterator("dynamic")) { - if (f.path().filename() != "textures.txt") + std::error_code error; + for (std::filesystem::recursive_directory_iterator f{"dynamic", std::filesystem::directory_options::skip_permission_denied, error}, end; f != end; f.increment(error)) { + if (f->path().filename() != "textures.txt") continue; - std::fstream stream(f.path(), std::ios_base::binary | std::ios_base::in); - ctx_path = std::filesystem::relative(f.path().parent_path(), "dynamic/"); + std::fstream stream(f->path(), std::ios_base::binary | std::ios_base::in); + ctx_path = std::filesystem::relative(f->path().parent_path(), "dynamic/", error); std::string line; while (std::getline(stream, line)) diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index b1f9e56d..4d699b52 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -5638,6 +5638,13 @@ void opengl33_renderer::Pick_Node_Callback(std::functionis_busy()) diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index d7adfb9e..46dd8001 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -116,6 +116,8 @@ class opengl33_renderer : public gfx_renderer { Pick_Control() const override { return m_pickcontrolitem; } scene::basic_node const * Pick_Node() const override { return m_picksceneryitem; } + void + Forget_Node( scene::basic_node const *Node ) override; glm::dvec3 Mouse_Position() const override { return m_worldmousecoordinates; } bool diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index d20626e3..3ce07491 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -1902,6 +1902,15 @@ opengl_renderer::Pick_Node_Callback( std::function m_node_pick_requests.emplace_back( Callback ); } +void +opengl_renderer::Forget_Node( scene::basic_node const *Node ) { + + if( m_picksceneryitem == Node ) { + m_picksceneryitem = nullptr; + } + std::replace_if( m_picksceneryitems.begin(), m_picksceneryitems.end(), [Node]( scene::basic_node const *Item ) { return Item == Node; }, nullptr ); +} + void opengl_renderer::Render( scene::basic_region *Region ) { diff --git a/rendering/openglrenderer.h b/rendering/openglrenderer.h index da3faad0..9779388c 100644 --- a/rendering/openglrenderer.h +++ b/rendering/openglrenderer.h @@ -104,6 +104,8 @@ public: Pick_Control() const override { return m_pickcontrolitem; } scene::basic_node const * Pick_Node() const override { return m_picksceneryitem; } + void + Forget_Node( scene::basic_node const *Node ) override; glm::dvec3 Mouse_Position() const override { return m_worldmousecoordinates; } glm::mat4 diff --git a/rendering/renderer.h b/rendering/renderer.h index 2c302a75..c87a8aad 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -96,6 +96,7 @@ public: virtual void Pick_Node_Callback( std::function Callback ) = 0; virtual auto Pick_Control() const -> TSubModel const * = 0; virtual auto Pick_Node() const -> scene::basic_node const * = 0; + virtual void Forget_Node( scene::basic_node const *Node ) {} virtual auto Mouse_Position() const -> glm::dvec3 = 0; virtual auto Mouse_Hit() const -> bool { return true; } diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index 70f9e307..ea217717 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -30,6 +30,7 @@ http://mozilla.org/MPL/2.0/. #include "scene/scenelayers.h" #include "vehicle/Train.h" #include "application/application.h" +#include "rendering/renderer.h" #include "utilities/Logs.h" #include "vehicle/Driver.h" @@ -561,6 +562,9 @@ std::pair state_manager::rebuild_include(scene::instance_handle const } void state_manager::delete_model(TAnimModel *model) { + if (GfxRenderer) { + GfxRenderer->Forget_Node(model); + } scene::Layers.count(model->layer(), scene::layer_item::model, -1); scene::Layers.forget(model); Region->erase(model); diff --git a/world/Segment.cpp b/world/Segment.cpp index 448d421f..2abdf075 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -121,6 +121,11 @@ bool TSegment::Init(glm::dvec3 &NewPoint1, glm::dvec3 NewCPointOut, glm::dvec3 N else { fLength = glm::length(Point1 - Point2); } + if( false == std::isfinite( fLength ) ) { + + ErrorLog( "Bad track: spline \"" + pOwner->name() + "\" with no finite length (location: " + to_string( glm::dvec3{ Point1 } ) + ")" ); + fLength = 0.01; + } if (fLength <= 0) { ErrorLog( "Bad track: zero length spline \"" + pOwner->name() + "\" (location: " + to_string( glm::dvec3{ Point1 } ) + ")" ); @@ -129,7 +134,7 @@ bool TSegment::Init(glm::dvec3 &NewPoint1, glm::dvec3 NewCPointOut, glm::dvec3 N fStoop = std::atan2(Point2.y - Point1.y, fLength); // pochylenie toru prostego, żeby nie liczyć wielokrotnie - fStep = fNewStep; + fStep = ( fNewStep > 0.0 && std::isfinite( fNewStep ) ) ? fNewStep : fLength; // NOTE: optionally replace this part with the commented version, after solving geometry issues with double switches if( pOwner->eType == tt_Switch && fStep * (3.0 * Global.SplineFidelity) > fLength ) { @@ -137,7 +142,7 @@ bool TSegment::Init(glm::dvec3 &NewPoint1, glm::dvec3 NewCPointOut, glm::dvec3 N fStep = fLength / ( 3.0 * Global.SplineFidelity ); } // iSegCount = static_cast( std::ceil( fLength / fStep ) ); // potrzebne do VBO - iSegCount = pOwner->eType == tt_Switch ? static_cast(6 * Global.SplineFidelity) : static_cast(std::ceil(fLength / fStep)); // potrzebne do VBO + iSegCount = pOwner->eType == tt_Switch ? static_cast(6 * Global.SplineFidelity) : static_cast(std::clamp(std::ceil(fLength / fStep), 1.0, 100000.0)); // potrzebne do VBO fStep = fLength / iSegCount; // update step to equalize size of individual pieces diff --git a/world/Track.cpp b/world/Track.cpp index ae071220..19a9d140 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -426,6 +426,7 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) std::string str; size_t i; //,state; //Ra: teraz już nie ma początkowego stanu zwrotnicy we wpisie std::string token; + bool tributary{false}; parser->getTokens(); *parser >> str; // typ toru @@ -467,11 +468,17 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) } else if (str == "tributary") { - eType = tt_Tributary; + ErrorLog("Bad track: type \"tributary\" of track \"" + m_name + "\" in file \"" + parser->Name() + "\" (line " + std::to_string(parser->Line()) + ") isn't supported, loaded as a river with its first path"); + eType = tt_Normal; iCategoryFlag = 4; + tributary = true; } else - eType = tt_Unknown; + { + ErrorLog("Bad track: unknown type \"" + str + "\" of track \"" + m_name + "\" in file \"" + parser->Name() + "\" (line " + std::to_string(parser->Line()) + "), loaded as a normal track"); + eType = tt_Normal; + iCategoryFlag = 1; + } if (Global.iWriteLogEnabled & 4) WriteLog(str); parser->getTokens(4); @@ -544,6 +551,10 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) // all subtypes contain at least one path m_paths.emplace_back(); m_paths.back().deserialize( *parser, pOrigin ); + if( tributary ) { + segment_data ignored; + ignored.deserialize( *parser, pOrigin ); + } switch( eType ) { case tt_Switch: case tt_Cross: From a168e2ccfc451a97adfa34bb8549c679cd675c67 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 08:59:54 +0200 Subject: [PATCH 004/145] fix(editor): an undo no longer moves a model which took the place of a deleted one The states of the objects which followed a track, kept in the history for the undo, held the pointer of a model after it was deleted. Only the address was checked before the model was moved back, so a node made later at the same address, of another type too, was taken for it. The states of a deleted model are dropped from the history with its other pointers. --- application/editormode.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/application/editormode.cpp b/application/editormode.cpp index 0b2ee811..0dd4c8ee 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -874,6 +874,7 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node) ui()->set_node(nullptr); infra_forget(node); + auto const followed = [node](infra::object_state const &State) { return State.saved.node == node; }; for (auto &s : m_history) { if (s.node_ptr == node) @@ -883,12 +884,14 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node) if (copy.model == node) copy.model = nullptr; } + s.infra.erase(std::remove_if(s.infra.begin(), s.infra.end(), followed), s.infra.end()); } for (auto &s : g_redo) { if (s.node_ptr == node) s.node_ptr = nullptr; + s.infra.erase(std::remove_if(s.infra.begin(), s.infra.end(), followed), s.infra.end()); } // deleted nodes also drop out of the "undo last fill" set From 3c9c90b4681be88247f0bbf7b1ebf0486b554571 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 09:00:59 +0200 Subject: [PATCH 005/145] refactor(editor): the structure gauge is a tool of its own The first step of taking the tools out of editor_mode: a tool keeps its own state and reaches the editor through editor_services, the few things it needs from it, instead of through every member of the editor. The editor holds its tools in a list for the hooks they share: a deleted node is forgotten by all of them in one place, and a self-test command the editor doesn't know goes to each of them. The gauge moves with its state, its window and its self-test command; the ImGui ids and the texts stay the same, and the self-test gives the same hits on the test scenery as before. --- application/editormode.cpp | 10 +-- application/editormode.h | 79 ++++------------------ application/editormodegauge.cpp | 87 ++++++++++++++---------- application/editormodegauge.h | 88 +++++++++++++++++++++++++ application/editormodeselftest.cpp | 3 + application/editormodeselftestsweep.cpp | 15 ----- application/editormodetrack.cpp | 15 +++++ application/editormodeviewport.cpp | 6 +- application/editortool.h | 52 +++++++++++++++ 9 files changed, 232 insertions(+), 123 deletions(-) create mode 100644 application/editormodegauge.h create mode 100644 application/editortool.h diff --git a/application/editormode.cpp b/application/editormode.cpp index 0dd4c8ee..9cd2df06 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -873,6 +873,8 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node) if (ui()->node() == node) ui()->set_node(nullptr); infra_forget(node); + for (auto *tool : m_tools) + tool->forget(node); auto const followed = [node](infra::object_state const &State) { return State.saved.node == node; }; for (auto &s : m_history) @@ -1480,8 +1482,8 @@ bool editor_mode::update() bend_drag_update(); draw_bend_overlay(); } - update_gauge(); - render_gauge_window(); + m_gauge.update(); + m_gauge.render_windows(); // roads: trajectories of the lanes, and the tools of the road window update_road_tool(); @@ -3549,8 +3551,8 @@ void editor_mode::render_view_menu() ImGui::MenuItem(STR_C("Show lanes"), nullptr, &m_roadtool.lanes); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("The lanes of the roads drawn as the paths the vehicles take")); - if (ImGui::MenuItem(STR_C("Structure gauge"), nullptr, m_gauge.open)) - m_gauge.open = !m_gauge.open; + if (ImGui::MenuItem(STR_C("Structure gauge"), nullptr, m_gauge.is_open())) + m_gauge.toggle(); 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::Separator(); diff --git a/application/editormode.h b/application/editormode.h index 05f2ec4e..7b0b0ade 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -14,6 +14,8 @@ http://mozilla.org/MPL/2.0/. #include "input/editormouseinput.h" #include "input/editorkeyboardinput.h" #include "vehicle/Camera.h" +#include "application/editormodegauge.h" +#include "application/editortool.h" #include "scene/sceneeditor.h" #include "scene/scenenode.h" #include "editor/editorTerrain.hpp" @@ -58,20 +60,10 @@ struct vehicle_desc; struct skin_set; } -class editor_mode : public application_mode, private editor_track::observer +class editor_mode : public application_mode, private editor_track::observer, private editor_services { public: - // model entering the structure gauge, at its deepest place - struct gauge_hit - { - std::string model; - glm::dvec3 point{0.0}; - double depth{0.0}; // m - TTrack const *track{nullptr}; // nearest one, may be gone by now - 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 { @@ -742,7 +734,10 @@ class editor_mode : public application_mode, private editor_track::observer gizmo_operation m_gizmo_op{gizmo_operation::translate}; // current transform mode (translate/rotate/scale) float m_gizmo_snap{1.0f}; // translation snap step (metres) applied while Ctrl is held - TTrack *selected_track() const; + TTrack *selected_track() const override; + bool track_editing() const override; + std::size_t history_size() const override; + glm::dvec3 camera_position() const override; void render_path_ui(); void draw_track_overlay() const; editor_track::point_ref track_handle_hit() const; @@ -862,55 +857,8 @@ class editor_mode : public application_mode, private editor_track::observer std::string m_track_events_missing; void render_path_events(TTrack &Track); std::string m_track_mode_notice; - // structure gauge along the line of the selected path, shown in the 3d view - struct gauge_state - { - bool open{false}; // window - bool enabled{true}; // tunnel along the selected path - std::vector profiles; - int profile{0}; // railway - int road{-1}; - TTrack const *track{nullptr}; // scanned - std::size_t history{0}; - bool pending{false}; // rescan once the edit ends - struct - { - std::vector surface; // triangles of the tunnel - std::vector edges; // lines - std::vector hits; - } line; // along the selected path - struct - { - bool scanned{false}; - std::size_t history{0}; - std::string profile; - std::vector hits; - } map; // whole scenery - struct - { - std::vector surface; - std::vector edges; - } spot; // tunnel around the hit the camera went to - int current{-1}; // hit the camera went to - bool published{false}; // overlay up to date - std::shared_ptr scan; // of the whole scenery, done in slices over the frames - } m_gauge; - void gauge_load(); - // the chosen railway and road outlines - gauge::choice gauge_choice() const; - std::vector const &gauge_hits() const; - void update_gauge(); - void scan_gauge(TTrack &Track); - void scan_gauge_map(); - void step_gauge_map(); - void finish_gauge_map(); - void gauge_publish(); - void gauge_focus(int Index); - void gauge_spot(gauge_hit const &Hit); - void render_gauge_window(); - void render_gauge_hits(); - bool render_gauge_choice(char const *Label, int &Index, bool Road); - bool render_gauge_outline(int Index); + gauge_tool m_gauge{*this}; + std::vector m_tools{&m_gauge}; struct route_design { TTrack *from{nullptr}; @@ -1673,7 +1621,7 @@ class editor_mode : public application_mode, private editor_track::observer void split_track_at(TTrack &Track, glm::dvec3 const &Point); void focus_track(TTrack &Track, int const Path, glm::dvec3 const &Point); // the camera flies over Duration seconds to the place Distance back from the target along Look, looking along it - void fly_to(glm::dvec3 const &Target, glm::dvec3 const &Look, double const Distance, double const Duration = 0.6); + void fly_to(glm::dvec3 const &Target, glm::dvec3 const &Look, double const Distance, double const Duration = 0.6) override; // paths found by their names struct track_search { @@ -2273,12 +2221,7 @@ class editor_mode : public application_mode, private editor_track::observer scripted_input input; }; selftest m_selftest; - enum class selftest_result - { - unhandled, - handled, - pause - }; + using selftest_result = editor_selftest_result; selftest_result selftest_scene(std::string const &command, std::istringstream &words); selftest_result selftest_sleepers(std::string const &command, std::istringstream &words); selftest_result selftest_sweep(std::string const &command, std::istringstream &words); diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 84af9df3..4c2f2187 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -8,7 +8,8 @@ http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" -#include "application/editormode.h" +#include "application/editormodegauge.h" +#include "editor/editorTrack.hpp" #include "application/editoruilayer.h" #include "editor/editorFormat.hpp" #include "editor/editorGauge.hpp" @@ -326,7 +327,7 @@ template void visit_triangles(TSubModel *Submodel, glm::dmat4 } // the triangle enters the gauge when any point of a grid spread over it does; the deepest such point goes to the hit -void scan_triangle(corridor const &Space, glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C, editor_mode::gauge_hit &Hit) +void scan_triangle(corridor const &Space, glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C, gauge_tool::gauge_hit &Hit) { auto const middle{(A + B + C) / 3.0}; if (false == Space.close(middle, std::max({glm::distance(middle, A), glm::distance(middle, B), glm::distance(middle, C)}))) @@ -353,24 +354,24 @@ void scan_triangle(corridor const &Space, glm::dvec3 const &A, glm::dvec3 const } // the deepest place of the model in the gauge, added to the hits when it enters it -void scan_model(corridor const &Space, TAnimModel &Instance, std::vector &Hits) +void scan_model(corridor const &Space, TAnimModel &Instance, std::vector &Hits) { if (false == Instance.visible() || Instance.Model() == nullptr || Instance.Model()->GetSMRoot() == nullptr) return; if (false == Space.nearby(Instance.location(), Instance.radius())) return; - editor_mode::gauge_hit hit{Instance.name(), Instance.location()}; + gauge_tool::gauge_hit hit{Instance.name(), Instance.location()}; visit_triangles(Instance.Model()->GetSMRoot(), placement(Instance), [&](glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C) { scan_triangle(Space, A, B, C, hit); }); if (hit.depth > 0.0) Hits.push_back(std::move(hit)); } // the same for the copies of a model laid along a curve -void scan_sweep(corridor const &Space, sweep_node &Sweep, std::vector &Hits) +void scan_sweep(corridor const &Space, sweep_node &Sweep, std::vector &Hits) { if (Sweep.m_editorremoved || false == Space.nearby(Sweep.location(), Sweep.radius())) return; - editor_mode::gauge_hit hit{Sweep.name(), Sweep.location()}; + gauge_tool::gauge_hit hit{Sweep.name(), Sweep.location()}; for (auto const &shape : Sweep.create_shapes()) { auto const &vertices{shape.data().vertices}; @@ -381,13 +382,13 @@ void scan_sweep(corridor const &Space, sweep_node &Sweep, std::vector &Hits) +void sort_hits(std::vector &Hits) { std::sort(Hits.begin(), Hits.end(), [](auto const &Left, auto const &Right) { return Left.depth > Right.depth; }); } // the models entering the gauge, the deepest first -void scan_models(corridor const &Space, std::vector &Hits) +void scan_models(corridor const &Space, std::vector &Hits) { for (auto *instance : simulation::Instances.sequence()) if (instance != nullptr) @@ -550,7 +551,7 @@ struct gauge_scan std::chrono::steady_clock::time_point started; }; -void editor_mode::gauge_load() +void gauge_tool::gauge_load() { if (m_gauge.profiles.empty()) m_gauge.profiles = gauge::load_profiles(gauge_file); @@ -568,18 +569,18 @@ void editor_mode::gauge_load() } } -gauge::choice editor_mode::gauge_choice() const +gauge::choice gauge_tool::gauge_choice() const { auto const pick{[&](int const Index) { return Index >= 0 ? &m_gauge.profiles[Index] : nullptr; }}; return {pick(m_gauge.profile), pick(m_gauge.road)}; } -std::vector const &editor_mode::gauge_hits() const +std::vector const &gauge_tool::gauge_hits() const { return m_gauge.map.scanned ? m_gauge.map.hits : m_gauge.line.hits; } -void editor_mode::update_gauge() +void gauge_tool::update() { if (m_gauge.scan) step_gauge_map(); @@ -590,9 +591,9 @@ void editor_mode::update_gauge() m_gauge.published = false; return; } - auto *track{m_gauge.enabled ? selected_track() : nullptr}; + auto *track{m_gauge.enabled ? m_services.selected_track() : nullptr}; // the geometry is rescanned once the edit ends - if (ImGui::IsMouseDown(0) || m_track_gizmo_using || m_dragging) + if (ImGui::IsMouseDown(0) || m_services.track_editing()) m_gauge.pending = true; else if (track == nullptr) { @@ -603,19 +604,19 @@ void editor_mode::update_gauge() m_gauge.published = false; } } - else if (track != m_gauge.track || m_history.size() != m_gauge.history || m_gauge.pending) + else if (track != m_gauge.track || m_services.history_size() != m_gauge.history || m_gauge.pending) { m_gauge.track = track; - m_gauge.history = m_history.size(); + m_gauge.history = m_services.history_size(); m_gauge.pending = false; scan_gauge(*track); m_gauge.published = false; } - if (false == m_gauge.published || glm::distance(Camera.Pos, EditorOverlay.origin) > gauge_origin_range) + if (false == m_gauge.published || glm::distance(m_services.camera_position(), EditorOverlay.origin) > gauge_origin_range) gauge_publish(); } -void editor_mode::scan_gauge(TTrack &Track) +void gauge_tool::scan_gauge(TTrack &Track) { gauge_load(); m_gauge.line = {}; @@ -631,7 +632,7 @@ void editor_mode::scan_gauge(TTrack &Track) m_gauge.current = -1; } -void editor_mode::scan_gauge_map() +void gauge_tool::scan_gauge_map() { gauge_load(); auto scan{std::make_shared()}; @@ -644,7 +645,7 @@ void editor_mode::scan_gauge_map() scan->space.emplace(scan->lines); m_gauge.map = {}; m_gauge.map.scanned = true; - m_gauge.map.history = m_history.size(); + m_gauge.map.history = m_services.history_size(); auto const name{[](gauge::profile const *Profile) { return Profile != nullptr ? Profile->name : std::string{"-"}; }}; m_gauge.map.profile = name(chosen.railway) + ", " + name(chosen.road); m_gauge.current = -1; @@ -652,7 +653,7 @@ void editor_mode::scan_gauge_map() m_gauge.scan = std::move(scan); } -void editor_mode::step_gauge_map() +void gauge_tool::step_gauge_map() { auto &scan{*m_gauge.scan}; auto const &instances{simulation::Instances.sequence()}; @@ -675,7 +676,7 @@ void editor_mode::step_gauge_map() finish_gauge_map(); } -void editor_mode::finish_gauge_map() +void gauge_tool::finish_gauge_map() { auto const scan{std::move(m_gauge.scan)}; auto &hits{m_gauge.map.hits}; @@ -690,11 +691,11 @@ void editor_mode::finish_gauge_map() gauge_focus(0); } -void editor_mode::gauge_publish() +void gauge_tool::gauge_publish() { m_gauge.published = true; EditorOverlay.clear(); - EditorOverlay.origin = Camera.Pos; + EditorOverlay.origin = m_services.camera_position(); auto const batch{[](glm::vec4 const &Color, unsigned int const Type, std::vector const &Points) { gfx::editor_overlay::batch result{Color, Type}; result.points.reserve(Points.size()); @@ -717,7 +718,7 @@ void editor_mode::gauge_publish() batch({0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES, m_gauge.spot.edges)}; } -void editor_mode::gauge_focus(int const Index) +void gauge_tool::gauge_focus(int const Index) { auto const &hits{gauge_hits()}; if (hits.empty()) @@ -729,12 +730,12 @@ void editor_mode::gauge_focus(int const Index) // along the track, tangent to the gauge at the height of the place, from the end nearer to the camera glm::dvec3 look{hit.direction.x, 0.0, hit.direction.y}; look = glm::length(look) > 1e-6 ? glm::normalize(look) : glm::dvec3(0.0, 0.0, 1.0); - if (glm::dot(Camera.Pos - hit.point, look) > 0.0) + if (glm::dot(m_services.camera_position() - hit.point, look) > 0.0) look = -look; - fly_to(hit.point, look, gauge_view_distance); + m_services.fly_to(hit.point, look, gauge_view_distance); } -void editor_mode::gauge_spot(gauge_hit const &Hit) +void gauge_tool::gauge_spot(gauge_hit const &Hit) { m_gauge.spot = {}; auto const &paths{simulation::Paths.sequence()}; @@ -751,7 +752,7 @@ void editor_mode::gauge_spot(gauge_hit const &Hit) build_tunnel(*line, m_gauge.spot.surface, m_gauge.spot.edges); } -void editor_mode::render_gauge_window() +void gauge_tool::render_windows() { if (false == m_gauge.open) return; @@ -798,7 +799,7 @@ void editor_mode::render_gauge_window() m_gauge.current = -1; m_gauge.published = false; } - else if (m_gauge.map.history != m_history.size()) + else if (m_gauge.map.history != m_services.history_size()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("The scenery changed since the scan (%s)"), m_gauge.map.profile.c_str()); } render_gauge_hits(); @@ -809,7 +810,7 @@ void editor_mode::render_gauge_window() ImGui::End(); } -void editor_mode::render_gauge_hits() +void gauge_tool::render_gauge_hits() { auto const &hits{gauge_hits()}; if (false == m_gauge.map.scanned && (false == m_gauge.enabled || m_gauge.track == nullptr)) @@ -841,7 +842,7 @@ void editor_mode::render_gauge_hits() ImGui::EndChild(); } -bool editor_mode::render_gauge_choice(char const *Label, int &Index, bool const Road) +bool gauge_tool::render_gauge_choice(char const *Label, int &Index, bool const Road) { bool changed{false}; ImGui::PushID(Label); @@ -863,7 +864,7 @@ bool editor_mode::render_gauge_choice(char const *Label, int &Index, bool const return changed; } -bool editor_mode::render_gauge_outline(int const Index) +bool gauge_tool::render_gauge_outline(int const Index) { if (Index < 0) return false; @@ -884,3 +885,23 @@ bool editor_mode::render_gauge_outline(int const Index) ImGui::TreePop(); return edited; } + +editor_selftest_result gauge_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + if (Command == "gauge") + { + std::string name; + Words >> name; + if (auto *track{simulation::Paths.find(name)}) + { + scan_gauge(*track); + WriteLog(format("SELFTEST gauge %s: %d hits", name.c_str(), static_cast(m_gauge.line.hits.size()))); + for (auto const &hit : m_gauge.line.hits) + WriteLog(format("SELFTEST gauge hit %s depth %.3f at %.2f %.2f %.2f", hit.model.c_str(), hit.depth, hit.point.x, hit.point.y, hit.point.z)); + } + else + WriteLog("SELFTEST gauge " + name + ": no such track"); + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} diff --git a/application/editormodegauge.h b/application/editormodegauge.h new file mode 100644 index 00000000..7e39d8d1 --- /dev/null +++ b/application/editormodegauge.h @@ -0,0 +1,88 @@ +/* +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/editortool.h" +#include "editor/editorGauge.hpp" + +#include +#include +#include + +class gauge_tool final : public editor_tool +{ + public: + // model entering the structure gauge, at its deepest place + struct gauge_hit + { + std::string model; + glm::dvec3 point{0.0}; + double depth{0.0}; // m + TTrack const *track{nullptr}; // nearest one, may be gone by now + int path{0}; + glm::dvec2 direction{0.0, 1.0}; // of the track there, in the plan + }; + explicit gauge_tool(editor_services &Services) : m_services{Services} {} + void update() override; + void render_windows() override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + bool is_open() const { return m_gauge.open; } + void toggle() { m_gauge.open = !m_gauge.open; } + + private: + editor_services &m_services; + // structure gauge along the line of the selected path, shown in the 3d view + struct gauge_state + { + bool open{false}; // window + bool enabled{true}; // tunnel along the selected path + std::vector profiles; + int profile{0}; // railway + int road{-1}; + TTrack const *track{nullptr}; // scanned + std::size_t history{0}; + bool pending{false}; // rescan once the edit ends + struct + { + std::vector surface; // triangles of the tunnel + std::vector edges; // lines + std::vector hits; + } line; // along the selected path + struct + { + bool scanned{false}; + std::size_t history{0}; + std::string profile; + std::vector hits; + } map; // whole scenery + struct + { + std::vector surface; + std::vector edges; + } spot; // tunnel around the hit the camera went to + int current{-1}; // hit the camera went to + bool published{false}; // overlay up to date + std::shared_ptr scan; // of the whole scenery, done in slices over the frames + } m_gauge; + void gauge_load(); + // the chosen railway and road outlines + gauge::choice gauge_choice() const; + std::vector const &gauge_hits() const; + void scan_gauge(TTrack &Track); + void scan_gauge_map(); + void step_gauge_map(); + void finish_gauge_map(); + void gauge_publish(); + void gauge_focus(int Index); + void gauge_spot(gauge_hit const &Hit); + void render_gauge_hits(); + bool render_gauge_choice(char const *Label, int &Index, bool Road); + bool render_gauge_outline(int Index); +}; diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 2a7909bd..dc810c1d 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -54,6 +54,9 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c for (auto const group : {&editor_mode::selftest_scene, &editor_mode::selftest_sleepers, &editor_mode::selftest_sweep, &editor_mode::selftest_track, &editor_mode::selftest_switch, &editor_mode::selftest_rails, &editor_mode::selftest_join, &editor_mode::selftest_signal, &editor_mode::selftest_terrain, &editor_mode::selftest_props, &editor_mode::selftest_monkey}) if (auto const result{(this->*group)(command, words)}; result != selftest_result::unhandled) return result; + for (auto *tool : m_tools) + if (auto const result{tool->selftest(command, words)}; result != selftest_result::unhandled) + return result; return selftest_result::unhandled; } diff --git a/application/editormodeselftestsweep.cpp b/application/editormodeselftestsweep.cpp index 03ba0e21..814562f3 100644 --- a/application/editormodeselftestsweep.cpp +++ b/application/editormodeselftestsweep.cpp @@ -396,21 +396,6 @@ editor_mode::selftest_result editor_mode::selftest_sweep(std::string const &comm ui()->expand_bend(); return selftest_result::handled; } - if (command == "gauge") - { - std::string name; - words >> name; - if (auto *track{simulation::Paths.find(name)}) - { - scan_gauge(*track); - WriteLog(format("SELFTEST gauge %s: %d hits", name.c_str(), static_cast(m_gauge.line.hits.size()))); - for (auto const &hit : m_gauge.line.hits) - WriteLog(format("SELFTEST gauge hit %s depth %.3f at %.2f %.2f %.2f", hit.model.c_str(), hit.depth, hit.point.x, hit.point.y, hit.point.z)); - } - else - WriteLog("SELFTEST gauge " + name + ": no such track"); - return selftest_result::handled; - } if (command == "ttincludes") { std::string name; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 73ade052..b84d6bf8 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -52,6 +52,21 @@ TTrack *editor_mode::selected_track() const return dynamic_cast(m_node); } +bool editor_mode::track_editing() const +{ + return m_track_gizmo_using || m_dragging; +} + +std::size_t editor_mode::history_size() const +{ + return m_history.size(); +} + +glm::dvec3 editor_mode::camera_position() const +{ + return Camera.Pos; +} + void editor_mode::select_track(scene::basic_node *Node) { auto *track = dynamic_cast(Node); diff --git a/application/editormodeviewport.cpp b/application/editormodeviewport.cpp index efa81240..7ba759f9 100644 --- a/application/editormodeviewport.cpp +++ b/application/editormodeviewport.cpp @@ -178,7 +178,7 @@ void editor_mode::work_area_tools(std::vector &Buttons) add(track_tab::turnout, false); add(track_tab::parallel, false); auto const advanced{m_track_tab == track_tab::turntable || m_track_tab == track_tab::signals || m_track_tab == track_tab::lineside || m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || - m_track_tab == track_tab::joints || m_gauge.open}; + m_track_tab == track_tab::joints || m_gauge.is_open()}; auto const more{EditorSettings.track_tools_more()}; Buttons.push_back({icon::more, more ? STR_C("Less") : STR_C("More"), nullptr, STR_C("Turntable, signals, objects, trainset, profile, speed, joints, gauge"), more, [more]() { EditorSettings.track_tools_more(false == more); @@ -197,8 +197,8 @@ void editor_mode::work_area_tools(std::vector &Buttons) add(track_tab::profile, true); add(track_tab::speed, false); add(track_tab::joints, false); - Buttons.push_back({icon::gauge, STR_C("Structure gauge"), nullptr, STR_C("Checks which models enter the structure gauge of the tracks and the clearance over the roads (skrajnia budowli)"), m_gauge.open, - [this]() { m_gauge.open = !m_gauge.open; }}); + Buttons.push_back({icon::gauge, STR_C("Structure gauge"), nullptr, STR_C("Checks which models enter the structure gauge of the tracks and the clearance over the roads (skrajnia budowli)"), m_gauge.is_open(), + [this]() { m_gauge.toggle(); }}); Buttons.back().accent = family_colour(2); break; } diff --git a/application/editortool.h b/application/editortool.h new file mode 100644 index 00000000..1ddfc50f --- /dev/null +++ b/application/editortool.h @@ -0,0 +1,52 @@ +/* +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 + +class TTrack; +namespace scene +{ +class basic_node; +} + +enum class editor_selftest_result +{ + unhandled, + handled, + pause +}; + +class editor_services +{ + public: + virtual TTrack *selected_track() const = 0; + virtual bool track_editing() const = 0; + virtual std::size_t history_size() const = 0; + virtual glm::dvec3 camera_position() const = 0; + virtual void fly_to(glm::dvec3 const &Target, glm::dvec3 const &Look, double const Distance, double const Duration = 0.6) = 0; + + protected: + ~editor_services() = default; +}; + +class editor_tool +{ + public: + virtual ~editor_tool() = default; + virtual void update() {} + virtual void render_windows() {} + virtual void forget(scene::basic_node const *Node) {} + virtual editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) { return editor_selftest_result::unhandled; } +}; From 0c2880f48f84393f6ff0315b5a4e5a50d2289115 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 09:10:27 +0200 Subject: [PATCH 006/145] refactor(editor): the speed check is a tool of its own The second tool out of editor_mode. Tools get two more hooks, the overlay drawn over the view and the options in the tool window, and the editor gives them the track it focuses on, the node it makes current, the norms of the route design and the snapshot of the tracks for the undo. The check gets a self-test command, speedcheck , which runs it through and logs the paths found too fast for their geometry. --- application/editormode.cpp | 4 +- application/editormode.h | 36 ++++-------------- application/editormodespeed.cpp | 61 ++++++++++++++++++++++--------- application/editormodespeed.h | 53 +++++++++++++++++++++++++++ application/editormodetrack.cpp | 15 +++++++- application/editormodetrackux.cpp | 3 +- application/editortool.h | 10 +++++ 7 files changed, 130 insertions(+), 52 deletions(-) create mode 100644 application/editormodespeed.h diff --git a/application/editormode.cpp b/application/editormode.cpp index 9cd2df06..dccc7b20 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1466,13 +1466,13 @@ bool editor_mode::update() draw_track_spread(); draw_build_overlay(); draw_track_intent(); - speed_step(); + m_speed.update(); joints_step(); draw_track_hints(); draw_empty_scenery_hint(); draw_profile_overlay(); draw_infra_overlay(); - draw_speed_overlay(); + m_speed.draw_overlay(); draw_joints_overlay(); } else diff --git a/application/editormode.h b/application/editormode.h index 7b0b0ade..2651ab36 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "input/editorkeyboardinput.h" #include "vehicle/Camera.h" #include "application/editormodegauge.h" +#include "application/editormodespeed.h" #include "application/editortool.h" #include "scene/sceneeditor.h" #include "scene/scenenode.h" @@ -745,7 +746,9 @@ class editor_mode : public application_mode, private editor_track::observer, pri void render_track_gizmo(); void commit_track_drag(bool const Force); void cancel_track_tools(); - void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}); + void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}) override; + void set_current_node(scene::basic_node *Node) override; + alignment::limits const &route_norms() const override; // false, with the reason, when one of the paths can't be changed, its file not taking the changes back static bool tracks_editable(std::vector const &Tracks, std::string &Reason); void trim_history(); @@ -857,8 +860,6 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::string m_track_events_missing; void render_path_events(TTrack &Track); std::string m_track_mode_notice; - gauge_tool m_gauge{*this}; - std::vector m_tools{&m_gauge}; struct route_design { TTrack *from{nullptr}; @@ -1619,7 +1620,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri 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); + void focus_track(TTrack &Track, int const Path, glm::dvec3 const &Point) override; // the camera flies over Duration seconds to the place Distance back from the target along Look, looking along it void fly_to(glm::dvec3 const &Target, glm::dvec3 const &Look, double const Distance, double const Duration = 0.6) override; // paths found by their names @@ -2458,30 +2459,9 @@ class editor_mode : public application_mode, private editor_track::observer, pri void infra_attach(EditorSnapshot &Snapshot); void render_infra_body(); - // speed limits of the paths checked against what their geometry allows - struct speed_state - { - bool open{false}; - int scope{0}; // 0: the line through the selected path, 1: the whole scenery - speed_check::options options; - std::vector> queue; - std::size_t next{0}; - std::vector results; - bool show_all{false}; - int limits{0}; // 0: all the paths, 1: only those with a speed limit set, 2: only those without - int selected{-1}; - std::size_t history{0}; // size of the undo history at the check - std::string status; - }; - speed_state m_speed; - void render_speed_body(); - void speed_start(); - void speed_step(); - void speed_focus(int const Index); - // with Branches the limits of the switch branches count too, otherwise they're only warnings - void speed_apply(std::vector const &Indices, bool const Branches); - std::vector speed_listed() const; - void draw_speed_overlay() const; + gauge_tool m_gauge{*this}; + speed_tool m_speed{*this}; + std::vector m_tools{&m_gauge, &m_speed}; // joints of the paths: ends which almost meet, steps, kinks, jumps of the cant and of the grade struct joints_state diff --git a/application/editormodespeed.cpp b/application/editormodespeed.cpp index 5eb8ea44..583c489f 100644 --- a/application/editormodespeed.cpp +++ b/application/editormodespeed.cpp @@ -8,8 +8,7 @@ http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" -#include "application/editormode.h" -#include "application/editoruilayer.h" +#include "application/editormodespeed.h" #include "application/editorprojection.h" #include "application/editormodeshared.h" #include "editor/editorFormat.hpp" @@ -44,7 +43,7 @@ std::string speed_text(double const Speed) } // namespace -void editor_mode::speed_start() +void speed_tool::speed_start() { auto &state{m_speed}; state.queue.clear(); @@ -52,14 +51,14 @@ void editor_mode::speed_start() state.results.clear(); state.selected = -1; state.status.clear(); - state.options.norms = m_route.design.norms; + state.options.norms = m_services.route_norms(); auto const add = [&](TTrack *Track) { for (int i = 0; i < static_cast(Track->m_paths.size()); ++i) state.queue.emplace_back(Track, i); }; if (state.scope == 0) { - auto *track{selected_track()}; + auto *track{m_services.selected_track()}; if (track == nullptr || false == rail(*track)) { state.status = STR_C("Select a track first"); @@ -88,10 +87,10 @@ void editor_mode::speed_start() if (track != nullptr && rail(*track)) add(track); } - state.history = m_history.size(); + state.history = m_services.history_size(); } -void editor_mode::speed_step() +void speed_tool::update() { auto &state{m_speed}; if (state.next >= state.queue.size()) @@ -122,7 +121,7 @@ void editor_mode::speed_step() WriteLog("Editor: speed check - " + state.status, logtype::generic); } -std::vector editor_mode::speed_listed() const +std::vector speed_tool::speed_listed() const { std::vector listed; for (int i = 0; i < static_cast(m_speed.results.size()); ++i) @@ -136,7 +135,7 @@ std::vector editor_mode::speed_listed() const return listed; } -void editor_mode::speed_focus(int const Index) +void speed_tool::speed_focus(int const Index) { auto &state{m_speed}; if (Index < 0 || Index >= static_cast(state.results.size())) @@ -146,12 +145,11 @@ void editor_mode::speed_focus(int const Index) auto const &paths{simulation::Paths.sequence()}; if (std::find(paths.begin(), paths.end(), verdict.track) == paths.end() || verdict.track->m_editorremoved) return; - m_node = verdict.track; - ui()->set_node(m_node); - focus_track(*verdict.track, verdict.path, verdict.position); + m_services.set_current_node(verdict.track); + m_services.focus_track(*verdict.track, verdict.path, verdict.position); } -void editor_mode::speed_apply(std::vector const &Indices, bool const Branches) +void speed_tool::speed_apply(std::vector const &Indices, bool const Branches) { auto &state{m_speed}; struct change @@ -191,7 +189,7 @@ void editor_mode::speed_apply(std::vector const &Indices, bool const Branch state.status = STR_C("Nothing changed") + skipped; return; } - push_track_snapshot(std::move(states)); + m_services.push_track_snapshot(std::move(states)); for (auto const &entry : changes) { auto const velocity{entry.track->eType == tt_Switch ? speed_check::switch_velocity(entry.limits[0], entry.limits[1]) : entry.limits[0]}; @@ -200,12 +198,12 @@ void editor_mode::speed_apply(std::vector const &Indices, bool const Branch } for (auto &verdict : state.results) verdict.set = speed_check::path_velocity(*verdict.track, verdict.path); - state.history = m_history.size(); + state.history = m_services.history_size(); state.status = format(STR_C("Speed limits of %zu paths lowered to the safe ones, Ctrl+Z brings them back"), changes.size()) + skipped; WriteLog("Editor: speed check - " + state.status, logtype::generic); } -void editor_mode::render_speed_body() +void speed_tool::render_options() { auto &state{m_speed}; ImGui::PushTextWrapPos(0.0f); @@ -264,7 +262,7 @@ void editor_mode::render_speed_body() ImGui::TextWrapped("%s", state.status.c_str()); if (state.results.empty()) return; - if (state.history != m_history.size()) + if (state.history != m_services.history_size()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("The scenery changed since the check")); auto const *const within{STR_C("Within their limits too")}; auto const checkbox{ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x + ImGui::CalcTextSize(within).x}; @@ -357,7 +355,7 @@ void editor_mode::render_speed_body() } } -void editor_mode::draw_speed_overlay() const +void speed_tool::draw_overlay() const { auto const &state{m_speed}; if (false == state.open || state.results.empty()) @@ -380,3 +378,30 @@ void editor_mode::draw_speed_overlay() const drawlist->AddText(ImVec2(screen.x + 14.0f, screen.y - 7.0f), colour, text.c_str()); } } + +void speed_tool::check_line() +{ + m_speed.scope = 0; + speed_start(); +} + +editor_selftest_result speed_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + if (Command == "speedcheck") + { + int scope{0}; + Words >> scope; + m_speed.scope = scope; + speed_start(); + while (m_speed.next < m_speed.queue.size()) + update(); + WriteLog("SELFTEST speedcheck: " + m_speed.status); + for (auto const index : speed_listed()) + { + auto const &verdict{m_speed.results[index]}; + WriteLog(format("SELFTEST speedcheck %s path %d: set %.0f safe %.0f", verdict.track->name().c_str(), verdict.path, verdict.set, verdict.safe)); + } + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} diff --git a/application/editormodespeed.h b/application/editormodespeed.h new file mode 100644 index 00000000..a477f59a --- /dev/null +++ b/application/editormodespeed.h @@ -0,0 +1,53 @@ +/* +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/editortool.h" +#include "editor/editorSpeed.hpp" + +#include +#include +#include + +class speed_tool final : public editor_tool +{ + public: + explicit speed_tool(editor_services &Services) : m_services{Services} {} + void update() override; + void draw_overlay() const override; + void render_options() override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + void set_open(bool const Open) { m_speed.open = Open; } + void check_line(); + + private: + editor_services &m_services; + // speed limits of the paths checked against what their geometry allows + struct speed_state + { + bool open{false}; + int scope{0}; // 0: the line through the selected path, 1: the whole scenery + speed_check::options options; + std::vector> queue; + std::size_t next{0}; + std::vector results; + bool show_all{false}; + int limits{0}; // 0: all the paths, 1: only those with a speed limit set, 2: only those without + int selected{-1}; + std::size_t history{0}; // size of the undo history at the check + std::string status; + }; + speed_state m_speed; + void speed_start(); + void speed_focus(int const Index); + // with Branches the limits of the switch branches count too, otherwise they're only warnings + void speed_apply(std::vector const &Indices, bool const Branches); + std::vector speed_listed() const; +}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index b84d6bf8..f00f060f 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -67,6 +67,17 @@ glm::dvec3 editor_mode::camera_position() const return Camera.Pos; } +void editor_mode::set_current_node(scene::basic_node *Node) +{ + m_node = Node; + ui()->set_node(m_node); +} + +alignment::limits const &editor_mode::route_norms() const +{ + return m_route.design.norms; +} + void editor_mode::select_track(scene::basic_node *Node) { auto *track = dynamic_cast(Node); @@ -514,7 +525,7 @@ void editor_mode::render_track_inspector() { auto const &io{ImGui::GetIO()}; 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_speed.set_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) @@ -547,7 +558,7 @@ void editor_mode::render_track_inspector() { switch (m_track_tab) { - case track_tab::speed: render_speed_body(); break; + case track_tab::speed: m_speed.render_options(); break; case track_tab::joints: render_joints_body(); break; default: render_infra_body(); break; } diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 9518c5ca..9c551577 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -814,9 +814,8 @@ void editor_mode::render_track_context() } if (ImGui::MenuItem(STR_C("Speed check of the line"), "V")) { - m_speed.scope = 0; show_track_tab(track_tab::speed); - speed_start(); + m_speed.check_line(); } ImGui::Separator(); if (ImGui::MenuItem(track_selected(track) ? STR_C("Take out of the selection") : STR_C("Add to the selection"), STR_C("Shift+LMB"))) diff --git a/application/editortool.h b/application/editortool.h index 1ddfc50f..2e3eb275 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -12,9 +12,13 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include +#include #include +#include "editor/editorTrack.hpp" + class TTrack; namespace scene { @@ -36,6 +40,10 @@ class editor_services virtual std::size_t history_size() const = 0; virtual glm::dvec3 camera_position() const = 0; virtual void fly_to(glm::dvec3 const &Target, glm::dvec3 const &Look, double const Distance, double const Duration = 0.6) = 0; + virtual void focus_track(TTrack &Track, int const Path, glm::dvec3 const &Point) = 0; + virtual void set_current_node(scene::basic_node *Node) = 0; + virtual alignment::limits const &route_norms() const = 0; + virtual void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}) = 0; protected: ~editor_services() = default; @@ -46,6 +54,8 @@ class editor_tool public: virtual ~editor_tool() = default; virtual void update() {} + virtual void draw_overlay() const {} + virtual void render_options() {} virtual void render_windows() {} virtual void forget(scene::basic_node const *Node) {} virtual editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) { return editor_selftest_result::unhandled; } From 8ba8345faf45ec9327f5e1a292fcbaca4ac57363 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 09:23:36 +0200 Subject: [PATCH 007/145] refactor(editor): the joints check is a tool of its own The third tool out of editor_mode. It reaches the terrain model and the places of the tracks on it through the services, the place of a track for the terrain model is a type of its own now, and the laying and the switches ask the tool for the kink it allows. A self-test command can ask to be run again in the next frame, as the check against the terrain model does while the model comes in. --- application/editormode.cpp | 4 +- application/editormode.h | 77 +++------------- application/editormodejoints.cpp | 117 +++++++++++++++++++------ application/editormodejoints.h | 82 +++++++++++++++++ application/editormodelay.cpp | 2 +- application/editormodeselftest.cpp | 6 +- application/editormodeselftestjoin.cpp | 58 ------------ application/editormodeswitchplace.cpp | 4 +- application/editormodetrack.cpp | 24 ++++- application/editortool.h | 21 ++++- 10 files changed, 232 insertions(+), 163 deletions(-) create mode 100644 application/editormodejoints.h diff --git a/application/editormode.cpp b/application/editormode.cpp index dccc7b20..19559df6 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1467,13 +1467,13 @@ bool editor_mode::update() draw_build_overlay(); draw_track_intent(); m_speed.update(); - joints_step(); + m_joints.update(); draw_track_hints(); draw_empty_scenery_hint(); draw_profile_overlay(); draw_infra_overlay(); m_speed.draw_overlay(); - draw_joints_overlay(); + m_joints.draw_overlay(); } else { diff --git a/application/editormode.h b/application/editormode.h index 2651ab36..3d5c3140 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "input/editorkeyboardinput.h" #include "vehicle/Camera.h" #include "application/editormodegauge.h" +#include "application/editormodejoints.h" #include "application/editormodespeed.h" #include "application/editortool.h" #include "scene/sceneeditor.h" @@ -65,19 +66,6 @@ class editor_mode : public application_mode, private editor_track::observer, pri { public: - // joint of the paths found wrong by the joints check - struct joint_issue - { - enum class kind { gap, step, kink, cant, grade, model, open }; - kind type{kind::gap}; - TTrack *track{nullptr}; - editor_track::point_ref point; - TTrack *other{nullptr}; - editor_track::point_ref theirs; - glm::dvec3 position{0.0}; - double value{0.0}; // m, deg, mm, per mille; m of the track over the terrain model - bool fixed{false}; - }; // constructors editor_mode(); ~editor_mode() override; @@ -318,16 +306,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool add_chunk_from_ground(heightmap::chunk_key const &Key, float const Spacing, std::uint16_t const Layer = 0); // the terrain from the digital terrain model of GUGiK (editormodenmt.cpp) - struct nmt_place - { - glm::dvec3 position{0.0}; // on the axis of the path - double formation{0.0}; // the height of the formation under it, the foot of the ballast - double crown{0.0}; // the height of the top of the ballast under the rails: what the terrain model measures on a track - double half_width{3.0}; // from the axis to the foot of the ballast - TTrack *track{nullptr}; - int path{0}; - bool shapes{true}; // not a bridge nor a tunnel - }; + using nmt_place = ::nmt_place; struct nmt_state { double offset{0.0}; // added to the heights of the model, so they meet the tracks of the scenery @@ -383,8 +362,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri }; editor_nmt m_nmt; nmt_state m_nmt_state; - std::vector nmt_track_places(double const Step) const; - void nmt_locate(); + std::vector nmt_track_places(double const Step) const override; + void nmt_locate() override; bool nmt_choice_card(int const Index, char const *Title, char const *Text, float const Width, float const TextHeight, bool const Chosen) const; void nmt_fit(); void nmt_finish_fit(); @@ -749,6 +728,10 @@ class editor_mode : public application_mode, private editor_track::observer, pri void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}) override; void set_current_node(scene::basic_node *Node) override; alignment::limits const &route_norms() const override; + void clear_track_point() override; + editor_nmt &terrain_model() override; + double terrain_model_offset() const override; + bool terrain_model_fitted() const override; // false, with the reason, when one of the paths can't be changed, its file not taking the changes back static bool tracks_editable(std::vector const &Tracks, std::string &Reason); void trim_history(); @@ -2461,49 +2444,9 @@ class editor_mode : public application_mode, private editor_track::observer, pri gauge_tool m_gauge{*this}; speed_tool m_speed{*this}; - std::vector m_tools{&m_gauge, &m_speed}; + joints_tool m_joints{*this}; + std::vector m_tools{&m_gauge, &m_speed, &m_joints}; - // 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 - double model{0.5}; // m, the track over or under the terrain model (NMT) - bool free_ends{false}; - bool model_check{true}; - std::vector queue; - std::size_t next{0}; - // the places of the tracks compared with the terrain model, in runs of one path each, once the model under them is in - std::vector places; - std::vector> runs; - std::size_t run_next{0}; - bool model_waiting{false}; - bool model_asked{false}; - double model_polled{0.0}; - bool model_unplaced{false}; - std::vector issues; - int filter{-1}; - int selected{-1}; - std::size_t history{0}; - std::string status; - }; - joints_state m_joints; - void joints_start(); - void joints_step(); - void joints_check(TTrack &Track); - // the paths lying off the terrain model by more than the threshold. returns: false while the model under them is coming - bool joints_model(); - void joints_focus(int const Index); - bool joint_fix(joint_issue &Issue, std::vector> &States, std::vector &Changed); - void joints_fix(std::vector const &Indices); - void render_joints_body(); - void draw_joints_overlay() const; void infra_rebase(); void render_infra_search(); void render_infra_candidates(); diff --git a/application/editormodejoints.cpp b/application/editormodejoints.cpp index 856623cc..5177de0e 100644 --- a/application/editormodejoints.cpp +++ b/application/editormodejoints.cpp @@ -8,9 +8,9 @@ http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" -#include "application/editormode.h" +#include "application/editormodejoints.h" +#include "editor/editorNmt.hpp" #include "application/editormodeshared.h" -#include "application/editoruilayer.h" #include "application/editorprojection.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" @@ -46,7 +46,7 @@ std::size_t const kModelRun{200}; // places of a path whose model is asked for a double const kModelPoll{0.25}; // s between the looks whether the model is in double const kUnfitted{30.0}; // m; a scenery whose tracks lie this far from the model as a whole isn't fitted to it -using kind = editor_mode::joint_issue::kind; +using kind = joints_tool::joint_issue::kind; bool rail(TTrack const &Track) { @@ -109,7 +109,7 @@ char const *kind_name(kind const Kind) } } -std::string value_text(editor_mode::joint_issue const &Issue) +std::string value_text(joints_tool::joint_issue const &Issue) { switch (Issue.type) { @@ -146,7 +146,7 @@ std::string name_of(TTrack const *Track) } // namespace -void editor_mode::joints_start() +void joints_tool::joints_start() { auto &state{m_joints}; state.queue.clear(); @@ -164,7 +164,7 @@ void editor_mode::joints_start() continue; state.queue.push_back(track); } - state.history = m_history.size(); + state.history = m_services.history_size(); state.places.clear(); state.runs.clear(); state.run_next = 0; @@ -174,14 +174,14 @@ void editor_mode::joints_start() state.model_unplaced = false; if (false == state.model_check || state.queue.empty()) return; - nmt_locate(); - if (false == m_nmt.placed()) + m_services.nmt_locate(); + if (false == m_services.terrain_model().placed()) { state.model_unplaced = true; return; } std::set const queued(state.queue.begin(), state.queue.end()); - for (auto const &place : nmt_track_places(kModelStep)) + for (auto const &place : m_services.nmt_track_places(kModelStep)) if (place.shapes && queued.count(place.track) > 0) { if (state.places.empty() || state.places.back().track != place.track || state.places.back().path != place.path || state.places.size() - state.runs.back().first >= kModelRun) @@ -192,7 +192,7 @@ void editor_mode::joints_start() state.model_waiting = false == state.places.empty(); } -bool editor_mode::joints_model() +bool joints_tool::joints_model() { auto &state{m_joints}; auto const now{ImGui::GetTime()}; @@ -216,27 +216,27 @@ bool editor_mode::joints_model() for (auto const &run : state.runs) { box(run, low, high); - m_nmt.request(low, high); + m_services.terrain_model().request(low, high); } state.model_asked = true; } while (state.run_next < state.runs.size()) { box(state.runs[state.run_next], low, high); - if (false == m_nmt.request(low, high)) + if (false == m_services.terrain_model().request(low, high)) return false; ++state.run_next; } std::vector differences(state.places.size(), std::numeric_limits::quiet_NaN()); std::vector known; for (std::size_t i = 0; i < state.places.size(); ++i) - if (double height; m_nmt.height(state.places[i].position.x, state.places[i].position.z, height)) + if (double height; m_services.terrain_model().height(state.places[i].position.x, state.places[i].position.z, height)) { - differences[i] = state.places[i].crown - (height + m_nmt_state.offset); + differences[i] = state.places[i].crown - (height + m_services.terrain_model_offset()); known.push_back(differences[i]); } auto shift{0.0}; - if (false == m_nmt_state.fitted && false == known.empty()) + if (false == m_services.terrain_model_fitted() && false == known.empty()) { std::nth_element(known.begin(), known.begin() + known.size() / 2, known.end()); if (std::abs(known[known.size() / 2]) > kUnfitted) @@ -275,7 +275,7 @@ bool editor_mode::joints_model() return true; } -void editor_mode::joints_check(TTrack &Track) +void joints_tool::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; }; @@ -366,7 +366,7 @@ void editor_mode::joints_check(TTrack &Track) } } -void editor_mode::joints_step() +void joints_tool::update() { auto &state{m_joints}; if (state.next >= state.queue.size() && false == state.model_waiting) @@ -401,7 +401,7 @@ void editor_mode::joints_step() WriteLog("Editor: joints check - " + state.status, logtype::generic); } -void editor_mode::joints_focus(int const Index) +void joints_tool::joints_focus(int const Index) { auto &state{m_joints}; if (Index < 0 || Index >= static_cast(state.issues.size())) @@ -411,13 +411,12 @@ void editor_mode::joints_focus(int const 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); + m_services.set_current_node(issue.track); + m_services.clear_track_point(); + m_services.focus_track(*issue.track, issue.point.path, issue.position); } -bool editor_mode::joint_fix(joint_issue &Issue, std::vector> &States, std::vector &Changed) +bool joints_tool::joint_fix(joint_issue &Issue, std::vector> &States, std::vector &Changed) { if (Issue.fixed || Issue.type == kind::open || Issue.type == kind::model || Issue.track == nullptr || Issue.other == nullptr || Issue.track->m_editorremoved || Issue.other->m_editorremoved) return false; @@ -481,7 +480,7 @@ bool editor_mode::joint_fix(joint_issue &Issue, std::vector const &Indices) +void joints_tool::joints_fix(std::vector const &Indices) { auto &state{m_joints}; std::vector> states; @@ -499,16 +498,16 @@ void editor_mode::joints_fix(std::vector const &Indices) 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)); + m_services.push_track_snapshot(std::move(states)); editor_track::commit(changed); for (auto *track : changed) track->mark_dirty(); - state.history = m_history.size(); + state.history = m_services.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() +void joints_tool::render_options() { auto &state{m_joints}; ImGui::PushTextWrapPos(0.0f); @@ -581,7 +580,7 @@ void editor_mode::render_joints_body() ImGui::TextWrapped("%s", state.status.c_str()); if (state.issues.empty()) return; - if (state.history != m_history.size()) + if (state.history != m_services.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); @@ -649,7 +648,7 @@ void editor_mode::render_joints_body() } } -void editor_mode::draw_joints_overlay() const +void joints_tool::draw_overlay() const { auto const &state{m_joints}; if (false == state.open || state.issues.empty()) @@ -671,3 +670,63 @@ void editor_mode::draw_joints_overlay() const drawlist->AddText(ImVec2(screen.x + 13.0f, screen.y - 7.0f), colour, text.c_str()); } } + +editor_selftest_result joints_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + if (Command == "jointsmodel") + { + // the whole scenery against the terrain model, as the check of the panel does it, waiting for the model + static bool started{false}; + if (false == started) + { + double threshold{0.5}; + Words >> threshold; + m_joints.scope = 1; + m_joints.model_check = true; + m_joints.model = threshold; + joints_start(); + started = true; + } + update(); + if (m_joints.next < m_joints.queue.size() || m_joints.model_waiting) + return editor_selftest_result::again; + started = false; + std::size_t count{0}; + for (auto const &issue : m_joints.issues) + if (issue.type == joint_issue::kind::model && count++ < 40) + WriteLog(format("SELFTEST jointsmodel: %+.2f m at %.1f %.1f, %s path %d", issue.value, issue.position.x, issue.position.z, issue.track != nullptr ? issue.track->name().c_str() : "-", issue.point.path)); + WriteLog("SELFTEST jointsmodel: " + m_joints.status); + return editor_selftest_result::handled; + } + if (Command == "joints") + { + std::vector points; + for (glm::dvec3 point{0.0}; Words >> point.x >> point.z;) + points.push_back(point); + auto const scope{m_joints.scope}; + auto const model{m_joints.model_check}; + m_joints.scope = 1; + m_joints.model_check = false; + joints_start(); + while (m_joints.next < m_joints.queue.size()) + update(); + m_joints.scope = scope; + m_joints.model_check = model; + char const *const kinds[]{"gap", "step", "kink", "cant jump", "grade break", "off the terrain model", "free end"}; + int around{0}; + for (auto const &issue : m_joints.issues) + { + if (false == points.empty() && issue.type == joint_issue::kind::open) + continue; + if (false == points.empty() && std::none_of(points.begin(), points.end(), [&](glm::dvec3 const &Point) { return geometry::plan_distance(Point, issue.position) < 5.0; })) + continue; + ++around; + auto const name = [](TTrack const *Track) { return Track == nullptr ? std::string{"-"} : Track->name() + (Track->eType == tt_Switch ? "(switch)" : ""); }; + auto const rise{issue.other != nullptr ? editor_track::point_position(*issue.other, issue.theirs).y - issue.position.y : 0.0}; + WriteLog(format("SELFTEST joints: %s %.3f at %.1f %.1f, %s / %s, rise %.3f", kinds[static_cast(issue.type)], issue.value, issue.position.x, issue.position.z, name(issue.track).c_str(), name(issue.other).c_str(), rise)); + } + WriteLog(format("SELFTEST joints: %d issues around the points, %zu in all", around, m_joints.issues.size())); + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} diff --git a/application/editormodejoints.h b/application/editormodejoints.h new file mode 100644 index 00000000..34dc5cbe --- /dev/null +++ b/application/editormodejoints.h @@ -0,0 +1,82 @@ +/* +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/editortool.h" + +#include +#include +#include + +class joints_tool final : public editor_tool +{ + public: + // joint of the paths found wrong by the joints check + struct joint_issue + { + enum class kind { gap, step, kink, cant, grade, model, open }; + kind type{kind::gap}; + TTrack *track{nullptr}; + editor_track::point_ref point; + TTrack *other{nullptr}; + editor_track::point_ref theirs; + glm::dvec3 position{0.0}; + double value{0.0}; // m, deg, mm, per mille; m of the track over the terrain model + bool fixed{false}; + }; + explicit joints_tool(editor_services &Services) : m_services{Services} {} + void update() override; + void draw_overlay() const override; + void render_options() override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + void set_open(bool const Open) { m_joints.open = Open; } + double kink_angle() const { return m_joints.angle; } + + private: + editor_services &m_services; + // 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 + double model{0.5}; // m, the track over or under the terrain model (NMT) + bool free_ends{false}; + bool model_check{true}; + std::vector queue; + std::size_t next{0}; + // the places of the tracks compared with the terrain model, in runs of one path each, once the model under them is in + std::vector places; + std::vector> runs; + std::size_t run_next{0}; + bool model_waiting{false}; + bool model_asked{false}; + double model_polled{0.0}; + bool model_unplaced{false}; + std::vector issues; + int filter{-1}; + int selected{-1}; + std::size_t history{0}; + std::string status; + }; + joints_state m_joints; + void joints_start(); + void joints_check(TTrack &Track); + // the paths lying off the terrain model by more than the threshold. returns: false while the model under them is coming + bool joints_model(); + void joints_focus(int const Index); + bool joint_fix(joint_issue &Issue, std::vector> &States, std::vector &Changed); + void joints_fix(std::vector const &Indices); +}; diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index d66b7608..f6cbb46b 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -470,7 +470,7 @@ std::vector editor_mode::join_pieces(TTrack const *From, editor_tr request.variant = m_join_variant; request.transitions = m_lay.transitions; request.speed = design_speed(From, End.track); - request.kink = m_joints.angle * 0.5; + request.kink = m_joints.kink_angle() * 0.5; request.shape = m_route.design.shape; request.norms = laying_norms(); auto result{alignment::join(request)}; diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index dc810c1d..731c0b11 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -188,8 +188,12 @@ void editor_mode::selftest_step() words >> test.wait; return; } - if (selftest_dispatch(command, words) == selftest_result::pause) + if (auto const result{selftest_dispatch(command, words)}; result == selftest_result::pause || result == selftest_result::again) + { + if (result == selftest_result::again) + --test.next; return; + } if (command == "quit") { if (false == test.quitting) diff --git a/application/editormodeselftestjoin.cpp b/application/editormodeselftestjoin.cpp index 17f1791d..f5ac36d8 100644 --- a/application/editormodeselftestjoin.cpp +++ b/application/editormodeselftestjoin.cpp @@ -236,64 +236,6 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma WriteLog(format("SELFTEST layclicks %d paths, ballast foot over the ground up to %.3f m, under it up to %.3f m", paths, hanging, buried)); return selftest_result::handled; } - if (command == "jointsmodel") - { - // the whole scenery against the terrain model, as the check of the panel does it, waiting for the model - static bool started{false}; - if (false == started) - { - double threshold{0.5}; - words >> threshold; - m_joints.scope = 1; - m_joints.model_check = true; - m_joints.model = threshold; - joints_start(); - started = true; - } - joints_step(); - if (m_joints.next < m_joints.queue.size() || m_joints.model_waiting) - { - --m_selftest.next; - return selftest_result::pause; - } - started = false; - std::size_t count{0}; - for (auto const &issue : m_joints.issues) - if (issue.type == joint_issue::kind::model && count++ < 40) - WriteLog(format("SELFTEST jointsmodel: %+.2f m at %.1f %.1f, %s path %d", issue.value, issue.position.x, issue.position.z, issue.track != nullptr ? issue.track->name().c_str() : "-", issue.point.path)); - WriteLog("SELFTEST jointsmodel: " + m_joints.status); - return selftest_result::handled; - } - if (command == "joints") - { - std::vector points; - for (glm::dvec3 point{0.0}; words >> point.x >> point.z;) - points.push_back(point); - auto const scope{m_joints.scope}; - auto const model{m_joints.model_check}; - m_joints.scope = 1; - m_joints.model_check = false; - joints_start(); - while (m_joints.next < m_joints.queue.size()) - joints_step(); - m_joints.scope = scope; - m_joints.model_check = model; - char const *const kinds[]{"gap", "step", "kink", "cant jump", "grade break", "off the terrain model", "free end"}; - int around{0}; - for (auto const &issue : m_joints.issues) - { - if (false == points.empty() && issue.type == joint_issue::kind::open) - continue; - if (false == points.empty() && std::none_of(points.begin(), points.end(), [&](glm::dvec3 const &Point) { return geometry::plan_distance(Point, issue.position) < 5.0; })) - continue; - ++around; - auto const name = [](TTrack const *Track) { return Track == nullptr ? std::string{"-"} : Track->name() + (Track->eType == tt_Switch ? "(switch)" : ""); }; - auto const rise{issue.other != nullptr ? editor_track::point_position(*issue.other, issue.theirs).y - issue.position.y : 0.0}; - WriteLog(format("SELFTEST joints: %s %.3f at %.1f %.1f, %s / %s, rise %.3f", kinds[static_cast(issue.type)], issue.value, issue.position.x, issue.position.z, name(issue.track).c_str(), name(issue.other).c_str(), rise)); - } - WriteLog(format("SELFTEST joints: %d issues around the points, %zu in all", around, m_joints.issues.size())); - return selftest_result::handled; - } if (command == "gizmo") { std::string action; diff --git a/application/editormodeswitchplace.cpp b/application/editormodeswitchplace.cpp index 2275c361..1856afce 100644 --- a/application/editormodeswitchplace.cpp +++ b/application/editormodeswitchplace.cpp @@ -583,7 +583,7 @@ bool editor_mode::plan_switch_link(switch_link &Plan) const } if (shape.length > line.length - 0.02) return false; - auto const kink{glm::radians(m_joints.angle * 0.5)}; + auto const kink{glm::radians(m_joints.kink_angle() * 0.5)}; auto const plan = [&](double const Along) { Plan.along = Along; Plan.paths = place(Along); @@ -643,7 +643,7 @@ bool editor_mode::plan_branch_link(editor_track::branch_link &Link, std::string return false; auto const &diverging{tool.track->m_paths[1]}; auto const radius{editor_track::branch_radius(diverging)}; - Link = editor_track::link_branch(branch_start(diverging), radius, branch_target(target), glm::radians(m_joints.angle * 0.5), + Link = editor_track::link_branch(branch_start(diverging), radius, branch_target(target), glm::radians(m_joints.kink_angle() * 0.5), editor_track::reverse_insert(std::min(editor_track::diverging_speed(radius), design_speed(tool.track, target.track))), stretchable(target)); Reason = link_problem(target, Link); return Reason.empty(); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index f00f060f..3aa605f1 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -78,6 +78,26 @@ alignment::limits const &editor_mode::route_norms() const return m_route.design.norms; } +void editor_mode::clear_track_point() +{ + m_track_point = {}; +} + +editor_nmt &editor_mode::terrain_model() +{ + return m_nmt; +} + +double editor_mode::terrain_model_offset() const +{ + return m_nmt_state.offset; +} + +bool editor_mode::terrain_model_fitted() const +{ + return m_nmt_state.fitted; +} + void editor_mode::select_track(scene::basic_node *Node) { auto *track = dynamic_cast(Node); @@ -527,7 +547,7 @@ void editor_mode::render_track_inspector() m_profile.open = m_track_window_open && m_track_tab == track_tab::profile; m_speed.set_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; + m_joints.set_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 = false; @@ -559,7 +579,7 @@ void editor_mode::render_track_inspector() switch (m_track_tab) { case track_tab::speed: m_speed.render_options(); break; - case track_tab::joints: render_joints_body(); break; + case track_tab::joints: m_joints.render_options(); break; default: render_infra_body(); break; } } diff --git a/application/editortool.h b/application/editortool.h index 2e3eb275..2c746dc8 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorTrack.hpp" class TTrack; +class editor_nmt; namespace scene { class basic_node; @@ -29,7 +30,19 @@ enum class editor_selftest_result { unhandled, handled, - pause + pause, + again +}; + +struct nmt_place +{ + glm::dvec3 position{0.0}; // on the axis of the path + double formation{0.0}; // the height of the formation under it, the foot of the ballast + double crown{0.0}; // the height of the top of the ballast under the rails: what the terrain model measures on a track + double half_width{3.0}; // from the axis to the foot of the ballast + TTrack *track{nullptr}; + int path{0}; + bool shapes{true}; // not a bridge nor a tunnel }; class editor_services @@ -43,6 +56,12 @@ class editor_services virtual void focus_track(TTrack &Track, int const Path, glm::dvec3 const &Point) = 0; virtual void set_current_node(scene::basic_node *Node) = 0; virtual alignment::limits const &route_norms() const = 0; + virtual void clear_track_point() = 0; + virtual void nmt_locate() = 0; + virtual std::vector nmt_track_places(double const Step) const = 0; + virtual editor_nmt &terrain_model() = 0; + virtual double terrain_model_offset() const = 0; + virtual bool terrain_model_fitted() const = 0; virtual void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}) = 0; protected: From c45c4713d653db8c45622ee699a0e989934495d5 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 09:44:17 +0200 Subject: [PATCH 008/145] refactor(editor): the trainset is a tool of its own The fourth tool out of editor_mode. The track under the cursor and the first steps of the tutorial become types of their own, which the editor gives to the tools, and a tool shown as a tab of the lineside panel keeps its open and expanded flags in a tool_tab. The trainsets waiting for the save are handed to the editor by the tool, which hears back when they were written. The self-test command trainset moves with it. --- application/editormode.cpp | 22 ++----- application/editormode.h | 66 +++----------------- application/editormodeselftestscene.cpp | 23 ------- application/editormodeselftestsweep.cpp | 2 +- application/editormodeselftesttrack.cpp | 2 +- application/editormodetrack.cpp | 7 ++- application/editormodetrackux.cpp | 4 +- application/editormodevehicle.cpp | 82 ++++++++++++++++++++----- application/editormodevehicle.h | 79 ++++++++++++++++++++++++ application/editormodeviewport.cpp | 4 +- application/editortool.h | 18 ++++++ 11 files changed, 188 insertions(+), 121 deletions(-) create mode 100644 application/editormodevehicle.h diff --git a/application/editormode.cpp b/application/editormode.cpp index 19559df6..29f6347f 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1286,9 +1286,8 @@ bool editor_mode::update() { Global.editor_tracks = (current_work_area() == work_area::tracks); selftest_step(); - if (m_vehicle.leave) + if (m_vehicle.take_leave()) { - m_vehicle.leave = false; Application.pop_mode(); return true; } @@ -1456,7 +1455,7 @@ bool editor_mode::update() { draw_hekto_overlay(); draw_fouling_overlay(); - draw_vehicle_marker(); + m_vehicle.draw_overlay(); } render_track_context(); update_build_tools(); @@ -2937,13 +2936,7 @@ bool editor_mode::save() auto const controls{turntable_controls(rootstatements)}; auto const noses{nose_controls(rootstatements)}; std::vector trailing; - std::erase_if(m_vehicle.trainsets, [](auto const &Trainset) { - auto const *track{simulation::Paths.find(Trainset.track)}; - return track == nullptr || track->m_editorremoved; - }); - auto const placed{m_vehicle.trainsets.size()}; - for (auto const &trainset : m_vehicle.trainsets) - trailing.insert(trailing.end(), trainset.lines.begin(), trainset.lines.end()); + auto const placed{m_vehicle.trainset_lines(trailing)}; if (placed == 0) starter_trainset(trailing); auto const result = scene::Layers.save(rootstatements, trailing); @@ -2952,10 +2945,7 @@ bool editor_mode::save() if (result.success && noses) m_nose_included = true; if (result.success && placed > 0) - { - m_vehicle.trainsets.clear(); - m_vehicle.status = format(STR_C("%d trainset(s) written to the scenery"), static_cast(placed)); - } + m_vehicle.trainsets_written(); if (result.success && placed == 0 && trailing.size() == 3) { std::istringstream header{trailing.front()}; @@ -5301,12 +5291,12 @@ void editor_mode::render_first_steps() } case trial::trainset: { - ImGui::BeginDisabled(ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::lineside && m_vehicle.open); + ImGui::BeginDisabled(ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::lineside && m_vehicle.tab().open); if (ImGui::Button(STR_C("The trainset tool"))) { choose_work_area(work_area::tracks); show_track_tab(track_tab::lineside); - m_vehicle.expand = true; + m_vehicle.tab().expand = true; } ImGui::EndDisabled(); break; diff --git a/application/editormode.h b/application/editormode.h index 3d5c3140..3f6708b7 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormodegauge.h" #include "application/editormodejoints.h" #include "application/editormodespeed.h" +#include "application/editormodevehicle.h" #include "application/editortool.h" #include "scene/sceneeditor.h" #include "scene/scenenode.h" @@ -55,12 +56,6 @@ class TAnimModel; // a tool of a field of work of the editor, a button in the toolbar and over the 3d view struct editor_tool_button; class TTrack; -namespace ui -{ -class vehicles_bank; -struct vehicle_desc; -struct skin_set; -} class editor_mode : public application_mode, private editor_track::observer, private editor_services { @@ -240,9 +235,9 @@ class editor_mode : public application_mode, private editor_track::observer, pri void render_help_menu(); void render_shortcuts_window(); bool m_shortcuts_open{false}; - enum class first_step { tracks, lay, laid, wheel, join, turnout, undo, save, drive, look, top, select, shape, profile, profile_point, profile_apply, terrain, nmt, plan, plan_band, plan_apply, signal, posts, count }; + using first_step = editor_first_step; void render_first_steps(); - void first_step_done(first_step const Step); + void first_step_done(first_step const Step) override; bool m_first_steps_open{false}; std::array(first_step::count)> m_first_steps_done{}; int m_tutorial_page{0}; @@ -718,6 +713,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool track_editing() const override; std::size_t history_size() const override; glm::dvec3 camera_position() const override; + track_hover const &hovered_track() const override; void render_path_ui(); void draw_track_overlay() const; editor_track::point_ref track_handle_hit() const; @@ -1516,13 +1512,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri // a scenery with no tracks yet: in the middle of the view, how the first one is laid void draw_empty_scenery_hint(); // path under the cursor, outlined before a click so it's known which one the click takes - struct track_hover - { - TTrack *track{nullptr}; - int path{0}; - glm::dvec3 point{0.0}; - double since{0.0}; - }; + using track_hover = ::track_hover; track_hover m_hover; track_hover track_under_cursor(TTrack const *Only = nullptr, float const Pixels = 0.0f) const; track_hover split_spot() const; @@ -1925,49 +1915,6 @@ class editor_mode : public application_mode, private editor_track::observer, pri void render_fouling_ui(); void draw_fouling_overlay() const; - // a trainset put on the selected path, written to the scenery on save, or only to be driven - 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 picked{-1}; // vehicle of the train whose buttons are shown - std::set minis; // skins whose side view was asked for - int facing{1}; - bool write{true}; - char timetable[64]{"none"}; - double velocity{0.0}; - struct trainset - { - std::string head; - std::string track; - std::vector lines; - }; - std::vector trainsets; - }; - vehicle_tool m_vehicle; - void vehicle_start(TTrack &Track, glm::dvec3 const &Point); - bool vehicle_place(); - void render_vehicle_ui(); - void draw_vehicle_marker() const; - struct signal_template { std::string file; @@ -2445,7 +2392,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri gauge_tool m_gauge{*this}; speed_tool m_speed{*this}; joints_tool m_joints{*this}; - std::vector m_tools{&m_gauge, &m_speed, &m_joints}; + vehicle_tool m_vehicle{*this}; + std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle}; void infra_rebase(); void render_infra_search(); diff --git a/application/editormodeselftestscene.cpp b/application/editormodeselftestscene.cpp index b74dc2de..46c8e2e0 100644 --- a/application/editormodeselftestscene.cpp +++ b/application/editormodeselftestscene.cpp @@ -293,29 +293,6 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm WriteLog(format("SELFTEST mapstate orthophoto %d top %d extent %.0f mark %s camera %.1f %.1f %.1f", m_orthophoto.enabled() ? 1 : 0, Global.EditorOrtho ? 1 : 0, Global.EditorOrthoExtent, marked.empty() ? "-" : marked.front().c_str(), Camera.Pos.x, Camera.Pos.y, Camera.Pos.z)); return selftest_result::handled; } - if (command == "trainset") - { - std::string name; - std::string vehicle; - std::string skin; - int facing{1}; - words >> name >> vehicle >> skin >> facing >> m_vehicle.velocity; - auto *track{simulation::Paths.find(name)}; - if (track == nullptr) - return selftest_result::handled; - auto desc{std::make_shared()}; - desc->path = vehicle; - auto set{std::make_shared()}; - set->skin = skin; - m_vehicle.consist.clear(); - m_vehicle.vehicle = desc; - m_vehicle.skin = set; - vehicle_start(*track, geometry::bezier{track->m_paths.front()}.point(0.5)); - m_vehicle.facing = facing; - auto const placed{vehicle_place()}; - WriteLog(format("SELFTEST trainset %s placed %d pending %d: %s", name.c_str(), placed ? 1 : 0, static_cast(m_vehicle.trainsets.size()), m_vehicle.status.c_str())); - return selftest_result::handled; - } if (command == "selectmodel") { glm::dvec3 point{0.0}; diff --git a/application/editormodeselftestsweep.cpp b/application/editormodeselftestsweep.cpp index 814562f3..084994a2 100644 --- a/application/editormodeselftestsweep.cpp +++ b/application/editormodeselftestsweep.cpp @@ -388,7 +388,7 @@ editor_mode::selftest_result editor_mode::selftest_sweep(std::string const &comm std::string tab; words >> tab; show_track_tab(tab == "parallel" ? track_tab::parallel : track_tab::lineside); - (tab == "vehicle" ? m_vehicle.expand : tab == "hekto" ? m_hekto.expand : tab == "fouling" ? m_fouling.expand : m_parallel.expand) = true; + (tab == "vehicle" ? m_vehicle.tab().expand : tab == "hekto" ? m_hekto.expand : tab == "fouling" ? m_fouling.expand : m_parallel.expand) = true; return selftest_result::handled; } if (command == "bendpanel") diff --git a/application/editormodeselftesttrack.cpp b/application/editormodeselftesttrack.cpp index 0c8ca5b5..dad7a6a5 100644 --- a/application/editormodeselftesttrack.cpp +++ b/application/editormodeselftesttrack.cpp @@ -142,7 +142,7 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm m_hekto.expand = tab == 1; m_fouling.expand = tab == 2; m_parallel.expand = tab == 3; - m_vehicle.expand = tab == 4; + m_vehicle.tab().expand = tab == 4; return selftest_result::handled; } if (command == "curve") diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 3aa605f1..4866c4fd 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -67,6 +67,11 @@ glm::dvec3 editor_mode::camera_position() const return Camera.Pos; } +editor_mode::track_hover const &editor_mode::hovered_track() const +{ + return m_hover; +} + void editor_mode::set_current_node(scene::basic_node *Node) { m_node = Node; @@ -554,7 +559,7 @@ void editor_mode::render_track_inspector() m_hekto.open = false; m_fouling.open = false; m_parallel.open = false; - m_vehicle.open = false; + m_vehicle.tab().open = false; } ui()->set_track(m_track_window_open); if (false == m_track_window_open) diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 9c551577..cf67bba7 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -793,7 +793,7 @@ void editor_mode::render_track_context() if (ImGui::MenuItem(STR_C("Place a vehicle here"))) { show_track_tab(track_tab::lineside); - vehicle_start(*track, point); + m_vehicle.start(*track, point); } if (ImGui::MenuItem(STR_C("Hectometre posts from here"))) { @@ -1479,7 +1479,7 @@ void editor_mode::render_lineside_ui() std::array const tabs{{ {"Hectometre posts", &m_hekto.open, &m_hekto.expand, [this]() { render_hekto_ui(); }}, {"Fouling points", &m_fouling.open, &m_fouling.expand, [this]() { render_fouling_ui(); }}, - {"Trainset", &m_vehicle.open, &m_vehicle.expand, [this]() { render_vehicle_ui(); }}, + {"Trainset", &m_vehicle.tab().open, &m_vehicle.tab().expand, [this]() { m_vehicle.render_options(); }}, }}; 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")); diff --git a/application/editormodevehicle.cpp b/application/editormodevehicle.cpp index 719d5a01..daf22ceb 100644 --- a/application/editormodevehicle.cpp +++ b/application/editormodevehicle.cpp @@ -8,8 +8,7 @@ http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" -#include "application/editormode.h" -#include "application/editoruilayer.h" +#include "application/editormodevehicle.h" #include "application/editorprojection.h" #include "application/editormodeshared.h" #include "application/application.h" @@ -154,18 +153,18 @@ double distance_along(TTrack const &Track, int const Path, glm::dvec3 const &Poi } // namespace -void editor_mode::vehicle_start(TTrack &Track, glm::dvec3 const &Point) +void vehicle_tool::start(TTrack &Track, glm::dvec3 const &Point) { m_vehicle.track = &Track; m_vehicle.point = Point; - m_vehicle.expand = true; + m_tab.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() +bool vehicle_tool::place() { auto &tool{m_vehicle}; if (tool.track == nullptr || tool.track->m_editorremoved) @@ -210,7 +209,7 @@ bool editor_mode::vehicle_place() return false; } tool.placed = head; - first_step_done(first_step::drive); + m_services.first_step_done(editor_first_step::drive); auto const trackname{tool.track->name().empty() ? std::string{STR("(no name)")} : tool.track->name()}; auto const behind{static_cast(vehicles.size()) - 1}; tool.status = behind > 0 ? format(STR_P("%s and %d vehicle behind it stand on %s", "%s and %d vehicles behind it stand on %s", behind), head.c_str(), behind, trackname.c_str()) : format(STR_C("%s stands on %s"), head.c_str(), trackname.c_str()); @@ -220,7 +219,7 @@ bool editor_mode::vehicle_place() { std::string timetable{tool.timetable}; timetable.erase(std::remove_if(timetable.begin(), timetable.end(), [](unsigned char const Character) { return std::isspace(Character) != 0; }), timetable.end()); - vehicle_tool::trainset entry{head, tool.track->name(), {}}; + vehicle_state::trainset entry{head, tool.track->name(), {}}; entry.lines.push_back("trainset " + (timetable.empty() ? std::string{"none"} : timetable) + " " + tool.track->name() + " " + to_string(offset, 2) + " " + to_string(tool.velocity, 0)); if (against) entry.lines.push_back("reversed"); @@ -238,14 +237,15 @@ bool editor_mode::vehicle_place() return true; } -void editor_mode::render_vehicle_ui() +void vehicle_tool::render_options() { auto &tool{m_vehicle}; - auto *selected{selected_track()}; - if (selected != nullptr && selected == tool.track && m_hover.track == selected && ImGui::IsMouseClicked(0) && false == ImGui::GetIO().WantCaptureMouse) - vehicle_start(*selected, m_hover.point); + auto *selected{m_services.selected_track()}; + auto const &hover{m_services.hovered_track()}; + if (selected != nullptr && selected == tool.track && hover.track == selected && ImGui::IsMouseClicked(0) && false == ImGui::GetIO().WantCaptureMouse) + start(*selected, hover.point); else if (selected != nullptr && selected != tool.track) - vehicle_start(*selected, m_hover.track == selected ? m_hover.point : bezier{selected->m_paths.front()}.point(0.5)); + start(*selected, hover.track == selected ? hover.point : bezier{selected->m_paths.front()}.point(0.5)); if (tool.track == nullptr) { ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("LMB on a track: where the vehicle stands")); @@ -430,11 +430,11 @@ void editor_mode::render_vehicle_ui() ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); if (ImGui::Button(STR_C("Place"), ImVec2(half, tall))) - vehicle_place(); + place(); item_tooltip("A trainset on the track, placed where the track was clicked, or in the middle of the path"); ImGui::PopStyleColor(2); ImGui::SameLine(); - if (ImGui::Button(STR_C("Place and drive"), ImVec2(-1.0f, tall)) && vehicle_place()) + if (ImGui::Button(STR_C("Place and drive"), ImVec2(-1.0f, tall)) && place()) { Global.editor_enter_vehicle = tool.placed; tool.leave = true; @@ -461,10 +461,10 @@ void editor_mode::render_vehicle_ui() tool.trainsets.erase(tool.trainsets.begin() + static_cast(*drop)); } -void editor_mode::draw_vehicle_marker() const +void vehicle_tool::draw_overlay() const { auto const &tool{m_vehicle}; - if (false == tool.open || tool.track == nullptr) + if (false == m_tab.open || tool.track == nullptr) return; screen_projection const projection; ImVec2 screen; @@ -490,3 +490,53 @@ void editor_mode::draw_vehicle_marker() const 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); } + +bool vehicle_tool::take_leave() +{ + return std::exchange(m_vehicle.leave, false); +} + +std::size_t vehicle_tool::trainset_lines(std::vector &Lines) +{ + std::erase_if(m_vehicle.trainsets, [](auto const &Trainset) { + auto const *track{simulation::Paths.find(Trainset.track)}; + return track == nullptr || track->m_editorremoved; + }); + for (auto const &trainset : m_vehicle.trainsets) + Lines.insert(Lines.end(), trainset.lines.begin(), trainset.lines.end()); + return m_vehicle.trainsets.size(); +} + +void vehicle_tool::trainsets_written() +{ + m_vehicle.status = format(STR_C("%d trainset(s) written to the scenery"), static_cast(m_vehicle.trainsets.size())); + m_vehicle.trainsets.clear(); +} + +editor_selftest_result vehicle_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + if (Command == "trainset") + { + std::string name; + std::string vehicle; + std::string skin; + int facing{1}; + Words >> name >> vehicle >> skin >> facing >> m_vehicle.velocity; + auto *track{simulation::Paths.find(name)}; + if (track == nullptr) + return editor_selftest_result::handled; + auto desc{std::make_shared()}; + desc->path = vehicle; + auto set{std::make_shared()}; + set->skin = skin; + m_vehicle.consist.clear(); + m_vehicle.vehicle = desc; + m_vehicle.skin = set; + start(*track, geometry::bezier{track->m_paths.front()}.point(0.5)); + m_vehicle.facing = facing; + auto const placed{place()}; + WriteLog(format("SELFTEST trainset %s placed %d pending %d: %s", name.c_str(), placed ? 1 : 0, static_cast(m_vehicle.trainsets.size()), m_vehicle.status.c_str())); + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} diff --git a/application/editormodevehicle.h b/application/editormodevehicle.h new file mode 100644 index 00000000..15b74c13 --- /dev/null +++ b/application/editormodevehicle.h @@ -0,0 +1,79 @@ +/* +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/editortool.h" + +#include +#include +#include +#include + +namespace ui +{ +class vehicles_bank; +struct vehicle_desc; +struct skin_set; +} + +// a trainset put on the selected path, written to the scenery on save, or only to be driven +class vehicle_tool final : public editor_tool +{ + public: + explicit vehicle_tool(editor_services &Services) : m_services{Services} {} + void draw_overlay() const override; + void render_options() override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + tool_tab &tab() { return m_tab; } + void start(TTrack &Track, glm::dvec3 const &Point); + bool take_leave(); + std::size_t trainset_lines(std::vector &Lines); + void trainsets_written(); + + private: + editor_services &m_services; + tool_tab m_tab; + struct vehicle_state + { + 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 picked{-1}; // vehicle of the train whose buttons are shown + std::set minis; // skins whose side view was asked for + int facing{1}; + bool write{true}; + char timetable[64]{"none"}; + double velocity{0.0}; + struct trainset + { + std::string head; + std::string track; + std::vector lines; + }; + std::vector trainsets; + }; + vehicle_state m_vehicle; + bool place(); +}; diff --git a/application/editormodeviewport.cpp b/application/editormodeviewport.cpp index 7ba759f9..1bbed3fa 100644 --- a/application/editormodeviewport.cpp +++ b/application/editormodeviewport.cpp @@ -160,7 +160,7 @@ void editor_mode::work_area_tools(std::vector &Buttons) } case work_area::tracks: { - auto const vehicle{m_track_tab == track_tab::lineside && m_vehicle.open}; + auto const vehicle{m_track_tab == track_tab::lineside && m_vehicle.tab().open}; auto const add = [&](track_tab const Tab, bool const Group) { auto const &modes{track_modes()}; auto const found{std::find_if(modes.begin(), modes.end(), [&](track_mode const &Mode) { return Mode.tab == Tab; })}; @@ -191,7 +191,7 @@ void editor_mode::work_area_tools(std::vector &Buttons) add(track_tab::lineside, false); Buttons.push_back({icon::vehicle, STR_C("Trainset"), nullptr, STR_C("A trainset on the selected track: written to the scenery on save, and to drive at once"), vehicle, [this, vehicle]() { show_track_tab(vehicle ? track_tab::path : track_tab::lineside); - m_vehicle.expand = false == vehicle; + m_vehicle.tab().expand = false == vehicle; }}); Buttons.back().accent = family_colour(1); add(track_tab::profile, true); diff --git a/application/editortool.h b/application/editortool.h index 2c746dc8..c4bbaa43 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -34,6 +34,22 @@ enum class editor_selftest_result again }; +enum class editor_first_step { tracks, lay, laid, wheel, join, turnout, undo, save, drive, look, top, select, shape, profile, profile_point, profile_apply, terrain, nmt, plan, plan_band, plan_apply, signal, posts, count }; + +struct track_hover +{ + TTrack *track{nullptr}; + int path{0}; + glm::dvec3 point{0.0}; + double since{0.0}; +}; + +struct tool_tab +{ + bool open{false}; + bool expand{false}; +}; + struct nmt_place { glm::dvec3 position{0.0}; // on the axis of the path @@ -52,6 +68,8 @@ class editor_services virtual bool track_editing() const = 0; virtual std::size_t history_size() const = 0; virtual glm::dvec3 camera_position() const = 0; + virtual track_hover const &hovered_track() const = 0; + virtual void first_step_done(editor_first_step const Step) = 0; virtual void fly_to(glm::dvec3 const &Target, glm::dvec3 const &Look, double const Distance, double const Duration = 0.6) = 0; virtual void focus_track(TTrack &Track, int const Path, glm::dvec3 const &Point) = 0; virtual void set_current_node(scene::basic_node *Node) = 0; From 6ee771bb223415ad91103805cdcdb064e3752654 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 09:50:38 +0200 Subject: [PATCH 009/145] refactor(editor): the hectometre posts and the fouling points are tools of their own The fifth and the sixth tool out of editor_mode, the two lineside ones sharing their file. Placing, moving and taking out the templates which stand by the track become services of the editor, with the template types of their own, and so do the tab of the track tools shown, the ground under a point and the first step done. The context menu, the click on a fouling point in the view and the lineside tabs go through the public calls of the tools. The self-test commands hekto and fouling move with them and log the same as before. --- application/editormode.cpp | 4 +- application/editormode.h | 146 ++--------------- application/editormodelineside.cpp | 209 +++++++++++++++++++----- application/editormodelineside.h | 142 ++++++++++++++++ application/editormodeselftestsweep.cpp | 106 +----------- application/editormodeselftesttrack.cpp | 4 +- application/editormodetrack.cpp | 4 +- application/editormodetrackux.cpp | 20 +-- application/editortool.h | 37 ++++- 9 files changed, 372 insertions(+), 300 deletions(-) create mode 100644 application/editormodelineside.h diff --git a/application/editormode.cpp b/application/editormode.cpp index 29f6347f..389d3b58 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1453,8 +1453,8 @@ bool editor_mode::update() draw_parallel_preview(); if (m_track_tab == track_tab::lineside) { - draw_hekto_overlay(); - draw_fouling_overlay(); + m_hekto.draw_overlay(); + m_fouling.draw_overlay(); m_vehicle.draw_overlay(); } render_track_context(); diff --git a/application/editormode.h b/application/editormode.h index 3f6708b7..c9881b6c 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -16,6 +16,7 @@ http://mozilla.org/MPL/2.0/. #include "vehicle/Camera.h" #include "application/editormodegauge.h" #include "application/editormodejoints.h" +#include "application/editormodelineside.h" #include "application/editormodespeed.h" #include "application/editormodevehicle.h" #include "application/editortool.h" @@ -225,7 +226,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri int m_selected_history_idx{-1}; glm::dvec3 clamp_mouse_offset_to_max(const glm::dvec3 &offset); // the ground under a point the cursor landed on, for things which are to stand on the ground - glm::dvec3 placement_on_ground(glm::dvec3 Location); + glm::dvec3 placement_on_ground(glm::dvec3 Location) override; // puts every instance of the model the selected node shows on the ground under it void drop_model_instances(); void render_object_menu(); @@ -1431,7 +1432,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void update_build_tools(); void draw_build_overlay() const; // modes of the track editor, one at a time: path is the plain selection - enum class track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals, parallel }; + using track_tab = editor_track_tab; struct track_mode { char const *label; @@ -1477,7 +1478,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri // the track tools in the tool options window, and the selected path in the inspector void render_track_tool_options(); void render_track_selection(); - void show_track_tab(track_tab const Wanted); + void show_track_tab(track_tab const Wanted) override; 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 @@ -1769,28 +1770,10 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool parallel_preview_visible() const; 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; - glm::dvec2 tilt{0.0}; // rot.x and rot.z of a described template, degrees - std::map values; - }; - 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; - }; + using template_item = ::template_item; + using standing_template = ::standing_template; // 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; + std::vector standing_templates(std::function const &Accept) const override; struct switch_drive { standing_template standing; @@ -1804,116 +1787,11 @@ class editor_mode : public application_mode, private editor_track::observer, pri void switch_drive_captured(TTrack const &Track); void switch_drives_committed(std::vector const &Tracks); // places them in the active layer, as one step of the undo. returns: how many were placed - std::size_t place_templates(std::vector const &Items, std::string &Error); + std::size_t place_templates(std::vector const &Items, std::string &Error) override; // moves them, as one step of the undo - std::size_t move_templates(std::vector> const &Moves); + std::size_t move_templates(std::vector> const &Moves) override; // 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{1}; // 0: the selected switch, 1: around the camera, 2: the whole scenery - double reach{1500.0}; - double spacing{3.5}; // m between the axes - double tolerance{2.0}; // m, a marker farther than this from its point is wrong - 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(); - // the markers put at the fouling points without one, or at the one only - void fouling_place(int const Only = -1); - // the markers standing wrong moved to their places, or the one found only - void fouling_fix(int const Only = -1); - // the fouling point without a marker or the marker standing wrong under the cursor: the index of the point or of the - // marker found, and whether it's a marker - std::optional> fouling_hit() const; - void render_fouling_ui(); - void draw_fouling_overlay() const; + std::size_t remove_templates(std::vector const &Instances) override; struct signal_template { @@ -2393,7 +2271,9 @@ class editor_mode : public application_mode, private editor_track::observer, pri speed_tool m_speed{*this}; joints_tool m_joints{*this}; vehicle_tool m_vehicle{*this}; - std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle}; + hekto_tool m_hekto{*this}; + fouling_tool m_fouling{*this}; + std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle, &m_hekto, &m_fouling}; void infra_rebase(); void render_infra_search(); diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp index e96858d8..f6de2967 100644 --- a/application/editormodelineside.cpp +++ b/application/editormodelineside.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" +#include "application/editormodelineside.h" #include "application/editormodeshared.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" @@ -365,18 +366,18 @@ std::size_t editor_mode::remove_templates(std::vector co return count; } -void editor_mode::hekto_start(TTrack &Track, glm::dvec3 const &Point) +void hekto_tool::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; + m_tab.expand = true; tool.status.clear(); } -void editor_mode::hekto_read_kilometrage() +void hekto_tool::hekto_read_kilometrage() { auto &tool{m_hekto}; auto const &found{tool.found}; @@ -417,13 +418,13 @@ void editor_mode::hekto_read_kilometrage() tool.status = format(STR_C("Kilometrage read from %zu posts standing along the line"), found.size()); } -void editor_mode::hekto_update() +void hekto_tool::hekto_update() { auto &tool{m_hekto}; - if (false == tool.dirty && tool.history == m_history.size()) + if (false == tool.dirty && tool.history == m_services.history_size()) return; tool.dirty = false; - tool.history = m_history.size(); + tool.history = m_services.history_size(); tool.posts.clear(); tool.found.clear(); tool.error.clear(); @@ -448,7 +449,7 @@ void editor_mode::hekto_update() 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; })) + for (auto const &standing : m_services.standing_templates([](std::string const &File) { return File.find("hekto") != std::string::npos; })) { hekto_found post; post.post = standing; @@ -487,7 +488,7 @@ void editor_mode::hekto_update() // 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.position = m_services.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 @@ -517,7 +518,7 @@ void editor_mode::hekto_update() } } -void editor_mode::hekto_remove_duplicates() +void hekto_tool::hekto_remove_duplicates() { auto &tool{m_hekto}; hekto_update(); @@ -525,13 +526,13 @@ void editor_mode::hekto_remove_duplicates() for (auto const &found : tool.found) if (found.duplicate) instances.push_back(found.post.instance); - auto const removed{remove_templates(instances)}; + auto const removed{m_services.remove_templates(instances)}; tool.status = format(STR_P("%zu duplicate hectometre post removed, Ctrl+Z brings it back", "%zu duplicate hectometre posts removed, Ctrl+Z brings them back", static_cast(removed)), removed); WriteLog("Editor: " + tool.status); tool.dirty = true; } -void editor_mode::hekto_place() +void hekto_tool::hekto_place() { auto &tool{m_hekto}; hekto_update(); @@ -548,15 +549,15 @@ void editor_mode::hekto_place() items.push_back(std::move(item)); } std::string error; - auto const placed{place_templates(items, error)}; + auto const placed{m_services.place_templates(items, error)}; tool.status = placed > 0 ? format(STR_P("%zu hectometre post placed, Ctrl+Z takes it back", "%zu hectometre posts placed, Ctrl+Z takes them back", static_cast(placed)), placed) : error; if (placed > 0) - first_step_done(first_step::posts); + m_services.first_step_done(editor_first_step::posts); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } -void editor_mode::hekto_fix() +void hekto_tool::hekto_fix() { auto &tool{m_hekto}; hekto_update(); @@ -570,16 +571,16 @@ void editor_mode::hekto_fix() item.yaw = post.yaw; moves.emplace_back(tool.found[post.standing].post, item); } - auto const moved{move_templates(moves)}; + auto const moved{m_services.move_templates(moves)}; tool.status = format(STR_P("%zu hectometre post moved to its place, Ctrl+Z takes it back", "%zu hectometre posts moved to their places, Ctrl+Z takes it back", static_cast(moved)), moved); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } -void editor_mode::render_hekto_ui() +void hekto_tool::render_options() { auto &tool{m_hekto}; - auto *selected{selected_track()}; + auto *selected{m_services.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) @@ -587,7 +588,7 @@ void editor_mode::render_hekto_ui() 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)); + start(*selected, bezier{selected->m_paths.front()}.point(0.0)); } auto const changed = [&](bool const Changed) { if (Changed) @@ -678,7 +679,7 @@ void editor_mode::render_hekto_ui() auto const label{post.duplicate ? format(STR_C("km %s duplicate##%u"), km_text(post.hectometres).c_str(), post.post.instance) : format(STR_C("km %s stands %.1f m from its place##%u"), km_text(post.hectometres).c_str(), post.distance, post.post.instance)}; if (ImGui::Selectable(label.c_str())) { - fly_to(post.post.location, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 20.0); + m_services.fly_to(post.post.location, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 20.0); } } ImGui::EndChild(); @@ -687,10 +688,10 @@ void editor_mode::render_hekto_ui() ImGui::TextWrapped("%s", tool.status.c_str()); } -void editor_mode::draw_hekto_overlay() const +void hekto_tool::draw_overlay() const { auto const &tool{m_hekto}; - if (false == tool.open || tool.track == nullptr) + if (false == m_tab.open || tool.track == nullptr) return; screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; @@ -731,16 +732,16 @@ void editor_mode::draw_hekto_overlay() const } } -void editor_mode::fouling_update() +void fouling_tool::fouling_update() { auto &tool{m_fouling}; - auto *selected{selected_track()}; + auto *selected{m_services.selected_track()}; if (tool.scope == 0 && selected != tool.selected) tool.dirty = true; - if (false == tool.dirty && tool.history == m_history.size()) + if (false == tool.dirty && tool.history == m_services.history_size()) return; tool.dirty = false; - tool.history = m_history.size(); + tool.history = m_services.history_size(); tool.selected = selected; tool.points.clear(); tool.found.clear(); @@ -791,7 +792,7 @@ void editor_mode::fouling_update() 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}); + point.position = tool.height_mode == 1 ? glm::dvec3{middle.x, 0.5 * (onbranch.y + onmain.y) + kRailHeight + tool.height, middle.z} : m_services.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); @@ -799,7 +800,7 @@ void editor_mode::fouling_update() } } // 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; })}; + auto const markers{m_services.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}; @@ -837,7 +838,7 @@ void editor_mode::fouling_update() tool.status = format(STR_C("%zu switches, %zu fouling points: %d without a marker, %d markers standing wrong"), switches.size(), tool.points.size(), static_cast(missing), static_cast(wrong)); } -void editor_mode::fouling_place(int const Only) +void fouling_tool::place(int const Only) { auto &tool{m_fouling}; fouling_update(); @@ -856,13 +857,13 @@ void editor_mode::fouling_place(int const Only) items.push_back(std::move(item)); } std::string error; - auto const placed{place_templates(items, error)}; + auto const placed{m_services.place_templates(items, error)}; tool.status = placed > 0 ? format(STR_P("%zu fouling point marker placed, Ctrl+Z takes it back", "%zu fouling point markers placed, Ctrl+Z takes them back", static_cast(placed)), placed) : error; WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } -void editor_mode::fouling_fix(int const Only) +void fouling_tool::fix(int const Only) { auto &tool{m_fouling}; fouling_update(); @@ -887,13 +888,13 @@ void editor_mode::fouling_fix(int const Only) } moves.emplace_back(found.marker, item); } - auto const moved{move_templates(moves)}; + auto const moved{m_services.move_templates(moves)}; tool.status = format(STR_P("%zu fouling point marker moved to its place, Ctrl+Z takes it back", "%zu fouling point markers moved to their places, Ctrl+Z takes it back", static_cast(moved)), moved); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } -std::optional> editor_mode::fouling_hit() const +std::optional> fouling_tool::hit() const { auto const &tool{m_fouling}; screen_projection const projection; @@ -921,7 +922,7 @@ std::optional> editor_mode::fouling_hit() const return result; } -void editor_mode::render_fouling_ui() +void fouling_tool::render_options() { auto &tool{m_fouling}; auto const changed = [&](bool const Changed) { @@ -943,7 +944,7 @@ void editor_mode::render_fouling_ui() ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); if (ImGui::Button(format(STR_P("Place %d marker", "Place %d markers", static_cast(missing)), static_cast(missing)).c_str(), ImVec2(-1.0f, tall))) - fouling_place(); + place(); ImGui::PopStyleColor(2); } if (wrong > 0) @@ -951,7 +952,7 @@ void editor_mode::render_fouling_ui() ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.4f, 0.1f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.48f, 0.15f, 1.0f)); if (ImGui::Button(format(STR_P("Move %d wrong marker to its place", "Move %d wrong markers to their places", static_cast(wrong)), static_cast(wrong)).c_str(), ImVec2(-1.0f, tall))) - fouling_fix(); + fix(); ImGui::PopStyleColor(2); } if (missing + wrong > 0) @@ -967,10 +968,10 @@ void editor_mode::render_fouling_ui() continue; ImGui::PushID(i); if (ImGui::SmallButton(STR_C("Place"))) - fouling_place(i); + place(i); ImGui::SameLine(); if (ImGui::Selectable(format(STR_C("%s: no W17"), point.track->name().c_str()).c_str())) - fly_to(point.position, look, 25.0); + m_services.fly_to(point.position, look, 25.0); ImGui::PopID(); } for (int i = 0; i < static_cast(tool.found.size()); ++i) @@ -980,10 +981,10 @@ void editor_mode::render_fouling_ui() continue; ImGui::PushID(1000000 + i); if (ImGui::SmallButton(STR_C("Fix"))) - fouling_fix(i); + fix(i); ImGui::SameLine(); if (ImGui::Selectable(format(STR_C("%s: W17 %.1f m off, %+.2f m high"), tool.points[found.point].track->name().c_str(), found.error, found.height_error).c_str())) - fly_to(found.marker.location, look, 25.0); + m_services.fly_to(found.marker.location, look, 25.0); ImGui::PopID(); } ImGui::EndChild(); @@ -1031,10 +1032,10 @@ void editor_mode::render_fouling_ui() tool.tolerance = std::clamp(tool.tolerance, 0.2, 20.0); } -void editor_mode::draw_fouling_overlay() const +void fouling_tool::draw_overlay() const { auto const &tool{m_fouling}; - if (false == tool.open) + if (false == m_tab.open) return; screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; @@ -1063,3 +1064,129 @@ void editor_mode::draw_fouling_overlay() const projection.line(drawlist, found.marker.location, tool.points[found.point].position, IM_COL32(255, 70, 60, 160), 1.5f); } } + +editor_selftest_result hekto_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + 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"); + return editor_selftest_result::handled; + } + 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); + } + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} + +glm::dvec3 fouling_tool::position_of(std::pair const &Hit) const +{ + return Hit.second ? m_fouling.found[Hit.first].marker.location : m_fouling.points[Hit.first].position; +} + +void fouling_tool::check_selected_switch() +{ + m_fouling.scope = 0; + m_fouling.dirty = true; + m_tab.expand = true; +} + +editor_selftest_result fouling_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + 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}; + if (values["show"] == "1") + { + m_services.show_track_tab(editor_track_tab::lineside); + m_tab.expand = true; + } + 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"] == "one") + { + // the first fouling point without a marker alone, as a click on it in the view + for (int index = 0; index < static_cast(tool.points.size()); ++index) + if (tool.points[index].standing < 0) + { + place(index); + break; + } + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling one: " + tool.status); + } + if (values["place"] == "1") + { + place(); + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling after placing: " + tool.status); + } + if (values["fix"] == "1") + { + fix(); + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling after the fix: " + tool.status); + } + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} diff --git a/application/editormodelineside.h b/application/editormodelineside.h new file mode 100644 index 00000000..d007cd54 --- /dev/null +++ b/application/editormodelineside.h @@ -0,0 +1,142 @@ +/* +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/editortool.h" + +#include +#include +#include +#include + +// hectometre posts along the line, at every 100 m of the kilometrage +class hekto_tool final : public editor_tool +{ + public: + explicit hekto_tool(editor_services &Services) : m_services{Services} {} + void draw_overlay() const override; + void render_options() override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + tool_tab &tab() { return m_tab; } + void start(TTrack &Track, glm::dvec3 const &Point); + + private: + editor_services &m_services; + tool_tab m_tab; + 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_state + { + 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_state m_hekto; + void hekto_update(); + void hekto_read_kilometrage(); + void hekto_place(); + void hekto_fix(); + void hekto_remove_duplicates(); +}; + +// fouling point markers (W17) where the tracks meeting at a switch are the spacing apart +class fouling_tool final : public editor_tool +{ + public: + explicit fouling_tool(editor_services &Services) : m_services{Services} {} + void draw_overlay() const override; + void render_options() override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + tool_tab &tab() { return m_tab; } + // the markers put at the fouling points without one, or at the one only + void place(int const Only = -1); + // the markers standing wrong moved to their places, or the one found only + void fix(int const Only = -1); + // the fouling point without a marker or the marker standing wrong under the cursor: the index of the point or of the + // marker found, and whether it's a marker + std::optional> hit() const; + glm::dvec3 position_of(std::pair const &Hit) const; + void check_selected_switch(); + + private: + editor_services &m_services; + tool_tab m_tab; + 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_state + { + int scope{1}; // 0: the selected switch, 1: around the camera, 2: the whole scenery + double reach{1500.0}; + double spacing{3.5}; // m between the axes + double tolerance{2.0}; // m, a marker farther than this from its point is wrong + 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_state m_fouling; + void fouling_update(); +}; diff --git a/application/editormodeselftestsweep.cpp b/application/editormodeselftestsweep.cpp index 084994a2..fd37edb9 100644 --- a/application/editormodeselftestsweep.cpp +++ b/application/editormodeselftestsweep.cpp @@ -126,110 +126,6 @@ editor_mode::selftest_result editor_mode::selftest_sweep(std::string const &comm WriteLog("SELFTEST drag: " + m_sweep.status); return selftest_result::handled; } - 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}; - if (values["show"] == "1") - { - show_track_tab(track_tab::lineside); - tool.expand = true; - } - 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"] == "one") - { - // the first fouling point without a marker alone, as a click on it in the view - for (int index = 0; index < static_cast(tool.points.size()); ++index) - if (tool.points[index].standing < 0) - { - fouling_place(index); - break; - } - tool.dirty = true; - fouling_update(); - WriteLog("SELFTEST fouling one: " + tool.status); - } - 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); - } - return selftest_result::handled; - } - 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"); - return selftest_result::handled; - } - 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); - } - return selftest_result::handled; - } if (command == "spread") { std::string name; @@ -388,7 +284,7 @@ editor_mode::selftest_result editor_mode::selftest_sweep(std::string const &comm std::string tab; words >> tab; show_track_tab(tab == "parallel" ? track_tab::parallel : track_tab::lineside); - (tab == "vehicle" ? m_vehicle.tab().expand : tab == "hekto" ? m_hekto.expand : tab == "fouling" ? m_fouling.expand : m_parallel.expand) = true; + (tab == "vehicle" ? m_vehicle.tab().expand : tab == "hekto" ? m_hekto.tab().expand : tab == "fouling" ? m_fouling.tab().expand : m_parallel.expand) = true; return selftest_result::handled; } if (command == "bendpanel") diff --git a/application/editormodeselftesttrack.cpp b/application/editormodeselftesttrack.cpp index dad7a6a5..7c1c5eac 100644 --- a/application/editormodeselftesttrack.cpp +++ b/application/editormodeselftesttrack.cpp @@ -139,8 +139,8 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm words >> tab; show_track_tab(track_tab::lineside); m_sweep.expand = tab == 0; - m_hekto.expand = tab == 1; - m_fouling.expand = tab == 2; + m_hekto.tab().expand = tab == 1; + m_fouling.tab().expand = tab == 2; m_parallel.expand = tab == 3; m_vehicle.tab().expand = tab == 4; return selftest_result::handled; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 4866c4fd..c1a4fcf3 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -556,8 +556,8 @@ void editor_mode::render_track_inspector() if (false == m_track_window_open || m_track_tab != track_tab::lineside) { m_sweep.open = false; - m_hekto.open = false; - m_fouling.open = false; + m_hekto.tab().open = false; + m_fouling.tab().open = false; m_parallel.open = false; m_vehicle.tab().open = false; } diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index cf67bba7..21629482 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -445,12 +445,12 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) return make(kind::switch_at, "Press and drag: a switch here, along sets its direction, sideways its side"); } } - if (m_track_tab == track_tab::lineside && m_fouling.open) - if (auto const hit{fouling_hit()}; hit.has_value()) + if (m_track_tab == track_tab::lineside && m_fouling.tab().open) + if (auto const hit{m_fouling.hit()}; hit.has_value()) { intent.index = hit->first; intent.path = hit->second ? 1 : 0; - intent.position = hit->second ? m_fouling.found[hit->first].marker.location : m_fouling.points[hit->first].position; + intent.position = m_fouling.position_of(*hit); return make(kind::fouling, hit->second ? "Click: move this W17 to its place" : "Click: put the W17 here"); } if (m_hover.track != nullptr) @@ -570,9 +570,9 @@ void editor_mode::track_press(track_intent const &Intent) case kind::switch_at: start_switch_placement(); return; case kind::fouling: if (Intent.path == 1) - fouling_fix(Intent.index); + m_fouling.fix(Intent.index); else - fouling_place(Intent.index); + m_fouling.place(Intent.index); return; case kind::extend_end: if (false == start_extend(Intent.snap)) @@ -798,14 +798,12 @@ void editor_mode::render_track_context() if (ImGui::MenuItem(STR_C("Hectometre posts from here"))) { show_track_tab(track_tab::lineside); - hekto_start(*track, point); + m_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; + m_fouling.check_selected_switch(); } if (ImGui::MenuItem(STR_C("Vertical profile of the line"), "P")) { @@ -1477,8 +1475,8 @@ void editor_mode::render_lineside_ui() std::function render; }; std::array const tabs{{ - {"Hectometre posts", &m_hekto.open, &m_hekto.expand, [this]() { render_hekto_ui(); }}, - {"Fouling points", &m_fouling.open, &m_fouling.expand, [this]() { render_fouling_ui(); }}, + {"Hectometre posts", &m_hekto.tab().open, &m_hekto.tab().expand, [this]() { m_hekto.render_options(); }}, + {"Fouling points", &m_fouling.tab().open, &m_fouling.tab().expand, [this]() { m_fouling.render_options(); }}, {"Trainset", &m_vehicle.tab().open, &m_vehicle.tab().expand, [this]() { m_vehicle.render_options(); }}, }}; if (selected_track() == nullptr) diff --git a/application/editortool.h b/application/editortool.h index c4bbaa43..791d7424 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -10,6 +10,9 @@ http://mozilla.org/MPL/2.0/. #pragma once #include +#include +#include +#include #include #include #include @@ -18,13 +21,10 @@ http://mozilla.org/MPL/2.0/. #include #include "editor/editorTrack.hpp" +#include "utilities/Classes.h" class TTrack; class editor_nmt; -namespace scene -{ -class basic_node; -} enum class editor_selftest_result { @@ -36,6 +36,8 @@ enum class editor_selftest_result enum class editor_first_step { tracks, lay, laid, wheel, join, turnout, undo, save, drive, look, top, select, shape, profile, profile_point, profile_apply, terrain, nmt, plan, plan_band, plan_apply, signal, posts, count }; +enum class editor_track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals, parallel }; + struct track_hover { TTrack *track{nullptr}; @@ -50,6 +52,27 @@ struct tool_tab bool expand{false}; }; +// 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; + glm::dvec2 tilt{0.0}; // rot.x and rot.z of a described template, degrees + std::map values; +}; +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; +}; + struct nmt_place { glm::dvec3 position{0.0}; // on the axis of the path @@ -70,6 +93,8 @@ class editor_services virtual glm::dvec3 camera_position() const = 0; virtual track_hover const &hovered_track() const = 0; virtual void first_step_done(editor_first_step const Step) = 0; + virtual void show_track_tab(editor_track_tab const Wanted) = 0; + virtual glm::dvec3 placement_on_ground(glm::dvec3 Location) = 0; virtual void fly_to(glm::dvec3 const &Target, glm::dvec3 const &Look, double const Distance, double const Duration = 0.6) = 0; virtual void focus_track(TTrack &Track, int const Path, glm::dvec3 const &Point) = 0; virtual void set_current_node(scene::basic_node *Node) = 0; @@ -80,6 +105,10 @@ class editor_services virtual editor_nmt &terrain_model() = 0; virtual double terrain_model_offset() const = 0; virtual bool terrain_model_fitted() const = 0; + virtual std::vector standing_templates(std::function const &Accept) const = 0; + virtual std::size_t place_templates(std::vector const &Items, std::string &Error) = 0; + virtual std::size_t move_templates(std::vector> const &Moves) = 0; + virtual std::size_t remove_templates(std::vector const &Instances) = 0; virtual void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}) = 0; protected: From 872e1b5e19bef1759b30db563e92fbfc1ab92e34 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 09:54:39 +0200 Subject: [PATCH 010/145] refactor(editor): the parallel track is a tool of its own The seventh tool out of editor_mode. It reads the selected set of tracks and the tab shown through the services, digitizes the tracks it lays and refreshes the plan after it sets one parallel to another. The panel shown for two selected tracks is a public call of the tool, and the self-test command parallel moves with it and lays the same tracks as before. --- application/editormode.cpp | 2 +- application/editormode.h | 44 +++-------------- application/editormodeparallel.cpp | 65 +++++++++++++++---------- application/editormodeparallel.h | 58 ++++++++++++++++++++++ application/editormodeselftestsweep.cpp | 2 +- application/editormodeselftesttrack.cpp | 11 +---- application/editormodetrack.cpp | 14 +++++- application/editormodetrackux.cpp | 2 +- application/editortool.h | 4 ++ 9 files changed, 125 insertions(+), 77 deletions(-) create mode 100644 application/editormodeparallel.h diff --git a/application/editormode.cpp b/application/editormode.cpp index 389d3b58..952697fa 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1450,7 +1450,7 @@ bool editor_mode::update() signal_move_update(); draw_signal_overlay(); } - draw_parallel_preview(); + m_parallel.draw_overlay(); if (m_track_tab == track_tab::lineside) { m_hekto.draw_overlay(); diff --git a/application/editormode.h b/application/editormode.h index c9881b6c..ea8a680a 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormodegauge.h" #include "application/editormodejoints.h" #include "application/editormodelineside.h" +#include "application/editormodeparallel.h" #include "application/editormodespeed.h" #include "application/editormodevehicle.h" #include "application/editortool.h" @@ -715,6 +716,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::size_t history_size() const override; glm::dvec3 camera_position() const override; track_hover const &hovered_track() const override; + std::vector const &track_set() const override; + editor_track_tab current_track_tab() const override; void render_path_ui(); void draw_track_overlay() const; editor_track::point_ref track_handle_hit() const; @@ -910,7 +913,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base, double const Middle = 0.0); void render_switch_record(TTrack const &Track) const; void route_store(); - bool digitize_drawn(std::vector const &Created, bool const Alone = true); + bool digitize_drawn(std::vector const &Created, bool const Alone = true) override; bool digitize_chain(editor_track::chain const &Chain, std::string *Reason = nullptr, TTrack **Culprit = nullptr); int digitize_parts(editor_track::chain const &Chain, std::string *Reason, int &Skipped); // the lines kept, the paths left out @@ -1016,7 +1019,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void apply_straight_tool(); double straight_tool_radius(editor_track::straight const &Line) const; void straight_reshape(editor_track::straight const &Line, double const From, double const To, std::function const &Tail, double const Radius); - void straight_refresh(); + void straight_refresh() override; 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(); @@ -1736,40 +1739,6 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::vector> m_track_similar; TTrack const *m_track_list_hover{nullptr}; // in the list of the selected tracks int m_track_list_hover_frame{-1}; - // second track alongside the selected path or the line through it, at a spacing, curves included - struct parallel_tool - { - 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}; - TTrack *pair_src{nullptr}; - TTrack *pair_dst{nullptr}; - double measured{0.0}; - std::vector pieces; - std::vector styles; - double length{0.0}; - std::string error; - std::string status; - }; - parallel_tool m_parallel; - TTrack *parallel_source() const; - TTrack *parallel_target() const; - void parallel_update(); - void render_parallel_controls(bool const Scope = true); - void render_parallel_ui(); - void render_parallel_set_ui(); - void parallel_build(); - void parallel_apply(); - bool parallel_preview_visible() const; - void draw_parallel_preview() const; - using template_item = ::template_item; using standing_template = ::standing_template; // includes of the templates whose file names the test accepts, with the location in p2..p4 and the rotation in p5 @@ -2273,7 +2242,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri vehicle_tool m_vehicle{*this}; hekto_tool m_hekto{*this}; fouling_tool m_fouling{*this}; - std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle, &m_hekto, &m_fouling}; + parallel_tool m_parallel{*this}; + std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle, &m_hekto, &m_fouling, &m_parallel}; void infra_rebase(); void render_infra_search(); diff --git a/application/editormodeparallel.cpp b/application/editormodeparallel.cpp index 7fb5b893..5180560c 100644 --- a/application/editormodeparallel.cpp +++ b/application/editormodeparallel.cpp @@ -8,8 +8,7 @@ http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" -#include "application/editormode.h" -#include "application/editoruilayer.h" +#include "application/editormodeparallel.h" #include "application/editorprojection.h" #include "application/editormodeshared.h" #include "editor/editorFormat.hpp" @@ -209,21 +208,23 @@ double lateral_of(TTrack const &Src, TTrack const &Dst) } // namespace -TTrack *editor_mode::parallel_source() const +TTrack *parallel_tool::parallel_source() const { - if (m_track_set.size() >= 2) - return m_track_set.front(); - return selected_track(); + auto const &set{m_services.track_set()}; + if (set.size() >= 2) + return set.front(); + return m_services.selected_track(); } -TTrack *editor_mode::parallel_target() const +TTrack *parallel_tool::parallel_target() const { - if (m_track_set.size() != 2) + auto const &set{m_services.track_set()}; + if (set.size() != 2) return nullptr; - return m_track_set.back() != m_track_set.front() ? m_track_set.back() : nullptr; + return set.back() != set.front() ? set.back() : nullptr; } -void editor_mode::parallel_update() +void parallel_tool::parallel_update() { auto &tool{m_parallel}; auto *track{parallel_source()}; @@ -244,13 +245,13 @@ void editor_mode::parallel_update() else if (track != nullptr && target != nullptr) tool.measured = std::abs(lateral_of(*track, *target)); - 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) + if (track == tool.built_for && tool.spacing == tool.built_spacing && tool.side == tool.built_side && tool.scope == tool.built_scope && m_services.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.built_history = m_services.history_size(); tool.pieces.clear(); tool.styles.clear(); tool.length = 0.0; @@ -319,7 +320,7 @@ void editor_mode::parallel_update() } } -void editor_mode::parallel_build() +void parallel_tool::parallel_build() { auto &tool{m_parallel}; parallel_update(); @@ -329,8 +330,8 @@ void editor_mode::parallel_build() for (std::size_t i = 0; i < tool.pieces.size(); ++i) created.push_back(editor_track::create_path(*tool.styles[i], tool.pieces[i])); editor_track::commit(created); - digitize_drawn(created); - push_track_snapshot({}, created); + m_services.digitize_drawn(created); + m_services.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")}; @@ -338,7 +339,7 @@ void editor_mode::parallel_build() tool.built_for = nullptr; } -void editor_mode::parallel_apply() +void parallel_tool::parallel_apply() { auto &tool{m_parallel}; auto *target{parallel_target()}; @@ -375,14 +376,14 @@ void editor_mode::parallel_apply() if (joint.other != nullptr) committed.push_back(joint.other); editor_track::commit(committed); - push_track_snapshot(std::move(states)); + m_services.push_track_snapshot(std::move(states)); tool.status = format(STR_C("%s set parallel to %s, %.2f m away; Ctrl+Z takes it back"), name_of(*target).c_str(), name_of(*parallel_source()).c_str(), tool.spacing); WriteLog("Editor: " + tool.status, logtype::generic); tool.built_for = nullptr; - straight_refresh(); + m_services.straight_refresh(); } -void editor_mode::render_parallel_controls(bool const Scope) +void parallel_tool::render_parallel_controls(bool const Scope) { auto &tool{m_parallel}; if (Scope) @@ -396,7 +397,7 @@ void editor_mode::render_parallel_controls(bool const Scope) value_chips("spacings", tool.spacing, {kSpacings[0], kSpacings[1], kSpacings[2], kSpacings[3], kSpacings[4], kSpacings[5]}, "%.2f"); } -void editor_mode::render_parallel_ui() +void parallel_tool::render_options() { auto &tool{m_parallel}; ImGui::PushTextWrapPos(0.0f); @@ -426,7 +427,7 @@ void editor_mode::render_parallel_ui() ImGui::TextWrapped("%s", tool.status.c_str()); } -void editor_mode::render_parallel_set_ui() +void parallel_tool::render_set_options() { auto &tool{m_parallel}; auto *source{parallel_source()}; @@ -450,16 +451,16 @@ void editor_mode::render_parallel_set_ui() ImGui::TextWrapped("%s", tool.status.c_str()); } -bool editor_mode::parallel_preview_visible() const +bool parallel_tool::parallel_preview_visible() const { if (m_parallel.pieces.empty() || parallel_source() != m_parallel.built_for) return false; - if (m_track_tab == track_tab::parallel) + if (m_services.current_track_tab() == editor_track_tab::parallel) return true; - return m_track_tab == track_tab::path && parallel_target() != nullptr; + return m_services.current_track_tab() == editor_track_tab::path && parallel_target() != nullptr; } -void editor_mode::draw_parallel_preview() const +void parallel_tool::draw_overlay() const { if (false == parallel_preview_visible()) return; @@ -488,3 +489,17 @@ void editor_mode::draw_parallel_preview() const drawlist->AddText(ImVec2(screen.x + 12.0f, screen.y - 8.0f), IM_COL32(90, 255, 120, 255), label.c_str()); } } + +editor_selftest_result parallel_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + if (Command == "parallel") + { + Words >> m_parallel.spacing >> m_parallel.side; + m_parallel.scope = 1; + m_parallel.built_for = nullptr; + parallel_build(); + WriteLog("SELFTEST parallel: " + m_parallel.status + " " + m_parallel.error); + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} diff --git a/application/editormodeparallel.h b/application/editormodeparallel.h new file mode 100644 index 00000000..f221c6f4 --- /dev/null +++ b/application/editormodeparallel.h @@ -0,0 +1,58 @@ +/* +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/editortool.h" + +#include +#include + +// second track alongside the selected path or the line through it, at a spacing, curves included +class parallel_tool final : public editor_tool +{ + public: + explicit parallel_tool(editor_services &Services) : m_services{Services} {} + void draw_overlay() const override; + void render_options() override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + tool_tab &tab() { return m_tab; } + void render_set_options(); + + private: + editor_services &m_services; + tool_tab m_tab; + struct parallel_state + { + 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}; + TTrack *pair_src{nullptr}; + TTrack *pair_dst{nullptr}; + double measured{0.0}; + std::vector pieces; + std::vector styles; + double length{0.0}; + std::string error; + std::string status; + }; + parallel_state m_parallel; + TTrack *parallel_source() const; + TTrack *parallel_target() const; + void parallel_update(); + void render_parallel_controls(bool const Scope = true); + void parallel_build(); + void parallel_apply(); + bool parallel_preview_visible() const; +}; diff --git a/application/editormodeselftestsweep.cpp b/application/editormodeselftestsweep.cpp index fd37edb9..38289631 100644 --- a/application/editormodeselftestsweep.cpp +++ b/application/editormodeselftestsweep.cpp @@ -284,7 +284,7 @@ editor_mode::selftest_result editor_mode::selftest_sweep(std::string const &comm std::string tab; words >> tab; show_track_tab(tab == "parallel" ? track_tab::parallel : track_tab::lineside); - (tab == "vehicle" ? m_vehicle.tab().expand : tab == "hekto" ? m_hekto.tab().expand : tab == "fouling" ? m_fouling.tab().expand : m_parallel.expand) = true; + (tab == "vehicle" ? m_vehicle.tab().expand : tab == "hekto" ? m_hekto.tab().expand : tab == "fouling" ? m_fouling.tab().expand : m_parallel.tab().expand) = true; return selftest_result::handled; } if (command == "bendpanel") diff --git a/application/editormodeselftesttrack.cpp b/application/editormodeselftesttrack.cpp index 7c1c5eac..c1182d20 100644 --- a/application/editormodeselftesttrack.cpp +++ b/application/editormodeselftesttrack.cpp @@ -141,7 +141,7 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm m_sweep.expand = tab == 0; m_hekto.tab().expand = tab == 1; m_fouling.tab().expand = tab == 2; - m_parallel.expand = tab == 3; + m_parallel.tab().expand = tab == 3; m_vehicle.tab().expand = tab == 4; return selftest_result::handled; } @@ -782,14 +782,5 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm WriteLog(format("SELFTEST digitize %s: %d", nearest != nullptr ? nearest->name().c_str() : "-", kept ? 1 : 0)); return selftest_result::handled; } - if (command == "parallel") - { - words >> m_parallel.spacing >> m_parallel.side; - m_parallel.scope = 1; - m_parallel.built_for = nullptr; - parallel_build(); - WriteLog("SELFTEST parallel: " + m_parallel.status + " " + m_parallel.error); - return selftest_result::handled; - } return selftest_result::unhandled; } diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index c1a4fcf3..e9ada20f 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -72,6 +72,16 @@ editor_mode::track_hover const &editor_mode::hovered_track() const return m_hover; } +std::vector const &editor_mode::track_set() const +{ + return m_track_set; +} + +editor_track_tab editor_mode::current_track_tab() const +{ + return m_track_tab; +} + void editor_mode::set_current_node(scene::basic_node *Node) { m_node = Node; @@ -558,7 +568,7 @@ void editor_mode::render_track_inspector() m_sweep.open = false; m_hekto.tab().open = false; m_fouling.tab().open = false; - m_parallel.open = false; + m_parallel.tab().open = false; m_vehicle.tab().open = false; } ui()->set_track(m_track_window_open); @@ -611,7 +621,7 @@ void editor_mode::render_track_tool_options() render_lay_ui(); break; case track_tab::parallel: - render_parallel_ui(); + m_parallel.render_options(); break; case track_tab::turnout: if (track != nullptr && track->eType == tt_Switch) diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 21629482..238b0cfe 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -1355,7 +1355,7 @@ void editor_mode::render_track_set_ui() ImGui::PushTextWrapPos(0.0f); ImGui::TextDisabled("%s", STR_C("Their properties are in the inspector, the gizmo moves them together")); ImGui::PopTextWrapPos(); - render_parallel_set_ui(); + m_parallel.render_set_options(); ImGui::Separator(); } diff --git a/application/editortool.h b/application/editortool.h index 791d7424..fd110f91 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -92,6 +92,8 @@ class editor_services virtual std::size_t history_size() const = 0; virtual glm::dvec3 camera_position() const = 0; virtual track_hover const &hovered_track() const = 0; + virtual std::vector const &track_set() const = 0; + virtual editor_track_tab current_track_tab() const = 0; virtual void first_step_done(editor_first_step const Step) = 0; virtual void show_track_tab(editor_track_tab const Wanted) = 0; virtual glm::dvec3 placement_on_ground(glm::dvec3 Location) = 0; @@ -109,6 +111,8 @@ class editor_services virtual std::size_t place_templates(std::vector const &Items, std::string &Error) = 0; virtual std::size_t move_templates(std::vector> const &Moves) = 0; virtual std::size_t remove_templates(std::vector const &Instances) = 0; + virtual bool digitize_drawn(std::vector const &Created, bool const Alone = true) = 0; + virtual void straight_refresh() = 0; virtual void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}) = 0; protected: From 747c6817f0e78704dfc5cc328c2329413ed35ec7 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 10:08:27 +0200 Subject: [PATCH 011/145] refactor(editor): the tools get the roles of the editor they need, not all of it editor_services was growing into one interface every tool depended on whole. It splits into the roles the editor plays for its tools: the selection, the view, the undo history, the templates by the track, the ground with the terrain model, the track design and the guide of the first steps. Each tool is handed only the roles it uses, so what it depends on can be read from its constructor; editor_services stays as the sum of the roles the editor implements. --- application/editormode.h | 14 ++--- application/editormodegauge.cpp | 20 +++---- application/editormodegauge.h | 7 ++- application/editormodejoints.cpp | 30 +++++----- application/editormodejoints.h | 7 ++- application/editormodelineside.cpp | 40 +++++++------- application/editormodelineside.h | 17 ++++-- application/editormodeparallel.cpp | 22 ++++---- application/editormodeparallel.h | 7 ++- application/editormodespeed.cpp | 16 +++--- application/editormodespeed.h | 7 ++- application/editormodevehicle.cpp | 6 +- application/editormodevehicle.h | 5 +- application/editortool.h | 88 ++++++++++++++++++++++++------ 14 files changed, 181 insertions(+), 105 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index ea8a680a..86e8b72d 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -2236,13 +2236,13 @@ class editor_mode : public application_mode, private editor_track::observer, pri void infra_attach(EditorSnapshot &Snapshot); void render_infra_body(); - gauge_tool m_gauge{*this}; - speed_tool m_speed{*this}; - joints_tool m_joints{*this}; - vehicle_tool m_vehicle{*this}; - hekto_tool m_hekto{*this}; - fouling_tool m_fouling{*this}; - parallel_tool m_parallel{*this}; + gauge_tool m_gauge{*this, *this, *this, *this}; + speed_tool m_speed{*this, *this, *this, *this}; + joints_tool m_joints{*this, *this, *this, *this}; + vehicle_tool m_vehicle{*this, *this}; + hekto_tool m_hekto{*this, *this, *this, *this, *this, *this}; + fouling_tool m_fouling{*this, *this, *this, *this, *this}; + parallel_tool m_parallel{*this, *this, *this, *this}; std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle, &m_hekto, &m_fouling, &m_parallel}; void infra_rebase(); diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 4c2f2187..4260033f 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -591,9 +591,9 @@ void gauge_tool::update() m_gauge.published = false; return; } - auto *track{m_gauge.enabled ? m_services.selected_track() : nullptr}; + auto *track{m_gauge.enabled ? m_selection.selected_track() : nullptr}; // the geometry is rescanned once the edit ends - if (ImGui::IsMouseDown(0) || m_services.track_editing()) + if (ImGui::IsMouseDown(0) || m_design.track_editing()) m_gauge.pending = true; else if (track == nullptr) { @@ -604,15 +604,15 @@ void gauge_tool::update() m_gauge.published = false; } } - else if (track != m_gauge.track || m_services.history_size() != m_gauge.history || m_gauge.pending) + else if (track != m_gauge.track || m_history.history_size() != m_gauge.history || m_gauge.pending) { m_gauge.track = track; - m_gauge.history = m_services.history_size(); + m_gauge.history = m_history.history_size(); m_gauge.pending = false; scan_gauge(*track); m_gauge.published = false; } - if (false == m_gauge.published || glm::distance(m_services.camera_position(), EditorOverlay.origin) > gauge_origin_range) + if (false == m_gauge.published || glm::distance(m_view.camera_position(), EditorOverlay.origin) > gauge_origin_range) gauge_publish(); } @@ -645,7 +645,7 @@ void gauge_tool::scan_gauge_map() scan->space.emplace(scan->lines); m_gauge.map = {}; m_gauge.map.scanned = true; - m_gauge.map.history = m_services.history_size(); + m_gauge.map.history = m_history.history_size(); auto const name{[](gauge::profile const *Profile) { return Profile != nullptr ? Profile->name : std::string{"-"}; }}; m_gauge.map.profile = name(chosen.railway) + ", " + name(chosen.road); m_gauge.current = -1; @@ -695,7 +695,7 @@ void gauge_tool::gauge_publish() { m_gauge.published = true; EditorOverlay.clear(); - EditorOverlay.origin = m_services.camera_position(); + EditorOverlay.origin = m_view.camera_position(); auto const batch{[](glm::vec4 const &Color, unsigned int const Type, std::vector const &Points) { gfx::editor_overlay::batch result{Color, Type}; result.points.reserve(Points.size()); @@ -730,9 +730,9 @@ void gauge_tool::gauge_focus(int const Index) // along the track, tangent to the gauge at the height of the place, from the end nearer to the camera glm::dvec3 look{hit.direction.x, 0.0, hit.direction.y}; look = glm::length(look) > 1e-6 ? glm::normalize(look) : glm::dvec3(0.0, 0.0, 1.0); - if (glm::dot(m_services.camera_position() - hit.point, look) > 0.0) + if (glm::dot(m_view.camera_position() - hit.point, look) > 0.0) look = -look; - m_services.fly_to(hit.point, look, gauge_view_distance); + m_view.fly_to(hit.point, look, gauge_view_distance); } void gauge_tool::gauge_spot(gauge_hit const &Hit) @@ -799,7 +799,7 @@ void gauge_tool::render_windows() m_gauge.current = -1; m_gauge.published = false; } - else if (m_gauge.map.history != m_services.history_size()) + else if (m_gauge.map.history != m_history.history_size()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("The scenery changed since the scan (%s)"), m_gauge.map.profile.c_str()); } render_gauge_hits(); diff --git a/application/editormodegauge.h b/application/editormodegauge.h index 7e39d8d1..0f0507bb 100644 --- a/application/editormodegauge.h +++ b/application/editormodegauge.h @@ -29,7 +29,7 @@ class gauge_tool final : public editor_tool int path{0}; glm::dvec2 direction{0.0, 1.0}; // of the track there, in the plan }; - explicit gauge_tool(editor_services &Services) : m_services{Services} {} + gauge_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_track_design &Design) : m_selection{Selection}, m_view{View}, m_history{History}, m_design{Design} {} void update() override; void render_windows() override; editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; @@ -37,7 +37,10 @@ class gauge_tool final : public editor_tool void toggle() { m_gauge.open = !m_gauge.open; } private: - editor_services &m_services; + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + editor_track_design &m_design; // structure gauge along the line of the selected path, shown in the 3d view struct gauge_state { diff --git a/application/editormodejoints.cpp b/application/editormodejoints.cpp index 5177de0e..a9afd279 100644 --- a/application/editormodejoints.cpp +++ b/application/editormodejoints.cpp @@ -164,7 +164,7 @@ void joints_tool::joints_start() continue; state.queue.push_back(track); } - state.history = m_services.history_size(); + state.history = m_history.history_size(); state.places.clear(); state.runs.clear(); state.run_next = 0; @@ -174,14 +174,14 @@ void joints_tool::joints_start() state.model_unplaced = false; if (false == state.model_check || state.queue.empty()) return; - m_services.nmt_locate(); - if (false == m_services.terrain_model().placed()) + m_ground.nmt_locate(); + if (false == m_ground.terrain_model().placed()) { state.model_unplaced = true; return; } std::set const queued(state.queue.begin(), state.queue.end()); - for (auto const &place : m_services.nmt_track_places(kModelStep)) + for (auto const &place : m_ground.nmt_track_places(kModelStep)) if (place.shapes && queued.count(place.track) > 0) { if (state.places.empty() || state.places.back().track != place.track || state.places.back().path != place.path || state.places.size() - state.runs.back().first >= kModelRun) @@ -216,27 +216,27 @@ bool joints_tool::joints_model() for (auto const &run : state.runs) { box(run, low, high); - m_services.terrain_model().request(low, high); + m_ground.terrain_model().request(low, high); } state.model_asked = true; } while (state.run_next < state.runs.size()) { box(state.runs[state.run_next], low, high); - if (false == m_services.terrain_model().request(low, high)) + if (false == m_ground.terrain_model().request(low, high)) return false; ++state.run_next; } std::vector differences(state.places.size(), std::numeric_limits::quiet_NaN()); std::vector known; for (std::size_t i = 0; i < state.places.size(); ++i) - if (double height; m_services.terrain_model().height(state.places[i].position.x, state.places[i].position.z, height)) + if (double height; m_ground.terrain_model().height(state.places[i].position.x, state.places[i].position.z, height)) { - differences[i] = state.places[i].crown - (height + m_services.terrain_model_offset()); + differences[i] = state.places[i].crown - (height + m_ground.terrain_model_offset()); known.push_back(differences[i]); } auto shift{0.0}; - if (false == m_services.terrain_model_fitted() && false == known.empty()) + if (false == m_ground.terrain_model_fitted() && false == known.empty()) { std::nth_element(known.begin(), known.begin() + known.size() / 2, known.end()); if (std::abs(known[known.size() / 2]) > kUnfitted) @@ -411,9 +411,9 @@ void joints_tool::joints_focus(int const Index) auto const &paths{simulation::Paths.sequence()}; if (std::find(paths.begin(), paths.end(), issue.track) == paths.end() || issue.track->m_editorremoved) return; - m_services.set_current_node(issue.track); - m_services.clear_track_point(); - m_services.focus_track(*issue.track, issue.point.path, issue.position); + m_selection.set_current_node(issue.track); + m_selection.clear_track_point(); + m_view.focus_track(*issue.track, issue.point.path, issue.position); } bool joints_tool::joint_fix(joint_issue &Issue, std::vector> &States, std::vector &Changed) @@ -498,11 +498,11 @@ void joints_tool::joints_fix(std::vector const &Indices) state.status = STR_C("Nothing could be fixed: the paths are switches or in the files which can't be changed"); return; } - m_services.push_track_snapshot(std::move(states)); + m_history.push_track_snapshot(std::move(states)); editor_track::commit(changed); for (auto *track : changed) track->mark_dirty(); - state.history = m_services.history_size(); + state.history = m_history.history_size(); state.status = format(STR_C("%zu joints fixed, Ctrl+Z takes them back"), count); WriteLog("Editor: joints check - " + state.status, logtype::generic); } @@ -580,7 +580,7 @@ void joints_tool::render_options() ImGui::TextWrapped("%s", state.status.c_str()); if (state.issues.empty()) return; - if (state.history != m_services.history_size()) + if (state.history != m_history.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); diff --git a/application/editormodejoints.h b/application/editormodejoints.h index 34dc5cbe..626846e7 100644 --- a/application/editormodejoints.h +++ b/application/editormodejoints.h @@ -31,7 +31,7 @@ class joints_tool final : public editor_tool double value{0.0}; // m, deg, mm, per mille; m of the track over the terrain model bool fixed{false}; }; - explicit joints_tool(editor_services &Services) : m_services{Services} {} + joints_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_ground &Ground) : m_selection{Selection}, m_view{View}, m_history{History}, m_ground{Ground} {} void update() override; void draw_overlay() const override; void render_options() override; @@ -40,7 +40,10 @@ class joints_tool final : public editor_tool double kink_angle() const { return m_joints.angle; } private: - editor_services &m_services; + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + editor_ground &m_ground; // joints of the paths: ends which almost meet, steps, kinks, jumps of the cant and of the grade struct joints_state { diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp index f6de2967..56e93528 100644 --- a/application/editormodelineside.cpp +++ b/application/editormodelineside.cpp @@ -421,10 +421,10 @@ void hekto_tool::hekto_read_kilometrage() void hekto_tool::hekto_update() { auto &tool{m_hekto}; - if (false == tool.dirty && tool.history == m_services.history_size()) + if (false == tool.dirty && tool.history == m_history.history_size()) return; tool.dirty = false; - tool.history = m_services.history_size(); + tool.history = m_history.history_size(); tool.posts.clear(); tool.found.clear(); tool.error.clear(); @@ -449,7 +449,7 @@ void hekto_tool::hekto_update() double distance; tool.origin = project(tool.samples, tool.point, distance); auto const length{tool.samples.back().chainage}; - for (auto const &standing : m_services.standing_templates([](std::string const &File) { return File.find("hekto") != std::string::npos; })) + for (auto const &standing : m_templates.standing_templates([](std::string const &File) { return File.find("hekto") != std::string::npos; })) { hekto_found post; post.post = standing; @@ -488,7 +488,7 @@ void hekto_tool::hekto_update() // 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 = m_services.placement_on_ground({planar.x, axis.y, planar.y}); + post.position = m_ground.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 @@ -526,7 +526,7 @@ void hekto_tool::hekto_remove_duplicates() for (auto const &found : tool.found) if (found.duplicate) instances.push_back(found.post.instance); - auto const removed{m_services.remove_templates(instances)}; + auto const removed{m_templates.remove_templates(instances)}; tool.status = format(STR_P("%zu duplicate hectometre post removed, Ctrl+Z brings it back", "%zu duplicate hectometre posts removed, Ctrl+Z brings them back", static_cast(removed)), removed); WriteLog("Editor: " + tool.status); tool.dirty = true; @@ -549,10 +549,10 @@ void hekto_tool::hekto_place() items.push_back(std::move(item)); } std::string error; - auto const placed{m_services.place_templates(items, error)}; + auto const placed{m_templates.place_templates(items, error)}; tool.status = placed > 0 ? format(STR_P("%zu hectometre post placed, Ctrl+Z takes it back", "%zu hectometre posts placed, Ctrl+Z takes them back", static_cast(placed)), placed) : error; if (placed > 0) - m_services.first_step_done(editor_first_step::posts); + m_guide.first_step_done(editor_first_step::posts); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } @@ -571,7 +571,7 @@ void hekto_tool::hekto_fix() item.yaw = post.yaw; moves.emplace_back(tool.found[post.standing].post, item); } - auto const moved{m_services.move_templates(moves)}; + auto const moved{m_templates.move_templates(moves)}; tool.status = format(STR_P("%zu hectometre post moved to its place, Ctrl+Z takes it back", "%zu hectometre posts moved to their places, Ctrl+Z takes it back", static_cast(moved)), moved); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; @@ -580,7 +580,7 @@ void hekto_tool::hekto_fix() void hekto_tool::render_options() { auto &tool{m_hekto}; - auto *selected{m_services.selected_track()}; + auto *selected{m_selection.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) @@ -679,7 +679,7 @@ void hekto_tool::render_options() 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())) { - m_services.fly_to(post.post.location, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 20.0); + m_view.fly_to(post.post.location, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 20.0); } } ImGui::EndChild(); @@ -735,13 +735,13 @@ void hekto_tool::draw_overlay() const void fouling_tool::fouling_update() { auto &tool{m_fouling}; - auto *selected{m_services.selected_track()}; + auto *selected{m_selection.selected_track()}; if (tool.scope == 0 && selected != tool.selected) tool.dirty = true; - if (false == tool.dirty && tool.history == m_services.history_size()) + if (false == tool.dirty && tool.history == m_history.history_size()) return; tool.dirty = false; - tool.history = m_services.history_size(); + tool.history = m_history.history_size(); tool.selected = selected; tool.points.clear(); tool.found.clear(); @@ -792,7 +792,7 @@ void fouling_tool::fouling_update() 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} : m_services.placement_on_ground({middle.x, std::max(onbranch.y, onmain.y), middle.z}); + point.position = tool.height_mode == 1 ? glm::dvec3{middle.x, 0.5 * (onbranch.y + onmain.y) + kRailHeight + tool.height, middle.z} : m_ground.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); @@ -800,7 +800,7 @@ void fouling_tool::fouling_update() } } // markers standing: each goes to the nearest fouling point, the others are left alone - auto const markers{m_services.standing_templates([](std::string const &File) { return File.find("w17") != std::string::npos || File.find("ukres") != std::string::npos; })}; + auto const markers{m_templates.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}; @@ -857,7 +857,7 @@ void fouling_tool::place(int const Only) items.push_back(std::move(item)); } std::string error; - auto const placed{m_services.place_templates(items, error)}; + auto const placed{m_templates.place_templates(items, error)}; tool.status = placed > 0 ? format(STR_P("%zu fouling point marker placed, Ctrl+Z takes it back", "%zu fouling point markers placed, Ctrl+Z takes them back", static_cast(placed)), placed) : error; WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; @@ -888,7 +888,7 @@ void fouling_tool::fix(int const Only) } moves.emplace_back(found.marker, item); } - auto const moved{m_services.move_templates(moves)}; + auto const moved{m_templates.move_templates(moves)}; tool.status = format(STR_P("%zu fouling point marker moved to its place, Ctrl+Z takes it back", "%zu fouling point markers moved to their places, Ctrl+Z takes it back", static_cast(moved)), moved); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; @@ -971,7 +971,7 @@ void fouling_tool::render_options() place(i); ImGui::SameLine(); if (ImGui::Selectable(format(STR_C("%s: no W17"), point.track->name().c_str()).c_str())) - m_services.fly_to(point.position, look, 25.0); + m_view.fly_to(point.position, look, 25.0); ImGui::PopID(); } for (int i = 0; i < static_cast(tool.found.size()); ++i) @@ -984,7 +984,7 @@ void fouling_tool::render_options() fix(i); ImGui::SameLine(); if (ImGui::Selectable(format(STR_C("%s: W17 %.1f m off, %+.2f m high"), tool.points[found.point].track->name().c_str(), found.error, found.height_error).c_str())) - m_services.fly_to(found.marker.location, look, 25.0); + m_view.fly_to(found.marker.location, look, 25.0); ImGui::PopID(); } ImGui::EndChild(); @@ -1146,7 +1146,7 @@ editor_selftest_result fouling_tool::selftest(std::string const &Command, std::i auto &tool{m_fouling}; if (values["show"] == "1") { - m_services.show_track_tab(editor_track_tab::lineside); + m_view.show_track_tab(editor_track_tab::lineside); m_tab.expand = true; } tool.scope = std::stoi(values.count("scope") ? values["scope"] : "2"); diff --git a/application/editormodelineside.h b/application/editormodelineside.h index d007cd54..d7b23606 100644 --- a/application/editormodelineside.h +++ b/application/editormodelineside.h @@ -20,7 +20,7 @@ http://mozilla.org/MPL/2.0/. class hekto_tool final : public editor_tool { public: - explicit hekto_tool(editor_services &Services) : m_services{Services} {} + hekto_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_templates &Templates, editor_ground &Ground, editor_guide &Guide) : m_selection{Selection}, m_view{View}, m_history{History}, m_templates{Templates}, m_ground{Ground}, m_guide{Guide} {} void draw_overlay() const override; void render_options() override; editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; @@ -28,7 +28,12 @@ class hekto_tool final : public editor_tool void start(TTrack &Track, glm::dvec3 const &Point); private: - editor_services &m_services; + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + editor_templates &m_templates; + editor_ground &m_ground; + editor_guide &m_guide; tool_tab m_tab; struct hekto_post { @@ -85,7 +90,7 @@ class hekto_tool final : public editor_tool class fouling_tool final : public editor_tool { public: - explicit fouling_tool(editor_services &Services) : m_services{Services} {} + fouling_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_templates &Templates, editor_ground &Ground) : m_selection{Selection}, m_view{View}, m_history{History}, m_templates{Templates}, m_ground{Ground} {} void draw_overlay() const override; void render_options() override; editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; @@ -101,7 +106,11 @@ class fouling_tool final : public editor_tool void check_selected_switch(); private: - editor_services &m_services; + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + editor_templates &m_templates; + editor_ground &m_ground; tool_tab m_tab; struct fouling_point { diff --git a/application/editormodeparallel.cpp b/application/editormodeparallel.cpp index 5180560c..f5ac7c5d 100644 --- a/application/editormodeparallel.cpp +++ b/application/editormodeparallel.cpp @@ -210,15 +210,15 @@ double lateral_of(TTrack const &Src, TTrack const &Dst) TTrack *parallel_tool::parallel_source() const { - auto const &set{m_services.track_set()}; + auto const &set{m_selection.track_set()}; if (set.size() >= 2) return set.front(); - return m_services.selected_track(); + return m_selection.selected_track(); } TTrack *parallel_tool::parallel_target() const { - auto const &set{m_services.track_set()}; + auto const &set{m_selection.track_set()}; if (set.size() != 2) return nullptr; return set.back() != set.front() ? set.back() : nullptr; @@ -245,13 +245,13 @@ void parallel_tool::parallel_update() else if (track != nullptr && target != nullptr) tool.measured = std::abs(lateral_of(*track, *target)); - if (track == tool.built_for && tool.spacing == tool.built_spacing && tool.side == tool.built_side && tool.scope == tool.built_scope && m_services.history_size() == tool.built_history) + if (track == tool.built_for && tool.spacing == tool.built_spacing && tool.side == tool.built_side && tool.scope == tool.built_scope && m_history.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_services.history_size(); + tool.built_history = m_history.history_size(); tool.pieces.clear(); tool.styles.clear(); tool.length = 0.0; @@ -330,8 +330,8 @@ void parallel_tool::parallel_build() for (std::size_t i = 0; i < tool.pieces.size(); ++i) created.push_back(editor_track::create_path(*tool.styles[i], tool.pieces[i])); editor_track::commit(created); - m_services.digitize_drawn(created); - m_services.push_track_snapshot({}, created); + m_design.digitize_drawn(created); + m_history.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")}; @@ -376,11 +376,11 @@ void parallel_tool::parallel_apply() if (joint.other != nullptr) committed.push_back(joint.other); editor_track::commit(committed); - m_services.push_track_snapshot(std::move(states)); + m_history.push_track_snapshot(std::move(states)); tool.status = format(STR_C("%s set parallel to %s, %.2f m away; Ctrl+Z takes it back"), name_of(*target).c_str(), name_of(*parallel_source()).c_str(), tool.spacing); WriteLog("Editor: " + tool.status, logtype::generic); tool.built_for = nullptr; - m_services.straight_refresh(); + m_design.straight_refresh(); } void parallel_tool::render_parallel_controls(bool const Scope) @@ -455,9 +455,9 @@ bool parallel_tool::parallel_preview_visible() const { if (m_parallel.pieces.empty() || parallel_source() != m_parallel.built_for) return false; - if (m_services.current_track_tab() == editor_track_tab::parallel) + if (m_view.current_track_tab() == editor_track_tab::parallel) return true; - return m_services.current_track_tab() == editor_track_tab::path && parallel_target() != nullptr; + return m_view.current_track_tab() == editor_track_tab::path && parallel_target() != nullptr; } void parallel_tool::draw_overlay() const diff --git a/application/editormodeparallel.h b/application/editormodeparallel.h index f221c6f4..d10ef41b 100644 --- a/application/editormodeparallel.h +++ b/application/editormodeparallel.h @@ -18,7 +18,7 @@ http://mozilla.org/MPL/2.0/. class parallel_tool final : public editor_tool { public: - explicit parallel_tool(editor_services &Services) : m_services{Services} {} + parallel_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_track_design &Design) : m_selection{Selection}, m_view{View}, m_history{History}, m_design{Design} {} void draw_overlay() const override; void render_options() override; editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; @@ -26,7 +26,10 @@ class parallel_tool final : public editor_tool void render_set_options(); private: - editor_services &m_services; + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + editor_track_design &m_design; tool_tab m_tab; struct parallel_state { diff --git a/application/editormodespeed.cpp b/application/editormodespeed.cpp index 583c489f..c033d9cd 100644 --- a/application/editormodespeed.cpp +++ b/application/editormodespeed.cpp @@ -51,14 +51,14 @@ void speed_tool::speed_start() state.results.clear(); state.selected = -1; state.status.clear(); - state.options.norms = m_services.route_norms(); + state.options.norms = m_design.route_norms(); auto const add = [&](TTrack *Track) { for (int i = 0; i < static_cast(Track->m_paths.size()); ++i) state.queue.emplace_back(Track, i); }; if (state.scope == 0) { - auto *track{m_services.selected_track()}; + auto *track{m_selection.selected_track()}; if (track == nullptr || false == rail(*track)) { state.status = STR_C("Select a track first"); @@ -87,7 +87,7 @@ void speed_tool::speed_start() if (track != nullptr && rail(*track)) add(track); } - state.history = m_services.history_size(); + state.history = m_history.history_size(); } void speed_tool::update() @@ -145,8 +145,8 @@ void speed_tool::speed_focus(int const Index) auto const &paths{simulation::Paths.sequence()}; if (std::find(paths.begin(), paths.end(), verdict.track) == paths.end() || verdict.track->m_editorremoved) return; - m_services.set_current_node(verdict.track); - m_services.focus_track(*verdict.track, verdict.path, verdict.position); + m_selection.set_current_node(verdict.track); + m_view.focus_track(*verdict.track, verdict.path, verdict.position); } void speed_tool::speed_apply(std::vector const &Indices, bool const Branches) @@ -189,7 +189,7 @@ void speed_tool::speed_apply(std::vector const &Indices, bool const Branche state.status = STR_C("Nothing changed") + skipped; return; } - m_services.push_track_snapshot(std::move(states)); + m_history.push_track_snapshot(std::move(states)); for (auto const &entry : changes) { auto const velocity{entry.track->eType == tt_Switch ? speed_check::switch_velocity(entry.limits[0], entry.limits[1]) : entry.limits[0]}; @@ -198,7 +198,7 @@ void speed_tool::speed_apply(std::vector const &Indices, bool const Branche } for (auto &verdict : state.results) verdict.set = speed_check::path_velocity(*verdict.track, verdict.path); - state.history = m_services.history_size(); + state.history = m_history.history_size(); state.status = format(STR_C("Speed limits of %zu paths lowered to the safe ones, Ctrl+Z brings them back"), changes.size()) + skipped; WriteLog("Editor: speed check - " + state.status, logtype::generic); } @@ -262,7 +262,7 @@ void speed_tool::render_options() ImGui::TextWrapped("%s", state.status.c_str()); if (state.results.empty()) return; - if (state.history != m_services.history_size()) + if (state.history != m_history.history_size()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("The scenery changed since the check")); auto const *const within{STR_C("Within their limits too")}; auto const checkbox{ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x + ImGui::CalcTextSize(within).x}; diff --git a/application/editormodespeed.h b/application/editormodespeed.h index a477f59a..cafb8d21 100644 --- a/application/editormodespeed.h +++ b/application/editormodespeed.h @@ -19,7 +19,7 @@ http://mozilla.org/MPL/2.0/. class speed_tool final : public editor_tool { public: - explicit speed_tool(editor_services &Services) : m_services{Services} {} + speed_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_track_design &Design) : m_selection{Selection}, m_view{View}, m_history{History}, m_design{Design} {} void update() override; void draw_overlay() const override; void render_options() override; @@ -28,7 +28,10 @@ class speed_tool final : public editor_tool void check_line(); private: - editor_services &m_services; + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + editor_track_design &m_design; // speed limits of the paths checked against what their geometry allows struct speed_state { diff --git a/application/editormodevehicle.cpp b/application/editormodevehicle.cpp index daf22ceb..e83353f2 100644 --- a/application/editormodevehicle.cpp +++ b/application/editormodevehicle.cpp @@ -209,7 +209,7 @@ bool vehicle_tool::place() return false; } tool.placed = head; - m_services.first_step_done(editor_first_step::drive); + m_guide.first_step_done(editor_first_step::drive); auto const trackname{tool.track->name().empty() ? std::string{STR("(no name)")} : tool.track->name()}; auto const behind{static_cast(vehicles.size()) - 1}; tool.status = behind > 0 ? format(STR_P("%s and %d vehicle behind it stand on %s", "%s and %d vehicles behind it stand on %s", behind), head.c_str(), behind, trackname.c_str()) : format(STR_C("%s stands on %s"), head.c_str(), trackname.c_str()); @@ -240,8 +240,8 @@ bool vehicle_tool::place() void vehicle_tool::render_options() { auto &tool{m_vehicle}; - auto *selected{m_services.selected_track()}; - auto const &hover{m_services.hovered_track()}; + auto *selected{m_selection.selected_track()}; + auto const &hover{m_selection.hovered_track()}; if (selected != nullptr && selected == tool.track && hover.track == selected && ImGui::IsMouseClicked(0) && false == ImGui::GetIO().WantCaptureMouse) start(*selected, hover.point); else if (selected != nullptr && selected != tool.track) diff --git a/application/editormodevehicle.h b/application/editormodevehicle.h index 15b74c13..dabde1ce 100644 --- a/application/editormodevehicle.h +++ b/application/editormodevehicle.h @@ -27,7 +27,7 @@ struct skin_set; class vehicle_tool final : public editor_tool { public: - explicit vehicle_tool(editor_services &Services) : m_services{Services} {} + vehicle_tool(editor_selection &Selection, editor_guide &Guide) : m_selection{Selection}, m_guide{Guide} {} void draw_overlay() const override; void render_options() override; editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; @@ -38,7 +38,8 @@ class vehicle_tool final : public editor_tool void trainsets_written(); private: - editor_services &m_services; + editor_selection &m_selection; + editor_guide &m_guide; tool_tab m_tab; struct vehicle_state { diff --git a/application/editortool.h b/application/editortool.h index fd110f91..19e301bf 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -84,37 +84,91 @@ struct nmt_place bool shapes{true}; // not a bridge nor a tunnel }; -class editor_services +class editor_selection { public: virtual TTrack *selected_track() const = 0; - virtual bool track_editing() const = 0; - virtual std::size_t history_size() const = 0; - virtual glm::dvec3 camera_position() const = 0; - virtual track_hover const &hovered_track() const = 0; virtual std::vector const &track_set() const = 0; - virtual editor_track_tab current_track_tab() const = 0; - virtual void first_step_done(editor_first_step const Step) = 0; - virtual void show_track_tab(editor_track_tab const Wanted) = 0; - virtual glm::dvec3 placement_on_ground(glm::dvec3 Location) = 0; + virtual track_hover const &hovered_track() const = 0; + virtual void set_current_node(scene::basic_node *Node) = 0; + virtual void clear_track_point() = 0; + + protected: + ~editor_selection() = default; +}; + +class editor_view +{ + public: + virtual glm::dvec3 camera_position() const = 0; virtual void fly_to(glm::dvec3 const &Target, glm::dvec3 const &Look, double const Distance, double const Duration = 0.6) = 0; virtual void focus_track(TTrack &Track, int const Path, glm::dvec3 const &Point) = 0; - virtual void set_current_node(scene::basic_node *Node) = 0; - virtual alignment::limits const &route_norms() const = 0; - virtual void clear_track_point() = 0; + virtual editor_track_tab current_track_tab() const = 0; + virtual void show_track_tab(editor_track_tab const Wanted) = 0; + + protected: + ~editor_view() = default; +}; + +class editor_history +{ + public: + virtual std::size_t history_size() const = 0; + virtual void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}) = 0; + + protected: + ~editor_history() = default; +}; + +class editor_templates +{ + public: + virtual std::vector standing_templates(std::function const &Accept) const = 0; + virtual std::size_t place_templates(std::vector const &Items, std::string &Error) = 0; + virtual std::size_t move_templates(std::vector> const &Moves) = 0; + virtual std::size_t remove_templates(std::vector const &Instances) = 0; + + protected: + ~editor_templates() = default; +}; + +class editor_ground +{ + public: + virtual glm::dvec3 placement_on_ground(glm::dvec3 Location) = 0; virtual void nmt_locate() = 0; virtual std::vector nmt_track_places(double const Step) const = 0; virtual editor_nmt &terrain_model() = 0; virtual double terrain_model_offset() const = 0; virtual bool terrain_model_fitted() const = 0; - virtual std::vector standing_templates(std::function const &Accept) const = 0; - virtual std::size_t place_templates(std::vector const &Items, std::string &Error) = 0; - virtual std::size_t move_templates(std::vector> const &Moves) = 0; - virtual std::size_t remove_templates(std::vector const &Instances) = 0; + + protected: + ~editor_ground() = default; +}; + +class editor_track_design +{ + public: + virtual bool track_editing() const = 0; + virtual alignment::limits const &route_norms() const = 0; virtual bool digitize_drawn(std::vector const &Created, bool const Alone = true) = 0; virtual void straight_refresh() = 0; - virtual void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}) = 0; + protected: + ~editor_track_design() = default; +}; + +class editor_guide +{ + public: + virtual void first_step_done(editor_first_step const Step) = 0; + + protected: + ~editor_guide() = default; +}; + +class editor_services : public editor_selection, public editor_view, public editor_history, public editor_templates, public editor_ground, public editor_track_design, public editor_guide +{ protected: ~editor_services() = default; }; From 87bacede74a3ce934e8a2a75171a497158afdd04 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 10:28:42 +0200 Subject: [PATCH 012/145] refactor(editor): the helpers repeated over the editor files are shared The name of a node or "(no name)" was written out in twenty places and four local functions, a lower-case copy of a text in four files, the test for a number, the file name of a template made comparable and the heading of a direction in three and two, the green main button of a panel in five and the key=value parameters of a self-test command in five. Each of them is one helper now: name_of, tall_button_height and main_button with the panel helpers, numeric with format, normalized with the include helpers, heading_of with the plan geometry and selftest_values with the tools; lower gives way to ToLower. --- application/editormode.cpp | 4 +- application/editormodedigitize.cpp | 3 +- application/editormodeinfra.cpp | 7 --- application/editormodejoints.cpp | 14 +++--- application/editormodelay.cpp | 2 +- application/editormodelineside.cpp | 52 +++------------------ application/editormodeparallel.cpp | 10 +--- application/editormodeprofile.cpp | 2 +- application/editormoderoute.cpp | 9 ++-- application/editormodeselection.cpp | 5 +- application/editormodeselftestsignal.cpp | 9 +--- application/editormodeselftestsweep.cpp | 18 +------- application/editormodeshared.h | 19 ++++++++ application/editormodesignal.cpp | 59 ++++++++++-------------- application/editormodespeed.cpp | 4 +- application/editormodesweep.cpp | 23 ++++----- application/editormodeswitchslide.cpp | 9 +--- application/editormodetrack.cpp | 6 +-- application/editormodetrackprops.cpp | 5 -- application/editormodetrackux.cpp | 16 ++----- application/editormodeturntable.cpp | 23 ++------- application/editormodevehicle.cpp | 22 +++------ application/editortool.h | 13 ++++++ editor/editorFormat.hpp | 8 ++++ editor/editorGeometry.hpp | 6 +++ editor/editorIncludeInfo.cpp | 6 +++ editor/editorIncludeInfo.hpp | 1 + 27 files changed, 137 insertions(+), 218 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 952697fa..8b31acf4 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -527,7 +527,7 @@ editor_mode::editor_mode() { std::string reason; if (Node->from_template() || false == scene::Layers.editable(Node, &reason)) { - ui()->set_status(format(STR_C("\"%s\" can't be edited: %s"), (Node->name().empty() ? std::string{STR_C("(no name)")} : Node->name()).c_str(), reason.c_str()), true); + ui()->set_status(format(STR_C("\"%s\" can't be edited: %s"), name_of(*Node).c_str(), reason.c_str()), true); return; } m_track_window_open = false; @@ -6594,7 +6594,7 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const } else if (node->from_template() || false == scene::Layers.editable(node, &reason)) { - ui()->set_status(format(STR_C("\"%s\" can't be edited: %s"), (node->name().empty() ? std::string{STR_C("(no name)")} : node->name()).c_str(), reason.c_str()), true); + ui()->set_status(format(STR_C("\"%s\" can't be edited: %s"), name_of(*node).c_str(), reason.c_str()), true); node = nullptr; } } diff --git a/application/editormodedigitize.cpp b/application/editormodedigitize.cpp index 9eb5f83e..99ea6d26 100644 --- a/application/editormodedigitize.cpp +++ b/application/editormodedigitize.cpp @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" +#include "application/editormodeshared.h" #include "application/editoruilayer.h" #include "editor/editorAlignmentStore.hpp" #include "editor/editorConnections.hpp" @@ -328,7 +329,7 @@ void editor_mode::digitize_crossovers(digitize_report &Report, bool const Write) if (spacing < kSpacingLeast || spacing > kSpacingMost) continue; ++Report.crossovers; - auto const name = [](TTrack const *Track) { return Track->name().empty() ? STR("(no name)") : Track->name(); }; + auto const name = [](TTrack const *Track) { return name_of(*Track); }; auto const note = [&](std::string const &Why) { Report.notes.push_back(name(first) + " + " + name(second) + ": " + Why); }; if (false == switch_store::connections(*first).empty()) { diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index 2644d07c..b8bdfd2e 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -134,13 +134,6 @@ struct include_place std::string category; // of the description of the template }; -bool numeric(std::string const &Text, double &Value) -{ - char *end{nullptr}; - Value = std::strtod(Text.c_str(), &end); - return false == Text.empty() && *end == '\0'; -} - bool include_place_of(scene::instance_handle const Instance, include_place &Place) { if (false == scene::Layers.tracked(Instance)) diff --git a/application/editormodejoints.cpp b/application/editormodejoints.cpp index a9afd279..6b135db1 100644 --- a/application/editormodejoints.cpp +++ b/application/editormodejoints.cpp @@ -137,11 +137,9 @@ ImU32 kind_colour(kind const Kind) } } -std::string name_of(TTrack const *Track) +std::string label_of(TTrack const *Track) { - if (Track == nullptr) - return "-"; - return Track->name().empty() ? STR("(no name)") : Track->name(); + return Track == nullptr ? std::string{"-"} : ::name_of(*Track); } } // namespace @@ -538,7 +536,7 @@ void joints_tool::render_options() { ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(family_colour(2, 0.55f))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(family_colour(2, 0.75f))); - if (ImGui::Button(STR_C("Check"), ImVec2(-1.0f, std::round(ImGui::GetFrameHeight() * 1.3f)))) + if (ImGui::Button(STR_C("Check"), ImVec2(-1.0f, tall_button_height()))) joints_start(); ImGui::PopStyleColor(2); item_tooltip("Finds the gaps between the ends of the tracks, the steps, the kinks and the jumps of the cant and of the grade"); @@ -632,9 +630,9 @@ void joints_tool::render_options() 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::TextUnformatted(label_of(issue.track).c_str()); ImGui::NextColumn(); - ImGui::TextUnformatted(name_of(issue.other).c_str()); + ImGui::TextUnformatted(label_of(issue.other).c_str()); ImGui::NextColumn(); } ImGui::Columns(1); @@ -642,7 +640,7 @@ void joints_tool::render_options() 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()); + ImGui::Text("%s %s: %s / %s", STR_C(kind_name(issue.type)), value_text(issue).c_str(), label_of(issue.track).c_str(), label_of(issue.other).c_str()); if (false == issue.fixed && issue.type != kind::open && issue.type != kind::model && ImGui::Button(STR_C("Fix this one"))) joints_fix({state.selected}); } diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index f6cbb46b..45cf586d 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -1159,7 +1159,7 @@ glm::dvec3 editor_mode::lay_angle_snap(glm::dvec3 const &Mouse, std::string &Lab ratio = n; } } - auto const name{axis->name().empty() ? std::string{STR_C("(no name)")} : axis->name()}; + auto const name{name_of(*axis)}; auto const degrees{glm::degrees(std::abs(std::remainder(angle, glm::pi())))}; Label = ratio > 0.0 ? format(STR_C("1:%g to %s"), ratio, name.c_str()) : degrees < 0.1 ? format(STR_C("parallel to %s"), name.c_str()) : format(STR_C("%.0f deg to %s"), degrees, name.c_str()); } diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp index 56e93528..61073625 100644 --- a/application/editormodelineside.cpp +++ b/application/editormodelineside.cpp @@ -35,6 +35,7 @@ namespace using geometry::bezier; using geometry::plan_distance; +using geometry::heading_of; using geometry::plan_of; double const kSampleStep{2.0}; // m between the samples of the line @@ -47,19 +48,6 @@ 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)}; @@ -68,12 +56,6 @@ std::string number(double const Value, int const Decimals) 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) { @@ -238,7 +220,7 @@ std::vector editor_mode::standing_templates(std: auto const &included{scene::Layers.instance(handle)}; if (included.removed || included.dead || included.file == nullptr) continue; - auto const file{normalized(*included.file)}; + auto const file{editor_includes::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; @@ -649,11 +631,8 @@ void hekto_tool::render_options() tool.posts.empty() ? "-" : km_text(std::max(tool.posts.front().hectometres, tool.posts.back().hectometres)).c_str(), tool.found.size(), static_cast(wrong)); if (missing > 0) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); - if (ImGui::Button(format(STR_P("Place %d post", "Place %d posts", static_cast(missing)), static_cast(missing)).c_str(), ImVec2(-1.0f, std::round(ImGui::GetFrameHeight() * 1.3f)))) + if (main_button(format(STR_P("Place %d post", "Place %d posts", static_cast(missing)), static_cast(missing)).c_str(), ImVec2(-1.0f, tall_button_height()))) hekto_place(); - ImGui::PopStyleColor(2); } if (duplicates > 0) { @@ -938,14 +917,11 @@ void fouling_tool::render_options() ImGui::TextWrapped("%s", tool.status.c_str()); auto const missing{std::count_if(tool.points.begin(), tool.points.end(), [](fouling_point const &Point) { return Point.standing < 0; })}; auto const wrong{std::count_if(tool.found.begin(), tool.found.end(), [](fouling_found const &Found) { return Found.wrong; })}; - auto const tall{std::round(ImGui::GetFrameHeight() * 1.3f)}; + auto const tall{tall_button_height()}; if (missing > 0) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); - if (ImGui::Button(format(STR_P("Place %d marker", "Place %d markers", static_cast(missing)), static_cast(missing)).c_str(), ImVec2(-1.0f, tall))) + if (main_button(format(STR_P("Place %d marker", "Place %d markers", static_cast(missing)), static_cast(missing)).c_str(), ImVec2(-1.0f, tall))) place(); - ImGui::PopStyleColor(2); } if (wrong > 0) { @@ -1069,14 +1045,7 @@ editor_selftest_result hekto_tool::selftest(std::string const &Command, std::ist { 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 values{selftest_values(Words)}; auto *track{simulation::Paths.find(values["track"])}; if (track == nullptr) { @@ -1135,14 +1104,7 @@ editor_selftest_result fouling_tool::selftest(std::string const &Command, std::i { 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 values{selftest_values(Words)}; auto &tool{m_fouling}; if (values["show"] == "1") { diff --git a/application/editormodeparallel.cpp b/application/editormodeparallel.cpp index f5ac7c5d..9253d51f 100644 --- a/application/editormodeparallel.cpp +++ b/application/editormodeparallel.cpp @@ -122,11 +122,6 @@ bool side_picker(int &Side, double const Spacing) return changed; } -std::string name_of(TTrack const &Track) -{ - return Track.name().empty() ? STR("(no name)") : Track.name(); -} - segment_data reversed(segment_data const &Path) { segment_data result{Path}; @@ -416,11 +411,8 @@ void parallel_tool::render_options() } else if (false == tool.pieces.empty()) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); - if (ImGui::Button(format(STR_P("Lay %zu track, %.1f m", "Lay %zu tracks, %.1f m", static_cast(tool.pieces.size())), tool.pieces.size(), tool.length).c_str(), ImVec2(-1.0f, std::round(ImGui::GetFrameHeight() * 1.3f)))) + if (main_button(format(STR_P("Lay %zu track, %.1f m", "Lay %zu tracks, %.1f m", static_cast(tool.pieces.size())), tool.pieces.size(), tool.length).c_str(), ImVec2(-1.0f, tall_button_height()))) parallel_build(); - ImGui::PopStyleColor(2); item_tooltip("A new track beside the one selected, as in the preview; the selected stays as it is. Ctrl+Z takes it back"); } if (false == tool.status.empty()) diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index cc076c1e..75df8b70 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -1493,7 +1493,7 @@ void editor_mode::profile_apply(bool const Terrain) state.status = format(STR_P("Grade line applied: %zu path changed, %zu added, largest departure %.3f m", "Grade line applied: %zu paths changed, %zu added, largest departure %.3f m", changed), changed, added, state.departure); if (false == scene::Layers.empty()) state.status += STR_C("\nThe paths and the design (//$p lines) go to the scenery files on save"); - auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{STR_C("(no name)")} : Track->name(); }; + auto const name = [](TTrack const *Track) { return name_of(*Track); }; auto const closed{std::count_if(gaps.begin(), gaps.end(), [](auto const &Gap) { return Gap.closed; })}; if (closed > 0) state.status += STR_C("\nPaths off the route brought to its height:"); diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index fc46f264..c1c651fd 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -381,7 +381,7 @@ int editor_mode::digitize_parts(editor_track::chain const &Chain, std::string *R if (count == 1) { ++Skipped; - reason = (Chain.tracks.front()->name().empty() ? std::string{STR_C("(no name)")} : Chain.tracks.front()->name()) + ": " + reason; + reason = name_of(*Chain.tracks.front()) + ": " + reason; WriteLog("Editor: digitizing - " + reason, logtype::generic); if (Reason != nullptr && Reason->empty()) *Reason = reason; @@ -805,7 +805,7 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, { auto &route{m_route}; auto const &result{route.result}; - auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{STR_C("(no name)")} : Track->name(); }; + auto const name = [](TTrack const *Track) { return name_of(*Track); }; auto const plan = [](glm::dvec3 const &Vector) { return glm::normalize(plan_of(Vector)); }; struct placement { @@ -1657,14 +1657,11 @@ void editor_mode::render_route_result() if (result.valid) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); - if (ImGui::Button(STR_C("Apply to the scenery"), ImVec2(-1.0f, 28.0f))) + if (main_button(STR_C("Apply to the scenery"), ImVec2(-1.0f, 28.0f))) { route_apply(); first_step_done(first_step::plan_apply); } - ImGui::PopStyleColor(2); } else { diff --git a/application/editormodeselection.cpp b/application/editormodeselection.cpp index 732bd721..42d3d5d5 100644 --- a/application/editormodeselection.cpp +++ b/application/editormodeselection.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" +#include "application/editormodeshared.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "editor/editorFormat.hpp" @@ -271,7 +272,7 @@ void editor_mode::group_selection() { if (reason.empty()) reason = STR("it's a part of a scenery template"); - ui()->set_status(format(STR_C("\"%s\" can't be grouped: %s"), node->name().empty() ? STR_C("(no name)") : node->name().c_str(), reason.c_str()), true); + ui()->set_status(format(STR_C("\"%s\" can't be grouped: %s"), name_of(*node).c_str(), reason.c_str()), true); return; } } @@ -334,7 +335,7 @@ void editor_mode::render_context_menu() if (count > 1) ImGui::TextDisabled(STR_C("%zu models selected"), count); else if (selected) - ImGui::TextDisabled("%s", m_node->name().empty() ? STR_C("(no name)") : m_node->name().c_str()); + ImGui::TextDisabled("%s", name_of(*m_node).c_str()); else ImGui::TextDisabled("%s", STR_C("Nothing selected")); ImGui::Separator(); diff --git a/application/editormodeselftestsignal.cpp b/application/editormodeselftestsignal.cpp index dac9c63d..fe64c7ef 100644 --- a/application/editormodeselftestsignal.cpp +++ b/application/editormodeselftestsignal.cpp @@ -52,14 +52,7 @@ editor_mode::selftest_result editor_mode::selftest_signal(std::string const &com { if (command == "signal" || command == "signalmove" || command == "signaldelete" || command == "signalrename" || command == "signalprobe") { - 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 values{selftest_values(words)}; show_track_tab(track_tab::signals); signal_scan(); signal_refresh(true); diff --git a/application/editormodeselftestsweep.cpp b/application/editormodeselftestsweep.cpp index 38289631..2524246e 100644 --- a/application/editormodeselftestsweep.cpp +++ b/application/editormodeselftestsweep.cpp @@ -52,14 +52,7 @@ editor_mode::selftest_result editor_mode::selftest_sweep(std::string const &comm { 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 values{selftest_values(words)}; 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; @@ -158,14 +151,7 @@ editor_mode::selftest_result editor_mode::selftest_sweep(std::string const &comm } if (command == "bend" || command == "bendset") { - 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 values{selftest_values(words)}; auto const vector = [](std::string Text) { std::replace(Text.begin(), Text.end(), ',', ' '); std::istringstream numbers(Text); diff --git a/application/editormodeshared.h b/application/editormodeshared.h index 149a4a09..0041e34a 100644 --- a/application/editormodeshared.h +++ b/application/editormodeshared.h @@ -24,6 +24,25 @@ http://mozilla.org/MPL/2.0/. float constexpr kDragPixels{5.0f}; +template inline std::string name_of(Node_ const &Node) +{ + return Node.name().empty() ? std::string{STR_C("(no name)")} : Node.name(); +} + +inline float tall_button_height() +{ + return std::round(ImGui::GetFrameHeight() * 1.3f); +} + +inline bool main_button(char const *Label, ImVec2 const &Size) +{ + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); + auto const pressed{ImGui::Button(Label, Size)}; + ImGui::PopStyleColor(2); + return pressed; +} + inline void item_tooltip(char const *Text) { if (ImGui::IsItemHovered()) diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 0156c914..e9d04354 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -36,6 +36,7 @@ namespace { using geometry::bezier; +using geometry::heading_of; using geometry::plan_of; double const kRailTop{0.18}; @@ -58,12 +59,6 @@ int kind_index(std::string const &Kind) return -1; } -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; -} - double nearest_on(TTrack const &Track, int const Path, glm::dvec3 const &Point) { bezier const curve{Track.m_paths[Path]}; @@ -93,12 +88,6 @@ glm::dvec2 right_of(glm::dvec2 const &Direction) return {-Direction.y, Direction.x}; } -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; -} - int lean_wanted(int const Side) { return Side > 0 ? 1 : 2; @@ -159,7 +148,7 @@ std::vector const &plate_textures() std::string stem; try { - stem = lower(file->path().stem().string()); + stem = ToLower(file->path().stem().string()); } catch (std::exception const &) { @@ -177,7 +166,7 @@ std::string plate_for(std::string const &Plate, std::string const &Name) if (false == Plate.empty()) return Plate; auto const &plates{plate_textures()}; - auto const name{lower(Name)}; + auto const name{ToLower(Name)}; return std::binary_search(plates.begin(), plates.end(), name) ? name : std::string{"0_nothing"}; } @@ -304,7 +293,7 @@ editor_mode::signal_template const *editor_mode::signal_armed() const editor_mode::signal_template const *editor_mode::signal_template_of(std::string const &File) const { - auto const file{lower(File)}; + auto const file{ToLower(File)}; for (auto const &item : m_signal.templates) if (item.file == file) return &item; @@ -350,7 +339,7 @@ std::string editor_mode::signal_intent_label(signal_template const &Template, si if (Template.read.empty()) return format(STR_C("Click: %s %s (%s)%s, the train doesn't read it"), STR(kKindLabels[kind_index(Template.kind)]).c_str(), name.c_str(), variant.c_str(), where.c_str()) + "\n" + STR("Drag along: the trains it faces run the way of the drag"); auto const event{editor_includes::substitute(Template.read, {name})}; - auto const path{Spot.track->name().empty() ? std::string{STR("(no name)")} : Spot.track->name()}; + auto const path{name_of(*Spot.track)}; return format(STR_C("Click: %s %s (%s)%s, read by %s %s of %s"), STR(kKindLabels[kind_index(Template.kind)]).c_str(), name.c_str(), variant.c_str(), where.c_str(), editor_track::event_keyword(Spot.list), event.c_str(), path.c_str()) + "\n" + STR("Drag along: the trains it faces run the way of the drag"); } @@ -376,7 +365,7 @@ void editor_mode::signal_refresh(bool const Force) for (int list = 0; list < 6; ++list) for (auto const &event : editor_track::events(*track, list)) if (false == event.first.empty()) - readers.emplace(lower(event.first), std::make_pair(track, list)); + readers.emplace(ToLower(event.first), std::make_pair(track, list)); } for (auto const &include : found) { @@ -388,7 +377,7 @@ void editor_mode::signal_refresh(bool const Force) if (false == item->read.empty()) { standing.read = editor_includes::substitute(item->read, include.values); - if (auto const reader{readers.find(lower(standing.read))}; reader != readers.end()) + if (auto const reader{readers.find(ToLower(standing.read))}; reader != readers.end()) { standing.track = reader->second.first; standing.list = reader->second.second; @@ -564,7 +553,7 @@ void editor_mode::signal_release() signal_detach_events(found->read, states); if (std::none_of(states.begin(), states.end(), [&](auto const &State) { return State.first == spot.track; })) states.emplace_back(spot.track, editor_track::capture(*spot.track)); - editor_track::events(*spot.track, spot.list).emplace_back(lower(found->read), nullptr); + editor_track::events(*spot.track, spot.list).emplace_back(ToLower(found->read), nullptr); std::string missing; editor_track::bind_events(*spot.track, missing); spot.track->mark_dirty(); @@ -585,7 +574,7 @@ void editor_mode::signal_release() m_history.back().directives.emplace_back(found->include.instance, std::move(before)); } tool.status = found->read.empty() ? format(STR_C("Signal %s moved, Ctrl+Z takes it back"), found->name.c_str()) - : format(STR_C("Signal %s moved, read by %s %s of %s, Ctrl+Z takes it back"), found->name.c_str(), editor_track::event_keyword(spot.list), found->read.c_str(), spot.track->name().empty() ? STR_C("(no name)") : spot.track->name().c_str()); + : format(STR_C("Signal %s moved, read by %s %s of %s, Ctrl+Z takes it back"), found->name.c_str(), editor_track::event_keyword(spot.list), found->read.c_str(), name_of(*spot.track).c_str()); auto const instance{found->include.instance}; signal_refresh(true); signal_select(instance); @@ -639,7 +628,7 @@ bool editor_mode::signal_place(signal_spot const &Spot, signal_template const &T } signal_refresh(true); auto const taken = [&](std::string const &Name) { - return std::any_of(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return lower(Standing.name) == lower(Name); }); + return std::any_of(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return ToLower(Standing.name) == ToLower(Name); }); }; for (int guard = 0; taken(name) && guard < 1000; ++guard) name = signal_next_name(name); @@ -663,7 +652,7 @@ bool editor_mode::signal_place(signal_spot const &Spot, signal_template const &T auto const instance{m_history.back().instances.front()}; auto const *placed{signal_template_of(file)}; auto const read{placed != nullptr && false == placed->read.empty() ? editor_includes::substitute(placed->read, {name}) : std::string{}}; - auto const path{Spot.track->name().empty() ? std::string{STR("(no name)")} : Spot.track->name()}; + auto const path{name_of(*Spot.track)}; std::string reason; if (read.empty()) tool.status = format(STR_C("Signal %s placed (%s), the train doesn't read it, Ctrl+Z takes it back"), name.c_str(), file.c_str()); @@ -676,7 +665,7 @@ bool editor_mode::signal_place(signal_spot const &Spot, signal_template const &T auto snap{std::move(m_history.back())}; m_history.pop_back(); auto const before{editor_track::capture(*Spot.track)}; - editor_track::events(*Spot.track, Spot.list).emplace_back(lower(read), nullptr); + editor_track::events(*Spot.track, Spot.list).emplace_back(ToLower(read), nullptr); std::string missing; editor_track::bind_events(*Spot.track, missing); Spot.track->mark_dirty(); @@ -693,7 +682,7 @@ bool editor_mode::signal_place(signal_spot const &Spot, signal_template const &T void editor_mode::signal_detach_events(std::string const &Read, std::vector> &States) { - auto const name{lower(Read)}; + auto const name{ToLower(Read)}; for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || track->m_editorremoved) @@ -701,7 +690,7 @@ void editor_mode::signal_detach_events(std::string const &Read, std::vectorm_editorremoved) @@ -755,15 +744,15 @@ void editor_mode::signal_renamed(std::string const &Before, std::string const &A bool carries{false}; for (int list = 0; list < 6 && false == carries; ++list) for (auto const &event : editor_track::events(*track, list)) - carries = carries || lower(event.first) == name; + carries = carries || ToLower(event.first) == name; if (false == carries) continue; if (std::none_of(Snapshot.tracks.begin(), Snapshot.tracks.end(), [&](auto const &State) { return State.first == track; })) Snapshot.tracks.emplace_back(track, editor_track::capture(*track)); for (int list = 0; list < 6; ++list) for (auto &event : editor_track::events(*track, list)) - if (lower(event.first) == name) - event = {lower(After), nullptr}; + if (ToLower(event.first) == name) + event = {ToLower(After), nullptr}; std::string missing; editor_track::bind_events(*track, missing); track->mark_dirty(); @@ -790,7 +779,7 @@ void editor_mode::render_signal_palette() auto &tool{m_signal}; ImGui::SetNextItemWidth(-1.0f); ImGui::InputTextWithHint("##signalfilter", STR_C("filter: name, w24, sbl..."), tool.filter, sizeof(tool.filter)); - auto const filter{lower(tool.filter)}; + auto const filter{ToLower(tool.filter)}; auto const *armed{signal_armed()}; std::vector> groups; std::vector keys; @@ -799,7 +788,7 @@ void editor_mode::render_signal_palette() auto const &item{tool.templates[i]}; if (item.kind != kKinds[tool.kind]) continue; - if (false == filter.empty() && item.name.find(filter) == std::string::npos && lower(item.description).find(filter) == std::string::npos) + if (false == filter.empty() && item.name.find(filter) == std::string::npos && ToLower(item.description).find(filter) == std::string::npos) continue; auto const key{group_of(item.mount, item.lamps)}; auto const found{std::find(keys.begin(), keys.end(), key)}; @@ -897,7 +886,7 @@ void editor_mode::render_signal_selected() if (found->read.empty()) ImGui::TextDisabled("%s", STR_C("The train doesn't read it")); else if (found->track != nullptr) - ImGui::TextDisabled(STR_C("Read by %s %s of %s"), editor_track::event_keyword(found->list), found->read.c_str(), found->track->name().empty() ? STR_C("(no name)") : found->track->name().c_str()); + ImGui::TextDisabled(STR_C("Read by %s %s of %s"), editor_track::event_keyword(found->list), found->read.c_str(), name_of(*found->track).c_str()); else ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), STR_C("No path has %s among its events: the train doesn't see it"), found->read.c_str()); } @@ -954,7 +943,7 @@ void editor_mode::render_signal_ui() if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Texture of the plate from textures/tabl, e.g. a-12 or tob. Left empty: the one named like the signal, if there is one")); auto const &plates{plate_textures()}; - auto const typed{lower(tool.plate)}; + auto const typed{ToLower(tool.plate)}; auto const exists{typed.empty() || std::binary_search(plates.begin(), plates.end(), typed)}; if (false == exists) ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR_C("no such texture")); @@ -987,7 +976,7 @@ void editor_mode::render_signal_ui() if (missing) { auto const &plates{plate_textures()}; - auto const typed{lower(tool.plate)}; + auto const typed{ToLower(tool.plate)}; int shown{0}; for (auto const &plate : plates) { diff --git a/application/editormodespeed.cpp b/application/editormodespeed.cpp index c033d9cd..8927886b 100644 --- a/application/editormodespeed.cpp +++ b/application/editormodespeed.cpp @@ -223,7 +223,7 @@ void speed_tool::render_options() { ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(family_colour(2, 0.55f))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(family_colour(2, 0.75f))); - if (ImGui::Button(STR_C("Check"), ImVec2(-1.0f, std::round(ImGui::GetFrameHeight() * 1.3f)))) + if (ImGui::Button(STR_C("Check"), ImVec2(-1.0f, tall_button_height()))) speed_start(); ImGui::PopStyleColor(2); item_tooltip("Looks at the radius and the cant, the cant ramps, the curves without the transitions and the switch branches"); @@ -312,7 +312,7 @@ void speed_tool::render_options() bool const violated{verdict.violated(state.options)}; bool const bad{violated || verdict.warned(state.options)}; auto const colour{violated ? ImVec4(1.0f, 0.45f, 0.4f, 1.0f) : ImVec4(1.0f, 0.75f, 0.3f, 1.0f)}; - auto name{verdict.track->name().empty() ? STR("(no name)") : verdict.track->name()}; + auto name{name_of(*verdict.track)}; if (verdict.track->eType == tt_Switch) name += verdict.path == 0 ? STR_C(" (main)") : STR_C(" (branch)"); if (bad) diff --git a/application/editormodesweep.cpp b/application/editormodesweep.cpp index 5fe783a6..e430e619 100644 --- a/application/editormodesweep.cpp +++ b/application/editormodesweep.cpp @@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "utilities/Globals.h" #include "utilities/Logs.h" +#include "utilities/utilities.h" #include "world/Sweep.h" #include "model/AnimModel.h" #include "world/Track.h" @@ -126,12 +127,6 @@ bool same_piece(segment_data const &A, segment_data const &B) 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() @@ -153,7 +148,7 @@ void editor_mode::sweep_scan_models() std::string name; try { - extension = lower(entry->path().extension().string()); + extension = ToLower(entry->path().extension().string()); if (extension != ".e3d" && extension != ".t3d") continue; name = std::filesystem::relative(entry->path(), "models", status).generic_string(); @@ -754,14 +749,14 @@ void editor_mode::render_sweep_ui() 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)}; + std::string const text{ToLower(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) + if (ToLower(model).find(text) == std::string::npos) continue; if (++listed > kSweepModelList) break; @@ -775,7 +770,7 @@ void editor_mode::render_sweep_ui() } // 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"}; + bool const istemplate{model.size() > 4 && ToLower(model.substr(model.size() - 4)) == ".inc"}; if (istemplate && model != tool.parameters_for) { tool.parameters_for = model; @@ -803,7 +798,7 @@ void editor_mode::render_sweep_ui() 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 const prefix{ToLower(word.substr(0, open))}; auto suffix{word.substr(close + 1)}; suffix = suffix.substr(0, suffix.find(':')); auto const slash{prefix.rfind('/')}; @@ -816,13 +811,13 @@ void editor_mode::render_sweep_ui() std::string stem; try { - stem = lower(entry->path().stem().string()); + stem = ToLower(entry->path().stem().string()); } catch (std::exception const &) { continue; } - 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)) + if (stem.size() > start.size() + suffix.size() && stem.compare(0, start.size(), start) == 0 && (suffix.empty() || stem.compare(stem.size() - suffix.size(), suffix.size(), ToLower(suffix)) == 0)) variants.insert(stem.substr(start.size(), stem.size() - start.size() - suffix.size())); } tool.parameter_choices[id - 1].assign(variants.begin(), variants.end()); @@ -1836,7 +1831,7 @@ void editor_mode::render_bend() else { tool.track = std::clamp(tool.track, 0, static_cast(tool.tracks.size()) - 1); - auto const label = [](std::pair const &Entry) { return format("%s (%.1f m)", Entry.first->name().empty() ? STR_C("(no name)") : Entry.first->name().c_str(), Entry.second); }; + auto const label = [](std::pair const &Entry) { return format("%s (%.1f m)", name_of(*Entry.first).c_str(), Entry.second); }; if (begin_fields("##bendsource", 0.35f)) { field_row("Track"); diff --git a/application/editormodeswitchslide.cpp b/application/editormodeswitchslide.cpp index 63040584..d58b9b5b 100644 --- a/application/editormodeswitchslide.cpp +++ b/application/editormodeswitchslide.cpp @@ -43,11 +43,6 @@ struct switch_end_move double shift{0.0}; }; -std::string track_label(TTrack const &Track) -{ - return Track.name().empty() ? std::string{STR_C("(no name)")} : Track.name(); -} - std::optional> switch_end_moves(TTrack const &Track, segment_data const &Main, bool const Online, std::pair const &Room, double const Offset, std::string &Status) { auto const axis{editor_track::main_axis(Track)}; @@ -83,7 +78,7 @@ std::string switch_rejoin(TTrack &Track, std::vector const auto const shift{glm::distance(editor_track::point_position(Track, joint.own), editor_track::point_position(*joint.other, joint.theirs))}; if (shift > 0.3 * bezier{theirs}.plan_length()) { - loose += (loose.empty() ? "" : ", ") + track_label(*joint.other); + loose += (loose.empty() ? "" : ", ") + name_of(*joint.other); continue; } following.push_back(joint); @@ -579,7 +574,7 @@ std::string editor_mode::plan_switch_branches(TTrack const &Track, std::vectorpart.tracks, reason)) { - notes += (notes.empty() ? "" : "\n") + format(STR_C("The curve past the end of the switch can't keep to it, %s follows the switch on its own"), track_label(*joint.other).c_str()); + notes += (notes.empty() ? "" : "\n") + format(STR_C("The curve past the end of the switch can't keep to it, %s follows the switch on its own"), name_of(*joint.other).c_str()); branch = Branches.erase(branch); continue; } diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index e9ada20f..45ba31c8 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -302,7 +302,7 @@ bool editor_mode::tracks_editable(std::vector const &Tracks, std::stri std::string reason; if (track != nullptr && false == editor_track::can_edit_geometry(*track, reason)) { - Reason = (track->name().empty() ? std::string{STR_C("(no name)")} : track->name()) + ": " + reason; + Reason = name_of(*track) + ": " + reason; return false; } } @@ -698,7 +698,7 @@ void editor_mode::render_track_selection() auto *track{tracks.front()}; if (false == sheet) { - ImGui::TextUnformatted(track->name().empty() ? STR_C("(no name)") : track->name().c_str()); + ImGui::TextUnformatted(name_of(*track).c_str()); ImGui::TextDisabled("%s, %.2f m", STR_C(type_name(*track)), track->Length()); return; } @@ -1743,7 +1743,7 @@ std::vector editor_mode::track_key_hints(bool const All) // values of the change in progress, shown next to the cursor std::string editor_mode::track_readout() const { - auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{STR_C("(no name)")} : Track->name(); }; + auto const name = [](TTrack const *Track) { return name_of(*Track); }; auto const azimuth = [](glm::dvec3 const &From, glm::dvec3 const &To) { auto const degrees{glm::degrees(std::atan2(To.x - From.x, To.z - From.z))}; return degrees < 0.0 ? degrees + 360.0 : degrees; diff --git a/application/editormodetrackprops.cpp b/application/editormodetrackprops.cpp index 4d507023..8804e819 100644 --- a/application/editormodetrackprops.cpp +++ b/application/editormodetrackprops.cpp @@ -57,11 +57,6 @@ 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{STR_C("(no name)")} : Track.name(); -} - char const *type_of(TTrack const &Track) { switch (Track.eType) diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 238b0cfe..3cb577f6 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -18,6 +18,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Globals.h" #include "rendering/renderer.h" #include "world/Track.h" +#include "utilities/utilities.h" #include "scene/scene.h" #include "scene/scenelayers.h" #include "simulation/simulation.h" @@ -61,17 +62,6 @@ 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{STR_C("(no name)")} : 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) @@ -843,7 +833,7 @@ void editor_mode::render_track_search() } 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)}; + std::string const text{ToLower(search.text)}; if (text.empty()) { search.searched.clear(); @@ -857,7 +847,7 @@ void editor_mode::render_track_search() search.total = 0; for (auto *track : simulation::Paths.sequence()) { - if (track == nullptr || false == rail(*track) || lower(track->name()).find(text) == std::string::npos) + if (track == nullptr || false == rail(*track) || ToLower(track->name()).find(text) == std::string::npos) continue; ++search.total; if (search.found.size() < kSearchLimit) diff --git a/application/editormodeturntable.cpp b/application/editormodeturntable.cpp index 1cc35b63..47b719c5 100644 --- a/application/editormodeturntable.cpp +++ b/application/editormodeturntable.cpp @@ -77,19 +77,6 @@ double line_angle(double const Angle) 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; @@ -280,8 +267,8 @@ bool editor_mode::turntable_read(TTrack *Table) 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; })}; + auto const file{editor_includes::normalized(*included.file)}; + auto const kind{std::find_if(tool.templates.begin(), tool.templates.end(), [&](turntable_template const &Template) { return editor_includes::normalized(Template.file) == file; })}; if (kind == tool.templates.end()) continue; std::string target; @@ -559,7 +546,7 @@ std::vector editor_mode::turntable_includes(TTrack const auto const &included{scene::Layers.instance(handle)}; if (included.removed || included.dead || included.file == nullptr) continue; - auto const kind{std::find_if(m_turntable.templates.begin(), m_turntable.templates.end(), [&](turntable_template const &Template) { return normalized(Template.file) == normalized(*included.file); })}; + auto const kind{std::find_if(m_turntable.templates.begin(), m_turntable.templates.end(), [&](turntable_template const &Template) { return editor_includes::normalized(Template.file) == editor_includes::normalized(*included.file); })}; std::string target; std::vector values; if (kind != m_turntable.templates.end() && editor_includes::parse_directive(scene::Layers.directive(handle), target, values) && kind->track <= static_cast(values.size()) && ToLower(values[kind->track - 1]) == Table.name()) @@ -959,7 +946,7 @@ void editor_mode::render_turntable_ui() else ImGui::TextDisabled(" - "); ImGui::SameLine(); - auto const name{fit.end.track->name().empty() ? std::string{STR_C("(no name)")} : fit.end.track->name()}; + auto const name{name_of(*fit.end.track)}; 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()) @@ -1158,7 +1145,7 @@ bool editor_mode::turntable_controls(std::vector &Rootstatements) auto const &included{scene::Layers.instance(handle)}; if (included.removed || included.dead || included.file == nullptr) continue; - auto const kind{std::find_if(m_turntable.templates.begin(), m_turntable.templates.end(), [&](turntable_template const &Template) { return normalized(Template.file) == normalized(*included.file); })}; + auto const kind{std::find_if(m_turntable.templates.begin(), m_turntable.templates.end(), [&](turntable_template const &Template) { return editor_includes::normalized(Template.file) == editor_includes::normalized(*included.file); })}; if (kind == m_turntable.templates.end() || kind->angles == 0) continue; std::string target; diff --git a/application/editormodevehicle.cpp b/application/editormodevehicle.cpp index e83353f2..1a640076 100644 --- a/application/editormodevehicle.cpp +++ b/application/editormodevehicle.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "utilities/Globals.h" #include "utilities/Logs.h" +#include "utilities/utilities.h" #include "vehicle/DynObj.h" #include "world/Track.h" @@ -37,12 +38,6 @@ 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_name(ui::skin_set const &Skin) { return Skin.meta != nullptr && false == Skin.meta->name.empty() ? Skin.meta->name : Skin.skin; @@ -191,7 +186,7 @@ bool vehicle_tool::place() std::string name; for (int index = 1; index < 10000; ++index) { - name = lower("editor_" + stem + "_" + std::to_string(index)); + name = ToLower("editor_" + stem + "_" + std::to_string(index)); if (simulation::Vehicles.find(name) == nullptr && std::find(names.begin(), names.end(), name) == names.end()) break; } @@ -210,7 +205,7 @@ bool vehicle_tool::place() } tool.placed = head; m_guide.first_step_done(editor_first_step::drive); - auto const trackname{tool.track->name().empty() ? std::string{STR("(no name)")} : tool.track->name()}; + auto const trackname{name_of(*tool.track)}; auto const behind{static_cast(vehicles.size()) - 1}; tool.status = behind > 0 ? format(STR_P("%s and %d vehicle behind it stand on %s", "%s and %d vehicles behind it stand on %s", behind), head.c_str(), behind, trackname.c_str()) : format(STR_C("%s stands on %s"), head.c_str(), trackname.c_str()); if (tool.write && tool.track->name().empty()) @@ -259,7 +254,7 @@ void vehicle_tool::render_options() 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::string const text{ToLower(tool.search)}; std::vector, std::shared_ptr>> listed; std::size_t total{0}; for (auto const &[path, vehicle] : tool.bank->vehicles) @@ -270,7 +265,7 @@ void vehicle_tool::render_options() { 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)) + if (false == text.empty() && ToLower(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) @@ -426,13 +421,10 @@ void vehicle_tool::render_options() if (false == tool.consist.empty() || (tool.vehicle != nullptr && tool.skin != nullptr)) { auto const half{std::floor((ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f)}; - auto const tall{std::round(ImGui::GetFrameHeight() * 1.3f)}; - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); - if (ImGui::Button(STR_C("Place"), ImVec2(half, tall))) + auto const tall{tall_button_height()}; + if (main_button(STR_C("Place"), ImVec2(half, tall))) place(); item_tooltip("A trainset on the track, placed where the track was clicked, or in the middle of the path"); - ImGui::PopStyleColor(2); ImGui::SameLine(); if (ImGui::Button(STR_C("Place and drive"), ImVec2(-1.0f, tall)) && place()) { diff --git a/application/editortool.h b/application/editortool.h index 19e301bf..f93503f8 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -84,6 +84,19 @@ struct nmt_place bool shapes{true}; // not a bridge nor a tunnel }; +inline std::map selftest_values(std::istringstream &Words) +{ + 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); + } + return values; +} + class editor_selection { public: diff --git a/editor/editorFormat.hpp b/editor/editorFormat.hpp index e22a9745..0b0940ee 100644 --- a/editor/editorFormat.hpp +++ b/editor/editorFormat.hpp @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #pragma once #include +#include #include // printf-style formatting into a string, for the messages and the data lines of the editor @@ -24,3 +25,10 @@ std::string format(char const *Format, Args... Arguments) text.resize(static_cast(length)); return text; } + +inline bool numeric(std::string const &Text, double &Value) +{ + char *end{nullptr}; + Value = std::strtod(Text.c_str(), &end); + return false == Text.empty() && *end == '\0'; +} diff --git a/editor/editorGeometry.hpp b/editor/editorGeometry.hpp index a9ec6667..6c2e6365 100644 --- a/editor/editorGeometry.hpp +++ b/editor/editorGeometry.hpp @@ -38,6 +38,12 @@ inline double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B) } // z component of the 3d cross product; for a unit Direction it's the offset of the Point to its left +inline 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; +} + inline double cross(glm::dvec2 const &Direction, glm::dvec2 const &Point) { return Direction.x * Point.y - Direction.y * Point.x; diff --git a/editor/editorIncludeInfo.cpp b/editor/editorIncludeInfo.cpp index 45129f57..55b7cd4f 100644 --- a/editor/editorIncludeInfo.cpp +++ b/editor/editorIncludeInfo.cpp @@ -590,6 +590,12 @@ bool complete(include_info const &Info, int const Parameters, std::string *Issue return issue.empty(); } +std::string normalized(std::string File) +{ + std::replace(File.begin(), File.end(), '\\', '/'); + return ToLower(File); +} + std::string number(double const Value) { std::ostringstream converter; diff --git a/editor/editorIncludeInfo.hpp b/editor/editorIncludeInfo.hpp index 0f78cd76..4b70de17 100644 --- a/editor/editorIncludeInfo.hpp +++ b/editor/editorIncludeInfo.hpp @@ -89,6 +89,7 @@ bool parse_directive(std::string const &Directive, std::string &File, std::vecto std::string compose_directive(std::string const &File, std::vector const &Values); // text of a number passed as a parameter value std::string number(double Value); +std::string normalized(std::string File); // number of the parameter with specified role, 0 if the description has none int parameter_with_role(include_info const &Info, std::string const &Role); From 7da0432dcaec3fdce1d2e098ea70d9f40b5aea3e Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 10:44:58 +0200 Subject: [PATCH 013/145] fix(editor): a signal goes to the other side of the track and turns around Dragging a standing signal over the track takes it to the other side; the side used to come from where the signal stood, so it never crossed. The selected signal gets "To the other side" and "Turn around": the first mirrors it over the axis facing the same trains, the second turns it to the trains of the other direction and moves its event to the other event list of the path. Either one swaps a head leaning to the track for its counterpart, and an include keeps the template its directive names, so undo brings the old file back too. --- application/editormode.cpp | 7 +- application/editormode.h | 12 +- application/editormodeselftestsignal.cpp | 37 ++-- application/editormodesignal.cpp | 226 +++++++++++++++++------ application/editormodetrack.cpp | 6 +- lang/editor_pl.po | 33 ++++ scene/scenelayers.cpp | 6 +- scene/scenelayers.h | 2 +- 8 files changed, 251 insertions(+), 78 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 8b31acf4..a8a92811 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -3058,7 +3058,12 @@ void editor_mode::select_include(scene::instance_handle const Instance) void editor_mode::set_include_directive(scene::instance_handle const Instance, std::string const &Directive) { - if (false == scene::Layers.modify(Instance, Directive)) + std::string file; + std::vector values; + if (scene::Layers.tracked(Instance) && editor_includes::parse_directive(Directive, file, values) && scene::Layers.instance(Instance).file != nullptr && + editor_includes::normalized(file) == editor_includes::normalized(*scene::Layers.instance(Instance).file)) + file.clear(); + if (false == scene::Layers.modify(Instance, Directive, file)) return; std::vector retired; simulation::State.rebuild_include(Instance, retired); diff --git a/application/editormode.h b/application/editormode.h index 86e8b72d..0205ff72 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1796,8 +1796,6 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::string read; TTrack *track{nullptr}; int list{0}; - signal_spot spot; - bool on_track{false}; }; struct signal_tool { @@ -1821,6 +1819,9 @@ class editor_mode : public application_mode, private editor_track::observer, pri int hover{-1}; scene::instance_handle selected{0}; bool moving{false}; + glm::dvec2 grab{0.0}; + signal_spot home; + bool home_valid{false}; signal_spot moved; bool moved_valid{false}; std::string status; @@ -1836,7 +1837,12 @@ class editor_mode : public application_mode, private editor_track::observer, pri void signal_release(); bool signal_place(signal_spot const &Spot, signal_template const &Template); void signal_move_update(); - void signal_move_finish(); + bool signal_standing_spot(signal_standing const &Standing, signal_spot &Spot) const; + bool signal_drop_at(signal_standing const &Standing, TTrack &Track, int const Path, glm::dvec3 const &Point, int const Side, signal_spot &Spot) const; + bool signal_move_to(signal_standing const &Standing, signal_spot const &Spot, char const *Read, char const *Unread); + bool signal_flip(scene::instance_handle const Instance); + bool signal_turn(scene::instance_handle const Instance); + signal_standing const *signal_standing_of(scene::instance_handle const Instance) const; bool signal_delete(scene::instance_handle const Instance); void signal_detach_events(std::string const &Read, std::vector> &States); void signal_renamed(std::string const &Before, std::string const &After, EditorSnapshot &Snapshot); diff --git a/application/editormodeselftestsignal.cpp b/application/editormodeselftestsignal.cpp index fe64c7ef..db7c1624 100644 --- a/application/editormodeselftestsignal.cpp +++ b/application/editormodeselftestsignal.cpp @@ -50,7 +50,7 @@ http://mozilla.org/MPL/2.0/. editor_mode::selftest_result editor_mode::selftest_signal(std::string const &command, std::istringstream &words) { - if (command == "signal" || command == "signalmove" || command == "signaldelete" || command == "signalrename" || command == "signalprobe") + if (command == "signal" || command == "signalmove" || command == "signaldelete" || command == "signalrename" || command == "signalprobe" || command == "signalflip" || command == "signalturn") { auto values{selftest_values(words)}; show_track_tab(track_tab::signals); @@ -107,29 +107,40 @@ editor_mode::selftest_result editor_mode::selftest_signal(std::string const &com else if (command == "signalmove") { auto const *standing{standing_named(values["name"])}; - auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr}; - if (item == nullptr) + auto *track{simulation::Paths.find(values["track"])}; + if (standing == nullptr || track == nullptr || track->m_paths.empty()) { - WriteLog("SELFTEST signalmove: no signal " + values["name"]); + WriteLog("SELFTEST signalmove: no signal " + values["name"] + " or no track " + values["track"]); return selftest_result::handled; } - auto const yaw{glm::radians(standing->include.yaw + 180.0)}; - glm::dvec2 const travel{std::sin(yaw), std::cos(yaw)}; - signal_spot probe; - if (false == spot_on(*item, 1, 1, probe)) + auto const path{std::clamp(values.count("path") ? std::stoi(values["path"]) : 0, 0, static_cast(track->m_paths.size()) - 1)}; + auto const at{values.count("at") ? std::stod(values["at"]) : 0.5}; + geometry::bezier const curve{track->m_paths[path]}; + auto point{curve.point(at)}; + auto const sided{values.count("side") != 0}; + auto side{values["side"] == "left" ? -1 : 1}; + if (values.count("off")) { - WriteLog("SELFTEST signalmove: no such track " + values["track"]); - return selftest_result::handled; + auto const tangent{glm::normalize(glm::dvec2{curve.first(at).x, curve.first(at).z})}; + auto const off{std::stod(values["off"])}; + point += glm::dvec3{-tangent.y * off, 0.0, tangent.x * off}; + side = 0; } - auto const facing{glm::dot(probe.travel, travel) >= 0.0 ? 1 : -1}; - auto const sideoftravel{glm::dot(geometry::plan_of(standing->include.location) - geometry::plan_of(standing->spot.axis), glm::dvec2{-travel.y, travel.x}) >= 0.0 ? 1 : -1}; + else if (signal_spot home; false == sided && signal_standing_spot(*standing, home)) + side = home.side * (home.list == 2 ? 1 : -1); signal_select(standing->include.instance); tool.moving = true; - tool.moved_valid = spot_on(*item, facing, sideoftravel * facing, tool.moved); + tool.moved_valid = signal_drop_at(*standing, *track, path, point, side, tool.moved); tool.pressed = {-1000.f, -1000.f}; signal_release(); WriteLog("SELFTEST signalmove: " + tool.status); } + else if (command == "signalflip" || command == "signalturn") + { + auto const *standing{standing_named(values["name"])}; + auto const done{standing != nullptr && (command == "signalflip" ? signal_flip(standing->include.instance) : signal_turn(standing->include.instance))}; + WriteLog("SELFTEST " + command + " " + values["name"] + (done ? ": " : ": failed, ") + tool.status); + } else if (command == "signaldelete") { auto const *standing{standing_named(values["name"])}; diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index e9d04354..2e3928a8 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -46,6 +46,7 @@ float const kSignalPick{12.0f}; float const kDragStart{6.0f}; double const kDragTravel{1.0}; double const kRefreshPeriod{2.0}; +double const kSignalReach{10.0}; char const *const kKinds[] = {"main", "block", "warning", "shunt", "stop", "substitute", "repeater"}; char const *const kKindLabels[] = {"Semaphore", "Automatic block", "Warning signal", "Shunting signal", "Stop signal", "Substitute signal", "Repeater"}; @@ -88,6 +89,17 @@ glm::dvec2 right_of(glm::dvec2 const &Direction) return {-Direction.y, Direction.x}; } +glm::dvec2 travel_of(double const Yaw) +{ + auto const yaw{glm::radians(Yaw + 180.0)}; + return {std::sin(yaw), std::cos(yaw)}; +} + +int travel_side(int const Side, int const List) +{ + return Side * (List == 2 ? 1 : -1); +} + int lean_wanted(int const Side) { return Side > 0 ? 1 : 2; @@ -430,9 +442,12 @@ void editor_mode::signal_press(track_intent const &Intent) { if (Intent.index < 0 || Intent.index >= static_cast(tool.standing.size())) return; - signal_select(tool.standing[Intent.index].include.instance); + auto const &standing{tool.standing[Intent.index]}; + tool.grab = plan_of(standing.include.location) - plan_of(cursor_level(standing.include.location.y)); + tool.home_valid = signal_standing_spot(standing, tool.home); tool.moving = true; tool.moved_valid = false; + signal_select(standing.include.instance); return; } auto const *armed{signal_armed()}; @@ -473,39 +488,84 @@ void editor_mode::signal_move_update() return; if (false == dragged) return; - auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })}; - if (found == tool.standing.end()) + auto const *found{signal_standing_of(tool.selected)}; + if (found == nullptr) { tool.moved_valid = false; return; } - // the track under the cursor; off every track, as it is when the signal is dragged beside its track, the place of the - // track the signal stands by nearest the cursor + auto const &location{found->include.location}; + auto const cursor{cursor_level(location.y)}; + glm::dvec3 const held{cursor.x + tool.grab.x, location.y, cursor.z + tool.grab.y}; + // the track under the cursor; off every track, as it is when the signal is dragged beside its track, the track the + // signal stands by auto *track{m_hover.track}; auto path{m_hover.path}; - auto point{m_hover.point}; - if (track == nullptr && found->track != nullptr && false == found->track->m_editorremoved && false == found->track->m_paths.empty()) + if (track == nullptr && tool.home_valid) { - track = found->track; - path = std::clamp(found->spot.path, 0, static_cast(track->m_paths.size()) - 1); - point = bezier{track->m_paths[path]}.point(nearest_on(*track, path, cursor_level(found->spot.axis.y))); + track = tool.home.track; + path = tool.home.path; } + tool.moved_valid = track != nullptr && signal_drop_at(*found, *track, path, held, 0, tool.moved); +} + +editor_mode::signal_standing const *editor_mode::signal_standing_of(scene::instance_handle const Instance) const +{ + auto const &standing{m_signal.standing}; + auto const found{std::find_if(standing.begin(), standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == Instance; })}; + return found != standing.end() ? &*found : nullptr; +} + +bool editor_mode::signal_drop_at(signal_standing const &Standing, TTrack &Track, int const Path, glm::dvec3 const &Point, int const Side, signal_spot &Spot) const +{ + auto const *item{signal_template_of(Standing.include.file)}; + if (item == nullptr || Track.m_paths.empty() || Track.m_editorremoved) + return false; + auto const path{std::clamp(Path, 0, static_cast(Track.m_paths.size()) - 1)}; + auto const forward{forward_at(Track, path, nearest_on(Track, path, Point))}; + auto const facing{glm::dot(forward, travel_of(Standing.include.yaw)) >= 0.0 ? 1 : -1}; + return signal_spot_at(Track, path, Point, Point, facing, Side * facing, *item, Spot); +} + +bool editor_mode::signal_standing_spot(signal_standing const &Standing, signal_spot &Spot) const +{ + auto const &location{Standing.include.location}; + TTrack *track{nullptr}; + int path{0}; + auto best{kSignalReach}; + auto const consider = [&](TTrack *Track) { + if (Track == nullptr || Track->m_editorremoved) + return; + for (int p = 0; p < static_cast(Track->m_paths.size()); ++p) + { + bezier const curve{Track->m_paths[p]}; + auto const low{glm::min(glm::min(plan_of(curve.p0), plan_of(curve.p1)), glm::min(plan_of(curve.p2), plan_of(curve.p3)))}; + auto const high{glm::max(glm::max(plan_of(curve.p0), plan_of(curve.p1)), glm::max(plan_of(curve.p2), plan_of(curve.p3)))}; + if (glm::length(glm::max(glm::max(low - plan_of(location), plan_of(location) - high), glm::dvec2{0.0})) >= best) + continue; + auto const distance{geometry::plan_distance(curve.point(nearest_on(*Track, p, location)), location)}; + if (distance < best) + { + best = distance; + track = Track; + path = p; + } + } + }; + consider(Standing.track); if (track == nullptr) - { - tool.moved_valid = false; - return; - } - auto const *item{signal_template_of(found->include.file)}; - if (item == nullptr) - return; - path = std::clamp(path, 0, static_cast(track->m_paths.size()) - 1); - auto const yaw{glm::radians(found->include.yaw + 180.0)}; - glm::dvec2 const travel{std::sin(yaw), std::cos(yaw)}; - auto const parameter{nearest_on(*track, path, point)}; - auto const forward{forward_at(*track, path, parameter)}; - auto const facing{glm::dot(forward, travel) >= 0.0 ? 1 : -1}; - auto const sideoftravel{glm::dot(plan_of(found->include.location) - plan_of(point), right_of(travel)) >= 0.0 ? 1 : -1}; - tool.moved_valid = signal_spot_at(*track, path, point, point, facing, sideoftravel * facing, *item, tool.moved); + for (auto *candidate : simulation::Paths.sequence()) + consider(candidate); + if (track == nullptr) + return false; + auto const axis{bezier{track->m_paths[path]}.point(nearest_on(*track, path, location))}; + auto const side{glm::dot(plan_of(location) - plan_of(axis), right_of(travel_of(Standing.include.yaw))) >= 0.0 ? 1 : -1}; + if (false == signal_drop_at(Standing, *track, path, location, side, Spot)) + return false; + Spot.offset = geometry::plan_distance(location, axis); + Spot.position = location; + Spot.yaw = Standing.include.yaw; + return true; } void editor_mode::signal_release() @@ -530,40 +590,53 @@ void editor_mode::signal_release() return; } tool.moved_valid = false; - auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })}; - if (found == tool.standing.end()) - return; - if (false == scene::Layers.tracked(found->include.instance) || false == scene::Layers.removable(found->include.instance)) + if (auto const *found{signal_standing_of(tool.selected)}) + signal_move_to(*found, tool.moved, STR_C("Signal %s moved, read by %s %s of %s, Ctrl+Z takes it back"), STR_C("Signal %s moved, Ctrl+Z takes it back")); +} + +bool editor_mode::signal_move_to(signal_standing const &Standing, signal_spot const &Spot, char const *Read, char const *Unread) +{ + auto &tool{m_signal}; + auto const instance{Standing.include.instance}; + if (false == scene::Layers.tracked(instance) || false == scene::Layers.removable(instance)) { tool.status = STR_C("This signal is in a file the editor doesn't change, it can't be moved"); - return; + return false; } - auto const &spot{tool.moved}; - auto const &included{scene::Layers.instance(found->include.instance)}; - auto values{found->include.values}; - auto const local{spot.position - included.context.offset}; + auto const &included{scene::Layers.instance(instance)}; + auto file{*included.file}; + signal_spot before; + auto const *item{signal_template_of(Standing.include.file)}; + if (item != nullptr && signal_standing_spot(Standing, before) && travel_side(before.side, before.list) != travel_side(Spot.side, Spot.list)) + file = signal_variant(*item, Spot); + auto values{Standing.include.values}; + auto const local{Spot.position - included.context.offset}; values[1] = editor_includes::number(local.x); values[2] = editor_includes::number(local.y); values[3] = editor_includes::number(local.z); - values[4] = editor_includes::number(spot.yaw); - auto before{scene::Layers.directive(found->include.instance)}; + values[4] = editor_includes::number(Spot.yaw); + auto const *next{signal_template_of(file)}; + auto const read{next != nullptr && false == next->read.empty() ? editor_includes::substitute(next->read, values) : Standing.read}; + auto directive{scene::Layers.directive(instance)}; std::vector> states; - if (false == found->read.empty() && spot.track->Dynamics.empty()) + if (false == Standing.read.empty() && Spot.track->Dynamics.empty()) { - signal_detach_events(found->read, states); - if (std::none_of(states.begin(), states.end(), [&](auto const &State) { return State.first == spot.track; })) - states.emplace_back(spot.track, editor_track::capture(*spot.track)); - editor_track::events(*spot.track, spot.list).emplace_back(ToLower(found->read), nullptr); + signal_detach_events(Standing.read, states); + if (std::none_of(states.begin(), states.end(), [&](auto const &State) { return State.first == Spot.track; })) + states.emplace_back(Spot.track, editor_track::capture(*Spot.track)); + editor_track::events(*Spot.track, Spot.list).emplace_back(ToLower(read), nullptr); std::string missing; - editor_track::bind_events(*spot.track, missing); - spot.track->mark_dirty(); + editor_track::bind_events(*Spot.track, missing); + Spot.track->mark_dirty(); } - set_include_directive(found->include.instance, editor_includes::compose_directive(*included.file, values)); + auto const name{Standing.name}; + auto const node{Standing.include.file}; + set_include_directive(instance, editor_includes::compose_directive(file, values)); if (states.empty()) { EditorSnapshot snap; - snap.directives.emplace_back(found->include.instance, std::move(before)); - snap.node_name = found->include.file; + snap.directives.emplace_back(instance, std::move(directive)); + snap.node_name = node; trim_history(); m_history.push_back(std::move(snap)); g_redo.clear(); @@ -571,13 +644,47 @@ void editor_mode::signal_release() else { push_track_snapshot(std::move(states)); - m_history.back().directives.emplace_back(found->include.instance, std::move(before)); + m_history.back().directives.emplace_back(instance, std::move(directive)); } - tool.status = found->read.empty() ? format(STR_C("Signal %s moved, Ctrl+Z takes it back"), found->name.c_str()) - : format(STR_C("Signal %s moved, read by %s %s of %s, Ctrl+Z takes it back"), found->name.c_str(), editor_track::event_keyword(spot.list), found->read.c_str(), name_of(*spot.track).c_str()); - auto const instance{found->include.instance}; signal_refresh(true); signal_select(instance); + auto const *moved{signal_standing_of(instance)}; + tool.status = moved != nullptr && moved->track != nullptr ? format(Read, name.c_str(), editor_track::event_keyword(moved->list), moved->read.c_str(), name_of(*moved->track).c_str()) : format(Unread, name.c_str()); + return true; +} + +bool editor_mode::signal_flip(scene::instance_handle const Instance) +{ + auto const *standing{signal_standing_of(Instance)}; + signal_spot spot; + if (standing == nullptr || false == signal_standing_spot(*standing, spot)) + { + m_signal.status = STR_C("No track by the signal to take it across"); + return false; + } + auto const across{plan_of(spot.position) - plan_of(spot.axis)}; + auto const mirrored{plan_of(spot.axis) + spot.travel * glm::dot(across, spot.travel) - right_of(spot.travel) * glm::dot(across, right_of(spot.travel))}; + spot.side = -spot.side; + spot.position = {mirrored.x, spot.position.y, mirrored.y}; + return signal_move_to(*standing, spot, STR_C("Signal %s moved to the other side of the track, read by %s %s of %s, Ctrl+Z takes it back"), STR_C("Signal %s moved to the other side of the track, Ctrl+Z takes it back")); +} + +bool editor_mode::signal_turn(scene::instance_handle const Instance) +{ + auto const *standing{signal_standing_of(Instance)}; + signal_spot spot; + if (standing == nullptr || false == signal_standing_spot(*standing, spot)) + { + m_signal.status = STR_C("No track by the signal to turn it to"); + return false; + } + auto const *item{signal_template_of(standing->include.file)}; + auto const position{spot.position}; + if (item == nullptr || false == signal_spot_at(*spot.track, spot.path, spot.axis, spot.axis, spot.list == 2 ? -1 : 1, spot.side, *item, spot)) + return false; + spot.position = position; + spot.yaw = std::fmod(standing->include.yaw + 180.0, 360.0); + return signal_move_to(*standing, spot, STR_C("Signal %s turned to the trains of the other direction, read by %s %s of %s, Ctrl+Z takes it back"), STR_C("Signal %s turned to the trains of the other direction, Ctrl+Z takes it back")); } std::string editor_mode::signal_variant(signal_template const &Template, signal_spot const &Spot) const @@ -876,9 +983,8 @@ void editor_mode::render_signal_palette() void editor_mode::render_signal_selected() { - auto &tool{m_signal}; - auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })}; - if (found == tool.standing.end()) + auto const *found{signal_standing_of(m_signal.selected)}; + if (found == nullptr) return; ImGui::Separator(); ImGui::TextUnformatted(format(STR_C("Selected: %s (%s)"), found->name.c_str(), found->include.file.c_str()).c_str()); @@ -889,6 +995,15 @@ void editor_mode::render_signal_selected() ImGui::TextDisabled(STR_C("Read by %s %s of %s"), editor_track::event_keyword(found->list), found->read.c_str(), name_of(*found->track).c_str()); else ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), STR_C("No path has %s among its events: the train doesn't see it"), found->read.c_str()); + auto const instance{found->include.instance}; + auto const width{(ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f}; + if (ImGui::Button(STR_C("To the other side"), ImVec2(width, 0.0f))) + signal_flip(instance); + item_tooltip("Takes the signal across the track, it keeps facing the same trains. Dragging it over the track does the same"); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Turn around"), ImVec2(width, 0.0f))) + signal_turn(instance); + item_tooltip("Turns the signal to the trains of the other direction, they read it by the other event of the path"); } void editor_mode::render_signal_ui() @@ -1038,10 +1153,7 @@ void editor_mode::draw_signal_overlay() const drawlist->AddText(ImVec2(head.x + 6.0f, head.y - 6.0f), colour, label.c_str()); } if (selected || hovered) - { - auto const yaw{glm::radians(standing.include.yaw + 180.0)}; - arrow(location, glm::dvec2{std::sin(yaw), std::cos(yaw)}, colour); - } + arrow(location, travel_of(standing.include.yaw), colour); } auto const preview = [&](signal_spot const &Spot, ImU32 const Colour) { projection.line(drawlist, Spot.axis + glm::dvec3{0.0, kRailTop, 0.0}, Spot.position, Colour, 2.0f); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 45ba31c8..73abd640 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1641,8 +1641,10 @@ std::vector editor_mode::track_key_hints(bool const All) hints = {{"LMB on a track", "the line the objects go along"}}; else if (m_track_window_open && m_track_tab == track_tab::parallel) hints = {{"LMB on a track", "the new track goes beside it"}, {"Click on a side in the panel", "the side of the new track"}}; - else if (m_track_window_open && m_track_tab == track_tab::signals && (m_signal.placing || m_signal.moving)) - hints = {{"Drag along", m_signal.moving ? "moves the signal along the track" : "the trains it faces run that way"}, {"Release", "done"}, {"Esc", "cancel"}}; + else if (m_track_window_open && m_track_tab == track_tab::signals && m_signal.moving) + hints = {{"Drag along", "moves the signal along the track"}, {"Drag over the track", "to the other side"}, {"Release", "done"}, {"Esc", "cancel"}}; + else if (m_track_window_open && m_track_tab == track_tab::signals && m_signal.placing) + hints = {{"Drag along", "the trains it faces run that way"}, {"Release", "done"}, {"Esc", "cancel"}}; else if (m_track_window_open && m_track_tab == track_tab::signals && signal_armed() == nullptr) hints = {{"List in the panel", "the type of the signal"}, {"Drag a signal", "moves it along the track"}, {"LMB on a signal", "selects it"}}; else if (m_track_window_open && m_track_tab == track_tab::signals) diff --git a/lang/editor_pl.po b/lang/editor_pl.po index bd6fef78..0defa02f 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -9955,3 +9955,36 @@ msgstr "Gdy tor wychodzi poza teren, NMT dobudowuje się wokół kursora na bie msgid "The ends have to be numbers, from 0.1 m to 100 km apart" msgstr "Końce muszą być liczbami, od 0,1 m do 100 km od siebie" + +msgid "To the other side" +msgstr "Na drugą stronę" + +msgid "Takes the signal across the track, it keeps facing the same trains. Dragging it over the track does the same" +msgstr "Przenosi sygnalizator na drugą stronę toru, dalej stoi przodem do tych samych pociągów. To samo robi przeciągnięcie go przez tor" + +msgid "Turns the signal to the trains of the other direction, they read it by the other event of the path" +msgstr "Obraca sygnalizator do pociągów z przeciwnego kierunku, odczytują go przez drugi event toru" + +msgid "No track by the signal to take it across" +msgstr "Przy sygnalizatorze nie ma toru, na którego drugą stronę można go przenieść" + +msgid "No track by the signal to turn it to" +msgstr "Przy sygnalizatorze nie ma toru, do którego można go obrócić" + +msgid "Signal %s moved to the other side of the track, read by %s %s of %s, Ctrl+Z takes it back" +msgstr "Sygnalizator %s przeniesiony na drugą stronę toru, pociąg odczytuje go przez %s %s toru %s, Ctrl+Z cofa" + +msgid "Signal %s moved to the other side of the track, Ctrl+Z takes it back" +msgstr "Sygnalizator %s przeniesiony na drugą stronę toru, Ctrl+Z cofa" + +msgid "Signal %s turned to the trains of the other direction, read by %s %s of %s, Ctrl+Z takes it back" +msgstr "Sygnalizator %s obrócony do pociągów z przeciwnego kierunku, pociąg odczytuje go przez %s %s toru %s, Ctrl+Z cofa" + +msgid "Signal %s turned to the trains of the other direction, Ctrl+Z takes it back" +msgstr "Sygnalizator %s obrócony do pociągów z przeciwnego kierunku, Ctrl+Z cofa" + +msgid "Drag over the track" +msgstr "Przeciągnij przez tor" + +msgid "to the other side" +msgstr "na drugą stronę" diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index 4edc1033..561ccdd0 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -1090,13 +1090,17 @@ std::string node_layers::directive(instance_handle const Instance) const } // replaces the directive of specified include, the scenery file receives it on save -bool node_layers::modify(instance_handle const Instance, std::string const &Directive) +bool node_layers::modify(instance_handle const Instance, std::string const &Directive, std::string const &File) { if (false == tracked(Instance) || m_instances[Instance - 1].dead || Directive.empty()) { return false; } m_instances[Instance - 1].directive = Directive; + if (false == File.empty()) + { + m_instances[Instance - 1].file = &(*m_templates.emplace(File).first); + } return true; } diff --git a/scene/scenelayers.h b/scene/scenelayers.h index daf3c587..1eb2e4d5 100644 --- a/scene/scenelayers.h +++ b/scene/scenelayers.h @@ -335,7 +335,7 @@ class node_layers // returns: empty text if the directive can't be read std::string directive(instance_handle Instance) const; // replaces the directive of specified include, the scenery file receives it on save. NOTE: the nodes aren't touched - bool modify(instance_handle Instance, std::string const &Directive); + bool modify(instance_handle Instance, std::string const &Directive, std::string const &File = {}); // takes the models specified include was loaded with out of the scene, once the editor shows its own in their place void rebuilt(instance_handle Instance); // true if specified node was replaced by the editor with a node showing the current state of its include From ddde8d12d9c1bdff0e954790373c7777756577d6 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 11:09:06 +0200 Subject: [PATCH 014/145] fix(editor): Esc takes back what a drag did Esc in the middle of dragging a point of a track kept the moved point, and the hint said so; dragging a straight was taken back but left an empty step in the history; a vertex or the radius grip of a curve stayed moved in the design. Now Esc puts back what was there before the drag in all three, and the history doesn't keep a step for it. --- application/editormode.cpp | 10 ++++++++++ application/editormode.h | 4 ++++ application/editormodelay.cpp | 1 + application/editormoderoute.cpp | 2 ++ application/editormodestraights.cpp | 16 +++++++++++++++- application/editormodetrack.cpp | 2 +- 6 files changed, 33 insertions(+), 2 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index a8a92811..8155f186 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -973,6 +973,16 @@ void editor_mode::undo_last() } } +void editor_mode::drop_step(std::size_t const Step) +{ + if (Step == 0 || m_history.size() != Step) + return; + auto const redo{g_redo.size()}; + undo_one(); + if (g_redo.size() > redo) + g_redo.erase(g_redo.begin() + redo, g_redo.end()); +} + void editor_mode::undo_one() { if (m_history.empty()) diff --git a/application/editormode.h b/application/editormode.h index 0205ff72..974111b1 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -174,6 +174,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void redo_last(); // a single step of the history, see undo_last() void undo_one(); + void drop_step(std::size_t const Step); void redo_one(); // the changes of the heightmap terrain since the last step become a step of the history: a stroke of the brush, // or what a frame did; the next change is recorded from now on @@ -984,6 +985,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool fit_has_before{false}; bool fit_has_after{false}; std::vector> drag_states; + std::size_t drag_step{0}; glm::dvec3 preview_start{0.0}; glm::dvec3 preview_end{0.0}; glm::dvec3 edit_start{0.0}; @@ -1551,6 +1553,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool moved{false}; glm::vec2 from{0.f}; double height{0.0}; + std::size_t step{0}; }; point_drag m_point_drag; struct handle_drag @@ -2258,6 +2261,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void draw_infra_overlay() const; bool m_route_dragging{false}; + alignment::design m_route_drag_from; float m_track_snap_radius{1.0f}; bool m_track_align_tangent{true}; bool m_track_drag_connected{true}; diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index 45cf586d..56cae9f1 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -351,6 +351,7 @@ void editor_mode::point_drag_update() for (auto *dragged : m_track_drag) states.emplace_back(dragged, editor_track::capture(*dragged)); push_track_snapshot(std::move(states)); + drag.step = m_history.size(); records_watch_begin(); } screen_projection const projection; diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index c1c651fd..de674d14 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -1102,6 +1102,8 @@ void editor_mode::render_route_gizmo() } return; } + if (false == m_route_dragging) + m_route_drag_from = route.design; m_route_dragging = true; if (grip) { diff --git a/application/editormodestraights.cpp b/application/editormodestraights.cpp index 2a22da38..6c9fcdab 100644 --- a/application/editormodestraights.cpp +++ b/application/editormodestraights.cpp @@ -638,6 +638,7 @@ void editor_mode::render_straight_gizmo() if (false == state.fitting) { push_track_snapshot(state.drag_states); + state.drag_step = m_history.size(); m_track_drag = tracks; } } @@ -731,6 +732,7 @@ void editor_mode::cancel_track_tools() m_track_dirty = true; commit_track_drag(true); m_track_drag.clear(); + drop_step(state.drag_step); } state.dragging = false; state.fitting = false; @@ -743,10 +745,22 @@ void editor_mode::cancel_track_tools() m_signal.placing = false; m_signal.moving = false; m_handle_drag = {}; + if (m_route_dragging) + { + m_route_dragging = false; + m_route.design = m_route_drag_from; + route_update(); + } if (m_point_drag.active) { + auto const step{m_point_drag.moved ? m_point_drag.step : 0}; m_track_snap = {}; - point_drag_finish(); + m_record_watch = {}; + m_point_drag = {}; + m_track_drag.clear(); + m_track_drag_points.clear(); + m_track_joints.clear(); + drop_step(step); } if (m_switch.placing) { diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 73abd640..2b80b872 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1695,7 +1695,7 @@ std::vector editor_mode::track_key_hints(bool const All) gizmo = true; } else if (m_point_drag.moved) - hints = {{"Drag", "moves the point, level"}, {"Over a free end", "the end joins it"}, {"Release", "done"}, {"Esc", "done"}}; + hints = {{"Drag", "moves the point, level"}, {"Over a free end", "the end joins it"}, {"Release", "done"}, {"Esc", "cancel"}}; else if (m_track_point.valid()) { char const *action{"moves the point along the axes"}; From 0b74da1474bb1bbea194256df1fa90563cdde3c4 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 11:09:16 +0200 Subject: [PATCH 015/145] fix(editor): a radius typed while dragging out of a free end reaches the curve The shape pulled out of a free end took a typed radius for its curve, but the typed keys were thrown away while the drag went on, so the value never got there. Digits now type the radius whenever the shape has a curve and a straight. --- application/editormodetrackux.cpp | 2 ++ lang/editor_pl.po | 3 +++ 2 files changed, 5 insertions(+) diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 3cb577f6..a7e5e697 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -891,6 +891,8 @@ char const *editor_mode::typed_meaning() const 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"; + if (m_extend.active && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc)) + return "type the radius of the curve"; return nullptr; } diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 0defa02f..f71b08db 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -9988,3 +9988,6 @@ msgstr "Przeciągnij przez tor" msgid "to the other side" msgstr "na drugą stronę" + +msgid "type the radius of the curve" +msgstr "wpisz promień łuku" From 19b7364bcaed390063da2181ac4ce1ecd39bacf0 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 11:09:16 +0200 Subject: [PATCH 016/145] fix(editor): the cant of a path is typed in millimetres The inspector took the cant as the roll angle in degrees while every readout shows it in millimetres of one rail over the other. --- application/editormodetrack.cpp | 10 ++++++---- lang/editor_pl.po | 6 ++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 2b80b872..0b6237e4 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1462,10 +1462,12 @@ void editor_mode::render_path_ui() { if (begin_fields("##cantradius", 0.3f)) { - field_row("Roll (deg)"); - if (auto rolls{path.rolls}; ImGui::DragFloat2("##roll", rolls.data(), 0.01f, -15.0f, 15.0f, "%.2f") && std::isfinite(rolls[0]) && std::isfinite(rolls[1])) - path.rolls = rolls; - item_tooltip("Cant at the start and at the end as the roll angle of the track"); + field_row("Cant (mm)"); + auto const lift = [](float const Roll) { return static_cast(1500.0 * std::sin(glm::radians(static_cast(Roll)))); }; + auto const roll = [](float const Lift) { return static_cast(glm::degrees(std::asin(std::clamp(static_cast(Lift) / 1500.0, -1.0, 1.0)))); }; + if (std::array cants{lift(path.rolls[0]), lift(path.rolls[1])}; ImGui::DragFloat2("##roll", cants.data(), 0.5f, -388.0f, 388.0f, "%.0f") && std::isfinite(cants[0]) && std::isfinite(cants[1])) + path.rolls = {roll(cants[0]), roll(cants[1])}; + item_tooltip("Cant at the start and at the end: how much higher one rail is, + raises the left rail looking along the path"); finish_edit(rebuild::geometry); field_row("Radius"); if (auto radius{path.radius}; ImGui::InputFloat("##radius", &radius, 0.0f, 0.0f, "%.1f") && std::isfinite(radius)) diff --git a/lang/editor_pl.po b/lang/editor_pl.po index f71b08db..8d7a78fa 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -9991,3 +9991,9 @@ msgstr "na drugą stronę" msgid "type the radius of the curve" msgstr "wpisz promień łuku" + +msgid "Cant (mm)" +msgstr "Przechyłka (mm)" + +msgid "Cant at the start and at the end: how much higher one rail is, + raises the left rail looking along the path" +msgstr "Przechyłka na początku i na końcu: o ile jedna szyna jest wyżej, + podnosi lewą szynę patrząc wzdłuż toru" From 825c91705d603d27959a815d68f6b959060fbadf Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 11:47:25 +0200 Subject: [PATCH 017/145] fix(editor): the free design gets no transition curves from the Plan tools Dragging an end of a straight refits the curve next to it with the cant and the transition curves recommended for the speed, keeping the curve's own only where it had some; a plain arc laid freely came out with transitions. Break and S-curve took the recommended transitions too. The recommendation of a design not checked against the norms is a plain arc now, the tools take the norms in effect rather than the ones the design was last computed with, and the Plan panel calls the automatic choice "None" in the free design. Self-test: norms free|plk. --- application/editormode.h | 1 + application/editormodelay.cpp | 13 +++++++++---- application/editormoderoute.cpp | 2 +- application/editormodeselftestjoin.cpp | 8 ++++++++ application/editormodestraights.cpp | 7 ++++--- editor/editorAlignment.cpp | 2 ++ lang/editor_pl.po | 3 +++ 7 files changed, 28 insertions(+), 8 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 974111b1..69c0bfa5 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1294,6 +1294,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool m_norms_free{true}; alignment::limits laying_norms() const; bool render_norms_choice(); + void set_norms_free(bool const Free); glm::dvec3 lay_angle_snap(glm::dvec3 const &Mouse, std::string &Label) const; // the straight track Alt turns the laid straight to: the nearest one the straight from From to To crosses, or one along From TTrack const *skew_reference(glm::dvec3 const &From, glm::dvec3 const &To, double const Reach) const; diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index 56cae9f1..8dd852d2 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -1344,17 +1344,22 @@ bool editor_mode::render_norms_choice() : "Transition curves and cant for the design speed, warnings when the radius, the cant or the transitions don't meet the norms"); if (clicked && false == chosen) { - m_norms_free = i == 0; - m_lay.transitions = false == m_norms_free; + set_norms_free(i == 0); changed = true; } } ImGui::PopID(); - if (changed && false == m_route.chain.tracks.empty()) - route_update(); return changed; } +void editor_mode::set_norms_free(bool const Free) +{ + m_norms_free = Free; + m_lay.transitions = false == m_norms_free; + if (false == m_route.chain.tracks.empty()) + route_update(); +} + void editor_mode::render_lay_ui() { auto &tool{m_lay}; diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index de674d14..f6d7de55 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -52,7 +52,7 @@ bool editor_mode::route_active() const void editor_mode::route_recommend(alignment::vertex &Vertex) const { - auto const recommended{alignment::recommend(m_route.design.speed, Vertex.radius, m_route.design.norms)}; + auto const recommended{alignment::recommend(m_route.design.speed, Vertex.radius, laying_norms())}; Vertex.cant = recommended.cant; Vertex.transition_in = recommended.transition; Vertex.transition_out = recommended.transition; diff --git a/application/editormodeselftestjoin.cpp b/application/editormodeselftestjoin.cpp index f5ac36d8..f645c522 100644 --- a/application/editormodeselftestjoin.cpp +++ b/application/editormodeselftestjoin.cpp @@ -122,6 +122,14 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma WriteLog(format("SELFTEST %s end: %.3f m from the target", command.c_str(), geometry::plan_distance(previous, b))); return selftest_result::handled; } + if (command == "norms") + { + std::string which; + words >> which; + set_norms_free(which != "plk"); + WriteLog(std::string{"SELFTEST norms: "} + (m_norms_free ? "free" : "plk")); + return selftest_result::handled; + } if (command == "tracktab") { std::string key; diff --git a/application/editormodestraights.cpp b/application/editormodestraights.cpp index 6c9fcdab..13a43cc9 100644 --- a/application/editormodestraights.cpp +++ b/application/editormodestraights.cpp @@ -442,7 +442,8 @@ void editor_mode::render_straight_ui() ImGui::TextDisabled("%s", STR_C("Transition curves")); int transitions{state.auto_transitions ? 0 : 1}; if (segmented("straighttransitions", transitions, - {{0, STR_C("From the line speed"), "Lengths of the transition curves from the speed of the paths and the limits of the curve design"}, + {m_norms_free ? segment_option{0, STR_C("None"), "Plain arcs: the free design has no transition curves, the PKP PLK norms work them out from the speed of the line"} + : segment_option{0, STR_C("From the line speed"), "Lengths of the transition curves from the speed of the paths and the limits of the curve design"}, {1, STR_C("Given length"), "The same length of the transition curves at every curve"}})) state.auto_transitions = transitions == 0; if (false == state.auto_transitions) @@ -1054,7 +1055,7 @@ void editor_mode::apply_straight_tool() double speed{-1.0}; for (auto const *track : line.tracks) speed = std::max(speed, editor_track::velocity(*track)); - auto const transition{state.auto_transitions ? alignment::recommend(speed > 0.0 ? speed : 100.0, radius, m_route.design.norms).transition : state.transition}; + auto const transition{state.auto_transitions ? alignment::recommend(speed > 0.0 ? speed : 100.0, radius, laying_norms()).transition : state.transition}; auto const reach{radius * std::tan(std::min(std::abs(angle), 3.0) * 0.5) + transition * 0.5 + 10.0}; straight_reshape(line, state.tool_at - reach, state.tool_at + reach, [pivot, angle](glm::dvec3 const &Point) { @@ -1182,7 +1183,7 @@ void editor_mode::apply_detour() double speed{-1.0}; for (auto const *track : line.tracks) speed = std::max(speed, editor_track::velocity(*track)); - auto const transition{state.auto_transitions ? alignment::recommend(speed > 0.0 ? speed : 100.0, radius, m_route.design.norms).transition : state.transition}; + auto const transition{state.auto_transitions ? alignment::recommend(speed > 0.0 ? speed : 100.0, radius, laying_norms()).transition : state.transition}; auto const reach{radius * std::tan(slope * 0.5) + transition * 0.5 + 5.0}; auto const from{std::max(0.0, stations[0] - reach)}; auto const to{std::min(line.length, stations[3] + reach)}; diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 2b0d9d59..232bb9f4 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -275,6 +275,8 @@ recommendation recommend(double const Speed, double const Radius, limits const & return result; result.cant_equilibrium = factor * Speed * Speed / Radius; result.cant_min = std::max(0.0, result.cant_equilibrium - allowance); + if (false == Limits.checked) + return result; result.cant = std::min(Limits.cant_max, ceil_to(std::max(result.cant_min, result.cant_equilibrium * 2.0 / 3.0), 5.0)); result.transition = std::min(5000.0, transition_length(Speed, Radius, result.cant, Limits)); return result; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 8d7a78fa..101e467a 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -9997,3 +9997,6 @@ msgstr "Przechyłka (mm)" msgid "Cant at the start and at the end: how much higher one rail is, + raises the left rail looking along the path" msgstr "Przechyłka na początku i na końcu: o ile jedna szyna jest wyżej, + podnosi lewą szynę patrząc wzdłuż toru" + +msgid "Plain arcs: the free design has no transition curves, the PKP PLK norms work them out from the speed of the line" +msgstr "Zwykłe łuki: projekt wolny nie ma krzywych przejściowych, normy PKP PLK liczą je z prędkości linii" From 9d27fd1ace0acf594cddd02a182558e251ff565f Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 11:47:25 +0200 Subject: [PATCH 018/145] fix(editor): a straight dragged by its free end past a curve isn't refused When the refitted curve needed more of the straight at its other end, the ends of the line were both checked against the design, and the free end that went along with the drag "missed" by as far as it was dragged. Only an end whose straight was cut is checked now. The longer transition curves of the PKP PLK norms ran into it more often. --- application/editormoderoute.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index f6d7de55..bfc1771b 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -722,8 +722,8 @@ void editor_mode::route_apply() to = editor_track::take_straight(editor_track::straight_beyond(before, true, m_straights.tolerance), to, before.end, route.result.end, states, created); std::string error; auto const found{editor_track::find_chain(from, to, chain, error, true)}; - auto const startoff{found ? plan_distance(chain.start, route.result.start) : 0.0}; - auto const endoff{found ? plan_distance(chain.end, route.result.end) : 0.0}; + auto const startoff{found && startextension > 0.0 ? plan_distance(chain.start, route.result.start) : 0.0}; + auto const endoff{found && endextension > 0.0 ? plan_distance(chain.end, route.result.end) : 0.0}; if (false == found || startoff > 5.0 || endoff > 5.0) { auto const startrun{editor_track::straight_beyond(before, false, m_straights.tolerance)}; From da18883558afe0a57e7f55de36f5c760694920ba Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 11:47:26 +0200 Subject: [PATCH 019/145] feat(editor): what the editor refuses shows by the mouse cursor An error put in the status bar also shows for 4 s by the cursor, and the refusals the tools kept in their panels (a curve which can't be laid, a switch which doesn't fit, a signal which can't be moved, a drive without the variant) go there as well. --- application/editormode.cpp | 6 ++++++ application/editormode.h | 2 ++ application/editormoderoute.cpp | 8 ++++++++ application/editormodesignal.cpp | 12 ++++++------ application/editormodestraights.cpp | 7 ++++--- application/editormodeswitchdrive.cpp | 12 ++++++------ application/editormodeswitchplace.cpp | 24 ++++++++++++------------ application/editoruilayer.cpp | 23 +++++++++++++++++++++++ application/editoruilayer.h | 3 +++ 9 files changed, 70 insertions(+), 27 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 8155f186..1282cf12 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -973,6 +973,12 @@ void editor_mode::undo_last() } } +std::string editor_mode::refused(std::string Text) +{ + ui()->set_status(Text, true); + return Text; +} + void editor_mode::drop_step(std::size_t const Step) { if (Step == 0 || m_history.size() != Step) diff --git a/application/editormode.h b/application/editormode.h index 69c0bfa5..7f60287f 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -175,6 +175,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri // a single step of the history, see undo_last() void undo_one(); void drop_step(std::size_t const Step); + std::string refused(std::string Text); void redo_one(); // the changes of the heightmap terrain since the last step become a step of the history: a stroke of the brush, // or what a frame did; the next change is recorded from now on @@ -955,6 +956,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri m_route.result = alignment::compute(m_route.design); } void route_apply(); + void route_lay(); bool route_relay_through_switches(editor_track::chain const &Chain, std::vector> &States, std::vector &Created, TTrack *&Last); void route_recommend(alignment::vertex &Vertex) const; void draw_route_overlay() const; diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index bfc1771b..9fd4f5b5 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -573,6 +573,14 @@ bool editor_mode::route_from_line(TTrack &Track) } void editor_mode::route_apply() +{ + m_route.error.clear(); + route_lay(); + if (false == m_route.error.empty()) + refused(m_route.error); +} + +void editor_mode::route_lay() { auto &route{m_route}; route.status.clear(); diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 2e3928a8..5366b5fa 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -586,7 +586,7 @@ void editor_mode::signal_release() return; if (false == tool.moved_valid) { - tool.status = STR_C("Drop the signal by a track"); + tool.status = refused(STR_C("Drop the signal by a track")); return; } tool.moved_valid = false; @@ -600,7 +600,7 @@ bool editor_mode::signal_move_to(signal_standing const &Standing, signal_spot co auto const instance{Standing.include.instance}; if (false == scene::Layers.tracked(instance) || false == scene::Layers.removable(instance)) { - tool.status = STR_C("This signal is in a file the editor doesn't change, it can't be moved"); + tool.status = refused(STR_C("This signal is in a file the editor doesn't change, it can't be moved")); return false; } auto const &included{scene::Layers.instance(instance)}; @@ -659,7 +659,7 @@ bool editor_mode::signal_flip(scene::instance_handle const Instance) signal_spot spot; if (standing == nullptr || false == signal_standing_spot(*standing, spot)) { - m_signal.status = STR_C("No track by the signal to take it across"); + m_signal.status = refused(STR_C("No track by the signal to take it across")); return false; } auto const across{plan_of(spot.position) - plan_of(spot.axis)}; @@ -675,7 +675,7 @@ bool editor_mode::signal_turn(scene::instance_handle const Instance) signal_spot spot; if (standing == nullptr || false == signal_standing_spot(*standing, spot)) { - m_signal.status = STR_C("No track by the signal to turn it to"); + m_signal.status = refused(STR_C("No track by the signal to turn it to")); return false; } auto const *item{signal_template_of(standing->include.file)}; @@ -730,7 +730,7 @@ bool editor_mode::signal_place(signal_spot const &Spot, signal_template const &T std::string name{tool.name}; if (name.empty() || name.find_first_of(" \t\";") != std::string::npos) { - tool.status = STR_C("Give the signal a name without spaces first"); + tool.status = refused(STR_C("Give the signal a name without spaces first")); return false; } signal_refresh(true); @@ -753,7 +753,7 @@ bool editor_mode::signal_place(signal_spot const &Spot, signal_template const &T std::string error; if (place_templates({item}, error) == 0) { - tool.status = STR_C("The signal couldn't be placed: ") + error; + tool.status = refused(STR_C("The signal couldn't be placed: ") + error); return false; } auto const instance{m_history.back().instances.front()}; diff --git a/application/editormodestraights.cpp b/application/editormodestraights.cpp index 13a43cc9..265a0e83 100644 --- a/application/editormodestraights.cpp +++ b/application/editormodestraights.cpp @@ -570,7 +570,7 @@ void editor_mode::straights_apply(std::vector const &Lin return false == finite(End.first) || false == finite(End.second) || false == (glm::distance(End.first, End.second) >= 0.1 && glm::distance(End.first, End.second) <= 100000.0); })) { - state.status = STR_C("The ends have to be numbers, from 0.1 m to 100 km apart"); + state.status = refused(STR_C("The ends have to be numbers, from 0.1 m to 100 km apart")); return; } auto const tracks{editor_track::straight_affected(Lines)}; @@ -627,6 +627,7 @@ void editor_mode::render_straight_gizmo() auto const tracks{state.fitting ? std::vector{} : editor_track::straight_affected(lines)}; if (false == tracks_editable(tracks, state.status)) { + refused(state.status); state.fitting = false; return; } @@ -947,7 +948,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co editor_track::apply(*entry.first, entry.second); editor_track::commit(tracks); undo_split(); - state.status = m_route.error; + state.status = refused(m_route.error); return; } m_route.design.start_reserve = 0.0; @@ -969,7 +970,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co editor_track::apply(*entry.first, entry.second); editor_track::commit(tracks); undo_split(); - state.status = m_route.result.errors.empty() ? STR_C("The curve can't be fitted") : m_route.result.errors.front(); + state.status = refused(m_route.result.errors.empty() ? STR_C("The curve can't be fitted") : m_route.result.errors.front()); return; } auto pieces{alignment::pieces(m_route.result, m_route.design, m_route.chain.tracks.size())}; diff --git a/application/editormodeswitchdrive.cpp b/application/editormodeswitchdrive.cpp index f692c2fa..9b19c419 100644 --- a/application/editormodeswitchdrive.cpp +++ b/application/editormodeswitchdrive.cpp @@ -193,7 +193,7 @@ bool editor_mode::place_switch_drive(TTrack &Switch) item.file = family.files[index]; if (item.file.empty()) { - tool.status = missing_drive_variant(tool.drive, spot->rightswitch, driveright); + tool.status = refused(missing_drive_variant(tool.drive, spot->rightswitch, driveright)); return false; } item.location = spot->location + glm::dvec3{0.0, family.heights[index], 0.0}; @@ -206,7 +206,7 @@ bool editor_mode::place_switch_drive(TTrack &Switch) std::string error; if (place_templates({item}, error) == 0) { - tool.status = STR_C("The drive couldn't be placed: ") + error; + tool.status = refused(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()); @@ -253,7 +253,7 @@ bool editor_mode::replace_switch_drive(TTrack &Switch, int const Family, bool co auto const present{find_switch_drive(Switch)}; if (false == present.has_value()) { - tool.status = format(STR_C("No drive of the switch %s among the described drives"), Switch.name().c_str()); + tool.status = refused(format(STR_C("No drive of the switch %s among the described drives"), Switch.name().c_str())); return false; } auto const &found{present->standing}; @@ -263,7 +263,7 @@ bool editor_mode::replace_switch_drive(TTrack &Switch, int const Family, bool co item.file = tool.drives[family].files[index]; if (item.file.empty()) { - tool.status = Flip ? format(STR_C("The drive %s has no variant with the drive on the other side"), tool.drives[family].name.c_str()) : missing_drive_variant(family, (index & 2) != 0, (index & 1) != 0); + tool.status = refused(Flip ? format(STR_C("The drive %s has no variant with the drive on the other side"), tool.drives[family].name.c_str()) : missing_drive_variant(family, (index & 2) != 0, (index & 1) != 0)); return false; } if (drive_file(item.file) == found.file) @@ -276,7 +276,7 @@ bool editor_mode::replace_switch_drive(TTrack &Switch, int const Family, bool co item.tilt = {angle_value(found.values, 5), angle_value(found.values, 6)}; if (remove_templates({found.instance}) == 0) { - tool.status = STR_C("The drive of this switch can't be changed in its file"); + tool.status = refused(STR_C("The drive of this switch can't be changed in its file")); return false; } std::string error; @@ -284,7 +284,7 @@ bool editor_mode::replace_switch_drive(TTrack &Switch, int const Family, bool co { scene::Layers.removed(found.instance, false); m_history.pop_back(); - tool.status = STR_C("The drive couldn't be placed: ") + error; + tool.status = refused(STR_C("The drive couldn't be placed: ") + error); return false; } join_last_steps(); diff --git a/application/editormodeswitchplace.cpp b/application/editormodeswitchplace.cpp index 1856afce..1ab64d54 100644 --- a/application/editormodeswitchplace.cpp +++ b/application/editormodeswitchplace.cpp @@ -88,7 +88,7 @@ bool editor_mode::start_switch_placement() auto *track{selected}; if (tool.templates[tool.armed].double_slip) { - tool.status = STR_C("A double slip goes only into a straight crossing another one"); + tool.status = refused(STR_C("A double slip goes only into a straight crossing another one")); return false; } editor_track::curve curve; @@ -96,12 +96,12 @@ bool editor_mode::start_switch_placement() std::string error; if (false == editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve)) { - tool.status = STR_C("The selected track is neither a recognized straight nor a curve"); + tool.status = refused(STR_C("The selected track is neither a recognized straight nor a curve")); return false; } if (false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) { - tool.status = error.empty() ? std::string{STR_C("The curve can't be followed")} : error; + tool.status = refused(error.empty() ? std::string{STR_C("The curve can't be followed")} : error); return false; } for (auto const &part : editor_track::chain_parts(chain)) @@ -305,7 +305,7 @@ bool editor_mode::cut_straight(editor_track::straight const &Line, double const std::string reason; if (false == tracks_editable(Line.tracks, reason)) { - m_switch.status = reason; + m_switch.status = refused(reason); ui()->set_status(reason, true); return false; } @@ -393,7 +393,7 @@ bool editor_mode::place_switch_on_straight(editor_track::straight const &Line, e auto const &shape{Shape}; if (shape.length > Line.length - 0.02) { - m_switch.status = format(STR_C("The straight is %.1f m long, too short for the %.1f m switch"), Line.length, shape.length); + m_switch.status = refused(format(STR_C("The straight is %.1f m long, too short for the %.1f m switch"), Line.length, shape.length)); return false; } auto const from{switch_from(Line, shape.length, Along, Direction, Snap)}; @@ -402,7 +402,7 @@ bool editor_mode::place_switch_on_straight(editor_track::straight const &Line, e TTrack *first{nullptr}; if (false == cut_straight(Line, from, to, States, Created, Removed, &first)) { - m_switch.status = STR_C("The switch would overlap another switch, move it onto plain track"); + m_switch.status = refused(STR_C("The switch would overlap another switch, move it onto plain track")); return false; } auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; }; @@ -426,7 +426,7 @@ std::vector editor_mode::create_switch_set(editor_track::switch_templa auto *link{two != nullptr ? editor_track::create_path(Style, Paths[4]) : nullptr}; if (link == nullptr) { - m_switch.status = STR_C("The switch couldn't be created from this template"); + m_switch.status = refused(STR_C("The switch couldn't be created from this template")); return {}; } editor_track::commit({one, two, link}); @@ -437,7 +437,7 @@ std::vector editor_mode::create_switch_set(editor_track::switch_templa auto *track{editor_track::create_switch(Shape, Paths, Style, Name)}; if (track == nullptr) { - m_switch.status = STR_C("The switch couldn't be created from this template"); + m_switch.status = refused(STR_C("The switch couldn't be created from this template")); return {}; } editor_track::commit({track}); @@ -974,7 +974,7 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv slip_plan plan; if (false == plan_double_slip(Line, Point, Shape, plan)) { - state.status = plan.problem; + state.status = refused(plan.problem); return false; } if (plan.replaces != nullptr) @@ -1041,7 +1041,7 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat switches[i] = simulation::Paths.find(base + "_" + static_cast('a' + i)); if (switches[i] == nullptr || switches[i]->m_editorremoved || switches[i]->eType != tt_Switch || switches[i]->m_paths.size() < 2) { - state.status = STR_C("The double slip isn't complete, it can't be replaced"); + state.status = refused(STR_C("The double slip isn't complete, it can't be replaced")); return false; } } @@ -1077,7 +1077,7 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat partner = i; if (partner < 0) { - state.status = STR_C("The double slip geometry isn't recognized"); + state.status = refused(STR_C("The double slip geometry isn't recognized")); return false; } auto const &a{ends[0]}; @@ -1346,7 +1346,7 @@ void editor_mode::insert_curved_switch(int const Direction, int const Side) auto const total{tool.frame.back().station}; if (shape.length > total - 0.02) { - tool.status = format(STR_C("The curve is %.1f m long, too short for the %.1f m switch"), total, shape.length); + tool.status = refused(format(STR_C("The curve is %.1f m long, too short for the %.1f m switch"), total, shape.length)); return; } auto const station{std::clamp(tool.along, Direction > 0 ? 0.0 : shape.length, Direction > 0 ? total - shape.length : total)}; diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index faf4df3b..10dbe775 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -646,6 +646,7 @@ void editor_ui::render_() // strips of the unpinned docks, inside the toolbar and the status bar; the pins over the tab bars drawn by now autohide_strips(); autohide_pins(); + render_refusal(); } // updates state of UI elements @@ -1026,6 +1027,28 @@ void editor_ui::set_status(std::string const &Status, bool const Error) { m_layerspanel.status = Status; m_layerspanel.status_error = Error; + if (Error && false == Status.empty()) + { + m_refusal = Status; + m_refusaltime = ImGui::GetTime(); + } +} + +void editor_ui::render_refusal() +{ + auto const age{ImGui::GetTime() - m_refusaltime}; + if (m_refusal.empty() || age > 4.0) + return; + auto const &io{ImGui::GetIO()}; + auto const alpha{static_cast(std::clamp(4.0 - age, 0.0, 1.0))}; + auto const wrap{std::round(ImGui::GetFontSize() * 30.0f)}; + auto const size{ImGui::CalcTextSize(m_refusal.c_str(), nullptr, false, wrap)}; + ImVec2 at{io.MousePos.x + 20.0f, io.MousePos.y - 24.0f - size.y}; + at.x = std::clamp(at.x, 10.0f, std::max(10.0f, io.DisplaySize.x - size.x - 10.0f)); + at.y = std::clamp(at.y, 10.0f, std::max(10.0f, io.DisplaySize.y - size.y - 10.0f)); + auto *draw{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())}; + draw->AddRectFilled(ImVec2(at.x - 6.0f, at.y - 4.0f), ImVec2(at.x + size.x + 6.0f, at.y + size.y + 4.0f), IM_COL32(40, 0, 0, static_cast(210 * alpha)), 4.0f); + draw->AddText(ImGui::GetFont(), ImGui::GetFontSize(), at, IM_COL32(255, 150, 130, static_cast(255 * alpha)), m_refusal.c_str(), nullptr, wrap); } void editor_ui::render_rotation_controls() diff --git a/application/editoruilayer.h b/application/editoruilayer.h index b6c8a934..9fe7ee28 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -165,6 +165,7 @@ class editor_ui : public ui_layer private: // methods + void render_refusal(); void render_mode_options(nodebank_panel::edit_mode const Mode); // windows of the editor drawn by the user interface itself void render_tool_options(); @@ -208,6 +209,8 @@ class editor_ui : public ui_layer std::function m_workspacebar; std::function m_editmenu; bool m_layoutreset{false}; // the default layout is to be built again on the next frame + std::string m_refusal; + double m_refusaltime{-100.0}; bool m_tooloptionsopen{true}; bool m_inspectoropen{true}; bool m_sceneopen{true}; From 6f25290eee20775da3e80ddcd2c004d79051bc34 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 13:20:20 +0200 Subject: [PATCH 020/145] feat(editor): the terrain model asks again for what it didn't get, can be stopped and says when the place is outside Poland --- editor/editorNmt.cpp | 73 +++++++++++++++++++++++++++++++++++++++++--- editor/editorNmt.hpp | 15 +++++++-- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/editor/editorNmt.cpp b/editor/editorNmt.cpp index ce29f607..a71e0e2a 100644 --- a/editor/editorNmt.cpp +++ b/editor/editorNmt.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include #include #include @@ -33,6 +34,12 @@ char const *const cache_folder{"editor/cache/nmt/grid1/"}; int constexpr worker_count{2}; // the service is quick, but isn't to be hammered int constexpr border{1}; // pixels of the neighbouring squares fetched around a square, so the heights interpolate across its edges float constexpr no_data{-1000.f}; // the model gives far lower values outside of the country +double constexpr retry_after{15.0}; // s, a square the service didn't give is asked for again + +double seconds() +{ + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); +} // the name of the model among the names the capabilities of the service list: the first which says it's a terrain model std::string coverage_of(std::string const &Capabilities) @@ -231,12 +238,17 @@ bool editor_nmt::request(glm::dvec2 const &Min, glm::dvec2 const &Max) auto const low{key_of(m_east - Max.x, m_north + Min.y)}; auto const high{key_of(m_east - Min.x, m_north + Max.y)}; auto const middle{key_of(m_east - 0.5 * (Min.x + Max.x), m_north + 0.5 * (Min.y + Max.y))}; + auto const now{seconds()}; for (auto e = low.first; e <= high.first; ++e) for (auto n = low.second; n <= high.second; ++n) { key const square{e, n}; - if (m_squares.contains(square)) - continue; + if (auto const found{m_squares.find(square)}; found != m_squares.end()) + { + if (false == found->second->failed || found->second->outside || now - found->second->failed_at < retry_after) + continue; + m_squares.erase(found); + } all = false; if (false == m_queued.contains(square)) wanted.push_back(square); @@ -265,10 +277,52 @@ std::size_t editor_nmt::waiting() const return m_queued.size(); } -std::string editor_nmt::error() const +bool editor_nmt::retrying(glm::dvec2 const &Min, glm::dvec2 const &Max) const { std::lock_guard lock{m_mutex}; - return m_error; + auto const low{key_of(m_east - Max.x, m_north + Min.y)}; + auto const high{key_of(m_east - Min.x, m_north + Max.y)}; + for (auto e = low.first; e <= high.first; ++e) + for (auto n = low.second; n <= high.second; ++n) + if (auto const found{m_squares.find({e, n})}; found != m_squares.end() && found->second->failed && false == found->second->outside) + return true; + return false; +} + +void editor_nmt::retry() +{ + std::lock_guard lock{m_mutex}; + std::erase_if(m_squares, [](auto const &Square) { return Square.second->failed && false == Square.second->outside; }); +} + +void editor_nmt::cancel() +{ + std::lock_guard lock{m_mutex}; + for (auto const &square : m_queue) + m_queued.erase(square); + m_queue.clear(); +} + +std::string editor_nmt::error(bool *Again) const +{ + std::lock_guard lock{m_mutex}; + std::string result; + bool outside{false}; + if (Again != nullptr) + *Again = false; + for (auto const &[key, square] : m_squares) + { + if (false == square->failed) + continue; + if (Again != nullptr && false == square->outside) + *Again = true; + if (result.empty() || (square->outside && false == outside)) + { + result = square->error; + outside = square->outside; + } + } + return result; } void editor_nmt::clear() @@ -300,7 +354,6 @@ void editor_nmt::work() continue; // cleared meanwhile m_queued.erase(square); m_squares[square] = std::move(fetched); - m_error = error; } } } @@ -324,6 +377,8 @@ std::shared_ptr editor_nmt::fetch(key const &Key, std: { Error = STRN("This build can't fetch from the internet"); result->failed = true; + result->outside = true; + result->error = Error; return result; } editor_http::client client; @@ -365,10 +420,18 @@ std::shared_ptr editor_nmt::fetch(key const &Key, std: } if (stop()) break; + if (std::string const text(data.begin(), data.end()); text.find("outside requested coverage") != std::string::npos) + { + Error = STRN("The place lies outside of the terrain model of Poland: check the location of the scenery"); + result->outside = true; + break; + } if (Error.empty()) Error = STRN("The service gave no terrain model"); } WriteLog("Editor: NMT square " + std::to_string(Key.first) + "," + std::to_string(Key.second) + " not fetched: " + Error, logtype::generic); result->failed = true; + result->error = Error; + result->failed_at = seconds(); return result; } diff --git a/editor/editorNmt.hpp b/editor/editorNmt.hpp index fdddac30..9d8fe330 100644 --- a/editor/editorNmt.hpp +++ b/editor/editorNmt.hpp @@ -46,8 +46,15 @@ class editor_nmt bool request(glm::dvec2 const &Min, glm::dvec2 const &Max); // squares asked for and not in yet std::size_t waiting() const; - // what the last fetch said went wrong, empty when it went right - std::string error() const; + // a square under the rectangle the service didn't give, which is asked for again after a while + bool retrying(glm::dvec2 const &Min, glm::dvec2 const &Max) const; + // the squares the service didn't give asked for again at once + void retry(); + // the squares waiting aren't fetched; those in stay + void cancel(); + // why squares in memory have no data, empty when all came; a place outside of the model is told first. Again: whether + // some of them are asked for again + std::string error(bool *Again = nullptr) const; // the squares dropped from the memory, and those waiting; the files on the disk stay void clear(); // whether this build can fetch from the service at all @@ -67,6 +74,9 @@ class editor_nmt int height{0}; std::vector heights; // rows from the north, columns from the west bool failed{false}; // the service gave nothing usable: the square has no data + bool outside{false}; // failed for it lies outside of the model: asked again, it gives nothing either + double failed_at{0.0}; // s + std::string error; }; key key_of(double const East, double const North) const; @@ -80,7 +90,6 @@ class editor_nmt std::set m_queued; std::map> m_squares; std::string m_coverage; // the name of the model in the service, read from its capabilities once - std::string m_error; double m_north{0.0}; double m_east{0.0}; bool m_stop{false}; From 0f9b00c74a2be142399ff73ba8b8c6527cedc4f9 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 13:20:25 +0200 Subject: [PATCH 021/145] feat(editor): a map of Poland to pick a place on, shared by the scenery wizard and the terrain model --- CMakeLists.txt | 1 + editor/editorGeoMap.cpp | 327 ++++++++++++++++++++++++++++++++++++ editor/editorGeoMap.hpp | 80 +++++++++ launcher/scenery_wizard.cpp | 237 ++------------------------ launcher/scenery_wizard.h | 31 +--- 5 files changed, 425 insertions(+), 251 deletions(-) create mode 100644 editor/editorGeoMap.cpp create mode 100644 editor/editorGeoMap.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 84ac684e..9d85d027 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -204,6 +204,7 @@ set(SOURCES "editor/editorOrthophoto.cpp" "editor/editorHttp.cpp" "editor/editorNmt.cpp" +"editor/editorGeoMap.cpp" "editor/editorPreviews.cpp" "editor/editorIcons.cpp" "editor/editorClips.cpp" diff --git a/editor/editorGeoMap.cpp b/editor/editorGeoMap.cpp new file mode 100644 index 00000000..52e837e5 --- /dev/null +++ b/editor/editorGeoMap.cpp @@ -0,0 +1,327 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorGeoMap.hpp" +#include "editor/editorFormat.hpp" +#include "editor/editorOrthophoto.hpp" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include "rendering/renderer.h" +#include "stb/stb_image.h" + +#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(); +} + +} // namespace + +namespace editor_geo +{ + +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 in_poland(glm::dvec2 const &Point) +{ + return Point.x >= kPolandLow.x && Point.x <= kPolandHigh.x && Point.y >= kPolandLow.y && Point.y <= kPolandHigh.y; +} + +bool parse_place(std::string const &Text, glm::dvec2 &Point) +{ + // the letters and the signs around the numbers out, as of "52.2297° N, 21.0122° E" or "X: 456787 Y: 579897" + std::string text; + for (unsigned char const letter : Text) + text += std::isdigit(letter) || letter == '.' || letter == ',' || letter == '-' ? static_cast(letter) : ' '; + std::vector words; + for (std::size_t at{0}; at < text.size();) + { + auto const begin{text.find_first_not_of(" ,", at)}; + if (begin == std::string::npos) + break; + auto end{text.find(' ', begin)}; + if (end == std::string::npos) + end = text.size(); + auto word{text.substr(begin, end - begin)}; + while (false == word.empty() && word.back() == ',') + word.pop_back(); + words.push_back(word); + at = end; + } + std::vector numbers; + if (words.size() == 1) + { + // one word: the comma parts the numbers, or the numbers and their decimals + std::vector parts; + for (std::size_t at{0};;) + { + auto const comma{words[0].find(',', at)}; + parts.push_back(words[0].substr(at, comma == std::string::npos ? std::string::npos : comma - at)); + if (comma == std::string::npos) + break; + at = comma + 1; + } + if (parts.size() == 2) + numbers = parts; + else if (parts.size() == 4) + numbers = {parts[0] + "." + parts[1], parts[2] + "." + parts[3]}; + } + else if (words.size() == 2) + for (auto word : words) + { + std::replace(word.begin(), word.end(), ',', '.'); + numbers.push_back(word); + } + if (numbers.size() != 2) + return false; + double values[2]; + for (int i = 0; i < 2; ++i) + { + char *end{nullptr}; + values[i] = std::strtod(numbers[i].c_str(), &end); + if (end == numbers[i].c_str() || *end != '\0') + return false; + } + auto const latitude = [](double const Value) { return Value >= 48.0 && Value <= 56.0; }; + auto const longitude = [](double const Value) { return Value >= 13.0 && Value <= 25.0; }; + if (latitude(values[0]) && longitude(values[1])) + Point = to_puwg(values[0], values[1]); + else if (longitude(values[0]) && latitude(values[1])) + Point = to_puwg(values[1], values[0]); + else if (values[0] > 1000.0 && values[1] > 1000.0) + Point = {values[1], values[0]}; + else + return false; + return in_poland(Point); +} + +} // namespace editor_geo + +editor_map::~editor_map() +{ + if (m_request.valid()) + m_request.wait(); + release(); +} + +void editor_map::release() +{ + if (m_texture != 0 && GfxRenderer && GfxRenderer->GetImguiRenderer() != nullptr) + GfxRenderer->GetImguiRenderer()->Release_Image(m_texture); + m_texture = 0; +} + +void editor_map::pick(glm::dvec2 const &Point) +{ + m_point = Point; + m_picked = true; +} + +void editor_map::show(glm::dvec2 const &Point, double const Scale) +{ + m_fitted = true; + m_centre = Point; + m_scale = std::min(m_scale, Scale); + m_dirty = true; +} + +bool editor_map::render(glm::vec2 const &Size) +{ + upload(); + 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(); + } + } + auto const clicked{ImGui::IsItemDeactivated() && false == m_dragging}; + if (clicked) + pick(to_world(mouse)); + + if (m_dirty && seconds() - m_changed > 0.25 && false == m_request.valid()) + request(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(static_cast(m_texture), to_screen({m_image_low.x, m_image_high.y}), to_screen({m_image_high.x, m_image_low.y}), ImVec2(0, 1), ImVec2(1, 0)); + 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{editor_geo::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_error.empty()) + info += (info.empty() ? "" : " ") + m_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(); + return clicked; +} + +void editor_map::request(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]() { + image 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 editor_map::upload() +{ + 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_error = result.error.empty() ? std::string{STR("The map service sent no image")} : STR("No map: ") + result.error; + return; + } + m_error.clear(); + release(); + auto *renderer{GfxRenderer ? GfxRenderer->GetImguiRenderer() : nullptr}; + m_texture = renderer != nullptr ? renderer->Create_Image(result.pixels.data(), result.width, result.height) : 0; + m_image_low = result.low; + m_image_high = result.high; +} diff --git a/editor/editorGeoMap.hpp b/editor/editorGeoMap.hpp new file mode 100644 index 00000000..340c7d25 --- /dev/null +++ b/editor/editorGeoMap.hpp @@ -0,0 +1,80 @@ +/* +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 + +// the place of a scenery in Poland: PUWG 1992 (EPSG:2180), the metres of the Polish maps, as (east, north) +namespace editor_geo +{ + +glm::dvec2 to_puwg(double const Latitude, double const Longitude); +// latitude, longitude +glm::dvec2 to_degrees(glm::dvec2 const &Point); +// two numbers as a map gives them: degrees (latitude, longitude, as the online maps copy them) or the X and Y of PUWG 1992 +// (north, then east, as geoportal.gov.pl shows them); false when they're neither, or the place isn't in Poland +bool parse_place(std::string const &Text, glm::dvec2 &Point); +bool in_poland(glm::dvec2 const &Point); + +} // namespace editor_geo + +// the map of Poland to pick a place on: the base map of OpenStreetMap from geoportal.gov.pl, the wheel zooms it, a drag moves +// it, a click picks the place +class editor_map +{ + public: + editor_map() = default; + ~editor_map(); + editor_map(editor_map const &) = delete; + editor_map &operator=(editor_map const &) = delete; + + // at the cursor, Size big; true when a place was clicked + bool render(glm::vec2 const &Size); + void pick(glm::dvec2 const &Point); + // the map goes there, at most Scale m a pixel + void show(glm::dvec2 const &Point, double const Scale = 4.0); + bool picked() const { return m_picked; } + glm::dvec2 const &point() const { return m_point; } + + private: + struct image + { + glm::dvec2 low{0.0}; + glm::dvec2 high{0.0}; + int width{0}; + int height{0}; + std::vector pixels; + std::string error; + }; + + void request(glm::vec2 const &Size); + void upload(); + void release(); + + glm::dvec2 m_centre{515000.0, 460000.0}; + double m_scale{1000.0}; // m a pixel + 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; + std::uint64_t m_texture{0}; + glm::dvec2 m_image_low{0.0}; + glm::dvec2 m_image_high{0.0}; + std::string m_error; + bool m_picked{false}; + glm::dvec2 m_point{0.0}; +}; diff --git a/launcher/scenery_wizard.cpp b/launcher/scenery_wizard.cpp index f53b88f1..092b4b91 100644 --- a/launcher/scenery_wizard.cpp +++ b/launcher/scenery_wizard.cpp @@ -11,15 +11,13 @@ http://mozilla.org/MPL/2.0/. #include "launcher/scenery_wizard.h" #include "application/application.h" #include "editor/editorFormat.hpp" +#include "editor/editorGeoMap.hpp" #include "editor/editorOrthophoto.hpp" #include "editor/editorSettings.hpp" #include "utilities/Globals.h" #include "utilities/Logs.h" #include "utilities/translation.h" -#include "stb/stb_image.h" -#include - #include #include #include @@ -32,61 +30,6 @@ http://mozilla.org/MPL/2.0/. 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 == '-'; }); @@ -96,22 +39,16 @@ bool valid_name(std::string const &Name) 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); -} +ui::scenerywizard_panel::~scenerywizard_panel() = default; 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}); + if (m_map.render(map - glm::vec2{16.f})) + pick(m_map.point()); ImGui::EndChild(); ImGui::SameLine(); ImGui::BeginChild("##wizardform", ImVec2(0.f, map.y), true); @@ -119,151 +56,10 @@ void ui::scenerywizard_panel::render_contents() 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((ImTextureID)(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)}; + m_map.pick(Point); + auto const degrees{editor_geo::to_degrees(Point)}; std::snprintf(m_latlon, sizeof(m_latlon), "%.6f, %.6f", degrees.x, degrees.y); } @@ -302,7 +98,7 @@ void ui::scenerywizard_panel::render_form() ImGui::InputTextWithHint("##title", STR_C("title, written in the file"), m_title, sizeof(m_title)); ImGui::Spacing(); - auto const placed{m_picked || false == m_realistic}; + auto const placed{m_map.picked() || false == m_realistic}; auto const ready{named && false == exists && placed}; if (false == ready) { @@ -322,8 +118,8 @@ void ui::scenerywizard_panel::render_form() ImGui::TextUnformatted(STR_C("Centre of the scenery, PUWG 1992")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("PUWG 1992: the coordinate system of the maps of Poland, in km")); - double east{m_point.x / 1000.0}; - double north{m_point.y / 1000.0}; + double east{m_map.point().x / 1000.0}; + double north{m_map.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); @@ -331,8 +127,7 @@ void ui::scenerywizard_panel::render_form() if (changed) { pick({east * 1000.0, north * 1000.0}); - m_centre = m_point; - m_dirty = true; + m_map.show(m_map.point(), 1000.0); } ImGui::SetNextItemWidth(-90.f); auto const entered{ImGui::InputTextWithHint("##latlon", STR_C("latitude, longitude"), m_latlon, sizeof(m_latlon), ImGuiInputTextFlags_EnterReturnsTrue)}; @@ -345,10 +140,8 @@ void ui::scenerywizard_panel::render_form() 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; + pick(editor_geo::to_puwg(latitude, longitude)); + m_map.show(m_map.point()); m_status.clear(); } else @@ -356,9 +149,9 @@ void ui::scenerywizard_panel::render_form() } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Degrees, as given by the online maps; the map moves there")); - if (m_picked) + if (m_map.picked()) { - auto const degrees{to_degrees(m_point)}; + auto const degrees{editor_geo::to_degrees(m_map.point())}; ImGui::TextDisabled(STR_C("%.5f N, %.5f E"), degrees.x, degrees.y); } ImGui::Spacing(); @@ -387,7 +180,7 @@ bool ui::scenerywizard_panel::create_scenery() 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); + std::snprintf(reference, sizeof(reference), "//$g PUWG1992 %.3f %.3f", m_map.point().x / 1000.0, m_map.point().y / 1000.0); if (m_realistic) { auto const &stored{EditorSettings.orthophoto()}; diff --git a/launcher/scenery_wizard.h b/launcher/scenery_wizard.h index cd22dff7..fcdf74dc 100644 --- a/launcher/scenery_wizard.h +++ b/launcher/scenery_wizard.h @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #pragma once #include "application/uilayer.h" +#include "editor/editorGeoMap.hpp" #include #include @@ -28,40 +29,12 @@ class scenerywizard_panel : public ui_panel 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}; + editor_map m_map; bool m_realistic{true}; - glm::dvec2 m_point{0.0}; char m_filename[64]{}; char m_title[128]{}; char m_latlon[64]{}; From a39a3d4dbed2e7d2d6758acf6e2bd3fd405dc1e0 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 13:20:39 +0200 Subject: [PATCH 022/145] feat(editor): the terrain model loads from a clearer panel and follows the tracks edited The panel gives the place on the map or typed, the area, one main button, a progress bar with Stop and a banner over the view while it loads. The terrain grown ahead of a track goes on at the height of the one beside it and skips the ground made by hand. The formations follow the tracks moved, laid and removed, in the same step of the history; a whole load is one step, the ground grown ahead none. --- application/editormode.cpp | 30 +- application/editormode.h | 53 +- application/editormodeinfra.cpp | 4 +- application/editormodenmt.cpp | 604 ++++++++++++++++++---- application/editormodeselftestterrain.cpp | 63 ++- application/editormodetrack.cpp | 2 +- application/editormodetrackbed.cpp | 7 +- editor/editorTrack.cpp | 4 +- editor/editorTrack.hpp | 1 + lang/editor_pl.po | 92 +++- 10 files changed, 738 insertions(+), 122 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 1282cf12..9a7da798 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -911,6 +911,9 @@ void editor_mode::terrain_history_step() { if (false == m_streamer.active()) return; + // the terrain loaded from the model is taken back as a whole + if (nmt_busy() && false == m_history_busy) + return; if (m_streamer.recording()) { if (terrain_brush() && mouseHold && false == m_history_busy) @@ -994,6 +997,12 @@ void editor_mode::undo_one() if (m_history.empty()) return; + m_nmt_state.restoring = true; + struct restored + { + bool &flag; + ~restored() { flag = false; } + } const restoring{m_nmt_state.restoring}; EditorSnapshot snap = m_history.back(); m_history.pop_back(); @@ -1157,6 +1166,12 @@ void editor_mode::redo_one() if (g_redo.empty()) return; + m_nmt_state.restoring = true; + struct restored + { + bool &flag; + ~restored() { flag = false; } + } const restoring{m_nmt_state.restoring}; EditorSnapshot snap = g_redo.back(); g_redo.pop_back(); @@ -1519,6 +1534,7 @@ bool editor_mode::update() render_shortcuts_window(); render_first_steps(); render_nmt_prompt(); + render_nmt_banner(); render_orthophoto_window(); render_switch_designer(); render_environment_window(); @@ -1607,14 +1623,16 @@ void editor_mode::render_terrain_ui() { if (false == m_streamer.active()) { - ImGui::TextWrapped("%s", STR_C("The scenery has no terrain yet. Make an empty one, or load it from the terrain model below")); - if (ImGui::Button(STR_C("Make heightmap terrain for this scenery"))) - ensure_terrain(); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Kept in the folder terrain// and written there with the scenery (Ctrl+S)")); + ImGui::TextUnformatted(STR_C("The scenery has no terrain yet")); // the terrain model makes the terrain itself if (ImGui::CollapsingHeader(STR_C("Terrain model (NMT)"), ImGuiTreeNodeFlags_DefaultOpen)) render_nmt_ui(); + ImGui::Spacing(); + ImGui::TextDisabled("%s", STR_C("Or without the real ground:")); + if (ImGui::Button(STR_C("Make an empty, flat terrain"))) + ensure_terrain(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Kept in the folder terrain// and written there with the scenery (Ctrl+S)")); return; } // the tools are chosen in the toolbar, here are their settings @@ -1647,6 +1665,8 @@ void editor_mode::render_terrain_ui() } } + if (nmt_busy()) + ImGui::SetNextItemOpen(true); if (ImGui::CollapsingHeader(STR_C("Terrain model (NMT)"))) render_nmt_ui(); diff --git a/application/editormode.h b/application/editormode.h index 7f60287f..7932570d 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -27,6 +27,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorTerrainStreamer.hpp" #include "editor/editorOrthophoto.hpp" #include "editor/editorNmt.hpp" +#include "editor/editorGeoMap.hpp" #include "editor/editorTrack.hpp" #include "editor/editorConnections.hpp" #include "editor/editorAlignment.hpp" @@ -323,6 +324,13 @@ class editor_mode : public application_mode, private editor_track::observer, pri double reach{60.0}; std::vector pending; // chunks to make once their model is in std::size_t made{0}; + std::size_t empty{0}; // chunks not made: the model has no data there + std::size_t foreign{0}; // chunks not made ahead: the terrain beside isn't of the model + std::optional ahead; // how much the terrain made ahead lies over the model + std::set changed; // tracks whose formations are to follow them + std::set gone; // names of the tracks removed, whose formations go with them + double changed_time{0.0}; + bool restoring{false}; // the history is taken back or forward: the terrain comes back with it std::size_t shaped{0}; std::string status; std::string error; @@ -354,13 +362,14 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::map, std::vector> anchor_cells; // the anchors in squares of the plan bool offered{false}; // the terrain was offered to the scenery begun without it bool prompt{false}; // the offer is shown - double north{0.0}; // the place typed into the offer, PUWG 1992 - double east{0.0}; int mode{0}; // 0: the tracks go onto the terrain, 1: the terrain is drawn to the tracks - bool moving{false}; // the place being typed again + bool moving{false}; // the place being picked again + char place[96]{}; // typed: degrees or PUWG 1992 + int shown{-1}; // the frame the terrain panel showed how far the work got }; editor_nmt m_nmt; nmt_state m_nmt_state; + editor_map m_nmt_map; std::vector nmt_track_places(double const Step) const override; void nmt_locate() override; bool nmt_choice_card(int const Index, char const *Title, char const *Text, float const Width, float const TextHeight, bool const Chosen) const; @@ -375,18 +384,50 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::vector nmt_keys_along_tracks() const; // Rebuild: the chunks there already are made anew too, and the formation laid before is forgotten, as when the tracks moved void nmt_make(std::vector const &Keys, bool const Rebuild = false); - bool add_chunk_from_nmt(heightmap::chunk_key const &Key); + enum class nmt_chunk + { + waiting, // for its squares of the model + made, + empty // the model has no data there + }; + // Offset: the heights of the model go up or down by it in the whole chunk, in place of the offsets of the fit + nmt_chunk add_chunk_from_nmt(heightmap::chunk_key const &Key, std::optional const Offset = std::nullopt); + // how much the terrain lies over the model at the points: none where there's too little terrain at them, nor where it + // doesn't follow the model (Foreign), as one made by hand + std::optional nmt_terrain_offset(std::vector const &Points, bool &Foreign) const; + // the same along the edges the chunk shares with the chunks beside it in memory + std::optional nmt_seam_offset(heightmap::chunk_key const &Key, bool &Foreign) const; + // the terrain beside the formation of the track is the one made from the model; Waiting: its squares of the model aren't in yet + bool nmt_terrain_along(TTrack const &Track, bool &Waiting); + // the tracks changed by hand are told to the terrain: their formations laid anew, of the tracks removed taken away, a + // track laid onto the terrain of the model given its own, once the mouse lets go; in the step of the history of the change + void nmt_track_changed(std::vector const &Tracks); + void nmt_track_gone(TTrack const &Track); + void nmt_follow_tracks(); + // the height the ground will have where the terrain is made ahead of the track laid and isn't there yet + bool nmt_ahead(double const X, double const Z, double &Height) const; + // the squares of the model under the rectangle are in, none of them waiting to be asked for again + bool nmt_ready(glm::dvec2 const &Min, glm::dvec2 const &Max); // Only: the paths of these tracks are shaped anew, the modifiers of those removed taken away; null: all of them. the number // of the modifiers laid std::size_t nmt_shape_tracks(std::vector const *Only = nullptr); void nmt_update(); void nmt_grow(); void render_nmt_ui(); + // where the scenery lies: the map to click on, the place typed; true once it's placed + bool render_nmt_place(bool const Moving, bool const Button); + // the work under way: what's left of it and the button to stop it; Compact: one line, for the 3d view + void render_nmt_progress(bool const Compact); + // over the 3d view while the terrain panel isn't seen + void render_nmt_banner(); + bool nmt_busy() const; + // the terrain made so far stays, what's waiting is dropped, the grade lines not fitted yet are left + void nmt_stop(); // a scenery begun from nothing, with no tracks nor terrain, is asked as the first track is laid whether it wants the terrain void nmt_offer(); void render_nmt_prompt(); - // the place typed in the terrain window: the terrain model and the orthophoto take it - void nmt_place_scenery(); + // the place picked in the terrain window: the terrain model and the orthophoto take it + void nmt_place_scenery(glm::dvec2 const &Point); void nmt_accept_offer(); // the whole scenery: tracks, includes and models, moved up or down as one undo step void nmt_lift_scenery(double const Offset); diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index b8bdfd2e..4e3dc7da 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -1194,6 +1194,7 @@ void editor_mode::track_captured(TTrack const &Track) void editor_mode::tracks_committed(std::vector const &Tracks) { + nmt_track_changed(Tracks); sweeps_committed(Tracks); switch_drives_committed(Tracks); if (m_infra_suspended || false == m_infra.follow) @@ -1273,8 +1274,9 @@ void editor_mode::tracks_committed(std::vector const &Tracks) } } -void editor_mode::track_retired(TTrack &) +void editor_mode::track_retired(TTrack &Track) { + nmt_track_gone(Track); tracks_committed({}); } diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index a540e010..f1aa3c73 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editormode.h" #include "application/editormodeshared.h" +#include "application/editoruilayer.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorSettings.hpp" @@ -252,6 +253,7 @@ void editor_mode::nmt_fit() { nmt_locate(); auto &state{m_nmt_state}; + state.error.clear(); state.under = false; state.fit.clear(); auto const places{nmt_track_places(25.0)}; @@ -276,6 +278,7 @@ void editor_mode::nmt_under_start() { nmt_locate(); auto &state{m_nmt_state}; + state.error.clear(); state.fit.clear(); state.anchors.clear(); state.anchor_cells.clear(); @@ -452,6 +455,9 @@ void editor_mode::nmt_make(std::vector const &Keys, bool c ++queued; } state.made = 0; + state.empty = 0; + state.foreign = 0; + state.error.clear(); if (queued > 0) state.grown = false; // the terrain comes with its photos @@ -460,12 +466,186 @@ void editor_mode::nmt_make(std::vector const &Keys, bool c state.status = queued > 0 ? format(STR_P("%zu chunk to make from the terrain model", "%zu chunks to make from the terrain model", queued), queued) : STR("There's terrain there already"); } -bool editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key) +bool editor_mode::nmt_ready(glm::dvec2 const &Min, glm::dvec2 const &Max) +{ + return m_nmt.request(Min, Max) && false == m_nmt.retrying(Min, Max); +} + +std::optional editor_mode::nmt_terrain_offset(std::vector const &Points, bool &Foreign) const +{ + Foreign = false; + std::vector differences; + for (auto const &point : Points) + if (double terrain, model; m_streamer.height_at(point.x, point.y, terrain) && m_nmt.height(point.x, point.y, model)) + differences.push_back(terrain - model); + if (differences.size() < 5) + return std::nullopt; + std::nth_element(differences.begin(), differences.begin() + differences.size() / 2, differences.end()); + auto const median{differences[differences.size() / 2]}; + // the formations of the tracks and the touch-ups of the brushes leave the terrain here and there, the rest keeps to the model + auto const along{std::count_if(differences.begin(), differences.end(), [&](double const Difference) { return std::abs(Difference - median) <= 0.3; })}; + if (static_cast(along) < 0.7 * static_cast(differences.size())) + { + Foreign = true; + return std::nullopt; + } + return median; +} + +std::optional editor_mode::nmt_seam_offset(heightmap::chunk_key const &Key, bool &Foreign) const +{ + auto const low{heightmap::chunk_corner(Key)}; + auto const size{heightmap::chunk_size}; + int constexpr steps{16}; + struct side + { + int dx; + int dz; + glm::dvec2 from; + glm::dvec2 to; + }; + side const sides[]{{-1, 0, low, {low.x, low.y + size}}, {1, 0, {low.x + size, low.y}, low + glm::dvec2{size}}, {0, -1, low, {low.x + size, low.y}}, {0, 1, {low.x, low.y + size}, low + glm::dvec2{size}}}; + std::vector points; + for (auto const &edge : sides) + if (m_streamer.exists({Key.first + edge.dx, Key.second + edge.dz})) + for (int i = 0; i <= steps; ++i) + points.push_back(edge.from + (edge.to - edge.from) * (static_cast(i) / steps) + glm::dvec2{edge.dx, edge.dz} * 0.01); + return nmt_terrain_offset(points, Foreign); +} + +bool editor_mode::nmt_terrain_along(TTrack const &Track, bool &Waiting) +{ + Waiting = false; + if (false == m_nmt.placed() || Track.m_paths.empty()) + return false; + auto const half{profile_ballast(Track).second}; + std::vector points; + glm::dvec2 low{std::numeric_limits::max()}; + glm::dvec2 high{std::numeric_limits::lowest()}; + for (auto const &path : Track.m_paths) + { + geometry::bezier const curve{path}; + auto const count{std::max(2, static_cast(std::ceil(curve.plan_length() / 10.0)))}; + for (int i = 0; i <= count; ++i) + { + auto const t{static_cast(i) / count}; + auto const at{geometry::plan_of(curve.point(t))}; + auto const along{geometry::plan_of(curve.first(t))}; + if (glm::length(along) < 1e-9) + continue; + auto const across{glm::dvec2{-along.y, along.x} / glm::length(along) * (half + 15.0)}; + for (auto const &point : {at + across, at - across}) + { + points.push_back(point); + low = glm::min(low, point); + high = glm::max(high, point); + } + } + } + if (points.empty()) + return false; + if (false == nmt_ready(low - glm::dvec2{2.0}, high + glm::dvec2{2.0})) + { + Waiting = true; + return false; + } + bool foreign{false}; + return nmt_terrain_offset(points, foreign).has_value(); +} + +void editor_mode::nmt_track_changed(std::vector const &Tracks) +{ + auto &state{m_nmt_state}; + if (state.restoring || state.stage != nmt_state::automatic::idle || Tracks.empty() || false == m_streamer.active()) + return; + for (auto *track : Tracks) + if (track != nullptr) + state.changed.insert(track); + state.changed_time = ImGui::GetTime(); +} + +void editor_mode::nmt_track_gone(TTrack const &Track) +{ + auto &state{m_nmt_state}; + if (state.restoring || state.stage != nmt_state::automatic::idle || Track.name().empty() || false == m_streamer.active()) + return; + state.gone.insert(Track.name()); + state.changed_time = ImGui::GetTime(); +} + +void editor_mode::nmt_follow_tracks() +{ + auto &state{m_nmt_state}; + if (state.changed.empty() && state.gone.empty()) + return; + if (state.stage != nmt_state::automatic::idle || state.fitting || ImGui::GetTime() - state.changed_time < 0.3 || ImGui::IsMouseDown(ImGuiMouseButton_Left) || ImGui::IsMouseDown(ImGuiMouseButton_Right)) + return; + if (false == m_streamer.active()) + { + state.changed.clear(); + state.gone.clear(); + return; + } + auto const &modifiers{m_streamer.modifiers()}; + auto const of = [](heightmap::modifier const &Modifier, std::string const &Track) { return Modifier.name == "nmt " + Track || Modifier.name.starts_with("nmt " + Track + " "); }; + std::vector dropped; + for (auto const &name : state.gone) + if (simulation::Paths.find(name) == nullptr) + for (auto const &modifier : modifiers) + if (of(modifier, name)) + dropped.push_back(modifier.name); + state.gone.clear(); + std::vector shaped; + std::set waiting; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == state.changed.contains(track) || track->m_editorremoved || false == rail(*track) || track->name().empty()) + continue; + if (std::any_of(modifiers.begin(), modifiers.end(), [&](heightmap::modifier const &Modifier) { return of(Modifier, track->name()); })) + { + shaped.push_back(track); + continue; + } + bool wait{false}; + if (state.shape && nmt_terrain_along(*track, wait)) + shaped.push_back(track); + else if (wait) + waiting.insert(track); + } + state.changed = std::move(waiting); + if (dropped.empty() && shaped.empty()) + return; + // the terrain follows in the step of the history of the change of the tracks + terrain_history_step(); + auto const *top{m_history.empty() ? nullptr : m_history.back().terrain.get()}; + for (auto const &name : dropped) + m_streamer.remove_modifier(name); + auto const laid{shaped.empty() ? std::size_t{0} : nmt_shape_tracks(&shaped)}; + if (false == dropped.empty()) + forget_ground(); + terrain_history_step(); + auto const count{m_history.size()}; + auto const of_tracks = [](EditorSnapshot const &Step) { return Step.action == EditorSnapshot::Action::TrackEdit || (Step.action == EditorSnapshot::Action::Terrain && Step.joined); }; + if (count >= 2 && m_history.back().action == EditorSnapshot::Action::Terrain && m_history.back().terrain.get() != top && of_tracks(m_history[count - 2])) + m_history.back().joined = true; + WriteLog(format("Editor: NMT - the terrain followed the tracks: %zu formations laid, %zu taken away", laid, dropped.size()), logtype::generic); +} + +bool editor_mode::nmt_ahead(double const X, double const Z, double &Height) const +{ + auto const &state{m_nmt_state}; + if (false == state.grow || false == state.ahead.has_value() || false == m_nmt.placed() || false == m_nmt.height(X, Z, Height)) + return false; + Height += *state.ahead; + return true; +} + +editor_mode::nmt_chunk editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key, std::optional const Offset) { auto const low{heightmap::chunk_corner(Key)}; auto const high{low + glm::dvec2{heightmap::chunk_size}}; - if (false == m_nmt.request(low - glm::dvec2{2.0}, high + glm::dvec2{2.0})) - return false; + if (false == nmt_ready(low - glm::dvec2{2.0}, high + glm::dvec2{2.0})) + return nmt_chunk::waiting; auto const cells{heightmap::cells_for(m_terrain_spacing)}; auto const side{static_cast(cells) + 1}; auto const step{heightmap::chunk_size / cells}; @@ -476,7 +656,7 @@ bool editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key) std::array offsets; for (int iz = 0; iz <= coarse; ++iz) for (int ix = 0; ix <= coarse; ++ix) - offsets[iz * (coarse + 1) + ix] = nmt_offset_at(low.x + ix * heightmap::chunk_size / coarse, low.y + iz * heightmap::chunk_size / coarse); + offsets[iz * (coarse + 1) + ix] = Offset.has_value() ? *Offset : nmt_offset_at(low.x + ix * heightmap::chunk_size / coarse, low.y + iz * heightmap::chunk_size / coarse); auto const offset_at = [&](double const X, double const Z) { auto const u{std::clamp((X - low.x) / heightmap::chunk_size * coarse, 0.0, static_cast(coarse))}; auto const v{std::clamp((Z - low.y) / heightmap::chunk_size * coarse, 0.0, static_cast(coarse))}; @@ -499,15 +679,16 @@ bool editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key) any = true; } } + if (false == any) + return nmt_chunk::empty; // where the model has no data the chunk goes on level with its nearest point - if (any) - fill_from_nearest(heights, found, side); + fill_from_nearest(heights, found, side); m_streamer.add_chunk(Key.first, Key.second, m_terrain_spacing, [&](double const X, double const Z) { auto const ix{static_cast(std::clamp(std::lround((X - low.x) / step), 0, cells))}; auto const iz{static_cast(std::clamp(std::lround((Z - low.y) / step), 0, cells))}; return static_cast(heights[iz * side + ix]); }); - return true; + return nmt_chunk::made; } std::size_t editor_mode::nmt_shape_tracks(std::vector const *Only) @@ -559,6 +740,20 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector const *Only) laid.push_back(std::move(modifier)); if (Only != nullptr) { + // the formations as they are already aren't led into the terrain again + auto const same = [](heightmap::modifier const &A, heightmap::modifier const &B) { + return A.slope == B.slope && A.reach == B.reach && A.rounding == B.rounding && A.points.size() == B.points.size() && + std::equal(A.points.begin(), A.points.end(), B.points.begin(), [](heightmap::corridor_point const &P, heightmap::corridor_point const &Q) { + return glm::distance(P.position, Q.position) < 1e-6 && std::abs(P.formation - Q.formation) < 1e-6 && std::abs(P.half_width - Q.half_width) < 1e-6; + }); + }; + auto const &existing{m_streamer.modifiers()}; + std::erase_if(laid, [&](heightmap::modifier const &Modifier) { + auto const found{std::find_if(existing.begin(), existing.end(), [&](heightmap::modifier const &Other) { return Other.name == Modifier.name; })}; + return found != existing.end() && same(*found, Modifier); + }); + for (auto *track : *Only) + state.changed.erase(track); // the modifiers the tracks don't have any more: of the pieces of a path gone, of the tracks removed std::vector gone; for (auto const *track : *Only) @@ -577,7 +772,11 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector const *Only) m_streamer.modifiers(std::move(laid)); forget_ground(); if (Only == nullptr) + { state.shaped = modifiers.size(); + state.changed.clear(); + state.gone.clear(); + } return count; } @@ -585,6 +784,7 @@ void editor_mode::nmt_update() { auto &state{m_nmt_state}; nmt_offer(); + nmt_follow_tracks(); if (state.stage != nmt_state::automatic::idle) { nmt_auto_step(); @@ -594,7 +794,7 @@ void editor_mode::nmt_update() { bool all{true}; for (auto const &place : state.fit) - all = m_nmt.request(glm::dvec2{place.position.x, place.position.z} - glm::dvec2{1.0}, glm::dvec2{place.position.x, place.position.z} + glm::dvec2{1.0}) && all; + all = nmt_ready(glm::dvec2{place.position.x, place.position.z} - glm::dvec2{1.0}, glm::dvec2{place.position.x, place.position.z} + glm::dvec2{1.0}) && all; if (all) nmt_finish_fit(); return; @@ -602,8 +802,17 @@ void editor_mode::nmt_update() nmt_grow(); if (state.pending.empty()) return; - // a few chunks a frame, those whose model is in + // a few chunks a frame, those whose model is in. the terrain made ahead of the track laid isn't a step of the history: + // Ctrl+Z takes back the track, not the ground under it + auto const unrecorded{state.grown && m_streamer.recording()}; + if (unrecorded) + { + terrain_history_step(); + m_streamer.end_change(); + } int made{0}; + std::size_t apart{0}; + std::set fresh; for (auto it{state.pending.begin()}; it != state.pending.end() && made < 4;) { if (m_streamer.exists(*it)) @@ -611,19 +820,59 @@ void editor_mode::nmt_update() it = state.pending.erase(it); continue; } - if (add_chunk_from_nmt(*it)) + // ahead of the track the terrain goes on from the one there is, at its height over the model + std::optional offset; + if (state.grown) { - it = state.pending.erase(it); - ++made; - ++state.made; + bool foreign{false}; + offset = nmt_seam_offset(*it, foreign); + if (foreign) + { + it = state.pending.erase(it); + ++state.foreign; + ++made; + continue; + } + if (false == offset.has_value()) + { + ++apart; + ++it; + continue; + } + } + auto const chunk{add_chunk_from_nmt(*it, offset)}; + if (chunk == nmt_chunk::waiting) + { + ++it; continue; } - ++it; + if (offset.has_value() && chunk == nmt_chunk::made) + { + state.ahead = offset; + fresh.insert(*it); + } + it = state.pending.erase(it); + ++made; + if (chunk == nmt_chunk::made) + ++state.made; + else + ++state.empty; + } + // the chunks ahead which no terrain made from the model touches any more are given up + if (made == 0 && apart > 0 && apart == state.pending.size()) + { + state.foreign += apart; + state.pending.clear(); + made = 1; } if (made > 0) { forget_ground(); state.status = format(STR_C("Chunks made from the terrain model: %zu, waiting for it: %zu"), state.made, state.pending.size()); + if (state.empty > 0) + state.status += format(STR_P("; %zu chunk not made, the model has no data there", "; %zu chunks not made, the model has no data there", state.empty), state.empty); + if (state.foreign > 0) + state.status += format(STR_P("; %zu chunk not made ahead, the terrain beside isn't the real one", "; %zu chunks not made ahead, the terrain beside isn't the real one", state.foreign), state.foreign); if (state.pending.empty()) { if (state.shape && false == state.grown) @@ -636,8 +885,24 @@ void editor_mode::nmt_update() first_step_done(first_step::nmt); } } - if (auto const error{m_nmt.error()}; false == error.empty()) - state.error = STR(error); + // the tracks laid ahead of the terrain get their formations once it's there under them + if (state.shape && false == fresh.empty()) + { + auto const crosses = [&](segment_data const &Path) { + geometry::bezier const curve{Path}; + auto const count{std::max(1, static_cast(std::ceil(curve.plan_length() / 50.0)))}; + for (int i = 0; i <= count; ++i) + if (auto const at{curve.point(static_cast(i) / count)}; fresh.contains(heightmap::chunk_at(at.x, at.z))) + return true; + return false; + }; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && rail(*track) && std::any_of(track->m_paths.begin(), track->m_paths.end(), crosses)) + state.changed.insert(track); + state.changed_time = ImGui::GetTime(); + } + if (unrecorded) + m_streamer.begin_change(); } void editor_mode::nmt_grow() @@ -670,6 +935,8 @@ void editor_mode::nmt_grow() { state.grown = true; state.made = 0; + state.empty = 0; + state.foreign = 0; } for (auto const &chunk : missing) state.pending.push_back(chunk.second); @@ -678,57 +945,191 @@ void editor_mode::nmt_grow() WriteLog(format("Editor: NMT - %zu chunks around the cursor to make", missing.size()), logtype::generic); } +bool editor_mode::nmt_busy() const +{ + auto const &state{m_nmt_state}; + return state.fitting || state.stage != nmt_state::automatic::idle || (false == state.pending.empty() && false == state.grown); +} + +void editor_mode::nmt_stop() +{ + auto &state{m_nmt_state}; + if (false == nmt_busy()) + return; + auto const lines{state.stage == nmt_state::automatic::fitting && state.lines > 0}; + m_nmt.cancel(); + state.fitting = false; + state.fit.clear(); + state.probes.clear(); + state.left.clear(); + state.done.clear(); + state.laid.clear(); + state.stage = nmt_state::automatic::idle; + state.pending.clear(); + m_profile.open = false; + state.status = format(STR_P("Stopped: %zu chunk made stays", "Stopped: %zu chunks made stay", state.made), state.made); + if (lines) + state.status += STR("; so do the grade lines fitted, Ctrl+Z takes them back"); + WriteLog("Editor: NMT - " + state.status, logtype::generic); +} + +void editor_mode::render_nmt_progress(bool const Compact) +{ + auto &state{m_nmt_state}; + float fraction{0.0f}; + std::string text; + if (state.stage == nmt_state::automatic::fetching || state.fitting) + { + auto const waiting{m_nmt.waiting()}; + fraction = -1.0f * static_cast(ImGui::GetTime()); + text = format(STR_P("Terrain model: %zu square left", "Terrain model: %zu squares left", waiting), waiting); + } + else if (state.stage == nmt_state::automatic::fitting && state.tracks > 0) + { + fraction = 1.0f - static_cast(state.left.size()) / static_cast(std::max(state.tracks, state.left.size())); + text = format(STR_P("Grade lines: %zu fitted, %zu track left", "Grade lines: %zu fitted, %zu tracks left", state.left.size()), state.lines, state.left.size()); + } + else if (false == state.pending.empty()) + { + auto const total{state.made + state.empty + state.pending.size()}; + fraction = static_cast(state.made + state.empty) / static_cast(std::max(1, total)); + text = format(STR_C("Chunks: %zu of %zu"), state.made + state.empty, total); + } + else + return; + auto const stoppable{nmt_busy()}; + auto const stop{STR_C("Stop")}; + auto const button{ImGui::CalcTextSize(stop).x + ImGui::GetStyle().FramePadding.x * 2.0f}; + auto const width{Compact ? std::round(ImGui::GetFontSize() * 16.0f) : stoppable ? -(button + ImGui::GetStyle().ItemSpacing.x) : -FLT_MIN}; + ImGui::ProgressBar(fraction, ImVec2(width, Compact ? 0.0f : tall_button_height()), text.c_str()); + if (false == stoppable) + return; + ImGui::SameLine(); + if (ImGui::Button(stop, ImVec2(0.0f, Compact ? 0.0f : tall_button_height()))) + nmt_stop(); + item_tooltip("Stops loading: the terrain made so far stays"); +} + +void editor_mode::render_nmt_banner() +{ + auto const &state{m_nmt_state}; + if (false == nmt_busy() || state.shown >= ImGui::GetFrameCount() - 1) + return; + auto const low{editor_ui::view_min()}; + auto const high{editor_ui::view_max()}; + ImGui::SetNextWindowPos(ImVec2((low.x + high.x) * 0.5f, low.y + std::round(ImGui::GetFontSize() * 3.6f)), ImGuiCond_Always, ImVec2(0.5f, 0.0f)); + ImGui::SetNextWindowViewport(ImGui::GetMainViewport()->ID); + ImGui::SetNextWindowBgAlpha(0.88f); + if (ImGui::Begin("##nmtbanner", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDocking)) + { + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(STR_C("Loading the real terrain")); + ImGui::SameLine(); + render_nmt_progress(true); + bool again{false}; + if (auto const error{m_nmt.error(&again)}; false == error.empty()) + { + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR(error).c_str()); + if (again) + { + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Try again"))) + m_nmt.retry(); + } + } + } + ImGui::End(); +} + +bool editor_mode::render_nmt_place(bool const Moving, bool const Button) +{ + auto &state{m_nmt_state}; + auto &map{m_nmt_map}; + auto const width{ImGui::GetContentRegionAvail().x}; + auto const height{std::round(std::min(width * 0.65f, ImGui::GetFontSize() * 16.0f))}; + ImGui::TextUnformatted(STR_C("Click the middle of the scenery on the map")); + item_tooltip("The point 0,0,0 of the scenery. The wheel zooms the map, a drag moves it"); + ImGui::BeginChild("##nmtmap", ImVec2(width, height), false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + if (map.render({width, height})) + { + auto const degrees{editor_geo::to_degrees(map.point())}; + std::snprintf(state.place, sizeof(state.place), "%.6f, %.6f", degrees.x, degrees.y); + } + ImGui::EndChild(); + ImGui::SetNextItemWidth(-FLT_MIN); + glm::dvec2 typed; + auto const understood{editor_geo::parse_place(state.place, typed)}; + if (ImGui::InputTextWithHint("##nmtplace", STR_C("or type it: 52.2297, 21.0122"), state.place, sizeof(state.place)) && editor_geo::parse_place(state.place, typed)) + { + map.pick(typed); + map.show(typed, 8.0); + } + item_tooltip("Degrees as the online maps copy them (latitude, longitude), or the X and Y of PUWG 1992, the coordinates of the Polish maps of geoportal.gov.pl"); + if (state.place[0] != '\0' && false == understood) + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR_C("Not a place in Poland: two numbers, degrees or the X and Y of PUWG 1992")); + else if (map.picked()) + ImGui::TextDisabled(STR_C("X %.0f, Y %.0f in PUWG 1992"), map.point().y, map.point().x); + if (Moving && m_streamer.resident() > 0) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.95f, 0.75f, 0.3f, 1.0f)); + ImGui::TextWrapped("%s", STR_C("The terrain made already stays as it is")); + ImGui::PopStyleColor(); + } + if (false == Button) + return false; + auto const cancel{STR_C("Cancel")}; + auto const cancel_width{ImGui::CalcTextSize(cancel).x + ImGui::GetStyle().FramePadding.x * 2.0f}; + bool placed{false}; + ImGui::BeginDisabled(false == map.picked()); + if (main_button(STR_C("The scenery is here"), ImVec2(Moving ? -(cancel_width + ImGui::GetStyle().ItemSpacing.x) : -FLT_MIN, tall_button_height()))) + { + nmt_place_scenery(map.point()); + placed = true; + } + ImGui::EndDisabled(); + if (Moving) + { + ImGui::SameLine(); + if (ImGui::Button(cancel, ImVec2(0.0f, tall_button_height()))) + state.moving = false; + } + return placed; +} + void editor_mode::render_nmt_ui() { auto &state{m_nmt_state}; + state.shown = ImGui::GetFrameCount(); nmt_locate(); - auto const busy{state.fitting || state.stage != nmt_state::automatic::idle}; + auto const busy{nmt_busy()}; step_heading(1, STR_C("Location")); if (false == m_nmt.placed() || state.moving) { - // where the scenery is, right here: the terrain model and the orthophoto take it - ImGui::PushTextWrapPos(0.0f); - ImGui::TextDisabled("%s", STR_C("Where the point 0,0 of the scenery lies on the map:")); - ImGui::PopTextWrapPos(); - ImGui::SetNextItemWidth(120.0f); - ImGui::InputDouble("X##nmtplacenorth", &state.north, 0.0, 0.0, "%.0f"); - item_tooltip("X of the point 0,0 in the PUWG 1992 coordinates of the Polish maps, metres to the north, as geoportal.gov.pl gives it"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(120.0f); - ImGui::InputDouble("Y##nmtplaceeast", &state.east, 0.0, 0.0, "%.0f"); - item_tooltip("Y of the point 0,0 in the PUWG 1992 coordinates of the Polish maps, metres to the east, as geoportal.gov.pl gives it"); - ImGui::BeginDisabled(state.north < 100000.0 || state.east < 100000.0); - if (ImGui::Button(STR_C("The scenery is there"))) - { - nmt_place_scenery(); + if (render_nmt_place(state.moving, true)) state.moving = false; - } - ImGui::EndDisabled(); - if (state.moving) - { - ImGui::SameLine(); - if (ImGui::Button(STR_C("Cancel"))) - state.moving = false; - } if (false == m_nmt.placed()) return; } else { auto const &config{m_orthophoto.settings()}; - ImGui::Text(STR_C("N %.0f, E %.0f"), config.north, config.east); - ImGui::SameLine(); - ImGui::TextDisabled("%s", STR_C("PUWG 1992")); + auto const degrees{editor_geo::to_degrees({config.east, config.north})}; + ImGui::AlignTextToFramePadding(); + ImGui::Text(STR_C("%.5f N, %.5f E"), degrees.x, degrees.y); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("X %.0f, Y %.0f in PUWG 1992, the coordinates of the Polish maps; the place is shared with the orthophoto"), config.north, config.east); ImGui::SameLine(); ImGui::BeginDisabled(busy); if (ImGui::SmallButton(STR_C("Change..."))) { - state.north = config.north; - state.east = config.east; + glm::dvec2 const point{config.east, config.north}; + m_nmt_map.pick(point); + m_nmt_map.show(point, 8.0); + std::snprintf(state.place, sizeof(state.place), "%.6f, %.6f", degrees.x, degrees.y); state.moving = true; } ImGui::EndDisabled(); - item_tooltip("The place is shared with the orthophoto"); } auto const tracks{std::any_of(simulation::Paths.sequence().begin(), simulation::Paths.sequence().end(), [](TTrack const *Track) { return Track != nullptr && rail(*Track); })}; @@ -755,57 +1156,47 @@ void editor_mode::render_nmt_ui() ImGui::PushTextWrapPos(0.0f); ImGui::TextDisabled("%s", STR_C("No tracks yet: the terrain is made around the camera, the tracks laid then go along it")); ImGui::PopTextWrapPos(); + if (begin_fields("##nmtarea")) + { + field_row("Area"); + segmented("nmtarea", m_terrain_add_count, {{2, "0.5 km", "A square of 500 m around the camera"}, {4, "1 km", "A square of 1 km around the camera"}, {8, "2 km", "A square of 2 km around the camera"}}); + ImGui::EndTable(); + } } step_heading(3, STR_C("Load")); - if (tracks && m_streamer.resident() > 0) + if (tracks && m_streamer.resident() > 0 && false == busy) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.95f, 0.75f, 0.3f, 1.0f)); ImGui::TextWrapped("%s", STR_C("The scenery has terrain already: along the tracks it's made anew from the model, its painting too")); ImGui::PopStyleColor(); } - ImGui::BeginDisabled(busy); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.45f, 0.75f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.26f, 0.55f, 0.88f, 1.0f)); - char const *label{tracks ? (state.mode == 0 ? STR_C("Load the terrain, put the tracks onto it") : STR_C("Load the terrain under the tracks")) : STR_C("Load the terrain around the camera")}; - if (ImGui::Button(label, ImVec2(-1.0f, ImGui::GetFrameHeight() * 1.5f))) + if (busy) + render_nmt_progress(false); + else { - if (false == tracks) + char const *label{tracks ? (state.mode == 0 ? STR_C("Load the terrain, put the tracks onto it") : STR_C("Load the terrain under the tracks")) : STR_C("Load the terrain around the camera")}; + if (main_button(label, ImVec2(-FLT_MIN, tall_button_height()))) { - state.offset = 0.0; - nmt_make(nmt_keys_around()); + if (false == tracks) + { + state.offset = 0.0; + nmt_make(nmt_keys_around()); + } + else if (state.mode == 0) + nmt_auto_start(); + else + nmt_under_start(); + } + if (ImGui::IsItemHovered()) + { + auto const *text{tracks ? (state.mode == 0 ? STR_C("The terrain model is loaded along the tracks and the tracks go onto it, wherever the scenery was built: the top of the ballast at the height of the model, the grade line of every line fitted to it within the limits of the line, switches and the tracks beside them on one grade line, the formation and the slopes made along them. Where the model is off the tracks keep their line and it's reported") : + STR_C("The tracks aren't touched: the terrain model is loaded along them and drawn to them, at each place by as much as the tracks lie above or below it there, fading out away from them, so its relief stays and the tracks don't end up in deep cuttings; then the formation and the slopes along them. For a scenery built level or at its own heights")) : + STR_C("Loads the terrain model, the real relief of the ground, around the camera")}; + ImGui::SetTooltip("%s\n%s", text, STR_C("Terrain model: GUGiK, geoportal.gov.pl")); } - else if (state.mode == 0) - nmt_auto_start(); - else - nmt_under_start(); - } - ImGui::PopStyleColor(2); - ImGui::EndDisabled(); - if (ImGui::IsItemHovered()) - { - auto const *text{tracks ? (state.mode == 0 ? STR_C("The terrain model is loaded along the tracks and the tracks go onto it, wherever the scenery was built: the top of the ballast at the height of the model, the grade line of every line fitted to it within the limits of the line, switches and the tracks beside them on one grade line, the formation and the slopes made along them. Where the model is off the tracks keep their line and it's reported") : - STR_C("The tracks aren't touched: the terrain model is loaded along them and drawn to them, at each place by as much as the tracks lie above or below it there, fading out away from them, so its relief stays and the tracks don't end up in deep cuttings; then the formation and the slopes along them. For a scenery built level or at its own heights")) : - STR_C("Loads the terrain model, the real relief of the ground, around the camera")}; - ImGui::SetTooltip("%s\n%s", text, STR_C("Terrain model: GUGiK, geoportal.gov.pl")); } toggle_chips("nmtgrow", {{STR_C("Make the terrain ahead while laying"), &state.grow, "Where a track is laid past the edge of the terrain, the terrain model is made around the cursor as it goes"}}); - // how far it got - if (state.stage == nmt_state::automatic::fetching || state.fitting) - { - auto const waiting{m_nmt.waiting()}; - ImGui::ProgressBar(-1.0f * static_cast(ImGui::GetTime()), ImVec2(-1.0f, 0.0f), format(STR_P("Terrain model: %zu square left", "Terrain model: %zu squares left", waiting), waiting).c_str()); - } - else if (state.stage == nmt_state::automatic::fitting && state.tracks > 0) - { - auto const fraction{1.0f - static_cast(state.left.size()) / static_cast(std::max(state.tracks, state.left.size()))}; - ImGui::ProgressBar(fraction, ImVec2(-1.0f, 0.0f), format(STR_P("Grade lines: %zu fitted, %zu track left", "Grade lines: %zu fitted, %zu tracks left", state.left.size()), state.lines, state.left.size()).c_str()); - } - else if (false == state.pending.empty()) - { - auto const total{state.made + state.pending.size()}; - ImGui::ProgressBar(static_cast(state.made) / static_cast(std::max(1, total)), ImVec2(-1.0f, 0.0f), format(STR_C("Chunks: %zu of %zu"), state.made, total).c_str()); - } if (false == state.status.empty()) { ImGui::PushTextWrapPos(0.0f); @@ -818,6 +1209,17 @@ void editor_mode::render_nmt_ui() ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", state.error.c_str()); ImGui::PopTextWrapPos(); } + bool again{false}; + if (auto const error{m_nmt.error(&again)}; false == error.empty()) + { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR(error).c_str()); + ImGui::PopTextWrapPos(); + if (again && ImGui::Button(STR_C("Try again"))) + m_nmt.retry(); + if (again) + item_tooltip("The squares of the model the service didn't give are asked for again; it happens by itself every 15 s too"); + } ImGui::PushID("nmtmore"); if (tracks && ImGui::TreeNode(STR_C("More settings"))) { @@ -917,16 +1319,22 @@ bool editor_mode::nmt_choice_card(int const Index, char const *Title, char const return clicked; } -void editor_mode::nmt_place_scenery() +void editor_mode::nmt_place_scenery(glm::dvec2 const &Point) { auto &state{m_nmt_state}; auto config{m_orthophoto.settings()}; - config.north = state.north; - config.east = state.east; + config.north = Point.y; + config.east = Point.x; m_orthophoto.settings(config); m_orthophoto_origin_edit = {config.north, config.east}; save_orthophoto_settings(); nmt_locate(); + // the heights fitted to the tracks at the place before mean nothing here + state.offset = 0.0; + state.fitted = false; + state.under = false; + state.anchors.clear(); + state.anchor_cells.clear(); } void editor_mode::nmt_offer() @@ -938,16 +1346,14 @@ void editor_mode::nmt_offer() if (m_streamer.resident() > 0 || std::any_of(simulation::Paths.sequence().begin(), simulation::Paths.sequence().end(), [](TTrack const *Track) { return Track != nullptr && rail(*Track); })) return; nmt_locate(); - state.north = m_orthophoto.settings().north; - state.east = m_orthophoto.settings().east; state.prompt = true; } void editor_mode::nmt_accept_offer() { auto &state{m_nmt_state}; - if (false == m_nmt.placed()) - nmt_place_scenery(); + if (false == m_nmt.placed() && m_nmt_map.picked()) + nmt_place_scenery(m_nmt_map.point()); // no tracks to fit to: the heights of the model as they are state.offset = 0.0; nmt_make(nmt_keys_around()); @@ -968,15 +1374,8 @@ void editor_mode::render_nmt_prompt() ImGui::TextWrapped("%s", STR_C("The scenery has no terrain yet. Load the terrain model and the aerial photos around here? The tracks laid then go along the real ground.")); bool const placed{m_nmt.placed()}; if (false == placed) - { - ImGui::TextWrapped("%s", STR_C("Where is it? PUWG 1992 of this point, X to the north and Y to the east, as geoportal.gov.pl gives them:")); - ImGui::SetNextItemWidth(150.0f); - ImGui::InputDouble("X##nmtnorth", &state.north, 0.0, 0.0, "%.0f"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(150.0f); - ImGui::InputDouble("Y##nmteast", &state.east, 0.0, 0.0, "%.0f"); - } - auto const known{placed || (state.north > 100000.0 && state.east > 100000.0)}; + render_nmt_place(false, false); + auto const known{placed || m_nmt_map.picked()}; ImGui::Spacing(); auto const gap{std::round(ImGui::GetFontSize() * 0.6f)}; auto const width{std::floor((ImGui::GetContentRegionAvail().x - gap) * 0.5f)}; @@ -986,7 +1385,7 @@ void editor_mode::render_nmt_prompt() ImGui::CloseCurrentPopup(); } if (false == known) - item_tooltip("Type where the scenery lies first"); + item_tooltip("Pick where the scenery lies first"); ImGui::SameLine(0.0f, gap); if (ground_card("##flat", STR_C("Without terrain"), STR_C("The tracks lie on the flat plane at the height 0; the terrain can be loaded later in Terrain (F5)"), false, true, width)) { @@ -1146,6 +1545,7 @@ std::size_t editor_mode::nmt_join_ends() void editor_mode::nmt_auto_start() { auto &state{m_nmt_state}; + state.error.clear(); state.under = false; nmt_locate(); state.probes.clear(); @@ -1445,7 +1845,7 @@ void editor_mode::nmt_auto_step() { bool all{true}; for (auto const &place : state.probes) - all = m_nmt.request(glm::dvec2{place.position.x, place.position.z} - glm::dvec2{1.0}, glm::dvec2{place.position.x, place.position.z} + glm::dvec2{1.0}) && all; + all = nmt_ready(glm::dvec2{place.position.x, place.position.z} - glm::dvec2{1.0}, glm::dvec2{place.position.x, place.position.z} + glm::dvec2{1.0}) && all; auto const waiting{m_nmt.waiting()}; state.status = format(STR_P("Fetching the terrain model under the tracks: %zu square left", "Fetching the terrain model under the tracks: %zu squares left", waiting), waiting); if (false == all) diff --git a/application/editormodeselftestterrain.cpp b/application/editormodeselftestterrain.cpp index 8f2c66ca..95244a8c 100644 --- a/application/editormodeselftestterrain.cpp +++ b/application/editormodeselftestterrain.cpp @@ -213,13 +213,72 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co WriteLog(format("SELFTEST nmtoffer: shown %d", m_nmt_state.prompt ? 1 : 0)); if (north > 0.0 && east > 0.0 && m_nmt_state.prompt) { - m_nmt_state.north = north; - m_nmt_state.east = east; + m_nmt_map.pick({east, north}); nmt_accept_offer(); WriteLog("SELFTEST nmtoffer taken: " + m_nmt_state.status); } return selftest_result::handled; } + if (command == "nmtplace") + { + // a place as typed into the terrain window: the PUWG 1992 and the degrees it's understood as + std::string text; + std::getline(words, text); + glm::dvec2 point; + if (false == editor_geo::parse_place(text, point)) + { + WriteLog("SELFTEST nmtplace" + text + ": not understood"); + return selftest_result::handled; + } + auto const degrees{editor_geo::to_degrees(point)}; + WriteLog(format("SELFTEST nmtplace%s: X %.1f Y %.1f, %.6f N %.6f E", text.c_str(), point.y, point.x, degrees.x, degrees.y)); + return selftest_result::handled; + } + if (command == "nmtgrow") + { + // the cursor of a track laid at the place: the terrain grows around it + auto const active{m_lay.active}; + words >> m_lay.mouse.x >> m_lay.mouse.z; + m_lay.active = true; + m_nmt_state.grow_time = -10.0; + nmt_grow(); + m_lay.active = active; + WriteLog(format("SELFTEST nmtgrow: %zu chunks waiting, grown %d", m_nmt_state.pending.size(), m_nmt_state.grown ? 1 : 0)); + return selftest_result::handled; + } + if (command == "terrainseams") + { + // the steps of the ground where two chunks in memory meet + std::vector chunks; + m_streamer.collect(chunks); + std::set keys; + for (auto const *chunk : chunks) + keys.insert(chunk->key()); + std::size_t edges{0}; + double worst{0.0}; + glm::dvec2 where{0.0}; + for (auto const &key : keys) + for (auto const [dx, dz] : {std::pair{1, 0}, std::pair{0, 1}}) + { + if (false == keys.contains({key.first + dx, key.second + dz})) + continue; + ++edges; + auto const corner{heightmap::chunk_corner({key.first + dx, key.second + dz})}; + for (int i = 0; i <= 32; ++i) + { + auto const along{heightmap::chunk_size * i / 32.0}; + glm::dvec2 const point{corner.x + (dx == 0 ? along : 0.0), corner.y + (dz == 0 ? along : 0.0)}; + glm::dvec2 const inside{dx * 0.01, dz * 0.01}; + if (double a, b; m_streamer.height_at(point.x - inside.x, point.y - inside.y, a) && m_streamer.height_at(point.x + inside.x, point.y + inside.y, b) && std::abs(a - b) > worst) + { + worst = std::abs(a - b); + where = point; + } + } + } + WriteLog(format("SELFTEST terrainseams: %zu chunks, %zu edges, the greatest step %.3f m at %.1f %.1f", keys.size(), edges, worst, where.x, where.y)); + return selftest_result::handled; + } if (command == "nmtwait") { // till the model is in and the chunks waiting for it are made diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 0b6237e4..fdc7de01 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1932,7 +1932,7 @@ std::string editor_mode::track_shaping(bool const Joined) const void editor_mode::draw_empty_scenery_hint() { - if ((m_track_tab != track_tab::path && m_track_tab != track_tab::lay) || (m_lay.active && false == m_lay.points.empty()) || m_first_steps_open || ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) + if (m_terrain_open || (m_track_tab != track_tab::path && m_track_tab != track_tab::lay) || (m_lay.active && false == m_lay.points.empty()) || m_first_steps_open || ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) return; auto const &paths{simulation::Paths.sequence()}; if (std::any_of(paths.begin(), paths.end(), [](TTrack const *Track) { return Track != nullptr && false == Track->m_editorremoved; })) diff --git a/application/editormodetrackbed.cpp b/application/editormodetrackbed.cpp index 322bda3c..92f1efc8 100644 --- a/application/editormodetrackbed.cpp +++ b/application/editormodetrackbed.cpp @@ -34,7 +34,7 @@ glm::dvec3 editor_mode::cursor_terrain(double const Height) { std::vector found; auto ground{ground_heights({glm::dvec3{point.x, std::max(point.y, static_cast(Global.pCamera.Pos.y)), point.z}}, true, &found).front()}; - if (found[0] == 0) + if (found[0] == 0 && false == nmt_ahead(point.x, point.z, ground)) ground = nearby_ground(point); if (std::isnan(ground)) return point; @@ -93,7 +93,10 @@ std::vector editor_mode::bed_heights(std::vector const &Poin probes.push_back(Points[i] - glm::dvec3{side.x, 0.0, side.y}); } std::vector found; - auto const heights{ground_heights(probes, true, &found)}; + auto heights{ground_heights(probes, true, &found)}; + for (std::size_t i = 0; i < probes.size(); ++i) + if (found[i] == 0 && nmt_ahead(probes[i].x, probes[i].z, heights[i])) + found[i] = 1; std::vector result(Points.size(), std::numeric_limits::quiet_NaN()); for (std::size_t i = 0; i < Points.size(); ++i) { diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index a2c6595a..a10beb53 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -3512,6 +3512,8 @@ void editor_track::retire(TTrack &Track) disconnect(Track); simulation::Region->erase_and_unregister(&Track); Track.m_editorremoved = true; + if (s_observer != nullptr) + s_observer->track_retired(Track); // the name is free for the path laid in its place, as the switches of a crossover laid again keep theirs simulation::Paths.rename(&Track, std::string{}); for (auto *neighbour : adjoining) @@ -3527,8 +3529,6 @@ void editor_track::retire(TTrack &Track) for (auto *crossing : crossings) if (crossing != &Track && false == crossing->m_editorremoved) rebuild_geometry(*crossing); - if (s_observer != nullptr) - s_observer->track_retired(Track); } void editor_track::revive(TTrack &Track) diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 43136197..5c8cf9b0 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -506,6 +506,7 @@ class editor_track virtual ~observer() = default; virtual void track_captured(TTrack const &Track) = 0; virtual void tracks_committed(std::vector const &Tracks) = 0; + // still under its name virtual void track_retired(TTrack &Track) = 0; // called before the commit; Original keeps the beginning of the path, given in First virtual void track_split(TTrack &Original, TTrack &Created, segment_data const &First) = 0; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 101e467a..66516729 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -6096,7 +6096,7 @@ msgid "The tracks lie %+.2f m against the terrain model (%zu places, they differ msgstr "Tory leżą %+.2f m względem NMT (%zu miejsc, rozrzut %.2f m)" msgid "Terrain model (NMT)" -msgstr "Numeryczny model terenu (NMT)" +msgstr "Prawdziwy teren z map (NMT)" msgid "No tracks to fit the terrain model to: its heights are taken as they are" msgstr "Brak torów do dopasowania: wysokości NMT bez przesunięcia" @@ -10000,3 +10000,93 @@ msgstr "Przechyłka na początku i na końcu: o ile jedna szyna jest wyżej, + p msgid "Plain arcs: the free design has no transition curves, the PKP PLK norms work them out from the speed of the line" msgstr "Zwykłe łuki: projekt wolny nie ma krzywych przejściowych, normy PKP PLK liczą je z prędkości linii" + +msgid "The place lies outside of the terrain model of Poland: check the location of the scenery" +msgstr "To miejsce leży poza NMT Polski: sprawdź położenie scenerii" + +msgid "; so do the grade lines fitted, Ctrl+Z takes them back" +msgstr "; dopasowane niwelety też zostają, Ctrl+Z je cofa" + +msgid "Stop" +msgstr "Przerwij" + +msgid "Stops loading: the terrain made so far stays" +msgstr "Przerywa wczytywanie: teren zrobiony do tej pory zostaje" + +msgid "Loading the real terrain" +msgstr "Wczytywanie prawdziwego terenu" + +msgid "Try again" +msgstr "Spróbuj ponownie" + +msgid "Click the middle of the scenery on the map" +msgstr "Kliknij na mapie środek scenerii" + +msgid "The point 0,0,0 of the scenery. The wheel zooms the map, a drag moves it" +msgstr "Punkt 0,0,0 scenerii. Kółko przybliża mapę, przeciąganie ją przesuwa" + +msgid "or type it: 52.2297, 21.0122" +msgstr "albo wpisz: 52.2297, 21.0122" + +msgid "Degrees as the online maps copy them (latitude, longitude), or the X and Y of PUWG 1992, the coordinates of the Polish maps of geoportal.gov.pl" +msgstr "Stopnie, tak jak kopiują je mapy internetowe (szerokość, długość), albo X i Y w PUWG 1992, układzie polskich map z geoportal.gov.pl" + +msgid "Not a place in Poland: two numbers, degrees or the X and Y of PUWG 1992" +msgstr "To nie jest miejsce w Polsce: wpisz dwie liczby, stopnie albo X i Y w PUWG 1992" + +msgid "X %.0f, Y %.0f in PUWG 1992" +msgstr "X %.0f, Y %.0f w PUWG 1992" + +msgid "The terrain made already stays as it is" +msgstr "Teren zrobiony wcześniej zostaje bez zmian" + +msgid "The scenery is here" +msgstr "Sceneria jest tutaj" + +msgid "X %.0f, Y %.0f in PUWG 1992, the coordinates of the Polish maps; the place is shared with the orthophoto" +msgstr "X %.0f, Y %.0f w PUWG 1992, układzie polskich map; to samo położenie ma ortofotomapa" + +msgid "Area" +msgstr "Obszar" + +msgid "A square of 500 m around the camera" +msgstr "Kwadrat 500 m wokół kamery" + +msgid "A square of 1 km around the camera" +msgstr "Kwadrat 1 km wokół kamery" + +msgid "A square of 2 km around the camera" +msgstr "Kwadrat 2 km wokół kamery" + +msgid "The squares of the model the service didn't give are asked for again; it happens by itself every 15 s too" +msgstr "Ponownie prosi serwis o brakujące fragmenty NMT; samo dzieje się to też co 15 s" + +msgid "Pick where the scenery lies first" +msgstr "Najpierw wskaż, gdzie leży sceneria" + +msgid "The scenery has no terrain yet" +msgstr "Sceneria nie ma jeszcze terenu" + +msgid "Or without the real ground:" +msgstr "Albo bez prawdziwego terenu:" + +msgid "Make an empty, flat terrain" +msgstr "Utwórz pusty, płaski teren" + +msgid "; %zu chunk not made, the model has no data there" +msgid_plural "; %zu chunks not made, the model has no data there" +msgstr[0] "; %zu kafel pominięty, NMT nie ma tam danych" +msgstr[1] "; %zu kafle pominięte, NMT nie ma tam danych" +msgstr[2] "; %zu kafli pominiętych, NMT nie ma tam danych" + +msgid "; %zu chunk not made ahead, the terrain beside isn't the real one" +msgid_plural "; %zu chunks not made ahead, the terrain beside isn't the real one" +msgstr[0] "; %zu kafel przed torem pominięty, teren obok nie jest z NMT" +msgstr[1] "; %zu kafle przed torem pominięte, teren obok nie jest z NMT" +msgstr[2] "; %zu kafli przed torem pominiętych, teren obok nie jest z NMT" + +msgid "Stopped: %zu chunk made stays" +msgid_plural "Stopped: %zu chunks made stay" +msgstr[0] "Przerwane: zrobiony %zu kafel zostaje" +msgstr[1] "Przerwane: zrobione %zu kafle zostają" +msgstr[2] "Przerwane: zrobionych %zu kafli zostaje" From eea0900928583406174683d5b21a73e8fa1d2181 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 13:20:47 +0200 Subject: [PATCH 023/145] fix(editor): an arc and a straight laid to a point on the circle of the radius aren't refused --- editor/editorTrackShape.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/editor/editorTrackShape.cpp b/editor/editorTrackShape.cpp index 9767a2d7..ff0a2e01 100644 --- a/editor/editorTrackShape.cpp +++ b/editor/editorTrackShape.cpp @@ -81,15 +81,17 @@ std::vector arc_then_straight(glm::dvec3 const &Point, glm::dvec2 turn += 2.0 * glm::pi(); auto const heading{turned(Direction, Side * turn)}; auto const remaining{glm::dot(plan_of(Target) - centre - radial, heading)}; - if (remaining > 0.0 && (rest < 0.0 || turn < angle)) + if (remaining > -1e-3 && (rest < 0.0 || turn < angle)) { - rest = remaining; + rest = std::max(0.0, remaining); angle = turn; } } if (rest < 0.0 || angle > glm::pi()) return {}; auto pieces{arc_pieces(Point, Direction, Grade, Radius, angle, Side)}; + if (rest < 0.01) + return pieces; auto const tail{straight_piece(pieces.back().points[segment_data::point::end], turned(Direction, Side * angle), Grade, rest)}; pieces.insert(pieces.end(), tail.begin(), tail.end()); return pieces; From d2d06817bc36582cdcfef617e5d94ba2daec5fe9 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 13:20:47 +0200 Subject: [PATCH 024/145] feat(editor): the track laid on the terrain gets its grade line from the profile solver In place of a line bent every ten metres to keep within 0.1 m of every bump, the grades and the vertical curves are fitted to the ground the way the profile fits them. Where the formation is made in the terrain model the line keeps to the radius of the norms and cuts and fills; over the ground left as it is it bends as sharply as it has to. The joints keep the height and the grade of the track joined, no grade is capped and no piece is under 5 m. --- application/editormode.h | 2 + application/editormodenmt.cpp | 14 ++++ application/editormodetrackbed.cpp | 30 +++++++- editor/editorGroundFollow.cpp | 108 +++++++++++++++++------------ editor/editorGroundFollow.hpp | 14 +++- 5 files changed, 120 insertions(+), 48 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 7932570d..da6b1501 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -406,6 +406,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri void nmt_follow_tracks(); // the height the ground will have where the terrain is made ahead of the track laid and isn't there yet bool nmt_ahead(double const X, double const Z, double &Height) const; + // a track laid through the points gets its formation from the terrain of the model: it needn't keep to every bump of it + bool nmt_formed(std::vector const &Points) const; // the squares of the model under the rectangle are in, none of them waiting to be asked for again bool nmt_ready(glm::dvec2 const &Min, glm::dvec2 const &Max); // Only: the paths of these tracks are shaped anew, the modifiers of those removed taken away; null: all of them. the number diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index f1aa3c73..9fdcaa78 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -640,6 +640,20 @@ bool editor_mode::nmt_ahead(double const X, double const Z, double &Height) cons return true; } +bool editor_mode::nmt_formed(std::vector const &Points) const +{ + auto const &state{m_nmt_state}; + if (false == state.shape || false == m_nmt.placed() || false == m_streamer.active()) + return false; + bool foreign{false}; + if (nmt_terrain_offset(Points, foreign).has_value()) + return true; + if (foreign) + return false; + double height; + return std::all_of(Points.begin(), Points.end(), [&](glm::dvec2 const &Point) { return false == m_streamer.height_at(Point.x, Point.y, height) && nmt_ahead(Point.x, Point.y, height); }); +} + editor_mode::nmt_chunk editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key, std::optional const Offset) { auto const low{heightmap::chunk_corner(Key)}; diff --git a/application/editormodetrackbed.cpp b/application/editormodetrackbed.cpp index 92f1efc8..64abc0da 100644 --- a/application/editormodetrackbed.cpp +++ b/application/editormodetrackbed.cpp @@ -25,6 +25,11 @@ namespace using geometry::plan_distance; using geometry::plan_of; +double constexpr kSteepest{1000.0}; // per mille: the grades of the user's own track aren't capped +double constexpr kFormed{0.5}; // m, how closely the line keeps to the ground the formation is made in +double constexpr kBare{0.1}; // m, to the ground left as it is +double constexpr kBareRadius{300.0}; // m, the vertical curves over it + } // namespace glm::dvec3 editor_mode::cursor_terrain(double const Height) @@ -124,7 +129,30 @@ double editor_mode::bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direct void editor_mode::follow_ground(std::vector &Pieces, double const Height0, double const Grade0, double const Height1, double const Grade1, TTrack const *Style) const { - ground_follow::follow(Pieces, {Height0, Grade0, Height1, Grade1}, [&](std::vector const &Points, std::vector const &Directions) { return bed_heights(Points, Directions, Style); }); + std::vector points; + for (auto const &piece : Pieces) + { + geometry::bezier const curve{piece}; + auto const count{std::max(6, static_cast(std::ceil(curve.plan_length() / 10.0)))}; + for (int i = 0; i <= count; ++i) + points.push_back(plan_of(curve.point(static_cast(i) / count))); + } + // the ground the formation is made in lets the line keep to the norms of the profile, cutting and filling; the ground left as + // it is the line follows, bending as sharply as it has to + auto const formed{nmt_formed(points)}; + ground_follow::grading grading; + grading.settings.norms = m_profile.line.norms; + grading.settings.norms.grade_max = kSteepest; + grading.settings.norms.curve_threshold = 0.0; + if (false == formed) + { + grading.settings.norms.radius_factor = 0.0; + grading.settings.norms.radius_min = kBareRadius; + } + grading.settings.speed = design_speed(Style, nullptr); + grading.fit.grade_max = kSteepest; + grading.fit.tolerance = formed ? kFormed : kBare; + ground_follow::follow(Pieces, {Height0, Grade0, Height1, Grade1}, [&](std::vector const &Points, std::vector const &Directions) { return bed_heights(Points, Directions, Style); }, grading); } glm::dvec3 editor_mode::lay_bed(glm::dvec3 const &Point) diff --git a/editor/editorGroundFollow.cpp b/editor/editorGroundFollow.cpp index cdd24a30..d30030a8 100644 --- a/editor/editorGroundFollow.cpp +++ b/editor/editorGroundFollow.cpp @@ -13,7 +13,9 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include +#include namespace ground_follow { @@ -27,8 +29,7 @@ using geometry::part_of; using geometry::plan_of; double constexpr kStep{2.0}; -double constexpr kTolerance{0.1}; -double constexpr kSpacing{10.0}; +double constexpr kShortest{5.0}; // m, the shortest piece the line cuts off struct samples { @@ -61,27 +62,39 @@ samples sample(std::vector const &Pieces, std::vector cons return result; } -void fit(knot_line &Line, std::vector const &Chainages, std::vector const &Targets) +bool grade_line(samples const &Points, std::vector const &Targets, double const Total, ends const &Ends, grading const &Grading, profile::line &Line) { - for (std::size_t pass = 0; pass < Chainages.size(); ++pass) - { - auto worst{kTolerance}; - auto pick{Chainages.size()}; - for (std::size_t j = 0; j < Chainages.size(); ++j) - { - if (std::isnan(Targets[j])) - continue; - double grade{0.0}; - auto const miss{std::abs(Line.at(Chainages[j], grade) - Targets[j])}; - if (miss <= worst || false == Line.room(Chainages[j], kSpacing)) - continue; - worst = miss; - pick = j; - } - if (pick == Chainages.size()) - return; - Line.insert(Chainages[pick], Targets[pick]); - } + auto const none{std::numeric_limits::quiet_NaN()}; + std::vector chainages{0.0}; + std::vector ground{none}; + chainages.insert(chainages.end(), Points.chainages.begin(), Points.chainages.end()); + ground.insert(ground.end(), Targets.begin(), Targets.end()); + chainages.push_back(Total); + ground.push_back(none); + if (std::none_of(ground.begin(), ground.end(), [](double const Height) { return std::isfinite(Height); })) + return false; + profile::context context; + context.length = Total; + context.start_elevation = Ends.height0; + context.end_elevation = Ends.height1; + context.start_joined = false == std::isnan(Ends.grade0); + context.end_joined = false == std::isnan(Ends.grade1); + context.start_grade = context.start_joined ? Ends.grade0 : 0.0; + context.end_grade = context.end_joined ? Ends.grade1 : 0.0; + // the start of the track stays where it's laid from, a free end goes where the line takes it + auto fit{Grading.fit}; + if (false == context.start_joined) + fit.bounds.push_back({0.0, Ends.height0, Ends.height0}); + auto settings{Grading.settings}; + settings.join_start = true; + settings.join_end = true; + Line = profile::fit_ground(chainages, ground, context, settings, fit, {}, std::vector(chainages.size(), Grading.reach)); + if (Line.points.size() < 2) + return false; + profile::complete(Line, context); + if (false == context.start_joined) + Line.points.front().elevation = Ends.height0; + return true; } std::vector cut(std::vector const &Pieces, std::vector const &Lengths, std::vector const &Knots, std::vector> &Spans) @@ -96,7 +109,7 @@ std::vector cut(std::vector const &Pieces, std::vect double parameter{0.0}; for (auto const knot : Knots) { - if (knot < cut + 0.5 || knot > to - 0.5) + if (knot < cut + kShortest || knot > to - kShortest) continue; auto const next{curve.parameter(knot - from)}; result.push_back(part_of(Pieces[i], parameter, next)); @@ -111,7 +124,7 @@ std::vector cut(std::vector const &Pieces, std::vect return result; } -void lift(segment_data &Piece, std::pair const &Span, knot_line const &Line) +void lift(segment_data &Piece, std::pair const &Span, std::function const &At) { auto &points{Piece.points}; auto const [a, b]{Span}; @@ -122,8 +135,8 @@ void lift(segment_data &Piece, std::pair const &Span, knot_line auto &finish{points[segment_data::point::end]}; auto &control1{points[segment_data::point::control1]}; auto &control2{points[segment_data::point::control2]}; - start.y = Line.at(a, begin); - finish.y = Line.at(b, end); + start.y = At(a, begin); + finish.y = At(b, end); if (has_controls(Piece)) { control1.y = begin * glm::length(plan_of(control1)); @@ -156,14 +169,6 @@ void knot_line::refresh() slopes.back() = grade1; } -void knot_line::insert(double const Chainage, double const Height) -{ - auto const where{std::upper_bound(knots.begin(), knots.end(), Chainage) - knots.begin()}; - knots.insert(knots.begin() + where, Chainage); - heights.insert(heights.begin() + where, Height); - refresh(); -} - double knot_line::at(double const Chainage, double &Grade) const { auto const next{static_cast(std::upper_bound(knots.begin(), knots.end(), Chainage) - knots.begin())}; @@ -172,13 +177,7 @@ double knot_line::at(double const Chainage, double &Grade) const return geometry::hermite(length, heights[i], slopes[i], heights[i + 1], slopes[i + 1], std::clamp((Chainage - knots[i]) / length, 0.0, 1.0), Grade); } -bool knot_line::room(double const Chainage, double const Spacing) const -{ - auto const next{std::upper_bound(knots.begin(), knots.end(), Chainage)}; - return next != knots.end() && *next - Chainage >= Spacing && Chainage - *std::prev(next) >= Spacing; -} - -void follow(std::vector &Pieces, ends const &Ends, bed_query const &Bed) +void follow(std::vector &Pieces, ends const &Ends, bed_query const &Bed, grading const &Grading) { std::vector lengths; double total{0.0}; @@ -188,13 +187,34 @@ void follow(std::vector &Pieces, ends const &Ends, bed_query const return; auto const points{sample(Pieces, lengths, total, Ends)}; auto const targets{Bed(points.points, points.directions)}; + std::vector> spans; + if (profile::line graded; grade_line(points, targets, total, Ends, Grading, graded)) + { + // the ends of the track go on in the height and the grade of the tracks they join, whatever the line found + auto result{cut(Pieces, lengths, profile::breaks(graded), spans)}; + for (std::size_t i = 0; i < result.size(); ++i) + lift(result[i], spans[i], [&](double const Chainage, double &Grade) { + if (Chainage < 1e-6 && false == std::isnan(Ends.grade0)) + { + Grade = Ends.grade0; + return Ends.height0; + } + if (Chainage > total - 1e-6 && false == std::isnan(Ends.grade1)) + { + Grade = Ends.grade1; + return Ends.height1; + } + Grade = profile::grade(graded, Chainage); + return profile::elevation(graded, Chainage); + }); + Pieces = std::move(result); + return; + } knot_line line{{0.0, total}, {Ends.height0, Ends.height1}, {}, Ends.grade0, Ends.grade1}; line.refresh(); - fit(line, points.chainages, targets); - std::vector> spans; auto result{cut(Pieces, lengths, line.knots, spans)}; for (std::size_t i = 0; i < result.size(); ++i) - lift(result[i], spans[i], line); + lift(result[i], spans[i], [&](double const Chainage, double &Grade) { return line.at(Chainage, Grade); }); Pieces = std::move(result); } diff --git a/editor/editorGroundFollow.hpp b/editor/editorGroundFollow.hpp index 9b532b35..cb843996 100644 --- a/editor/editorGroundFollow.hpp +++ b/editor/editorGroundFollow.hpp @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include "editor/editorProfile.hpp" #include "world/Segment.h" namespace ground_follow @@ -37,11 +38,18 @@ struct knot_line double grade1{0.0}; void refresh(); - void insert(double const Chainage, double const Height); double at(double const Chainage, double &Grade) const; - bool room(double const Chainage, double const Spacing) const; }; -void follow(std::vector &Pieces, ends const &Ends, bed_query const &Bed); +// the grade line the track is laid on where there's ground under it: the solver of the grade lines fits it to the ground +struct grading +{ + profile::line settings; // the speed and the norms of the line + profile::ground_fit fit; // how closely it keeps to the ground + double reach{100.0}; // m, how far the ground may part from the line and still be followed +}; + +// where Bed finds no ground, the heights lead from one end to the other +void follow(std::vector &Pieces, ends const &Ends, bed_query const &Bed, grading const &Grading = {}); } // namespace ground_follow From e86a536a9d0c0b529215af35f4086d24b103999a Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 17:25:17 +0200 Subject: [PATCH 025/145] fix(editor): a point taken out of a grade line leaves its neighbours with the radius of the limits, and paths meeting at a sharp break keep their own grades The points of a grade line read from the tracks keep the radius measured there; once a point between them was taken out, the breaks of its neighbours changed while their radii stayed, so a break without a curve read as 0.4 per mille became one of 9 per mille with no curve, and the tracks took it as a kink. The neighbours of the point taken out follow the limits now, with the button, Del and the self-test alike (profile_remove_point). Applying a line with a break without a curve gave the path which ends at the break the grade of the element after it, so its Hermite heights bowed 0.12 m off the line over 100 m before the break; each path takes the grade on its own side now. Self-test commands vprofilerun, vprofiledel and vprofileput. --- application/editormode.h | 2 + application/editormodeprofile.cpp | 25 +++++++---- application/editormodeselftesttrack.cpp | 55 +++++++++++++++++++++++++ editor/editorTrack.cpp | 6 ++- 4 files changed, 78 insertions(+), 10 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index da6b1501..10d20e32 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -2179,6 +2179,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri void profile_forget(); void profile_open_stored(std::size_t const Index); void profile_edited(); + // the radii of its neighbours were for the breaks of the grade it made, they follow the limits after it + void profile_remove_point(int const Index); void render_profile_body(); void render_profile_strip(); void render_profile_header(); diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 75df8b70..24e495f6 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -630,6 +630,20 @@ void editor_mode::profile_edited() first_step_done(first_step::profile_point); } +void editor_mode::profile_remove_point(int const Index) +{ + auto &state{m_profile}; + auto &points{state.line.points}; + if (Index <= 0 || Index + 1 >= static_cast(points.size())) + return; + points.erase(points.begin() + Index); + for (auto const neighbour : {Index - 1, Index}) + if (neighbour > 0 && neighbour + 1 < static_cast(points.size()) && false == points[neighbour].joint) + points[neighbour].automatic = true; + state.selected = -1; + profile_edited(); +} + void editor_mode::profile_open(TTrack *From, TTrack *To) { auto &state{m_profile}; @@ -2086,9 +2100,8 @@ void editor_mode::render_profile_point() ImGui::SameLine(); if (ImGui::Button(STR_C("Remove Del"))) { - points.erase(points.begin() + index); - state.selected = -1; - changed = true; + profile_remove_point(index); + return; } } } @@ -2513,11 +2526,7 @@ bool editor_mode::profile_canvas_edit(profile_view const &View, bool const Hover else state.curve_grip = -1; if ((Hovered || ImGui::IsWindowFocused()) && state.selected > 0 && state.selected + 1 < static_cast(points.size()) && false == points[state.selected].joint && ImGui::IsKeyPressed(ImGuiKey_Delete)) - { - points.erase(points.begin() + state.selected); - state.selected = -1; - changed = true; - } + profile_remove_point(state.selected); return changed; } diff --git a/application/editormodeselftesttrack.cpp b/application/editormodeselftesttrack.cpp index c1182d20..9068b20a 100644 --- a/application/editormodeselftesttrack.cpp +++ b/application/editormodeselftesttrack.cpp @@ -396,6 +396,61 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm WriteLog(format("SELFTEST vprofile zone %.3f %.3f %s", zone.from, zone.to, zone.name.c_str())); return selftest_result::handled; } + if (command == "vprofilerun" || command == "vprofiledel" || command == "vprofileput") + { + // the grade line of the line through a track, a point of it taken out as with Del, the line applied + auto &state{m_profile}; + if (command == "vprofilerun") + { + std::string name; + double length{std::numeric_limits::infinity()}; + words >> name >> length; + auto *track{simulation::Paths.find(name)}; + if (track == nullptr) + { + WriteLog("SELFTEST vprofilerun: no track " + name); + return selftest_result::handled; + } + state.run_length = length; + profile_open_run(*track); + } + else if (command == "vprofiledel") + { + int index{-1}; + words >> index; + auto &points{state.line.points}; + if (index <= 0 || index + 1 >= static_cast(points.size()) || points[index].joint) + { + WriteLog(format("SELFTEST vprofiledel: point %d can't be taken out", index)); + return selftest_result::handled; + } + profile_remove_point(index); + } + else + { + std::string option; + words >> option; + profile_apply(option == "terrain"); + WriteLog("SELFTEST vprofileput: " + state.status + " " + state.error); + } + std::string points; + for (auto const &point : state.line.points) + points += format(" (%.1f %.3f R%.0f%s%s)", point.chainage, point.elevation, point.radius, point.automatic ? "" : " fixed", point.joint ? " joint" : ""); + std::string issues; + for (auto const &issue : state.issues) + issues += std::string{issue.error ? " | error: " : " | "} + issue.text; + double low{0.0}, high{0.0}, at_low{0.0}, at_high{0.0}; + for (auto const &sample : state.samples) + { + auto const off{sample.position.y - profile::elevation(state.line, sample.chainage)}; + if (off < low) + low = off, at_low = sample.chainage; + if (off > high) + high = off, at_high = sample.chainage; + } + WriteLog(format("SELFTEST %s: route %.1f m, %zu samples, track against the line %.3f at %.1f .. %+.3f at %.1f; points%s%s", command.c_str(), state.route.length, state.samples.size(), low, at_low, high, at_high, points.c_str(), issues.c_str())); + return selftest_result::handled; + } if (command == "vprofileapply") { // a plain grade line from the height of the start of the open route, applied to it diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index a10beb53..ba5a65be 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -2559,7 +2559,9 @@ std::vector editor_track::apply_profile(route &Route, auto const endchainage{span.forward ? span.to : span.from}; auto const sign{span.forward ? 1.0 : -1.0}; auto &path{span.track->m_paths[span.path]}; - set_heights(path, Elevation(startchainage), Elevation(endchainage), Grade(startchainage) * sign, Grade(endchainage) * sign); + // at a break of the grade without a curve each path takes the grade on its own side of it + auto const inward = [&](double const Chainage, double const Other) { return Grade(Chainage + std::min(1e-3, std::abs(Other - Chainage) * 0.5) * (Other > Chainage ? 1.0 : -1.0)); }; + set_heights(path, Elevation(startchainage), Elevation(endchainage), inward(startchainage, endchainage) * sign, inward(endchainage, startchainage) * sign); // the pieces of a path made of several curves follow the grade line along them, else the path would part from them and lose them std::vector lengths; double total{0.0}; @@ -2576,7 +2578,7 @@ std::vector editor_track::apply_profile(route &Route, { auto const first{at(along)}; auto const last{at(along + lengths[k])}; - set_heights(path.pieces[k], Elevation(first), Elevation(last), Grade(first) * sign, Grade(last) * sign); + set_heights(path.pieces[k], Elevation(first), Elevation(last), inward(first, last) * sign, inward(last, first) * sign); along += lengths[k]; } } From 62b0e925a7a0fe38f4d2b2af7b6b4df72a1df6a4 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 17:34:19 +0200 Subject: [PATCH 026/145] feat(editor): the readout of a track being laid or led out of an end gives its rise, its grades and the speed of its arc Next to the length and the radius, the readout at the cursor shows the difference of height from the start to the end of the track about to be laid, the grade along it (one value or the range it goes over) and the speed the sharpest arc lets the trains through at within the cant and the unbalanced acceleration of the laying limits. The self-test layclicks logs it. --- application/editormodeselftestjoin.cpp | 9 ++++++ application/editormodetrack.cpp | 8 ++++-- application/editormodetrackshared.h | 38 ++++++++++++++++++++++++++ lang/editor_pl.po | 9 ++++++ 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/application/editormodeselftestjoin.cpp b/application/editormodeselftestjoin.cpp index f645c522..46a2f94e 100644 --- a/application/editormodeselftestjoin.cpp +++ b/application/editormodeselftestjoin.cpp @@ -201,6 +201,15 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma m_lay.mouse_snap = editor_track::find_free_end(nullptr, 1, point, 3.0, {}); m_lay.mouse = m_lay.mouse_snap.track != nullptr ? point : lay_bed(point); WriteLog(format("SELFTEST layclicks ground %.3f, track %.3f", point.y, m_lay.mouse.y)); + if (false == m_lay.points.empty()) + { + auto points{m_lay.points}; + points.push_back(m_lay.mouse_snap.track != nullptr ? m_lay.mouse_snap.position : m_lay.mouse); + m_lay.preview = lay_pieces(points, m_lay.mouse_snap, m_lay.preview_length, m_lay.preview_error); + auto readout{track_readout()}; + std::replace(readout.begin(), readout.end(), '\n', '|'); + WriteLog("SELFTEST layclicks readout: " + readout); + } lay_click(); WriteLog(format("SELFTEST layclicks %.1f %.1f: %s", point.x, point.z, m_lay.status.c_str())); } diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index fdc7de01..b7e01380 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1771,7 +1771,9 @@ std::string editor_mode::track_readout() const return link_summary(target, link, editor_track::branch_radius(m_extend.track->m_paths[1])); if (snapped && pieces.empty()) return format(STR_C("no track fits to the end of %s"), name(target.track).c_str()); - auto text{radius > 0.0 ? format("L %.2f m R %.0f m", length, radius) : format("L %.2f m %s", length, STR_C("straight"))}; + auto text{radius > 0.0 ? format("L %.2f m R %.0f m", length, radius) + speed_of(radius, laying_norms()) : format("L %.2f m %s", length, STR_C("straight"))}; + if (auto const vertical{vertical_of(pieces)}; false == vertical.empty()) + text += "\n" + vertical; if (snapped) text += "\n" + format(STR_C("joins %s"), name(target.track).c_str()); if (false == reason.empty()) @@ -1797,9 +1799,11 @@ std::string editor_mode::track_readout() const radius = r; } if (radius > 0.0) - text = straight ? format("L %.1f m %s + R %.0f m", lay.preview_length, STR_C("straight"), radius) : format("L %.1f m R %.0f m", lay.preview_length, radius); + text = (straight ? format("L %.1f m %s + R %.0f m", lay.preview_length, STR_C("straight"), radius) : format("L %.1f m R %.0f m", lay.preview_length, radius)) + speed_of(radius, laying_norms()); else text = format("L %.1f m %s", lay.preview_length, STR_C("straight")); + if (auto const vertical{vertical_of(lay.preview)}; false == vertical.empty()) + text += "\n" + vertical; } if (false == lay.points.empty() && lay.mouse_snap.track != nullptr) text += "\n" + format(STR_C("joins %s"), name(lay.mouse_snap.track).c_str()); diff --git a/application/editormodetrackshared.h b/application/editormodetrackshared.h index 91ad98ca..9e60c62f 100644 --- a/application/editormodetrackshared.h +++ b/application/editormodetrackshared.h @@ -19,6 +19,9 @@ http://mozilla.org/MPL/2.0/. #include "world/Track.h" #include "imgui/imgui.h" +#include +#include +#include #include #include #include @@ -60,6 +63,41 @@ inline double piece_plan_radius(segment_data const &Path) return curve.plan_length() / turn; } +// the speed an arc of the radius lets the trains through at, within the cant and the unbalanced acceleration of the limits +inline std::string speed_of(double const Radius, alignment::limits const &Limits) +{ + auto const speed{std::floor(alignment::speed_limit(Radius, Limits) / 5.0) * 5.0}; + return speed > 0.0 ? format(STR_C(" (up to %.0f km/h)"), speed) : std::string{}; +} + +// the rise of the track from its start to its end, and the grades along it +inline std::string vertical_of(std::vector const &Pieces) +{ + double low{std::numeric_limits::max()}; + double high{std::numeric_limits::lowest()}; + for (auto const &piece : Pieces) + { + bezier const curve{piece}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= 8; ++k) + { + auto const next{curve.point(k / 8.0)}; + if (auto const run{plan_distance(previous, next)}; run > 1e-3) + { + low = std::min(low, (next.y - previous.y) / run * 1000.0); + high = std::max(high, (next.y - previous.y) / run * 1000.0); + } + previous = next; + } + } + if (low > high) + return {}; + auto const rise{bezier{Pieces.back()}.point(1.0).y - bezier{Pieces.front()}.point(0.0).y}; + if (high - low < 0.1) + return format(STR_C("height %+.2f m grade %+.1f per mille"), rise, (low + high) * 0.5); + return format(STR_C("height %+.2f m grade %+.1f to %+.1f per mille"), rise, low, high); +} + inline std::string lines_of(std::vector const &Texts) { std::string text; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 66516729..c84d879d 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10090,3 +10090,12 @@ msgid_plural "Stopped: %zu chunks made stay" msgstr[0] "Przerwane: zrobiony %zu kafel zostaje" msgstr[1] "Przerwane: zrobione %zu kafle zostają" msgstr[2] "Przerwane: zrobionych %zu kafli zostaje" + +msgid " (up to %.0f km/h)" +msgstr " (do %.0f km/h)" + +msgid "height %+.2f m grade %+.1f per mille" +msgstr "różnica wysokości %+.2f m pochylenie %+.1f‰" + +msgid "height %+.2f m grade %+.1f to %+.1f per mille" +msgstr "różnica wysokości %+.2f m pochylenie od %+.1f do %+.1f‰" From a154fce67b12bda7559c3125cdda938303113417 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 17:34:26 +0200 Subject: [PATCH 027/145] feat(editor): the data of a track under the cursor give its grade there and its cant, and the radius of a curve without one written After the cursor rests on a track, its label gives the grade at the place pointed at and the cant of the path, besides the name, the length, the radius and the speed; a curve whose radius isn't written in the scenery gets the one of its geometry instead of being called straight. --- application/editormodetrackux.cpp | 21 +++++++++++++++++++-- lang/editor_pl.po | 3 +++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index a7e5e697..8b210d33 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "application/editormodeshared.h" +#include "application/editormodetrackshared.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" @@ -652,10 +653,26 @@ void editor_mode::draw_track_intent() const 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"); + bezier const curve{path}; + details += format(" L %.1f m", curve.plan_length()); + auto const radius{::track_mode::piece_plan_radius(path)}; + details += radius >= 0.5 ? format(" R %.0f m", 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{}; + double nearest{std::numeric_limits::max()}; + double at{0.0}; + for (int k = 0; k <= 64; ++k) + if (auto const distance{geometry::plan_distance(curve.point(k / 64.0), m_hover.point)}; distance < nearest) + { + nearest = distance; + at = k / 64.0; + } + auto const tangent{curve.first(at)}; + if (auto const run{glm::length(plan_of(tangent))}; run > 1e-9) + details += "\n" + format(STR_C("grade %+.1f per mille"), tangent.y / run * 1000.0); + auto const roll{std::max(std::abs(path.rolls[0]), std::abs(path.rolls[1]))}; + if (roll > 1e-3f) + details += format(STR_C(" cant %.0f mm"), 1500.0 * std::sin(glm::radians(static_cast(roll)))); } auto *foreground{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())}; auto const actionsize{ImGui::CalcTextSize(intent.action.c_str())}; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index c84d879d..af3611fa 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10099,3 +10099,6 @@ msgstr "różnica wysokości %+.2f m pochylenie %+.1f‰" msgid "height %+.2f m grade %+.1f to %+.1f per mille" msgstr "różnica wysokości %+.2f m pochylenie od %+.1f do %+.1f‰" + +msgid "grade %+.1f per mille" +msgstr "pochylenie %+.1f‰" From 4db3dd70d3c4ccdc0a32e0644c7955c3837ecadc Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 17:34:36 +0200 Subject: [PATCH 028/145] feat(editor): the free ends of the tracks are marked red in the track mode Each end of a rail track joined to nothing, where a train would run off, gets a red dot next to the white dots of the joints, in the 3D view near the camera and from above over the whole view. The ends are looked for again after an edit or once the camera leaves the place they were found for, not every frame. --- application/editormode.h | 10 ++++++++++ application/editormodelay.cpp | 15 +++++++++++++++ application/editormodetrack.cpp | 10 ++++++++++ application/editormodetrackshared.h | 1 + 4 files changed, 36 insertions(+) diff --git a/application/editormode.h b/application/editormode.h index 10d20e32..19114605 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1593,6 +1593,16 @@ class editor_mode : public application_mode, private editor_track::observer, pri 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; + // the free ends of the rail tracks around the camera, looked for again after an edit or once the camera leaves the place + struct free_end_cache + { + int edits{-1}; + glm::dvec3 centre{0.0}; + double reach{0.0}; + std::vector points; + }; + mutable free_end_cache m_free_end_cache; + std::vector const &free_end_points(glm::dvec3 const &Centre, double const Reach) const; struct point_drag { bool active{false}; diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index 8dd852d2..e6943593 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -219,6 +219,21 @@ editor_track::snap_target editor_mode::snap_free_end(glm::dvec3 const &Near, glm return result; } +std::vector const &editor_mode::free_end_points(glm::dvec3 const &Centre, double const Reach) const +{ + auto &cache{m_free_end_cache}; + if (cache.edits != m_track_edits || glm::distance(cache.centre, Centre) > cache.reach * 0.25 || Reach > cache.reach * 1.01 || Reach < cache.reach * 0.5) + { + cache.edits = m_track_edits; + cache.centre = Centre; + cache.reach = Reach * 1.25; + cache.points.clear(); + for (auto const &end : editor_track::free_ends(nullptr, kRailCategory, Centre, cache.reach, {})) + cache.points.push_back(end.position); + } + return cache.points; +} + void editor_mode::draw_free_ends(TTrack const *Self, int const Category, std::vector const &Exclude) const { screen_projection const projection; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index b7e01380..fa749cda 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -2122,6 +2122,16 @@ void editor_mode::draw_build_overlay() const drawlist->AddCircleFilled(screen, 3.5f, kJointColor); } } + // the ends joined to nothing, where a train would run off + for (auto const &point : free_end_points(eye, Global.EditorOrtho ? std::max(kDotsRange, static_cast(Global.EditorOrthoExtent) * 2.0) : kDotsRange)) + { + ImVec2 screen; + if (in_reach(point) && projection.project(point, screen) && screen.x >= 0.0f && screen.y >= 0.0f && screen.x <= display.x && screen.y <= display.y) + { + drawlist->AddCircleFilled(screen, 5.0f, kFreeEndColor); + drawlist->AddCircle(screen, 5.0f, IM_COL32(0, 0, 0, 160), 12, 1.0f); + } + } if (m_extend.active) { if (m_extend.track != nullptr) diff --git a/application/editormodetrackshared.h b/application/editormodetrackshared.h index 9e60c62f..a3278716 100644 --- a/application/editormodetrackshared.h +++ b/application/editormodetrackshared.h @@ -151,6 +151,7 @@ double const kDrawnReach{100000.0}; double const kJointRange{400.0}; // the ends of the paths are drawn for the tracks this close to the camera double const kDotsRange{500.0}; // m, the dots of the ends of the tracks are drawn this close to the camera in the 3D view ImU32 const kJointColor{IM_COL32(255, 255, 255, 200)}; +ImU32 const kFreeEndColor{IM_COL32(240, 60, 60, 235)}; int const kRailCategory{1}; inline editor_track::branch_end branch_start(segment_data const &Diverging) From d0ff7be1c22a61a3a514e86af0e7f1489e91b3c1 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 17:34:41 +0200 Subject: [PATCH 029/145] feat(editor): the fields of the features switched off in the build are shown greyed with the reason instead of hidden The frog, movable nose and 3D sleeper and timber fields of the track properties and the frog chips of the switch panel stay in place while track_features::crossings or track_features::sleepers are off, greyed, with a tooltip saying the feature is switched off in this build; nothing changes them, copying and pasting the properties leaves them out. --- application/editormodetrack.cpp | 7 +++++-- application/editormodetrackprops.cpp | 23 +++++++++++++++-------- lang/editor_pl.po | 3 +++ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index fa749cda..1da2fb0c 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1081,18 +1081,21 @@ void editor_mode::render_turnout_ui() drive_ui(*track); if (set.has_value()) drive_ui(*set->second); - if (track_features::crossings) { bool frogs{track->SwitchExtension->m_frogs}; bool movable{track->SwitchExtension->m_movablenose}; char const *const frogtip{"Cuts the rails of the switch at its frog and lays wing rails and check rails.\n" "Off for a switch whose include has its own model of them"}; char const *const nosetip{"The nose of the frog moves with the blades over to the wing rail of the track set, leaving no gap; the frog has no check rails"}; + ImGui::BeginDisabled(false == track_features::crossings); if (frogs) toggle_chips("switchfrogs", {{STR_C("Frog and check rails made from the rails"), &frogs, frogtip}, {STR_C("Movable nose"), &movable, nosetip}}); else toggle_chips("switchfrogs", {{STR_C("Frog and check rails made from the rails"), &frogs, frogtip}}); - if (frogs != track->SwitchExtension->m_frogs || movable != track->SwitchExtension->m_movablenose) + ImGui::EndDisabled(); + if (false == track_features::crossings && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", STR_C("Switched off in this build of the simulator")); + if (track_features::crossings && (frogs != track->SwitchExtension->m_frogs || movable != track->SwitchExtension->m_movablenose)) { auto const before{editor_track::capture(*track)}; track->SwitchExtension->m_frogs = frogs; diff --git a/application/editormodetrackprops.cpp b/application/editormodetrackprops.cpp index 8804e819..1ca44c6d 100644 --- a/application/editormodetrackprops.cpp +++ b/application/editormodetrackprops.cpp @@ -239,6 +239,7 @@ struct track_property char const *empty{nullptr}; // hint of an empty field std::vector choices; std::function applies; + bool off{false}; // the feature isn't in this build: the field is shown greyed and nothing changes it std::function get; // false: the text doesn't fit the track or changes nothing, the track stays as it was std::function set; @@ -434,7 +435,8 @@ std::vector make_properties() { auto &property{add(list, "frogs", field::check, section::look, group::frogs, "Frog and check rails made from the rails", "Cuts the rails of the switch at its frog and lays wing rails and check rails.\nOff for a switch whose include has its own model of them")}; property.rebuild = true; - property.applies = [](TTrack const &Track) { return track_features::crossings && Track.eType == tt_Switch && Track.SwitchExtension != nullptr; }; + property.applies = [](TTrack const &Track) { return Track.eType == tt_Switch && Track.SwitchExtension != nullptr; }; + property.off = false == track_features::crossings; property.get = [](TTrack const &Track) { return std::string{Track.SwitchExtension->m_frogs ? "1" : "0"}; }; property.set = [](TTrack &Track, std::string const &Text) { auto const frogs{Text == "1"}; @@ -447,7 +449,8 @@ std::vector make_properties() { auto &property{add(list, "movablenose", field::check, section::look, group::frogs, "Movable nose", "The nose of the frog moves with the blades over to the wing rail of the track set, leaving no gap; the frog has no check rails")}; property.rebuild = true; - property.applies = [](TTrack const &Track) { return track_features::crossings && Track.eType == tt_Switch && Track.SwitchExtension != nullptr; }; + property.applies = [](TTrack const &Track) { return Track.eType == tt_Switch && Track.SwitchExtension != nullptr; }; + property.off = false == track_features::crossings; property.get = [](TTrack const &Track) { return std::string{Track.SwitchExtension->m_movablenose ? "1" : "0"}; }; property.set = [](TTrack &Track, std::string const &Text) { auto const movable{Text == "1"}; @@ -559,7 +562,7 @@ std::vector make_properties() { auto &property{add(list, "sleeperpack", field::choice, section::look_sleepers, group::sleepers, "Sleeper model from the pack", "Model of the sleepers of the track running alone, of the models of sleepers in the scenery data, laid as 3D sleepers\nin place of the one set with sleepers3dplain in eu07.ini. (as in eu07.ini): the one set there")}; property.rebuild = true; - property.applies = [](TTrack const &Track) { return track_features::sleepers && Track.m_visible; }; + property.off = false == track_features::sleepers; property.choices = choices(sleepermodels); property.get = [index_of](TTrack const &Track) { return std::to_string(Track.m_sleeper_enabled ? index_of(sleepermodels, Track.m_sleeper_model_name) : 0); }; property.set = [](TTrack &Track, std::string const &Text) { @@ -577,7 +580,7 @@ std::vector make_properties() { auto &property{add(list, "timbermodel", field::choice, section::look_sleepers, group::sleepers, "Timber model", "Model of the timbers under the switch or the crossing the track is part of, of the models of timbers in the scenery data,\nin place of the one set with sleepers3d in eu07.ini. It's put together from its submodels begin, main and end to the length of each timber")}; property.rebuild = true; - property.applies = [](TTrack const &Track) { return track_features::sleepers && Track.m_visible; }; + property.off = false == track_features::sleepers; property.choices = choices(timbermodels); property.get = [index_of](TTrack const &Track) { return std::to_string(index_of(timbermodels, Track.m_timber_model_name)); }; property.set = [](TTrack &Track, std::string const &Text) { @@ -594,7 +597,7 @@ std::vector make_properties() { auto &property{add(list, "timberskin", field::text, section::look_sleepers, group::sleepers, "Timber skin", "Replaceable skin of the timber model, empty: the one of the model")}; property.rebuild = true; - property.applies = [](TTrack const &Track) { return track_features::sleepers && Track.m_visible; }; + property.off = false == track_features::sleepers; property.empty = "of the model"; property.get = [](TTrack const &Track) { return Track.m_timber_skin_name; }; property.set = [](TTrack &Track, std::string const &Text) { @@ -810,7 +813,7 @@ std::size_t editor_mode::track_property_apply(int const Property, std::vector targets; for (auto *track : Tracks) - if (track != nullptr && false == track->m_editorremoved && property.applies(*track)) + if (track != nullptr && false == property.off && false == track->m_editorremoved && property.applies(*track)) targets.push_back(track); auto const changed{tracks_apply(targets, [&](TTrack &Track) { return property.set(Track, Text); }, property.rebuild, property.label)}; if (changed == 0 && targets.size() == 1) @@ -1231,6 +1234,7 @@ void editor_mode::render_track_properties(std::vector const &Tracks) if (list[i].part != Part || list[i].more != More || cache.applies[i] == 0) continue; ImGui::PushID(list[i].key); + ImGui::BeginDisabled(list[i].off); row_label(i); switch (list[i].kind) { @@ -1239,6 +1243,9 @@ void editor_mode::render_track_properties(std::vector const &Tracks) case field::texture: texture_row(i); break; default: text_row(i); break; } + ImGui::EndDisabled(); + if (list[i].off && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", STR_C("Switched off in this build of the simulator")); ImGui::PopID(); } ImGui::EndTable(); @@ -1285,7 +1292,7 @@ void editor_mode::copy_track_style(TTrack const &Track) style.values.clear(); style.from = name_of(Track); for (auto const &property : properties()) - if (property.applies(Track)) + if (false == property.off && property.applies(Track)) style.values.emplace_back(property.key, property.get(Track)); ui()->set_status(format(STR_C("Properties of %s copied: Ctrl+Shift+V pastes them on the selected tracks"), style.from.c_str()), false); } @@ -1306,7 +1313,7 @@ std::size_t editor_mode::paste_track_style(std::vector const &Tracks) if (index < 0) continue; auto const &property{list[index]}; - if (property.late != late || false == style.groups[property.group]) + if (property.off || property.late != late || false == style.groups[property.group]) continue; pasted.emplace_back(&property, value); rebuild |= property.rebuild; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index af3611fa..8cebb007 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10102,3 +10102,6 @@ msgstr "różnica wysokości %+.2f m pochylenie od %+.1f do %+.1f‰" msgid "grade %+.1f per mille" msgstr "pochylenie %+.1f‰" + +msgid "Switched off in this build of the simulator" +msgstr "Wyłączone w tej wersji symulatora" From 27ea2a74564e5231968b067309b6509fc58f6d90 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 17:41:51 +0200 Subject: [PATCH 030/145] feat(editor): the joints of the tracks just changed are checked at once and their faults marked in the view After a change of the tracks, once the mouse is let go, the joints check looks at the joints of the tracks changed and of the tracks they join, with the limits of the Joints panel: ends almost meeting, steps, kinks, jumps of the cant and of the grade get a marker with the value in the view while the panel is closed, and go away once a later change or an undo puts them right. The check of a track is shared with the panel (joints_check takes the list and the pairs seen), the pairs already looked at are kept by the tool instead of a global. Self-test command jointslive. --- application/editormodeinfra.cpp | 1 + application/editormodejoints.cpp | 88 ++++++++++++++++++++++++++------ application/editormodejoints.h | 17 +++++- 3 files changed, 88 insertions(+), 18 deletions(-) diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index 4e3dc7da..1fef9e8c 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -1195,6 +1195,7 @@ void editor_mode::track_captured(TTrack const &Track) void editor_mode::tracks_committed(std::vector const &Tracks) { nmt_track_changed(Tracks); + m_joints.tracks_committed(Tracks); sweeps_committed(Tracks); switch_drives_committed(Tracks); if (m_infra_suspended || false == m_infra.follow) diff --git a/application/editormodejoints.cpp b/application/editormodejoints.cpp index 6b135db1..5b46320e 100644 --- a/application/editormodejoints.cpp +++ b/application/editormodejoints.cpp @@ -85,16 +85,11 @@ double plan_angle(glm::dvec3 const &A, glm::dvec3 const &B) 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) +std::tuple 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) @@ -152,7 +147,7 @@ void joints_tool::joints_start() state.issues.clear(); state.selected = -1; state.status.clear(); - reported.clear(); + m_reported.clear(); glm::dvec3 const camera{Global.pCamera.Pos}; for (auto *track : simulation::Paths.sequence()) { @@ -273,10 +268,10 @@ bool joints_tool::joints_model() return true; } -void joints_tool::joints_check(TTrack &Track) +void joints_tool::joints_check(TTrack &Track, std::vector &Issues, std::set> &Seen, bool const Open) const { - 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; }; + auto const &state{m_joints}; + auto const once = [&](end_key const &A, end_key const &B) { return Seen.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}) @@ -314,17 +309,17 @@ void joints_tool::joints_check(TTrack &Track) { nearest.type = kind::gap; nearest.position = position; - state.issues.push_back(nearest); + Issues.push_back(nearest); } } - else if (state.free_ends) + else if (Open) { joint_issue open; open.type = kind::open; open.track = &Track; open.point = point; open.position = position; - state.issues.push_back(open); + Issues.push_back(open); } continue; } @@ -349,7 +344,7 @@ void joints_tool::joints_check(TTrack &Track) auto const add = [&](kind const Kind, double const Value) { issue.type = Kind; issue.value = Value; - state.issues.push_back(issue); + Issues.push_back(issue); }; if (step > state.step) add(kind::step, step); @@ -364,9 +359,44 @@ void joints_tool::joints_check(TTrack &Track) } } +void joints_tool::tracks_committed(std::vector const &Tracks) +{ + for (auto *track : Tracks) + if (track != nullptr) + m_live.pending.insert(track); +} + +void joints_tool::live_update() +{ + auto &live{m_live}; + auto const gone = [](TTrack const *Track) { return Track != nullptr && Track->m_editorremoved; }; + std::erase_if(live.issues, [&](joint_issue const &Issue) { return gone(Issue.track) || gone(Issue.other); }); + if (live.pending.empty() || ImGui::IsMouseDown(ImGuiMouseButton_Left) || ImGui::IsMouseDown(ImGuiMouseButton_Right)) + return; + // the joints of a track changed are looked at again, from its side and from the side of the tracks it joins + std::set checked; + for (auto *track : live.pending) + { + if (track->m_editorremoved || false == rail(*track)) + continue; + checked.insert(track); + for (auto const &path : track->m_paths) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + for (auto const &connection : editor_track::connected_points(*track, path.points[index])) + if (rail(*connection.first)) + checked.insert(connection.first); + } + live.pending.clear(); + std::erase_if(live.issues, [&](joint_issue const &Issue) { return checked.contains(Issue.track) || checked.contains(Issue.other); }); + std::set> seen; + for (auto *track : checked) + joints_check(*track, live.issues, seen, false); +} + void joints_tool::update() { auto &state{m_joints}; + live_update(); if (state.next >= state.queue.size() && false == state.model_waiting) return; auto const deadline{std::chrono::steady_clock::now() + std::chrono::milliseconds(15)}; @@ -375,7 +405,7 @@ void joints_tool::update() auto *track{state.queue[state.next++]}; if (track->m_editorremoved) continue; - joints_check(*track); + joints_check(*track, state.issues, m_reported, state.free_ends); } if (state.next < state.queue.size()) return; @@ -649,11 +679,26 @@ void joints_tool::render_options() void joints_tool::draw_overlay() const { auto const &state{m_joints}; - if (false == state.open || state.issues.empty()) - return; screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; glm::dvec3 const camera{Global.pCamera.Pos}; + if (false == state.open) + { + for (auto const &issue : m_live.issues) + { + ImVec2 screen; + if ((false == Global.EditorOrtho && glm::distance(issue.position, camera) > kOverlayRange) || false == projection.project(issue.position, screen)) + continue; + auto const colour{kind_colour(issue.type)}; + drawlist->AddCircle(screen, 9.0f, colour, 16, 2.0f); + auto const text{std::string{STR_C(kind_name(issue.type))} + " " + value_text(issue)}; + drawlist->AddRectFilled(ImVec2(screen.x + 11.0f, screen.y - 9.0f), ImVec2(screen.x + 17.0f + ImGui::CalcTextSize(text.c_str()).x, screen.y + 9.0f), IM_COL32(0, 0, 0, 150), 3.0f); + drawlist->AddText(ImVec2(screen.x + 14.0f, screen.y - 7.0f), colour, text.c_str()); + } + return; + } + if (state.issues.empty()) + return; for (int i = 0; i < static_cast(state.issues.size()); ++i) { auto const &issue{state.issues[i]}; @@ -696,6 +741,15 @@ editor_selftest_result joints_tool::selftest(std::string const &Command, std::is WriteLog("SELFTEST jointsmodel: " + m_joints.status); return editor_selftest_result::handled; } + if (Command == "jointslive") + { + live_update(); + std::string text; + for (auto const &issue : m_live.issues) + text += format(" [%s %s at %.1f %.1f]", kind_name(issue.type), value_text(issue).c_str(), issue.position.x, issue.position.z); + WriteLog(format("SELFTEST jointslive: %zu pending, %zu marked", m_live.pending.size(), m_live.issues.size()) + text); + return editor_selftest_result::handled; + } if (Command == "joints") { std::vector points; diff --git a/application/editormodejoints.h b/application/editormodejoints.h index 626846e7..01617d88 100644 --- a/application/editormodejoints.h +++ b/application/editormodejoints.h @@ -11,7 +11,9 @@ http://mozilla.org/MPL/2.0/. #include "application/editortool.h" +#include #include +#include #include #include @@ -37,6 +39,8 @@ class joints_tool final : public editor_tool void render_options() override; editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; void set_open(bool const Open) { m_joints.open = Open; } + // the tracks just changed, checked at their joints once the change is over + void tracks_committed(std::vector const &Tracks); double kink_angle() const { return m_joints.angle; } private: @@ -75,8 +79,19 @@ class joints_tool final : public editor_tool std::string status; }; joints_state m_joints; + using end_key = std::tuple; + // pairs of the ends already looked at in the check under way + std::set> m_reported; + // the joints of the tracks changed last, marked while the panel is closed + struct live_state + { + std::set pending; + std::vector issues; + }; + live_state m_live; + void live_update(); void joints_start(); - void joints_check(TTrack &Track); + void joints_check(TTrack &Track, std::vector &Issues, std::set> &Seen, bool const Open) const; // the paths lying off the terrain model by more than the threshold. returns: false while the model under them is coming bool joints_model(); void joints_focus(int const Index); From 6a5f0d735db30d8dd7fbb60e939ac8776aceb581 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 17:47:42 +0200 Subject: [PATCH 031/145] feat(editor): the tracks of the track mode can be coloured by their radius, speed or grade, with a legend View > Track colours draws the rail tracks over the view in bands of the radius of their paths, of their speed or of the grade along them, from above over the whole view and in the 3D view within 1.5 km of the camera, with the legend of the bands in the bottom right corner. Plain keeps the lines of the top view as they were. Self-test command trackcolours. --- application/editormode.cpp | 13 ++++ application/editormode.h | 4 ++ application/editormodeselftesttrack.cpp | 7 ++ application/editormodetrackux.cpp | 87 +++++++++++++++++++++++-- lang/editor_pl.po | 18 +++++ 5 files changed, 125 insertions(+), 4 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 9a7da798..b0bc1214 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1458,7 +1458,10 @@ bool editor_mode::update() render_track_inspector(); if (ui()->mode() == nodebank_panel::TRACK) + { draw_track_lines(); + draw_track_legend(); + } if (selected_track()) draw_track_overlay(); if (route_active()) @@ -3579,6 +3582,16 @@ void editor_mode::render_view_menu() } 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 (ImGui::BeginMenu(STR_C("Track colours"))) + { + char const *const names[]{"Plain", "By the radius", "By the speed", "By the grade"}; + for (int i = 0; i < 4; ++i) + if (ImGui::MenuItem(STR_C(names[i]), nullptr, static_cast(m_track_colouring) == i)) + m_track_colouring = static_cast(i); + ImGui::EndMenu(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The tracks of the track mode coloured by their radius, speed or grade, with the legend in the corner")); 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")); diff --git a/application/editormode.h b/application/editormode.h index 19114605..15be6dac 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1570,6 +1570,10 @@ class editor_mode : public application_mode, private editor_track::observer, pri track_hover split_spot() const; void update_track_hover(); void draw_track_lines() const; + // what the colours of the tracks in the view tell + enum class track_colouring { plain, radius, speed, grade }; + track_colouring m_track_colouring{track_colouring::plain}; + void draw_track_legend() const; void draw_track_hover() const; struct track_intent { diff --git a/application/editormodeselftesttrack.cpp b/application/editormodeselftesttrack.cpp index 9068b20a..021669ae 100644 --- a/application/editormodeselftesttrack.cpp +++ b/application/editormodeselftesttrack.cpp @@ -396,6 +396,13 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm WriteLog(format("SELFTEST vprofile zone %.3f %.3f %s", zone.from, zone.to, zone.name.c_str())); return selftest_result::handled; } + if (command == "trackcolours") + { + int colouring{0}; + words >> colouring; + m_track_colouring = static_cast(std::clamp(colouring, 0, 3)); + return selftest_result::handled; + } if (command == "vprofilerun" || command == "vprofiledel" || command == "vprofileput") { // the grade line of the line through a track, a point of it taken out as with Del, the line applied diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 8b210d33..f8c151dc 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -36,6 +36,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include namespace { @@ -82,6 +83,31 @@ double ortho_metres_per_pixel() return Global.EditorOrtho && height > 0.0f ? 2.0 * static_cast(Global.EditorOrthoExtent) / static_cast(height) : 0.0; } +// the bands of the colouring of the tracks: up to the value, the colour and its label in the legend +struct colour_band +{ + double below; + ImU32 colour; + char const *label; +}; +double constexpr kBeyond{std::numeric_limits::max()}; +ImU32 const kUncoloured{IM_COL32(200, 200, 200, 170)}; +double const kColouredReach{1500.0}; // m around the camera in the 3D view +std::vector const kRadiusBands{{300.0, IM_COL32(235, 60, 60, 235), "R < 300 m"}, {600.0, IM_COL32(245, 140, 50, 235), "R 300-600 m"}, {1200.0, IM_COL32(240, 220, 60, 235), "R 600-1200 m"}, + {3000.0, IM_COL32(110, 210, 80, 235), "R 1200-3000 m"}, {kBeyond, IM_COL32(70, 190, 235, 235), "R > 3000 m"}}; +std::vector const kSpeedBands{{40.5, IM_COL32(235, 60, 60, 235), "<= 40 km/h"}, {60.5, IM_COL32(245, 140, 50, 235), "50-60 km/h"}, {80.5, IM_COL32(240, 220, 60, 235), "70-80 km/h"}, + {100.5, IM_COL32(110, 210, 80, 235), "90-100 km/h"}, {120.5, IM_COL32(70, 190, 235, 235), "110-120 km/h"}, {kBeyond, IM_COL32(170, 120, 255, 235), "> 120 km/h"}}; +std::vector const kGradeBands{{2.0, kUncoloured, "0-2 \xe2\x80\xb0"}, {5.0, IM_COL32(110, 210, 80, 235), "2-5 \xe2\x80\xb0"}, {10.0, IM_COL32(240, 220, 60, 235), "5-10 \xe2\x80\xb0"}, + {20.0, IM_COL32(245, 140, 50, 235), "10-20 \xe2\x80\xb0"}, {kBeyond, IM_COL32(235, 60, 60, 235), "> 20 \xe2\x80\xb0"}}; + +ImU32 band_colour(std::vector const &Bands, double const Value) +{ + for (auto const &band : Bands) + if (Value < band.below) + return band.colour; + return kUncoloured; +} + bool plan_near(bezier const &Curve, glm::dvec2 const &Point, double const Reach) { auto const centre{(plan_of(Curve.p0) + plan_of(Curve.p1) + plan_of(Curve.p2) + plan_of(Curve.p3)) * 0.25}; @@ -172,34 +198,87 @@ void editor_mode::update_track_hover() void editor_mode::draw_track_lines() const { auto const metres{ortho_metres_per_pixel()}; - if (metres < kLinesFrom) + auto const colouring{m_track_colouring}; + if (metres < kLinesFrom && colouring == track_colouring::plain) return; auto const &io{ImGui::GetIO()}; screen_projection const projection; glm::dvec2 const centre{Global.pCamera.Pos.x, Global.pCamera.Pos.z}; - auto const reach{0.5 * metres * std::hypot(static_cast(io.DisplaySize.x), static_cast(io.DisplaySize.y))}; + auto const reach{Global.EditorOrtho ? 0.5 * metres * std::hypot(static_cast(io.DisplaySize.x), static_cast(io.DisplaySize.y)) : kColouredReach}; + auto const width{colouring == track_colouring::plain ? 1.5f : 3.0f}; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || false == rail(*track)) continue; + auto const velocity{editor_track::velocity(*track)}; for (auto const &path : track->m_paths) { bezier const curve{path}; if (false == plan_near(curve, centre, reach)) continue; - auto const count{std::clamp(static_cast(curve.plan_length(4) / metres / 8.0), 1, 32)}; + auto colour{IM_COL32(235, 235, 235, 150)}; + if (colouring == track_colouring::radius) + { + auto const radius{::track_mode::piece_plan_radius(path)}; + colour = radius >= 0.5 ? band_colour(kRadiusBands, radius) : kUncoloured; + } + else if (colouring == track_colouring::speed) + colour = velocity > 0.0 ? band_colour(kSpeedBands, velocity) : kUncoloured; + auto const length{curve.plan_length(4)}; + auto const count{std::clamp(static_cast(length / std::max(metres, 0.5) / 8.0), colouring == track_colouring::grade ? std::clamp(static_cast(length / 10.0), 1, 8) : 1, 32)}; 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(235, 235, 235, 150), 1.5f); + if (colouring == track_colouring::grade) + { + auto const run{geometry::plan_distance(previous, next)}; + colour = run > 1e-3 ? band_colour(kGradeBands, std::abs(next.y - previous.y) / run * 1000.0) : kUncoloured; + } + projection.line(drawlist, previous, next, colour, width); previous = next; } } } } +void editor_mode::draw_track_legend() const +{ + if (m_track_colouring == track_colouring::plain) + return; + auto const &bands{m_track_colouring == track_colouring::radius ? kRadiusBands : m_track_colouring == track_colouring::speed ? kSpeedBands : kGradeBands}; + std::vector> rows; + for (auto const &band : bands) + rows.emplace_back(band.colour, band.label); + if (m_track_colouring == track_colouring::radius) + rows.emplace_back(kUncoloured, STR("straight")); + else if (m_track_colouring == track_colouring::speed) + rows.emplace_back(kUncoloured, STR("no limit")); + char const *const titles[]{"", "By the radius", "By the speed", "By the grade"}; + std::string const title{STR_C(titles[static_cast(m_track_colouring)])}; + auto const line{ImGui::GetTextLineHeightWithSpacing()}; + auto const swatch{ImGui::GetTextLineHeight()}; + float width{ImGui::CalcTextSize(title.c_str()).x}; + for (auto const &row : rows) + width = std::max(width, swatch + 6.0f + ImGui::CalcTextSize(row.second.c_str()).x); + auto const pad{8.0f}; + // in the bottom right corner, over the profile strip when it's open + auto const bottom{m_profile.strip_top > editor_ui::view_min().y + 4.0f * line ? std::min(editor_ui::view_max().y, m_profile.strip_top) : editor_ui::view_max().y}; + ImVec2 const to{editor_ui::view_max().x - 12.0f, bottom - 12.0f}; + ImVec2 const from{to.x - width - pad * 2.0f, to.y - pad * 2.0f - line * static_cast(rows.size() + 1)}; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + drawlist->AddRectFilled(from, to, IM_COL32(0, 0, 0, 170), 4.0f); + ImVec2 at{from.x + pad, from.y + pad}; + drawlist->AddText(at, IM_COL32(255, 255, 255, 235), title.c_str()); + for (auto const &row : rows) + { + at.y += line; + drawlist->AddRectFilled(ImVec2(at.x, at.y + 2.0f), ImVec2(at.x + swatch, at.y + swatch - 2.0f), row.first, 2.0f); + drawlist->AddText(ImVec2(at.x + swatch + 6.0f, at.y), IM_COL32(235, 235, 235, 235), row.second.c_str()); + } +} + void editor_mode::draw_track_hover() const { using kind = track_intent::kind; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 8cebb007..0b6ca255 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10105,3 +10105,21 @@ msgstr "pochylenie %+.1f‰" msgid "Switched off in this build of the simulator" msgstr "Wyłączone w tej wersji symulatora" + +msgid "Track colours" +msgstr "Kolory torów" + +msgid "Plain" +msgstr "Zwykłe" + +msgid "By the radius" +msgstr "Według promienia" + +msgid "By the speed" +msgstr "Według prędkości" + +msgid "By the grade" +msgstr "Według pochylenia" + +msgid "The tracks of the track mode coloured by their radius, speed or grade, with the legend in the corner" +msgstr "Tory w dziale Tory pokolorowane według promienia, prędkości albo pochylenia, z legendą w rogu" From 8171bdc7d5eda5acbcffc28f0bb0c5c43b9ab777 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 17:51:45 +0200 Subject: [PATCH 032/145] feat(editor): zoomed in from above, the curves are labelled with their radius and length and the switches with their name and type In the top view at a metre a pixel or closer, each curved track of 5 m or more gets "R 600 L 120" by its middle and each switch its name and its type from the kept switch record ("Rz 60E1-300-1:9 P") by its start, on a dark plate as on a plan. A label which would cover another one is left out, the switches going first and then the sharper curves. View > Track labels in the top view switches them off. --- application/editormode.cpp | 4 ++ application/editormode.h | 3 ++ application/editormodetrackux.cpp | 63 +++++++++++++++++++++++++++++++ lang/editor_pl.po | 6 +++ 4 files changed, 76 insertions(+) diff --git a/application/editormode.cpp b/application/editormode.cpp index b0bc1214..4176d9e2 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1460,6 +1460,7 @@ bool editor_mode::update() if (ui()->mode() == nodebank_panel::TRACK) { draw_track_lines(); + draw_track_labels(); draw_track_legend(); } if (selected_track()) @@ -3592,6 +3593,9 @@ void editor_mode::render_view_menu() } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("The tracks of the track mode coloured by their radius, speed or grade, with the legend in the corner")); + ImGui::MenuItem(STR_C("Track labels in the top view"), nullptr, &m_track_labels); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Zoomed in from above, the curves get their radius and length and the switches their name and type, as on a plan; where they'd crowd, some are left out")); 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")); diff --git a/application/editormode.h b/application/editormode.h index 15be6dac..20d334df 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1574,6 +1574,9 @@ class editor_mode : public application_mode, private editor_track::observer, pri enum class track_colouring { plain, radius, speed, grade }; track_colouring m_track_colouring{track_colouring::plain}; void draw_track_legend() const; + // the radii and lengths of the curves and the names and types of the switches written by them, from above, as on a plan + bool m_track_labels{true}; + void draw_track_labels() const; void draw_track_hover() const; struct track_intent { diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index f8c151dc..26919c39 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormodetrackshared.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" +#include "editor/editorSwitchStore.hpp" #include "utilities/Globals.h" #include "rendering/renderer.h" @@ -93,6 +94,8 @@ struct colour_band double constexpr kBeyond{std::numeric_limits::max()}; ImU32 const kUncoloured{IM_COL32(200, 200, 200, 170)}; double const kColouredReach{1500.0}; // m around the camera in the 3D view +double const kLabelsTo{1.0}; // m a pixel of the top view, farther out the labels of the tracks are left out +double const kLabelledLength{5.0}; // m, shorter curves get no label std::vector const kRadiusBands{{300.0, IM_COL32(235, 60, 60, 235), "R < 300 m"}, {600.0, IM_COL32(245, 140, 50, 235), "R 300-600 m"}, {1200.0, IM_COL32(240, 220, 60, 235), "R 600-1200 m"}, {3000.0, IM_COL32(110, 210, 80, 235), "R 1200-3000 m"}, {kBeyond, IM_COL32(70, 190, 235, 235), "R > 3000 m"}}; std::vector const kSpeedBands{{40.5, IM_COL32(235, 60, 60, 235), "<= 40 km/h"}, {60.5, IM_COL32(245, 140, 50, 235), "50-60 km/h"}, {80.5, IM_COL32(240, 220, 60, 235), "70-80 km/h"}, @@ -243,6 +246,66 @@ void editor_mode::draw_track_lines() const } } +void editor_mode::draw_track_labels() const +{ + auto const metres{ortho_metres_per_pixel()}; + if (false == m_track_labels || metres <= 0.0 || metres > kLabelsTo) + return; + auto const &io{ImGui::GetIO()}; + screen_projection const projection; + glm::dvec2 const centre{Global.pCamera.Pos.x, Global.pCamera.Pos.z}; + auto const reach{0.5 * metres * std::hypot(static_cast(io.DisplaySize.x), static_cast(io.DisplaySize.y))}; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + auto const low{editor_ui::view_min()}; + auto const high{editor_ui::view_max()}; + // a label which would cover another one is left out: the switches go first, then the sharper curves + std::vector> taken; + auto const write = [&](glm::dvec3 const &Point, std::string const &Text, ImU32 const Colour) { + ImVec2 at; + if (false == projection.project(Point, at)) + return; + auto const size{ImGui::CalcTextSize(Text.c_str())}; + ImVec2 const from{at.x - size.x * 0.5f - 4.0f, at.y - size.y - 12.0f}; + ImVec2 const to{at.x + size.x * 0.5f + 4.0f, at.y - 8.0f}; + if (from.x < low.x || from.y < low.y || to.x > high.x || to.y > high.y) + return; + if (std::any_of(taken.begin(), taken.end(), [&](auto const &Rect) { return from.x < Rect.second.x + 6.0f && to.x > Rect.first.x - 6.0f && from.y < Rect.second.y + 3.0f && to.y > Rect.first.y - 3.0f; })) + return; + taken.emplace_back(from, to); + drawlist->AddRectFilled(from, to, IM_COL32(0, 0, 0, 165), 3.0f); + drawlist->AddText(ImVec2(from.x + 4.0f, from.y), Colour, Text.c_str()); + }; + struct curve_label + { + double radius; + glm::dvec3 point; + std::string text; + }; + std::vector curves; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == rail(*track) || track->m_paths.empty()) + continue; + bezier const first{track->m_paths.front()}; + if (false == plan_near(first, centre, reach)) + continue; + if (track->eType == tt_Switch) + { + auto text{name_of(*track)}; + if (auto const record{switch_store::of(*track)}) + text += " " + switch_store::label(*record); + write(first.point(0.0), text, IM_COL32(255, 220, 120, 255)); + continue; + } + auto const radius{::track_mode::piece_plan_radius(track->m_paths.front())}; + if (radius >= 0.5 && first.plan_length() >= kLabelledLength) + curves.push_back({radius, first.point(0.5), format("R %.0f L %.1f", radius, first.plan_length())}); + } + std::sort(curves.begin(), curves.end(), [](curve_label const &A, curve_label const &B) { return A.radius < B.radius; }); + for (auto const &curve : curves) + write(curve.point, curve.text, IM_COL32(235, 235, 235, 240)); +} + void editor_mode::draw_track_legend() const { if (m_track_colouring == track_colouring::plain) diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 0b6ca255..3280d254 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10123,3 +10123,9 @@ msgstr "Według pochylenia" msgid "The tracks of the track mode coloured by their radius, speed or grade, with the legend in the corner" msgstr "Tory w dziale Tory pokolorowane według promienia, prędkości albo pochylenia, z legendą w rogu" + +msgid "Track labels in the top view" +msgstr "Opisy torów w widoku z góry" + +msgid "Zoomed in from above, the curves get their radius and length and the switches their name and type, as on a plan; where they'd crowd, some are left out" +msgstr "Po przybliżeniu z góry łuki dostają promień i długość, a rozjazdy nazwę i typ, jak na planie; gdzie byłoby ich za gęsto, część się chowa" From 79642a95c7cac195e74f1d842dd2eb52024ebc6f Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 17:57:05 +0200 Subject: [PATCH 033/145] feat(editor): a measuring tape in the track mode, M: the distance, the difference of height, the grade and the azimuth between two clicks M in any tab of the track mode turns the tape on: a click puts its start, the next one its end, at a free end, on the axis of the track under the cursor or else on the ground. The readout at the cursor gives the distance in plan and in space, the difference of height and the grade between them and the azimuth, while the second point follows the cursor and once it's put; a further click starts a new measure, M or Esc puts the tape away. Self-test command tape. --- application/editormode.cpp | 6 ++- application/editormode.h | 13 ++++- application/editormodeselftesttrack.cpp | 17 ++++++ application/editormodetrack.cpp | 9 ++++ application/editormodetrackux.cpp | 69 +++++++++++++++++++++++++ lang/editor_pl.po | 21 ++++++++ 6 files changed, 133 insertions(+), 2 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 4176d9e2..43ad3783 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1494,12 +1494,14 @@ bool editor_mode::update() } render_track_context(); update_build_tools(); + tape_update(); update_track_intent(); draw_track_hover(); draw_track_set(); draw_track_spread(); draw_build_overlay(); draw_track_intent(); + draw_tape(); m_speed.update(); m_joints.update(); draw_track_hints(); @@ -6447,7 +6449,9 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in // the first Esc drops what is going on in the track mode, the next one goes back to the selection if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK) { - if (m_lay.active && false == m_lay.points.empty()) + if (m_tape.active) + m_tape = {}; + else if (m_lay.active && false == m_lay.points.empty()) lay_cancel(); else if (m_track_style.picking) m_track_style.picking = false; diff --git a/application/editormode.h b/application/editormode.h index 20d334df..dd841175 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1580,7 +1580,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void draw_track_hover() const; struct track_intent { - enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling }; + enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling, measure }; kind what{kind::none}; TTrack *track{nullptr}; int path{0}; @@ -1591,6 +1591,17 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::string action; }; track_intent m_intent; + // the measure between two points (M): on the ground, at a free end or on the axis of the track under the cursor + struct tape_state + { + bool active{false}; + std::vector points; + glm::dvec3 mouse{0.0}; + }; + tape_state m_tape; + void tape_update(); + std::string tape_readout() const; + void draw_tape() const; track_intent track_intent_at(int const Mods); bool track_gesture_on() const; void update_track_intent(); diff --git a/application/editormodeselftesttrack.cpp b/application/editormodeselftesttrack.cpp index 021669ae..845e9fd2 100644 --- a/application/editormodeselftesttrack.cpp +++ b/application/editormodeselftesttrack.cpp @@ -396,6 +396,23 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm WriteLog(format("SELFTEST vprofile zone %.3f %.3f %s", zone.from, zone.to, zone.name.c_str())); return selftest_result::handled; } + if (command == "tape") + { + // the measure between two points on the ground, as two clicks of the measuring tape take it + std::vector points; + for (glm::dvec3 point{0.0, 5000.0, 0.0}; words >> point.x >> point.z;) + points.push_back(point); + std::vector found; + auto const heights{ground_heights(points, true, &found)}; + for (std::size_t i = 0; i < points.size(); ++i) + points[i].y = found[i] != 0 ? heights[i] : 0.0; + m_tape = {true, points, points.empty() ? glm::dvec3{0.0} : points.back()}; + auto readout{tape_readout()}; + std::replace(readout.begin(), readout.end(), '\n', '|'); + WriteLog("SELFTEST tape: " + readout); + m_tape = {}; + return selftest_result::handled; + } if (command == "trackcolours") { int colouring{0}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 1da2fb0c..406924ec 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -501,6 +501,11 @@ void editor_mode::arm_switch() bool editor_mode::track_shortcut(int const Key) { + if (Key == GLFW_KEY_M) + { + m_tape = {false == m_tape.active, {}, m_tape.mouse}; + return true; + } auto const key{Key == GLFW_KEY_C ? GLFW_KEY_G : Key}; for (auto const &mode : track_modes()) { @@ -531,6 +536,8 @@ bool editor_mode::track_busy() const std::string editor_mode::track_mode_name() const { + if (m_tape.active) + return STR_C("Measure"); if (false == m_track_window_open) return STR_C("Select"); switch (m_track_tab) @@ -1750,6 +1757,8 @@ std::vector editor_mode::track_key_hints(bool const All) // values of the change in progress, shown next to the cursor std::string editor_mode::track_readout() const { + if (m_tape.active) + return tape_readout(); auto const name = [](TTrack const *Track) { return name_of(*Track); }; auto const azimuth = [](glm::dvec3 const &From, glm::dvec3 const &To) { auto const degrees{glm::degrees(std::atan2(To.x - From.x, To.z - From.z))}; diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 26919c39..0869faba 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -392,6 +392,11 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) }; auto *selected{selected_track()}; + if (m_tape.active) + { + intent.position = m_tape.mouse; + return make(kind::measure, m_tape.points.size() == 1 ? "Click: the end of the measure" : "Click: the start of the measure. M or Esc: done"); + } if (m_lay.active) { auto const &lay{m_lay}; @@ -637,11 +642,75 @@ void editor_mode::update_track_intent() } } +void editor_mode::tape_update() +{ + auto &tape{m_tape}; + if (false == tape.active || ImGui::GetIO().WantCaptureMouse) + return; + glm::vec2 const cursor{ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y}; + auto const ground{cursor_ground()}; + if (auto const end{snap_free_end(ground, cursor, nullptr, kRailCategory, {})}; end.track != nullptr) + tape.mouse = end.position; + else if (auto const under{track_under_cursor()}; under.track != nullptr) + tape.mouse = under.point; + else + tape.mouse = ground; +} + +std::string editor_mode::tape_readout() const +{ + auto const &tape{m_tape}; + if (tape.points.empty()) + return {}; + auto const &from{tape.points.front()}; + auto const &to{tape.points.size() >= 2 ? tape.points[1] : tape.mouse}; + auto const plan{geometry::plan_distance(from, to)}; + auto const rise{to.y - from.y}; + auto azimuth{glm::degrees(std::atan2(to.x - from.x, to.z - from.z))}; + if (azimuth < 0.0) + azimuth += 360.0; + auto text{format(STR_C("%.2f m in plan, %.2f m in space"), plan, glm::distance(from, to))}; + text += "\n" + format(STR_C("height %+.2f m"), rise) + (plan > 0.01 ? format(STR_C(" grade %+.1f per mille"), rise / plan * 1000.0) : std::string{}); + text += "\n" + format(STR_C("azimuth %.1f deg"), azimuth); + if (tape.points.size() >= 2) + text += "\n" + STR("Click: a new measure. M or Esc: done"); + return text; +} + +void editor_mode::draw_tape() const +{ + auto const &tape{m_tape}; + if (false == tape.active) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())}; + auto const colour{IM_COL32(255, 230, 60, 255)}; + std::vector points{tape.points}; + if (points.size() < 2) + points.push_back(tape.mouse); + for (std::size_t i = 0; i + 1 < points.size(); ++i) + projection.line(drawlist, points[i], points[i + 1], colour, 2.0f); + for (auto const &point : points) + { + ImVec2 screen; + if (projection.project(point, screen)) + { + drawlist->AddCircleFilled(screen, 4.0f, colour); + drawlist->AddCircle(screen, 7.0f, IM_COL32(0, 0, 0, 200), 12, 1.5f); + } + } +} + void editor_mode::track_press(track_intent const &Intent) { using kind = track_intent::kind; switch (Intent.what) { + case kind::measure: + if (m_tape.points.size() >= 2) + m_tape.points.clear(); + m_tape.points.push_back(m_tape.mouse); + return; case kind::lay_end: if (m_lay.points.empty() && start_extend(Intent.snap)) return; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 3280d254..ca1a37ee 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10129,3 +10129,24 @@ msgstr "Opisy torów w widoku z góry" msgid "Zoomed in from above, the curves get their radius and length and the switches their name and type, as on a plan; where they'd crowd, some are left out" msgstr "Po przybliżeniu z góry łuki dostają promień i długość, a rozjazdy nazwę i typ, jak na planie; gdzie byłoby ich za gęsto, część się chowa" + +msgid "%.2f m in plan, %.2f m in space" +msgstr "%.2f m w planie, %.2f m w przestrzeni" + +msgid "height %+.2f m" +msgstr "różnica wysokości %+.2f m" + +msgid "azimuth %.1f deg" +msgstr "azymut %.1f°" + +msgid "Click: a new measure. M or Esc: done" +msgstr "LPM: nowy pomiar. M albo Esc: koniec" + +msgid "Click: the end of the measure" +msgstr "LPM: koniec pomiaru" + +msgid "Click: the start of the measure. M or Esc: done" +msgstr "LPM: początek pomiaru. M albo Esc: koniec" + +msgid "Measure" +msgstr "Miarka" From 21595ba010f471209682627ff1ed01e28396d927 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 18:23:49 +0200 Subject: [PATCH 034/145] feat(editor): the tracks are laid to the PKP PLK norms unless the free laying is chosen, asked as the laying starts The free laying isn't the default any more. The first time a session opens the laying or the plan, a window asks how the new tracks are laid, with the two cards of the panels (the norms marked as chosen); a click on one of them takes it and closes the window, which comes before the offer of the terrain model of a new scenery. "Remember the choice, don't ask again" keeps it in eu07_editor.ini (track_norms ask, free or plk), and a choice kept follows a later click on the cards of the panels; the settings of the editor (General) choose between asking and either way. The self-test scripts start with the free laying, as they were written for it. --- application/editormode.cpp | 14 ++++++++++ application/editormode.h | 8 +++++- application/editormodelay.cpp | 41 +++++++++++++++++++++++++++--- application/editormodenmt.cpp | 2 +- application/editormodeselftest.cpp | 2 ++ application/editormodetrack.cpp | 7 +++++ editor/editorSettings.cpp | 3 +++ editor/editorSettings.hpp | 10 ++++++++ lang/editor_pl.po | 18 +++++++++++++ 9 files changed, 100 insertions(+), 5 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 43ad3783..886546bb 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -645,6 +645,7 @@ bool editor_mode::init() { EditorSettings.load(); Global.EditorOrthoBallast = EditorSettings.top_ballast(); + set_norms_free(EditorSettings.norms() == editorSettings::track_norms::free_laying); EditorModelSets.load(); Camera.Init({0, 15, 0}, {glm::radians(-30.0), glm::radians(180.0), 0}, nullptr); return m_input.init(); @@ -1539,6 +1540,7 @@ bool editor_mode::update() render_open_scenery_popup(); render_shortcuts_window(); render_first_steps(); + render_norms_prompt(); render_nmt_prompt(); render_nmt_banner(); render_orthophoto_window(); @@ -1581,6 +1583,18 @@ void editor_mode::render_settings() ImGui::Separator(); ImGui::Checkbox(STR_C("Transform gizmo (ImGuizmo)"), &m_gizmo_enabled); + ImGui::Separator(); + ImGui::TextUnformatted(STR_C("Laying of new tracks")); + const char *norms[] = {STR_C("Ask as the laying starts"), STR_C("Free laying"), STR_C("To the PKP PLK norms")}; + int chosen = static_cast(EditorSettings.norms()); + if (ImGui::Combo("##tracknorms", &chosen, norms, IM_ARRAYSIZE(norms))) + { + EditorSettings.norms(static_cast(chosen)); + EditorSettings.save(); + if (chosen > 0) + set_norms_free(chosen == 1); + } + ImGui::Separator(); ImGui::TextUnformatted(STR_C("Colours of the editor")); const char *palettes[] = {STR_C("Green, as the rest of the simulator"), STR_C("Amber, of the editor")}; diff --git a/application/editormode.h b/application/editormode.h index dd841175..c9053ffd 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1336,9 +1336,15 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::vector chain; }; lay_tool m_lay; - bool m_norms_free{true}; + bool m_norms_free{false}; + // the choice between the free laying and the norms, asked once a session as the laying or the plan is opened first + bool m_norms_prompt{false}; + bool m_norms_asked{false}; + bool m_norms_remember{false}; alignment::limits laying_norms() const; + // returns: a card was clicked bool render_norms_choice(); + void render_norms_prompt(); void set_norms_free(bool const Free); glm::dvec3 lay_angle_snap(glm::dvec3 const &Mouse, std::string &Label) const; // the straight track Alt turns the laid straight to: the nearest one the straight from From to To crosses, or one along From diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index e6943593..9023e1ed 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -10,6 +10,7 @@ http: #include "stdafx.h" #include "application/editormode.h" +#include "editor/editorSettings.hpp" #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "application/editormodeshared.h" @@ -1342,7 +1343,7 @@ bool geometry_card(char const *Id, char const *Title, char const *Text, bool con bool editor_mode::render_norms_choice() { - auto changed{false}; + auto picked{false}; auto const gap{std::round(ImGui::GetFontSize() * 0.5f)}; auto const width{std::floor((ImGui::GetContentRegionAvail().x - gap) * 0.5f)}; char const *const titles[]{STR_C("Free laying"), STR_C("To the PKP PLK norms")}; @@ -1357,14 +1358,48 @@ bool editor_mode::render_norms_choice() auto const clicked{geometry_card(i == 0 ? "##free" : "##norms", titles[i], texts[i], i == 1, chosen, width, height)}; item_tooltip(i == 0 ? "Plain arcs with no transition curves and no cant, no warnings about the norms: the track goes where it's clicked" : "Transition curves and cant for the design speed, warnings when the radius, the cant or the transitions don't meet the norms"); + picked |= clicked; if (clicked && false == chosen) { set_norms_free(i == 0); - changed = true; + // a kept choice follows the one made later + if (EditorSettings.norms() != editorSettings::track_norms::ask) + { + EditorSettings.norms(m_norms_free ? editorSettings::track_norms::free_laying : editorSettings::track_norms::plk); + EditorSettings.save(); + } } } ImGui::PopID(); - return changed; + return picked; +} + +void editor_mode::render_norms_prompt() +{ + if (false == m_norms_prompt) + return; + char const *const title{STR_C("How to lay the tracks###normsprompt")}; + if (false == ImGui::IsPopupOpen(title)) + ImGui::OpenPopup(title); + ImGui::SetNextWindowSize(ImVec2(520.0f * Global.ui_scale, 0.0f)); + if (false == ImGui::BeginPopupModal(title, nullptr, ImGuiWindowFlags_NoResize)) + return; + ImGui::TextWrapped("%s", STR_C("Choose how the new tracks are laid. It can be changed any time at the top of the panels of laying and of the plan.")); + ImGui::Spacing(); + if (render_norms_choice()) + { + if (m_norms_remember) + { + EditorSettings.norms(m_norms_free ? editorSettings::track_norms::free_laying : editorSettings::track_norms::plk); + EditorSettings.save(); + } + m_norms_prompt = false; + ImGui::CloseCurrentPopup(); + } + ImGui::Spacing(); + ImGui::Checkbox(STR_C("Remember the choice, don't ask again"), &m_norms_remember); + item_tooltip("Asking again can be switched on in the settings of the editor (General)"); + ImGui::EndPopup(); } void editor_mode::set_norms_free(bool const Free) diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index 9fdcaa78..94d5896a 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -1354,7 +1354,7 @@ void editor_mode::nmt_place_scenery(glm::dvec2 const &Point) void editor_mode::nmt_offer() { auto &state{m_nmt_state}; - if (state.offered || false == m_lay.active || false == EditorSettings.terrain_offer()) + if (state.offered || m_norms_prompt || false == m_lay.active || false == EditorSettings.terrain_offer()) return; state.offered = true; if (m_streamer.resident() > 0 || std::any_of(simulation::Paths.sequence().begin(), simulation::Paths.sequence().end(), [](TTrack const *Track) { return Track != nullptr && rail(*Track); })) diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 731c0b11..146ff6c9 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -171,6 +171,8 @@ void editor_mode::selftest_step() if (false == line.empty() && line.front() != '#') test.lines.push_back(line); WriteLog("SELFTEST: " + std::to_string(test.lines.size()) + " steps from " + file); + // the scripts were written for the free laying, "norms plk" takes the norms + set_norms_free(true); } ++editor_selftest::frames(); if (test.wait > 0) diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 406924ec..8e32ea20 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -10,6 +10,7 @@ http: #include "stdafx.h" #include "application/editormode.h" +#include "editor/editorSettings.hpp" #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "application/editormodeshared.h" @@ -37,6 +38,7 @@ http: #include #include #include +#include #include #include #include @@ -447,6 +449,11 @@ void editor_mode::show_track_tab(track_tab const Wanted) m_lay.active = Tab == track_tab::lay; m_switch.armed = -1; m_track_tab = Tab; + if ((Tab == track_tab::lay || Tab == track_tab::straights || Tab == track_tab::route) && false == m_norms_asked && EditorSettings.norms() == editorSettings::track_norms::ask && std::getenv("EU07_EDITOR_SELFTEST") == nullptr) + { + m_norms_asked = true; + m_norms_prompt = true; + } auto *track{selected_track()}; switch (Tab) { diff --git a/editor/editorSettings.cpp b/editor/editorSettings.cpp index 4a9b8232..b2590272 100644 --- a/editor/editorSettings.cpp +++ b/editor/editorSettings.cpp @@ -110,6 +110,8 @@ bool editorSettings::load() m_track_tools_more = (value == "1"); else if (key == "top_ballast") m_top_ballast = (value == "1"); + else if (key == "track_norms") + m_norms = value == "free" ? track_norms::free_laying : value == "plk" ? track_norms::plk : track_norms::ask; else if (key.rfind(origin_prefix, 0) == 0) { // northing;easting @@ -157,6 +159,7 @@ bool editorSettings::save() 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 << "track_norms " << (m_norms == track_norms::free_laying ? "free" : m_norms == track_norms::plk ? "plk" : "ask") << "\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 ddee1ab1..3adfe782 100644 --- a/editor/editorSettings.hpp +++ b/editor/editorSettings.hpp @@ -64,6 +64,15 @@ public: // 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; } + // how the new tracks are laid: asked as the laying starts, or the choice kept + enum class track_norms + { + ask, + free_laying, + plk + }; + track_norms norms() const { return m_norms; } + void norms(track_norms Norms) { m_norms = Norms; } private: movement_scheme m_movement{movement_scheme::wsad}; @@ -78,6 +87,7 @@ private: bool m_terrain_offer{true}; bool m_track_tools_more{false}; bool m_top_ballast{true}; + track_norms m_norms{track_norms::ask}; }; // global editor settings instance diff --git a/lang/editor_pl.po b/lang/editor_pl.po index ca1a37ee..d1d66e7f 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10150,3 +10150,21 @@ msgstr "LPM: początek pomiaru. M albo Esc: koniec" msgid "Measure" msgstr "Miarka" + +msgid "How to lay the tracks###normsprompt" +msgstr "Jak kłaść tory###normsprompt" + +msgid "Choose how the new tracks are laid. It can be changed any time at the top of the panels of laying and of the plan." +msgstr "Wybierz, jak mają się kłaść nowe tory. Zmienisz to w każdej chwili na górze paneli Układanie i Plan." + +msgid "Remember the choice, don't ask again" +msgstr "Zapamiętaj wybór, nie pytaj więcej" + +msgid "Asking again can be switched on in the settings of the editor (General)" +msgstr "Pytanie można włączyć z powrotem w ustawieniach edytora (Ogólne)" + +msgid "Laying of new tracks" +msgstr "Kładzenie nowych torów" + +msgid "Ask as the laying starts" +msgstr "Pytaj przy rozpoczęciu kładzenia" From 5764b328bec3b5f3f2a68596d0078d1194045c65 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 18:54:14 +0200 Subject: [PATCH 035/145] feat(editor): a curve of the plan traced on the orthophoto, the points clicked along the track give its radius or the radii and the shares of a compound curve --- application/editormode.cpp | 9 + application/editormode.h | 18 +- application/editormoderoute.cpp | 158 +++++++++ application/editormodeselftesttrack.cpp | 88 +++++ application/editormodetrackux.cpp | 22 +- editor/editorAlignment.cpp | 429 ++++++++++++++++++++++++ editor/editorAlignment.hpp | 19 ++ lang/editor_pl.po | 54 +++ 8 files changed, 795 insertions(+), 2 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 886546bb..7ed6c292 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -6465,6 +6465,8 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in { if (m_tape.active) m_tape = {}; + else if (m_route.trace.active && route_active()) + route_trace_end(false); else if (m_lay.active && false == m_lay.points.empty()) lay_cancel(); else if (m_track_style.picking) @@ -6493,12 +6495,19 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in m_fill_points.pop_back(); if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && m_lay.active && !m_lay.points.empty()) lay_back(); + if (is_press(Action) && m_route.trace.active && route_active() && !m_route.trace.points.empty()) + { + m_route.trace.points.pop_back(); + route_trace_fit(); + } break; case GLFW_KEY_ENTER: case GLFW_KEY_KP_ENTER: if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && m_lay.active) lay_enter(); + if (is_press(Action) && m_route.trace.active && route_active()) + route_trace_end(true); break; case GLFW_KEY_K: diff --git a/application/editormode.h b/application/editormode.h index c9053ffd..2b6bcdfa 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -926,6 +926,16 @@ class editor_mode : public application_mode, private editor_track::observer, pri float metres{1.0f}; // pixels of a metre and of a unit of the curvature, kept as the drag goes float curvature{1.0f}; } band; + // points clicked along the middle of the track seen on the orthophoto, the radii of the curves and their shares fitted to them after each click + struct trace_state + { + bool active{false}; + int radii{0}; // 0: as many as the points need, up to three + std::vector points; + alignment::design before; // taken back by Esc + double rms{0.0}; + double worst{0.0}; + } trace; }; void render_route_ui(); bool render_route_band(); @@ -933,6 +943,12 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool render_route_vertices(); bool render_route_vertex(int const Index); void render_route_result(); + void render_route_trace(); + void route_trace_start(); + void route_trace_fit(); + void route_trace_end(bool const Keep); + int route_trace_hit() const; + glm::dvec3 route_trace_cursor() const; void route_reset(); void route_bind_ends(); bool route_from_curve(TTrack &Track); @@ -1586,7 +1602,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void draw_track_hover() const; struct track_intent { - enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling, measure }; + enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling, measure, route_trace }; kind what{kind::none}; TTrack *track{nullptr}; int path{0}; diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index 9fd4f5b5..d4c2419b 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -83,6 +83,7 @@ void editor_mode::route_reset() route.vertex = -1; route.grip = -1; route.result = {}; + route.trace = {}; if (route.from == nullptr || route.to == nullptr) return; if (false == editor_track::find_chain(route.from, route.to, route.chain, route.error, true)) @@ -1086,6 +1087,162 @@ void editor_mode::draw_route_overlay() const } drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y - 18.0f), overlay_color::marked, format("W%d", i + 1).c_str()); } + if (route.trace.points.empty()) + return; + std::vector plan; + for (auto const &point : route.trace.points) + plan.push_back(plan_of(point)); + auto const projections{alignment::project(result, plan)}; + auto const hit{route_trace_hit()}; + for (std::size_t i = 0; i < route.trace.points.size(); ++i) + { + ImVec2 screen; + if (false == projection.project(route.trace.points[i], screen)) + continue; + auto const departure{i < projections.size() ? std::abs(projections[i].offset) : 0.0}; + auto const colour{departure <= 0.1 ? IM_COL32(90, 220, 110, 255) : departure <= 0.3 ? IM_COL32(255, 200, 60, 255) : IM_COL32(255, 90, 70, 255)}; + drawlist->AddCircle(screen, 5.0f, IM_COL32(0, 0, 0, 200), 12, 3.5f); + drawlist->AddCircle(screen, 5.0f, colour, 12, 2.0f); + if (static_cast(i) == hit) + drawlist->AddCircle(screen, 10.0f, overlay_color::highlight, 16, 2.0f); + if (i < projections.size()) + drawlist->AddText(ImVec2(screen.x + 8.0f, screen.y - 7.0f), colour, format("%.0f cm", departure * 100.0).c_str()); + } +} + +void editor_mode::render_route_trace() +{ + auto &trace{m_route.trace}; + if (false == trace.active) + { + if (ImGui::Button(STR_C("Trace the curve on the photo"))) + route_trace_start(); + item_tooltip("Click points along the middle of the track on the orthophoto: after each click the curve takes the radius, or the radii of a compound curve and their shares, which follow the points best. The straights and the transition curves stay"); + return; + } + ImGui::SeparatorText(STR_C("Tracing the curve")); + if (segmented("traceradii", trace.radii, + {{0, STR_C("Auto"), "As few radii as follow the points, up to three"}, {1, STR_C("1 R"), "One radius"}, {2, STR_C("2 R"), "A compound curve of two radii"}, {3, STR_C("3 R"), "A compound curve of three radii"}})) + route_trace_fit(); + if (trace.points.empty()) + { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", STR_C("Click points along the middle of the track on the photo, spread over the whole curve; a few on the straights at its ends help too")); + ImGui::PopTextWrapPos(); + } + else + { + auto const colour = [](double const Departure) { return Departure <= 0.1 ? IM_COL32(90, 220, 110, 255) : Departure <= 0.3 ? IM_COL32(255, 200, 60, 255) : IM_COL32(255, 90, 70, 255); }; + metric_tiles({{format("%zu", trace.points.size()), STR_C("points"), ImGui::GetColorU32(ImGuiCol_Text)}, + {format("%.0f cm", trace.rms * 100.0), STR_C("departure"), colour(trace.rms)}, + {format("%.0f cm", trace.worst * 100.0), STR_C("at most"), colour(trace.worst)}}); + } + if (main_button(STR_C("Apply Enter"), ImVec2(-1.0f, 0.0f))) + route_trace_end(true); + if (ImGui::Button(STR_C("Drop Esc"))) + route_trace_end(false); + ImGui::SameLine(); + ImGui::BeginDisabled(trace.points.empty()); + if (ImGui::Button(STR_C("Take out the last point Backspace"))) + { + trace.points.pop_back(); + route_trace_fit(); + } + ImGui::EndDisabled(); +} + +void editor_mode::route_trace_start() +{ + auto &trace{m_route.trace}; + trace.active = true; + trace.points.clear(); + trace.before = m_route.design; + trace.rms = 0.0; + trace.worst = 0.0; +} + +// every point goes to the curve nearest to it along the line, the curves without points keep their shape +void editor_mode::route_trace_fit() +{ + auto &route{m_route}; + auto &trace{route.trace}; + auto design{route.design}; + trace.rms = 0.0; + trace.worst = 0.0; + if (trace.points.empty() || design.vertices.empty() || false == route.result.valid) + return; + std::vector plan; + for (auto const &point : trace.points) + plan.push_back(plan_of(point)); + auto const projections{alignment::project(route.result, plan)}; + std::vector> owned(design.vertices.size()); + for (std::size_t i = 0; i < plan.size(); ++i) + { + std::size_t nearest{0}; + for (std::size_t k = 1; k < route.result.vertex_chainages.size() && k < owned.size(); ++k) + if (std::abs(route.result.vertex_chainages[k] - projections[i].chainage) < std::abs(route.result.vertex_chainages[nearest] - projections[i].chainage)) + nearest = k; + owned[nearest].push_back(plan[i]); + } + for (std::size_t k = 0; k < owned.size(); ++k) + { + if (owned[k].empty()) + continue; + auto const fit{alignment::fit_trace(design, k, owned[k], trace.radii)}; + if (fit.valid) + design.vertices[k] = fit.fitted; + } + route.design = design; + route_update(); + double sum{0.0}; + for (auto const &projection : alignment::project(route.result, plan)) + { + sum += projection.offset * projection.offset; + trace.worst = std::max(trace.worst, std::abs(projection.offset)); + } + trace.rms = std::sqrt(sum / static_cast(plan.size())); +} + +void editor_mode::route_trace_end(bool const Keep) +{ + auto &trace{m_route.trace}; + if (false == trace.active) + return; + auto const traced{false == trace.points.empty()}; + if (false == Keep) + { + m_route.design = trace.before; + route_update(); + } + trace = {}; + if (Keep && traced) + route_apply(); +} + +// the photo under the cursor where the depth finds it, else the level of the design there +glm::dvec3 editor_mode::route_trace_cursor() const +{ + if (GfxRenderer->Mouse_Hit()) + return cursor_ground(); + auto const &result{m_route.result}; + auto point{cursor_level(alignment::elevation(result, result.length * 0.5))}; + if (auto const projections{alignment::project(result, {plan_of(point)})}; false == projections.empty()) + point = cursor_level(alignment::elevation(result, projections.front().chainage)); + return point; +} + +int editor_mode::route_trace_hit() const +{ + screen_projection const projection; + float best{kHandleRadius * kHandleRadius}; + int hit{-1}; + for (int i = 0; i < static_cast(m_route.trace.points.size()); ++i) + if (auto const distance{projection.mouse_distance2(m_route.trace.points[i])}; distance < best) + { + best = distance; + hit = i; + } + return hit; } void editor_mode::render_route_gizmo() @@ -1166,6 +1323,7 @@ void editor_mode::render_route_ui() } item_tooltip("Reads the curves of the line from the scenery again, dropping the changes not applied"); auto changed{render_route_band()}; + render_route_trace(); render_route_stored(); changed |= render_route_vertices(); if (ImGui::CollapsingHeader(STR_C("Design speed and limits"))) diff --git a/application/editormodeselftesttrack.cpp b/application/editormodeselftesttrack.cpp index 845e9fd2..05ca9361 100644 --- a/application/editormodeselftesttrack.cpp +++ b/application/editormodeselftesttrack.cpp @@ -263,6 +263,94 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm WriteLog(format("SELFTEST %s: valid %d, %zu elements, extension %.3f / %.3f", command.c_str(), m_route.result.valid ? 1 : 0, m_route.result.elements.size(), m_route.result.start_extension, m_route.result.end_extension) + errors); return selftest_result::handled; } + if (command == "routeplain") + { + int index{0}; + double radius{1000.0}; + words >> index >> radius; + if (index >= 0 && index < static_cast(m_route.design.vertices.size())) + { + auto &vertex{m_route.design.vertices[index]}; + vertex.radius = radius; + vertex.compound = false; + vertex.share = 1.0; + vertex.arcs.clear(); + route_update(); + } + WriteLog(format("SELFTEST routeplain: valid %d", m_route.result.valid ? 1 : 0)); + return selftest_result::handled; + } + // points along the curves of the design as it is now, every Spacing metres from 20 m before the first curve to 20 m past the last, + // shifted across by up to Noise to and fro, as the trace takes them + if (command == "tracesample") + { + double spacing{20.0}; + double noise{0.0}; + words >> spacing >> noise; + route_trace_start(); + auto const &result{m_route.result}; + auto from{result.length}; + auto to{0.0}; + for (auto const &e : result.elements) + if (e.kind != alignment::element_kind::straight) + { + from = std::min(from, e.chainage); + to = std::max(to, e.chainage + e.length); + } + int count{0}; + for (auto chainage{std::max(0.0, from - 20.0)}; chainage <= std::min(result.length, to + 20.0); chainage += spacing, ++count) + { + auto const station{alignment::evaluate(result, chainage)}; + glm::dvec3 const across{-station.direction.z, 0.0, station.direction.x}; + auto const shift{noise * (count % 3 == 0 ? 1.0 : count % 3 == 1 ? -0.6 : 0.2)}; + m_route.trace.points.push_back(station.position + glm::normalize(across) * shift); + } + auto const &middle{m_route.trace.points.empty() ? glm::dvec3{0.0} : m_route.trace.points[m_route.trace.points.size() / 2]}; + WriteLog(format("SELFTEST tracesample: %zu points over %.1f m, the middle one at %.3f %.3f", m_route.trace.points.size(), to - from, middle.x, middle.z)); + return selftest_result::handled; + } + if (command == "traceradii" || command == "traceclick" || command == "traceundo") + { + auto const started{std::chrono::steady_clock::now()}; + if (command == "traceradii") + { + words >> m_route.trace.radii; + route_trace_fit(); + } + else if (command == "traceundo") + { + if (false == m_route.trace.points.empty()) + m_route.trace.points.pop_back(); + route_trace_fit(); + } + else + { + track_intent intent; + words >> intent.position.x >> intent.position.z; + intent.what = track_intent::kind::route_trace; + intent.index = -1; + track_press(intent); + } + auto const took{std::chrono::duration(std::chrono::steady_clock::now() - started).count()}; + WriteLog(format("SELFTEST %s: %zu points, departure %.3f m, at most %.3f m, valid %d, %.0f ms", command.c_str(), m_route.trace.points.size(), m_route.trace.rms, m_route.trace.worst, m_route.result.valid ? 1 : 0, took)); + for (std::size_t i = 0; i < m_route.design.vertices.size(); ++i) + { + auto const &v{m_route.design.vertices[i]}; + auto text{format("SELFTEST %s vertex %zu: R %.0f L %.1f / %.1f compound %d share %.3f", command.c_str(), i, v.radius, v.transition_in, v.transition_out, v.compound ? 1 : 0, v.share)}; + for (auto const &arc : v.arcs) + text += format(" [R %.0f L %.1f share %.3f]", arc.radius, arc.transition, arc.share); + WriteLog(text); + } + return selftest_result::handled; + } + if (command == "traceend") + { + int keep{1}; + words >> keep; + route_trace_end(keep != 0); + WriteLog(format("SELFTEST traceend: active %d, valid %d, ", m_route.trace.active ? 1 : 0, m_route.result.valid ? 1 : 0) + m_route.status + " " + m_route.error); + return selftest_result::handled; + } if (command == "routekm") { double km{0.0}; diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 0869faba..f40348d3 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -549,6 +549,18 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) return make(kind::straight_handle, hit == 2 ? "Drag: shift the straight sideways" : "Drag: move this end, the curves at it follow"); } } + if (m_route.trace.active && route_active()) + { + intent.index = route_trace_hit(); + if (control && intent.index >= 0) + { + intent.position = m_route.trace.points[intent.index]; + return make(kind::route_trace, "Click: take this point out"); + } + intent.index = -1; + intent.position = route_trace_cursor(); + return make(kind::route_trace, m_route.trace.points.empty() ? "Click: a point in the middle of the track on the photo. Enter: apply, Esc: drop" : "Click: one more point, the curve follows. Ctrl+click: take a point out"); + } if (int vertex, grip; route_hit(vertex, grip)) { intent.index = vertex >= 0 ? vertex : grip; @@ -711,6 +723,13 @@ void editor_mode::track_press(track_intent const &Intent) m_tape.points.clear(); m_tape.points.push_back(m_tape.mouse); return; + case kind::route_trace: + if (Intent.index >= 0) + m_route.trace.points.erase(m_route.trace.points.begin() + Intent.index); + else + m_route.trace.points.push_back(Intent.position); + route_trace_fit(); + return; case kind::lay_end: if (m_lay.points.empty() && start_extend(Intent.snap)) return; @@ -849,7 +868,8 @@ void editor_mode::draw_track_intent() const 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; + case kind::lay: + case kind::route_trace: drawlist->AddCircle(at, 6.0f, IM_COL32(255, 255, 255, 200), 12, 1.5f); break; default: break; } } diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 232bb9f4..dfc86eb2 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -1237,6 +1237,435 @@ station evaluate(result const &Result, double const Chainage) return result; } +namespace +{ + +// the plan from From to To in chords short enough to keep to the curves, the directions of its ends carried on far, so that a point +// past them is measured across the track +struct plan_line +{ + std::vector points; + std::vector chainages; +}; + +plan_line sample_plan(result const &Result, double const From, double const To, double const Step) +{ + constexpr double kFar{5000.0}; + plan_line line; + if (Result.elements.empty()) + return line; + auto const from{std::clamp(From, 0.0, Result.length)}; + auto const to{std::clamp(To, from, Result.length)}; + auto const push = [&](glm::dvec2 const &Position, double const Chainage) { + line.points.push_back(Position); + line.chainages.push_back(Chainage); + }; + for (auto const &e : Result.elements) + { + auto const low{std::max(from, e.chainage)}; + auto const high{std::min(to, e.chainage + e.length)}; + if (high < low) + continue; + auto const count{std::max(1, static_cast(std::ceil((high - low) / Step)))}; + auto const at = [&](int const Index) { return low + (high - low) * Index / count; }; + if (e.kind != element_kind::spiral) + { + glm::dvec2 position; + glm::dvec2 direction; + for (int i = 0; i <= count; ++i) + { + element_point(e, Result.shape, at(i) - e.chainage, position, direction); + push(position, at(i)); + } + continue; + } + // spiral_point integrates from the start of the element for every point, here it goes on from the previous one + auto const heading = [&](double const S) { return e.curvature_start * S + (e.curvature_end - e.curvature_start) * S * S / (2.0 * std::max(e.length, 1e-9)); }; + double x{0.0}; + double y{0.0}; + double s{0.0}; + auto const advance = [&](double const Distance) { + auto const pieces{std::max(1, static_cast(std::ceil((Distance - s) / 2.0)))}; + auto const h{(Distance - s) / pieces}; + for (int i = 0; i < pieces; ++i) + { + auto const a{heading(s + h * i)}; + auto const m{heading(s + h * (i + 0.5))}; + auto const b{heading(s + h * (i + 1))}; + x += h / 6.0 * (std::cos(a) + 4.0 * std::cos(m) + std::cos(b)); + y += h / 6.0 * (std::sin(a) + 4.0 * std::sin(m) + std::sin(b)); + } + s = Distance; + }; + for (int i = 0; i <= count; ++i) + { + advance(at(i) - e.chainage); + push(e.origin + e.direction * x + e.normal * y, at(i)); + } + } + if (line.points.size() < 2) + return line; + auto const away = [](glm::dvec2 const &End, glm::dvec2 const &Inner) { + auto const offset{End - Inner}; + return glm::length(offset) > 1e-9 ? offset / glm::length(offset) : glm::dvec2{0.0}; + }; + std::size_t first{1}; + while (first + 1 < line.points.size() && glm::distance(line.points[first], line.points.front()) < 1e-6) + ++first; + auto last{line.points.size() - 2}; + while (last > 0 && glm::distance(line.points[last], line.points.back()) < 1e-6) + --last; + auto const before{away(line.points.front(), line.points[first])}; + auto const after{away(line.points.back(), line.points[last])}; + line.points.insert(line.points.begin(), line.points.front() + before * kFar); + line.chainages.insert(line.chainages.begin(), line.chainages.front() - kFar); + push(line.points.back() + after * kFar, line.chainages.back() + kFar); + return line; +} + +plan_projection project_on(plan_line const &Line, glm::dvec2 const &Point) +{ + plan_projection best; + auto nearest{std::numeric_limits::max()}; + for (std::size_t i = 0; i + 1 < Line.points.size(); ++i) + { + auto const segment{Line.points[i + 1] - Line.points[i]}; + auto const length2{glm::dot(segment, segment)}; + if (length2 < 1e-18) + continue; + auto const t{std::clamp(glm::dot(Point - Line.points[i], segment) / length2, 0.0, 1.0)}; + auto const offset{Point - (Line.points[i] + segment * t)}; + auto const distance2{glm::dot(offset, offset)}; + if (distance2 >= nearest) + continue; + nearest = distance2; + best.chainage = Line.chainages[i] + (Line.chainages[i + 1] - Line.chainages[i]) * t; + best.offset = std::copysign(std::sqrt(distance2), cross(segment, offset)); + } + return best; +} + +double squares(std::vector const &Values) +{ + double sum{0.0}; + for (auto const value : Values) + sum += value * value; + return sum; +} + +using residuals_of = std::function const &, std::vector &)>; + +// Levenberg-Marquardt: the parameters of the least sum of the squares of the residuals; a step to where Residuals fails isn't taken +double least_squares(std::vector &Parameters, residuals_of const &Residuals) +{ + constexpr double kDifference{1e-4}; + std::vector residuals; + if (false == Residuals(Parameters, residuals)) + return std::numeric_limits::max(); + auto cost{squares(residuals)}; + auto const n{Parameters.size()}; + auto const m{residuals.size()}; + double damping{1e-3}; + std::vector trial; + for (int iteration = 0; iteration < 40 && cost > 1e-12; ++iteration) + { + std::vector> jacobian(n, std::vector(m, 0.0)); + for (std::size_t j = 0; j < n; ++j) + { + auto parameters{Parameters}; + parameters[j] += kDifference; + auto sign{1.0}; + if (false == Residuals(parameters, trial) || trial.size() != m) + { + parameters[j] = Parameters[j] - kDifference; + sign = -1.0; + if (false == Residuals(parameters, trial) || trial.size() != m) + continue; + } + for (std::size_t i = 0; i < m; ++i) + jacobian[j][i] = sign * (trial[i] - residuals[i]) / kDifference; + } + std::vector normal(n * n, 0.0); + std::vector gradient(n, 0.0); + for (std::size_t j = 0; j < n; ++j) + { + for (std::size_t i = 0; i < m; ++i) + gradient[j] += jacobian[j][i] * residuals[i]; + for (std::size_t k = 0; k < n; ++k) + for (std::size_t i = 0; i < m; ++i) + normal[j * n + k] += jacobian[j][i] * jacobian[k][i]; + } + auto better{false}; + auto settled{false}; + for (int attempt = 0; attempt < 12 && false == better; ++attempt) + { + auto system{normal}; + std::vector step(n); + for (std::size_t j = 0; j < n; ++j) + { + system[j * n + j] += damping * std::max(normal[j * n + j], 1e-9); + step[j] = -gradient[j]; + } + for (std::size_t c = 0; c < n; ++c) + { + auto pivot{c}; + for (std::size_t r = c + 1; r < n; ++r) + if (std::abs(system[r * n + c]) > std::abs(system[pivot * n + c])) + pivot = r; + if (std::abs(system[pivot * n + c]) < 1e-300) + break; + for (std::size_t k = 0; k < n; ++k) + std::swap(system[c * n + k], system[pivot * n + k]); + std::swap(step[c], step[pivot]); + for (std::size_t r = c + 1; r < n; ++r) + { + auto const factor{system[r * n + c] / system[c * n + c]}; + for (std::size_t k = c; k < n; ++k) + system[r * n + k] -= factor * system[c * n + k]; + step[r] -= factor * step[c]; + } + } + for (std::size_t c = n; c-- > 0;) + { + for (std::size_t k = c + 1; k < n; ++k) + step[c] -= system[c * n + k] * step[k]; + step[c] = std::abs(system[c * n + c]) > 1e-300 ? step[c] / system[c * n + c] : 0.0; + } + auto candidate{Parameters}; + for (std::size_t j = 0; j < n; ++j) + candidate[j] += step[j]; + if (Residuals(candidate, trial) && trial.size() == m && squares(trial) < cost) + { + auto const lower{squares(trial)}; + settled = cost - lower < 1e-6 * cost; + Parameters = candidate; + residuals = trial; + cost = lower; + damping = std::max(1e-9, damping * 0.3); + better = true; + } + else + { + damping *= 8.0; + } + } + if (false == better || settled) + break; + } + return cost; +} + +constexpr double kTraceRadiusMin{15.0}; +constexpr double kTraceRadiusMax{50000.0}; +constexpr double kTraceRobust{0.25}; + +// the parameters: the logarithms of the radii, then the weights of the shares of all but the first radius, as logarithms too +vertex traced_shape(vertex const &Base, std::vector const &Parameters, int const Radii) +{ + auto shape{Base}; + auto const radius = [&](int const Index) { return std::exp(std::clamp(Parameters[Index], std::log(kTraceRadiusMin), std::log(kTraceRadiusMax))); }; + shape.radius = radius(0); + shape.share = 1.0; + if (Radii == 1) + { + shape.compound = false; + return shape; + } + std::vector weights{1.0}; + for (int i = 1; i < Radii; ++i) + weights.push_back(std::exp(std::clamp(Parameters[Radii + i - 1], -5.0, 5.0))); + double total{0.0}; + for (auto const weight : weights) + total += weight; + shape.compound = true; + shape.share = weights[0] / total; + shape.arcs.clear(); + for (int i = 1; i < Radii; ++i) + { + compound_arc arc; + arc.radius = radius(i); + arc.transition = i - 1 < static_cast(Base.arcs.size()) ? Base.arcs[i - 1].transition : 0.0; + arc.share = weights[i] / total; + shape.arcs.push_back(arc); + } + return shape; +} + +} // namespace + +std::vector project(result const &Result, std::vector const &Points) +{ + std::vector projections; + if (Result.elements.empty()) + return projections; + auto const line{sample_plan(Result, 0.0, Result.length, std::max(0.5, Result.length / 4000.0))}; + for (auto const &point : Points) + projections.push_back(project_on(line, point)); + return projections; +} + +trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector const &Points, int const Radii) +{ + trace_fit best; + if (Vertex >= Design.vertices.size() || Points.empty()) + return best; + auto const &base{Design.vertices[Vertex]}; + auto const residuals = [&](int const Count) -> residuals_of { + return [&, Count](std::vector const &Parameters, std::vector &Residuals) { + auto trial{Design}; + trial.norms.checked = false; + trial.vertices[Vertex] = traced_shape(base, Parameters, Count); + auto const result{compute(trial)}; + if (false == result.valid) + return false; + auto from{std::numeric_limits::max()}; + auto to{std::numeric_limits::lowest()}; + for (auto const &e : result.elements) + { + if (e.vertex != static_cast(Vertex)) + continue; + from = std::min(from, e.chainage); + to = std::max(to, e.chainage + e.length); + } + if (from > to) + return false; + auto const line{sample_plan(result, from - 1.0, to + 1.0, std::clamp((to - from) / 300.0, 0.25, 2.0))}; + Residuals.clear(); + for (auto const &point : Points) + { + // a point clicked by mistake far off pulls the curve less than the square of its departure would + auto const offset{project_on(line, point).offset}; + auto const departure{std::abs(offset)}; + Residuals.push_back(departure <= kTraceRobust ? offset : std::copysign(std::sqrt(2.0 * kTraceRobust * departure - kTraceRobust * kTraceRobust), offset)); + } + return true; + }; + }; + auto const cost_of = [](residuals_of const &Residuals, std::vector const &Parameters) { + std::vector values; + return Residuals(Parameters, values) ? squares(values) : std::numeric_limits::max(); + }; + + // one radius: a scan over the whole range, then the golden section around the best of it + auto const one{residuals(1)}; + auto const low{std::log(kTraceRadiusMin)}; + auto const high{std::log(kTraceRadiusMax)}; + constexpr int kScan{60}; + auto const spacing{(high - low) / kScan}; + auto bestlog{std::log(std::max(kTraceRadiusMin, base.radius))}; + auto bestcost{cost_of(one, {bestlog})}; + for (int i = 0; i <= kScan; ++i) + if (auto const cost{cost_of(one, {low + spacing * i})}; cost < bestcost) + { + bestcost = cost; + bestlog = low + spacing * i; + } + if (bestcost == std::numeric_limits::max()) + return best; + { + constexpr double kGolden{0.6180339887498949}; + auto a{bestlog - spacing}; + auto b{bestlog + spacing}; + auto c{b - (b - a) * kGolden}; + auto d{a + (b - a) * kGolden}; + auto fc{cost_of(one, {c})}; + auto fd{cost_of(one, {d})}; + for (int i = 0; i < 50; ++i) + { + if (fc < fd) + { + b = d; + d = c; + fd = fc; + c = b - (b - a) * kGolden; + fc = cost_of(one, {c}); + } + else + { + a = c; + c = d; + fc = fd; + d = a + (b - a) * kGolden; + fd = cost_of(one, {d}); + } + } + if (auto const middle{(a + b) * 0.5}; cost_of(one, {middle}) < bestcost) + bestlog = middle; + } + + struct candidate + { + std::vector parameters; + double cost{std::numeric_limits::max()}; + }; + std::array fits; + fits[1] = {{bestlog}, cost_of(one, {bestlog})}; + auto const most{std::clamp(Radii > 0 ? Radii : 3, 1, std::max(1, static_cast(Points.size()) / 2))}; + auto const shift{std::log(1.3)}; + auto const share = [](double const Second, double const First) { return std::log(Second / First); }; + auto const rms = [&](int const Count) { return std::sqrt(fits[Count].cost / static_cast(Points.size())); }; + // the next radius has to halve the departure about, a curve which keeps to the points already gets none + constexpr double kClose{0.08}; + auto chosen{1}; + auto const wanted = [&](int const Count) { return Count <= most && (Radii > 0 || rms(chosen) > kClose); }; + auto const take = [&](int const Count) { + if (fits[Count].cost == std::numeric_limits::max()) + return; + if (Radii > 0 || rms(Count) < 0.6 * rms(chosen)) + chosen = Count; + }; + if (wanted(2)) + { + auto const two{residuals(2)}; + for (auto const fraction : {0.3, 0.5, 0.7}) + for (auto const sign : {-1.0, 1.0}) + { + std::vector parameters{bestlog + sign * shift, bestlog - sign * shift, share(1.0 - fraction, fraction)}; + if (auto const cost{least_squares(parameters, two)}; cost < fits[2].cost) + fits[2] = {parameters, cost}; + } + take(2); + } + if (wanted(3)) + { + auto const three{residuals(3)}; + std::vector> starts; + for (auto const sign : {-1.0, 1.0}) + starts.push_back({bestlog + sign * shift, bestlog - sign * shift, bestlog + sign * shift, 0.0, 0.0}); + if (false == fits[2].parameters.empty()) + { + auto const &two{fits[2].parameters}; + auto const second{std::exp(two[2])}; + starts.push_back({two[0], two[0], two[1], 0.0, std::log(2.0 * second)}); + starts.push_back({two[0], two[1], two[1], std::log(0.5 * second), std::log(0.5 * second)}); + } + for (auto parameters : starts) + if (auto const cost{least_squares(parameters, three)}; cost < fits[3].cost) + fits[3] = {parameters, cost}; + take(3); + } + + auto shape{traced_shape(base, fits[chosen].parameters, chosen)}; + shape.radius = std::max(kTraceRadiusMin, std::round(shape.radius)); + for (auto &arc : shape.arcs) + arc.radius = std::max(kTraceRadiusMin, std::round(arc.radius)); + auto trial{Design}; + trial.vertices[Vertex] = shape; + auto const result{compute(trial)}; + if (false == result.valid) + return best; + best.valid = true; + best.fitted = shape; + double sum{0.0}; + for (auto const &projection : project(result, Points)) + { + sum += projection.offset * projection.offset; + best.worst = std::max(best.worst, std::abs(projection.offset)); + } + best.rms = std::sqrt(sum / static_cast(Points.size())); + return best; +} + std::size_t minimum_pieces(result const &Result, design const &Design) { std::size_t count{0}; diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index 48cefcc5..eaba18a0 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -187,6 +187,25 @@ station evaluate(result const &Result, double const Chainage); double elevation(result const &Result, double const Chainage); double grade(result const &Result, double const Chainage); +// a point of the plan projected on the alignment, its offset positive to the left; past the ends it's measured from the straights carried on +struct plan_projection +{ + double chainage{0.0}; + double offset{0.0}; +}; +std::vector project(result const &Result, std::vector const &Points); + +struct trace_fit +{ + bool valid{false}; + vertex fitted; + double rms{0.0}; + double worst{0.0}; +}; +// the radii of the curve of the vertex and their shares which follow the points traced along the track best; the tangents, the +// transitions and the cant stay. Radii 0 takes the fewest radii, up to three, which follow the points about as well as more would +trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector const &Points, int const Radii); + std::size_t minimum_pieces(result const &Result, design const &Design); // Breaks: ascending chainages at which pieces have to end. Counts: least number of the pieces between the consecutive breaks, // one more than the breaks; Intervals receives the number of the interval of each piece diff --git a/lang/editor_pl.po b/lang/editor_pl.po index d1d66e7f..77d020a8 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10168,3 +10168,57 @@ msgstr "Kładzenie nowych torów" msgid "Ask as the laying starts" msgstr "Pytaj przy rozpoczęciu kładzenia" + +msgid "Trace the curve on the photo" +msgstr "Odrysuj łuk ze zdjęcia" + +msgid "Click points along the middle of the track on the orthophoto: after each click the curve takes the radius, or the radii of a compound curve and their shares, which follow the points best. The straights and the transition curves stay" +msgstr "Klikaj punkty po środku toru na ortofotomapie: po każdym kliknięciu łuk dobiera promień, albo promienie łuku koszowego i ich udziały, tak żeby szedł po punktach. Proste i krzywe przejściowe zostają" + +msgid "Tracing the curve" +msgstr "Odrysowywanie łuku" + +msgid "Auto" +msgstr "Auto" + +msgid "As few radii as follow the points, up to three" +msgstr "Najmniej promieni, przy których łuk trzyma się punktów, najwyżej trzy" + +msgid "One radius" +msgstr "Jeden promień" + +msgid "A compound curve of two radii" +msgstr "Łuk koszowy z dwóch promieni" + +msgid "A compound curve of three radii" +msgstr "Łuk koszowy z trzech promieni" + +msgid "Click points along the middle of the track on the photo, spread over the whole curve; a few on the straights at its ends help too" +msgstr "Klikaj punkty po środku toru na zdjęciu, rozłożone na cały łuk; kilka na prostych przy jego końcach też pomaga" + +msgid "points" +msgstr "punktów" + +msgid "departure" +msgstr "odchyłka" + +msgid "at most" +msgstr "największa" + +msgid "Apply Enter" +msgstr "Zastosuj Enter" + +msgid "Drop Esc" +msgstr "Porzuć Esc" + +msgid "Take out the last point Backspace" +msgstr "Usuń ostatni punkt Backspace" + +msgid "Click: take this point out" +msgstr "LPM: usuń ten punkt" + +msgid "Click: a point in the middle of the track on the photo. Enter: apply, Esc: drop" +msgstr "LPM: punkt na środku toru na zdjęciu. Enter: zastosuj, Esc: porzuć" + +msgid "Click: one more point, the curve follows. Ctrl+click: take a point out" +msgstr "LPM: kolejny punkt, łuk idzie za nimi. Ctrl+LPM: usuń punkt" From 415c7484ec4e6c7b9a7447ad585f3eef555ca858 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 19:51:56 +0200 Subject: [PATCH 036/145] feat(editor): laid to the norms, a track laid on from an end gets its transition curves, into the arc, between the arcs of a compound curve and out to the straight A track laid on from a free end in the PKP PLK laying, by a drag out of the end, by clicks from it or laid back onto it, starts from the curvature and the cant the end has. An arc turning the same way as the end is a compound curve: a transition of the norms ramps the curvature and the cant from the one arc into the other. From a straight the arc gets its entry transition, the arc then straight its exit one. Where the way is too short the transitions are cut and the cant lowered to what they ramp, and the readout says so; with no room at all the track is laid as before and the note says why. The curves traced on the orthophoto and the recommended values of a compound curve get the transitions between their arcs too. The free laying stays as it was. --- application/editormode.h | 4 + application/editormodelay.cpp | 53 +++- application/editormoderoute.cpp | 7 +- application/editormodetrack.cpp | 8 + application/editormodetrackshared.h | 28 +- editor/editorAlignment.cpp | 468 +++++++++++++++++++++++++++- editor/editorAlignment.hpp | 51 +++ editor/editorTrack.hpp | 8 + editor/editorTrackShape.cpp | 39 ++- lang/editor_pl.po | 21 ++ 10 files changed, 661 insertions(+), 26 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 2b6bcdfa..2ca12cac 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1482,6 +1482,10 @@ class editor_mode : public application_mode, private editor_track::observer, pri TTrack const *m_join_track{nullptr}; editor_track::point_ref m_join_point; mutable alignment::join_result m_join_last; + // the layout laid on from the end of From with the transition curves of the norms; none in the free laying, or where it doesn't fit + std::optional eased_layout(TTrack const *From, glm::dvec3 const &Point, glm::dvec2 const &Direction, glm::dvec3 const &Target, double const Radius) const; + mutable std::optional m_eased_last; + mutable std::optional>> m_eased_cache; bool track_wheel(double const Offset); // a touchpad sends fractions of a notch: a choice moves a step for each whole notch gathered, and only one in a gesture when // Gesture; a pause, another key or the other way starts over diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index 9023e1ed..f0e7081b 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -408,8 +408,40 @@ void editor_mode::point_drag_finish() m_track_joints.clear(); } +std::optional editor_mode::eased_layout(TTrack const *From, glm::dvec3 const &Point, glm::dvec2 const &Direction, glm::dvec3 const &Target, double const Radius) const +{ + m_eased_last.reset(); + if (false == m_lay.transitions || From == nullptr || From->eType != tt_Normal) + return std::nullopt; + auto const end{editor_track::bend_at(*From, Point)}; + if (false == end.has_value()) + return std::nullopt; + alignment::lay_request request; + request.start.position = Point; + request.start.direction = Direction; + request.start.roll = end->roll; + request.curvature = end->curvature; + request.target = Target; + switch (m_track_layout) + { + case track_layout::straight: request.kind = alignment::lay_kind::straight; break; + case track_layout::arc: request.kind = alignment::lay_kind::arc; break; + case track_layout::straight_arc: request.kind = alignment::lay_kind::straight_arc; break; + default: request.kind = alignment::lay_kind::arc_straight; break; + } + request.radius = Radius; + request.speed = design_speed(From, nullptr); + request.piece_length = m_lay.piece_length; + request.norms = laying_norms(); + if (false == m_eased_cache.has_value() || m_eased_cache->first != request) + m_eased_cache.emplace(request, alignment::lay_on(request)); + m_eased_last = m_eased_cache->second; + return m_eased_last; +} + std::vector editor_mode::extend_pieces() const { + m_eased_last.reset(); std::vector result; auto const &tool{m_extend}; editor_track::snap_target target; @@ -452,7 +484,8 @@ std::vector editor_mode::extend_pieces() const auto radius{m_lay.radius}; if (auto const values{typed_values()}; false == values.empty() && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) && std::abs(values[0]) >= 1.0) radius = std::abs(values[0]); - auto pieces{editor_track::layout_pieces(m_track_layout, tool.point, tool.direction, tool.grade, tool.mouse, radius)}; + auto const eased{eased_layout(tool.track, tool.point, tool.direction, tool.mouse, radius)}; + auto pieces{eased.has_value() && false == eased->pieces.empty() ? eased->pieces : editor_track::layout_pieces(m_track_layout, tool.point, tool.direction, tool.grade, tool.mouse, radius)}; follow_ground(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN(), tool.track); return pieces; } @@ -807,6 +840,7 @@ std::vector editor_mode::lay_pieces(std::vector const { auto const &tool{m_lay}; Length = 0.0; + m_eased_last.reset(); if (Points.size() < 2) return {}; if (Points.size() == 2) @@ -824,8 +858,10 @@ std::vector editor_mode::lay_pieces(std::vector const if (tool.start.track != nullptr) { auto const grade{-grade_of(tool.start.direction)}; - pieces = editor_track::layout_pieces(m_track_layout, Points[0], glm::normalize(leaving), grade, Points[1], tool.radius); - Error = narrowed(pieces, m_track_layout, tool.radius); + auto const eased{eased_layout(tool.start.track, Points[0], glm::normalize(leaving), Points[1], tool.radius)}; + auto const laid{eased.has_value() && false == eased->pieces.empty()}; + pieces = laid ? eased->pieces : editor_track::layout_pieces(m_track_layout, Points[0], glm::normalize(leaving), grade, Points[1], tool.radius); + Error = laid ? narrowed(m_track_layout, tool.radius, eased->radius) : narrowed(pieces, m_track_layout, tool.radius); follow_ground(pieces, Points[0].y, grade, Points[1].y, std::numeric_limits::quiet_NaN(), tool.start.track); } else if (End.track == nullptr) @@ -836,10 +872,12 @@ std::vector editor_mode::lay_pieces(std::vector const } else if (glm::length(plan_of(End.direction)) > 1e-6) { - auto const back{editor_track::layout_pieces(m_track_layout, End.position, glm::normalize(-plan_of(End.direction)), -grade_of(End.direction), Points[0], tool.radius)}; + auto const eased{eased_layout(End.track, End.position, glm::normalize(-plan_of(End.direction)), Points[0], tool.radius)}; + auto const laid{eased.has_value() && false == eased->pieces.empty()}; + auto const back{laid ? eased->pieces : editor_track::layout_pieces(m_track_layout, End.position, glm::normalize(-plan_of(End.direction)), -grade_of(End.direction), Points[0], tool.radius)}; for (auto piece{back.rbegin()}; piece != back.rend(); ++piece) pieces.push_back(reversed(*piece)); - Error = narrowed(pieces, m_track_layout, tool.radius); + Error = laid ? narrowed(m_track_layout, tool.radius, eased->radius) : narrowed(pieces, m_track_layout, tool.radius); follow_ground(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), End.position.y, grade_of(End.direction), End.track); } for (auto const &piece : pieces) @@ -1020,6 +1058,7 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) double length; std::string error; auto const pieces{lay_pieces(tool.points, End, length, error)}; + auto const eased{m_eased_last}; tool.status_time = ImGui::GetTime(); if (pieces.empty()) { @@ -1043,6 +1082,10 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) tool.status = format(STR_P("Laid %zu track, %.1f m, Ctrl+Z takes it back", "Laid %zu tracks, %.1f m, Ctrl+Z takes them back", static_cast(created.size())), created.size(), length); if (false == error.empty()) tool.status += "\n" + error; + if (eased.has_value() && eased->compound) + tool.status += "\n" + format(STR_C("Compound curve: R %.0f m goes on into R %.0f m through a transition curve of %.0f m"), eased->from_radius, eased->radius, eased->entry); + if (eased.has_value() && false == eased->note.empty()) + tool.status += "\n" + eased->note; { std::vector> run; if (tool.start.track != nullptr && tool.start.track->eType == tt_Normal) diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index d4c2419b..187b55b5 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -52,10 +52,15 @@ bool editor_mode::route_active() const void editor_mode::route_recommend(alignment::vertex &Vertex) const { - auto const recommended{alignment::recommend(m_route.design.speed, Vertex.radius, laying_norms())}; + auto sharpest{Vertex.radius}; + if (Vertex.compound) + for (auto const &arc : Vertex.arcs) + sharpest = std::min(sharpest, arc.radius); + auto const recommended{alignment::recommend(m_route.design.speed, sharpest, laying_norms())}; Vertex.cant = recommended.cant; Vertex.transition_in = recommended.transition; Vertex.transition_out = recommended.transition; + alignment::recommend_between(Vertex, m_route.design.speed, laying_norms()); } void editor_mode::route_bind_ends() diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 8e32ea20..1cbc072e 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1782,6 +1782,8 @@ std::string editor_mode::track_readout() const if (piece.radius != 0.0f && (radius == 0.0 || std::abs(piece.radius) < radius)) radius = std::abs(piece.radius); } + if (m_eased_last.has_value() && false == m_eased_last->pieces.empty()) + radius = m_eased_last->radius; editor_track::snap_target target; auto const snapped{extend_snap(target) && target.track != nullptr}; editor_track::branch_link link; @@ -1791,6 +1793,8 @@ std::string editor_mode::track_readout() const if (snapped && pieces.empty()) return format(STR_C("no track fits to the end of %s"), name(target.track).c_str()); auto text{radius > 0.0 ? format("L %.2f m R %.0f m", length, radius) + speed_of(radius, laying_norms()) : format("L %.2f m %s", length, STR_C("straight"))}; + if (auto const eased{eased_of(m_eased_last)}; false == eased.empty()) + text += "\n" + eased; if (auto const vertical{vertical_of(pieces)}; false == vertical.empty()) text += "\n" + vertical; if (snapped) @@ -1817,10 +1821,14 @@ std::string editor_mode::track_readout() const else if (radius == 0.0 || r < radius) radius = r; } + if (m_eased_last.has_value() && false == m_eased_last->pieces.empty()) + radius = m_eased_last->radius; if (radius > 0.0) text = (straight ? format("L %.1f m %s + R %.0f m", lay.preview_length, STR_C("straight"), radius) : format("L %.1f m R %.0f m", lay.preview_length, radius)) + speed_of(radius, laying_norms()); else text = format("L %.1f m %s", lay.preview_length, STR_C("straight")); + if (auto const eased{eased_of(m_eased_last)}; false == eased.empty()) + text += "\n" + eased; if (auto const vertical{vertical_of(lay.preview)}; false == vertical.empty()) text += "\n" + vertical; } diff --git a/application/editormodetrackshared.h b/application/editormodetrackshared.h index a3278716..67f92ef7 100644 --- a/application/editormodetrackshared.h +++ b/application/editormodetrackshared.h @@ -70,6 +70,21 @@ inline std::string speed_of(double const Radius, alignment::limits const &Limits return speed > 0.0 ? format(STR_C(" (up to %.0f km/h)"), speed) : std::string{}; } +// the transition curves and the cant the norms put in a layout, a compound curve where it goes on from an arc turning the same way +inline std::string eased_of(std::optional const &Eased) +{ + if (false == Eased.has_value()) + return {}; + if (Eased->pieces.empty() || (Eased->entry <= 0.0 && Eased->exit <= 0.0)) + return Eased->note; + auto text{Eased->compound ? format(STR_C("compound curve on from R %.0f m, a transition of %.0f m between the arcs"), Eased->from_radius, Eased->entry) : format(STR_C("transition curves %.0f / %.0f m"), Eased->entry, Eased->exit)}; + if (Eased->cant > 0.0) + text += format(STR_C(" cant %.0f mm"), Eased->cant); + if (false == Eased->note.empty()) + text += "\n" + Eased->note; + return text; +} + // the rise of the track from its start to its end, and the grades along it inline std::string vertical_of(std::vector const &Pieces) { @@ -106,17 +121,20 @@ inline std::string lines_of(std::vector const &Texts) return text; } +inline std::string narrowed(editor_track::layout const Layout, double const Radius, double const Arc) +{ + if ((Layout != editor_track::layout::arc_straight && Layout != editor_track::layout::straight_arc) || Arc == 0.0 || std::abs(Arc - Radius) < Radius * 0.01) + return {}; + return format(STR_C("R %.0f m doesn't reach the cursor, the arc goes through it with R %.0f m"), Radius, Arc); +} + inline std::string narrowed(std::vector const &Pieces, editor_track::layout const Layout, double const Radius) { - if (Layout != editor_track::layout::arc_straight && Layout != editor_track::layout::straight_arc) - return {}; double sharpest{0.0}; for (auto const &piece : Pieces) if (auto const radius{piece_plan_radius(piece)}; radius >= 0.5 && (sharpest == 0.0 || radius < sharpest)) sharpest = radius; - if (sharpest == 0.0 || sharpest >= Radius * 0.99) - return {}; - return format(STR_C("R %.0f m doesn't reach the cursor, the arc goes through it with R %.0f m"), Radius, sharpest); + return narrowed(Layout, Radius, sharpest > Radius ? 0.0 : sharpest); } inline std::string describe(editor_track::straight const &Line) diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index dfc86eb2..c8a88a32 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -265,6 +265,20 @@ double transition_length(double const Speed, double const Radius, double const C return length > 0.0 ? ceil_to(length, 5.0) : 0.0; } +double transition_between(double const Speed, double const Curvature0, double const Cant0, double const Curvature1, double const Cant1, limits const &Limits) +{ + auto const cant{std::abs(Cant1 - Cant0)}; + double length{0.0}; + if (cant > 0.0) + { + length = std::max(length, Speed * cant / (3.6 * Limits.cant_rate)); + length = std::max(length, cant / 1000.0 * Limits.ramp_factor * Speed); + } + auto const unbalanced = [&](double const Curvature, double const Cant) { return Speed * Speed / 12.96 * Curvature - kGravity * Cant / Limits.gauge; }; + length = std::max(length, std::abs(unbalanced(Curvature1, Cant1) - unbalanced(Curvature0, Cant0)) * Speed / (3.6 * Limits.jerk)); + return length > 0.0 ? ceil_to(length, 5.0) : 0.0; +} + recommendation recommend(double const Speed, double const Radius, limits const &Limits) { recommendation result; @@ -1124,6 +1138,450 @@ join_result join(join_request const &Request) return chosen; } +namespace +{ + +// elements laid one after another from a point in a direction, put together the way the alignment computes them; the cant along +// them is kept apart, a transition between two canted arcs ramps it from the one to the other +struct element_chain +{ + result shape; + glm::dvec2 position{0.0}; + glm::dvec2 direction{0.0, 1.0}; + std::vector> cants; // chainage, cant +}; + +void chain_push(element_chain &Chain, element Element, double const Cant0, double const Cant1) +{ + Element.chainage = Chain.shape.length; + Chain.shape.length += Element.length; + Chain.shape.elements.push_back(Element); + Chain.cants.push_back({Element.chainage, Cant0}); + Chain.cants.push_back({Chain.shape.length, Cant1}); +} + +void chain_straight(element_chain &Chain, double const Length) +{ + if (Length < 1e-6) + return; + element straight; + straight.kind = element_kind::straight; + straight.origin = Chain.position; + straight.direction = Chain.direction; + straight.length = Length; + Chain.position += Chain.direction * Length; + chain_push(Chain, straight, 0.0, 0.0); +} + +void chain_arc(element_chain &Chain, double const Curvature, double const Length, double const Cant) +{ + if (Length < 1e-6) + return; + if (std::abs(Curvature) < 1e-9) + { + chain_straight(Chain, Length); + return; + } + element arc; + arc.kind = element_kind::arc; + arc.turn = Curvature > 0.0 ? 1 : -1; + arc.radius = 1.0 / std::abs(Curvature); + arc.length = Length; + arc.origin = Chain.position + perpendicular(Chain.direction) / Curvature; + arc.normal = Chain.position - arc.origin; + arc.direction = Chain.direction; + auto const angle{arc.turn * Length / arc.radius}; + Chain.position = arc.origin + turned(arc.normal, angle); + Chain.direction = turned(Chain.direction, angle); + chain_push(Chain, arc, Cant, Cant); +} + +// a curvature going on through 0 turns the curve the other way: it's kept signed, the element turning to the left +void chain_spiral(element_chain &Chain, double const From, double const To, double const Length, double const Cant0, double const Cant1) +{ + if (Length < 1e-6) + return; + int const turn{(From * To < 0.0 || From + To >= 0.0) ? 1 : -1}; + element spiral; + spiral.kind = element_kind::spiral; + spiral.turn = turn; + spiral.curvature_start = From * turn; + spiral.curvature_end = To * turn; + spiral.curvature_peak = std::max(std::abs(From), std::abs(To)); + spiral.radius = spiral.curvature_peak > 1e-12 ? 1.0 / spiral.curvature_peak : 0.0; + spiral.length = Length; + spiral.origin = Chain.position; + spiral.direction = Chain.direction; + spiral.normal = perpendicular(Chain.direction) * static_cast(turn); + auto const local{spiral_point(spiral.curvature_start, spiral.curvature_end, Length, Length)}; + Chain.position = spiral.origin + spiral.direction * local.x + spiral.normal * local.y; + Chain.direction = glm::normalize(spiral.direction * std::cos(local.angle) + spiral.normal * std::sin(local.angle)); + chain_push(Chain, spiral, Cant0, Cant1); +} + +lay_result chain_pieces(element_chain &Chain, lay_request const &Request) +{ + auto &shape{Chain.shape}; + shape.gauge = Request.norms.gauge; + shape.start = Request.start.position; + shape.end = {Chain.position.x, Request.target.y, Chain.position.y}; + shape.profile = {{0.0, Request.start.position.y, 0.0, 0.0}, {shape.length, Request.target.y, 0.0, 0.0}}; + shape.valid = shape.length > 1e-3; + design style; + style.norms = Request.norms; + lay_result result; + result.length = shape.length; + auto const count{std::max(minimum_pieces(shape, style), static_cast(std::ceil(shape.length / std::max(5.0, Request.piece_length) - 1e-9)))}; + result.pieces = pieces(shape, style, count); + auto const cant_at = [&](double const Chainage) { + auto const &cants{Chain.cants}; + for (std::size_t i = 0; i + 1 < cants.size(); i += 2) + if (Chainage <= cants[i + 1][0] + 1e-6) + { + auto const span{cants[i + 1][0] - cants[i][0]}; + auto const t{span > 1e-9 ? std::clamp((Chainage - cants[i][0]) / span, 0.0, 1.0) : 1.0}; + return cants[i][1] + (cants[i + 1][1] - cants[i][1]) * t; + } + return cants.empty() ? 0.0 : cants.back()[1]; + }; + auto const roll = [&](double const Cant) { return static_cast(glm::degrees(std::asin(std::clamp(Cant / Request.norms.gauge, -1.0, 1.0)))); }; + double chainage{0.0}; + for (auto &piece : result.pieces) + { + piece.rolls[0] = roll(cant_at(chainage)); + chainage += geometry::bezier{piece}.plan_length(); + piece.rolls[1] = roll(cant_at(chainage)); + } + if (false == result.pieces.empty()) + result.pieces.front().rolls[0] = static_cast(Request.start.roll); + return result; +} + +// a transition of the length given from the curvature of the start, as the position and the direction it ends with +struct spiral_end +{ + double x{0.0}; + double y{0.0}; + double angle{0.0}; + int turn{1}; + + spiral_end(double const From, double const To, double const Length) + { + turn = (From * To < 0.0 || From + To >= 0.0) ? 1 : -1; + auto const local{spiral_point(From * turn, To * turn, Length, Length)}; + x = local.x; + y = local.y; + angle = local.angle; + } + void apply(glm::dvec2 &Position, glm::dvec2 &Direction) const + { + auto const normal{perpendicular(Direction) * static_cast(turn)}; + Position += Direction * x + normal * y; + Direction = glm::normalize(Direction * std::cos(angle) + normal * std::sin(angle)); + } +}; + +void arc_end(glm::dvec2 &Position, glm::dvec2 &Direction, double const Curvature, double const Length) +{ + auto const centre{Position + perpendicular(Direction) / Curvature}; + auto const angle{Curvature * Length}; + Position = centre + turned(Position - centre, angle); + Direction = turned(Direction, angle); +} + +} // namespace + +std::optional lay_on(lay_request const &Request) +{ + auto const &norms{Request.norms}; + if (glm::length(Request.start.direction) < 1e-9) + return std::nullopt; + glm::dvec2 const start{Request.start.position.x, Request.start.position.z}; + auto const direction{glm::normalize(Request.start.direction)}; + glm::dvec2 const target{Request.target.x, Request.target.z}; + auto const offset{target - start}; + auto const chord{glm::length(offset)}; + auto const lateral{cross(direction, offset)}; + if (chord < 0.5 || glm::dot(offset, direction) <= 0.0) + return std::nullopt; + auto const k0{Request.curvature}; + auto const c0{norms.gauge * std::sin(glm::radians(Request.start.roll))}; + if (Request.kind == lay_kind::straight && std::abs(k0) < 1e-9 && std::abs(c0) < 5.0) + return std::nullopt; + if (Request.kind == lay_kind::arc && std::abs(lateral) < 0.005 * chord + 0.05 && std::abs(k0) < 1e-9 && std::abs(c0) < 5.0) + return std::nullopt; + int const side{lateral > 0.0 ? 1 : -1}; + // the speed the arc allows and its cant; a transition is ramped for the lower speed of its two ends + auto const allowed = [&](double const Curvature) { return std::abs(Curvature) < 1e-9 ? Request.speed : std::max(1.0, std::min(Request.speed, speed_limit(1.0 / std::abs(Curvature), norms))); }; + auto const cant_of = [&](double const Curvature) { return std::abs(Curvature) < 1e-9 ? 0.0 : std::copysign(recommend(allowed(Curvature), 1.0 / std::abs(Curvature), norms).cant, Curvature); }; + auto const slower = [&](double const From, double const To) { return std::min(allowed(From), allowed(To)); }; + auto const between = [&](double const From, double const Fromcant, double const To, double const Tocant) { + if (std::abs(To - From) <= 0.05 * std::max(std::abs(From), std::abs(To)) && std::abs(Tocant - Fromcant) < 5.0) + return 0.0; + return transition_between(slower(From, To), From, Fromcant, To, Tocant, norms); + }; + // the change of the cant a transition of the length ramps at the speed + auto const ramped = [&](double const Length, double const Speed) { return std::min(Length * 1000.0 / (norms.ramp_factor * Speed), norms.cant_rate * 3.6 * Length / Speed); }; + + // the transitions of the norms cut to the share, the cant of the arc lowered to what they can ramp; none where the cant of the + // end can't be ramped to one leaning into the arc + struct layout_plan + { + double out{0.0}; // out of the curvature of the end to the straight, before the straight of the layout + double entry{0.0}; + double exit{0.0}; + double cant{0.0}; + double from{0.0}; // the curvature and the cant the entry starts from + double fromcant{0.0}; + bool lowered{false}; + }; + auto const planned = [&](double const Curvature, bool const Lead, bool const Exit, double const Share) -> std::optional { + layout_plan result; + auto const wanted{cant_of(Curvature)}; + if (Lead) + { + result.out = between(k0, c0, 0.0, 0.0) * Share; + if (std::abs(c0) > ramped(result.out, slower(k0, 0.0)) + 1e-6) + return std::nullopt; + } + else + { + result.from = k0; + result.fromcant = c0; + } + result.entry = between(result.from, result.fromcant, Curvature, wanted) * Share; + result.exit = Exit ? between(Curvature, wanted, 0.0, 0.0) * Share : 0.0; + auto cant{wanted}; + if (result.entry > 0.0) + { + auto const reach{ramped(result.entry, slower(result.from, Curvature))}; + cant = std::clamp(cant, result.fromcant - reach, result.fromcant + reach); + } + else + cant = result.fromcant; + if (Exit) + { + auto const reach{result.exit > 0.0 ? ramped(result.exit, allowed(Curvature)) : 0.0}; + cant = std::clamp(cant, -reach, reach); + } + if (cant * Curvature < -1e-9 || (result.entry > 0.0 && std::abs(cant - result.fromcant) > ramped(result.entry, slower(result.from, Curvature)) + 1e-6) || (result.entry <= 0.0 && std::abs(cant - result.fromcant) > 1e-6)) + return std::nullopt; + result.cant = cant; + result.lowered = std::abs(cant) < std::abs(wanted) - 0.5; + return result; + }; + auto const begin = [&]() { + element_chain chain; + chain.position = start; + chain.direction = direction; + return chain; + }; + auto const made = [&](element_chain &Chain, double const Curvature, layout_plan const &Plan, double const Share) { + auto result{chain_pieces(Chain, Request)}; + result.radius = std::abs(Curvature) > 1e-9 ? 1.0 / std::abs(Curvature) : 0.0; + result.cant = std::abs(Plan.cant); + result.entry = Plan.out > 0.0 ? Plan.out : Plan.entry; + result.exit = Plan.out > 0.0 ? Plan.entry : Plan.exit; + result.from_radius = std::abs(k0) > 1e-9 ? 1.0 / std::abs(k0) : 0.0; + result.compound = k0 * Curvature > 0.0 && Plan.out <= 0.0 && Plan.entry > 0.0; + if (Share < 1.0) + result.note = format(STR_C("The transition curves are cut to %.0f%% of what the norms ask for, there's no more room"), Share * 100.0); + if (Plan.lowered) + result.note += (result.note.empty() ? "" : "\n") + format(STR_C("The cant is lowered to %.0f mm, what the transition curves ramp"), std::abs(Plan.cant)); + return result; + }; + + auto const arc_through = [&](double const Share) -> std::optional { + auto const reference{2.0 * lateral / (chord * chord)}; + auto curvature{reference}; + std::optional plan; + // the transition and the cant follow the curvature found, which moves with them + for (int pass = 0; pass < 4; ++pass) + { + plan = planned(curvature, false, false, Share); + if (false == plan.has_value()) + return std::nullopt; + // over the curvatures turning to the target, from flat to sharp, it goes from inside of the circle of the arc to outside + auto const outside = [&](double const Curvature) { + auto position{start}; + auto heading{direction}; + spiral_end{k0, Curvature, plan->entry}.apply(position, heading); + return glm::distance(target, position + perpendicular(heading) / Curvature) - 1.0 / std::abs(Curvature); + }; + auto low{std::log(1e-6)}; + auto high{std::log(0.1)}; + if (outside(side * std::exp(low)) >= 0.0 || outside(side * std::exp(high)) <= 0.0) + return std::nullopt; + for (int i = 0; i < 60; ++i) + { + auto const middle{(low + high) * 0.5}; + (outside(side * std::exp(middle)) < 0.0 ? low : high) = middle; + } + curvature = side * std::exp((low + high) * 0.5); + } + // a transition too long for the way to the target squeezes the arc after it, a shorter one is tried then + if (std::abs(curvature) > 1.3 * std::abs(reference) + 1e-5) + return std::nullopt; + auto chain{begin()}; + chain_spiral(chain, k0, curvature, plan->entry, c0, plan->cant); + auto const centre{chain.position + perpendicular(chain.direction) / curvature}; + auto const angle{geometry::signed_angle(chain.position - centre, target - centre) * side}; + if (angle <= 1e-4 || angle >= kPi) + return std::nullopt; + chain_arc(chain, curvature, angle / std::abs(curvature), plan->cant); + return made(chain, curvature, *plan, Share); + }; + + auto const arc_straight = [&](double const Share) -> std::optional { + auto const curvature{side / std::max(1.0, Request.radius)}; + auto const plan{planned(curvature, false, true, Share)}; + if (false == plan.has_value()) + return std::nullopt; + auto position{start}; + auto heading{direction}; + spiral_end{k0, curvature, plan->entry}.apply(position, heading); + spiral_end const out{curvature, 0.0, plan->exit}; + // the arc grows until the straight after the transition out of it runs through the target + auto const miss = [&](double const Arc) { + auto at{position}; + auto towards{heading}; + arc_end(at, towards, curvature, Arc); + out.apply(at, towards); + return std::array{cross(towards, target - at) * side, glm::dot(towards, target - at)}; + }; + if (miss(0.0)[0] <= 0.0) + return std::nullopt; + auto const radius{1.0 / std::abs(curvature)}; + auto const step{std::max(0.5, radius * glm::radians(2.0))}; + double low{0.0}; + double high{-1.0}; + for (auto arc{step}; arc < 2.0 * kPi * radius; arc += step) + { + if (miss(arc)[0] <= 0.0) + { + high = arc; + break; + } + low = arc; + } + if (high < 0.0) + return std::nullopt; + for (int i = 0; i < 60; ++i) + { + auto const middle{(low + high) * 0.5}; + (miss(middle)[0] > 0.0 ? low : high) = middle; + } + auto const arc{(low + high) * 0.5}; + auto const straight{miss(arc)[1]}; + if (straight < 0.0) + return std::nullopt; + auto chain{begin()}; + chain_spiral(chain, k0, curvature, plan->entry, c0, plan->cant); + chain_arc(chain, curvature, arc, plan->cant); + chain_spiral(chain, curvature, 0.0, plan->exit, plan->cant, 0.0); + chain_straight(chain, straight); + return made(chain, curvature, *plan, Share); + }; + + auto const straight_arc = [&](double const Share) -> std::optional { + auto const curvature{side / std::max(1.0, Request.radius)}; + auto const plan{planned(curvature, true, false, Share)}; + if (false == plan.has_value()) + return std::nullopt; + auto lead{begin()}; + chain_spiral(lead, k0, 0.0, plan->out, c0, 0.0); + spiral_end const into{0.0, curvature, plan->entry}; + // the end of the arc with no straight before it; the straight moves it along the lead, the arc sideways + auto const end = [&](double const Arc) { + auto at{lead.position}; + auto towards{lead.direction}; + into.apply(at, towards); + arc_end(at, towards, curvature, Arc); + return at; + }; + auto const miss = [&](double const Arc) { return cross(lead.direction, target - end(Arc)) * side; }; + auto const most{kPi / std::abs(curvature)}; + if (miss(0.0) <= 0.0 || miss(most) > 0.0) + return std::nullopt; + double low{0.0}; + double high{most}; + for (int i = 0; i < 60; ++i) + { + auto const middle{(low + high) * 0.5}; + (miss(middle) > 0.0 ? low : high) = middle; + } + auto const arc{(low + high) * 0.5}; + auto const straight{glm::dot(lead.direction, target - end(arc))}; + if (straight < 0.5) + return std::nullopt; + auto chain{lead}; + chain_straight(chain, straight); + chain_spiral(chain, 0.0, curvature, plan->entry, 0.0, plan->cant); + chain_arc(chain, curvature, arc, plan->cant); + return made(chain, curvature, *plan, Share); + }; + + auto const straight = [&](double const Share) -> std::optional { + auto const plan{planned(0.0, true, false, Share)}; + if (false == plan.has_value()) + return std::nullopt; + auto chain{begin()}; + chain_spiral(chain, k0, 0.0, plan->out, c0, 0.0); + auto const along{glm::dot(chain.direction, target - chain.position)}; + if (along < 0.5) + return std::nullopt; + chain_straight(chain, along); + return made(chain, 0.0, *plan, Share); + }; + + auto const first = [](auto const &Layout) -> std::optional { + for (auto const share : {1.0, 0.75, 0.5, 0.35, 0.25}) + if (auto laid{Layout(share)}; laid.has_value()) + return laid; + return std::nullopt; + }; + std::optional laid; + switch (Request.kind) + { + case lay_kind::straight: laid = first(straight); break; + case lay_kind::arc: laid = first(arc_through); break; + case lay_kind::straight_arc: + laid = first(straight_arc); + if (false == laid.has_value()) + laid = first(arc_through); + break; + default: + laid = first(arc_straight); + if (false == laid.has_value()) + laid = first(arc_through); + break; + } + if (laid.has_value()) + return laid; + lay_result refused; + refused.note = std::abs(c0) >= 5.0 ? format(STR_C("No room to ramp the cant of %.0f mm the track ends with, the track is laid without transition curves"), std::abs(c0)) + : std::string{STR_C("No room for the transition curves of the norms, the track is laid without them")}; + return refused; +} + +void recommend_between(vertex &Vertex, double const Speed, limits const &Limits) +{ + if (false == Vertex.compound || Vertex.arcs.empty()) + return; + auto sharpest{std::max(1.0, Vertex.radius)}; + for (auto const &arc : Vertex.arcs) + sharpest = std::min(sharpest, std::max(1.0, arc.radius)); + auto const speed{std::max(1.0, std::min(Speed, speed_limit(sharpest, Limits)))}; + auto const cant_of = [&](double const Radius) { return Vertex.cant * sharpest / Radius; }; + auto previous{std::max(1.0, Vertex.radius)}; + for (auto &arc : Vertex.arcs) + { + auto const radius{std::max(1.0, arc.radius)}; + arc.transition = transition_between(speed, 1.0 / previous, cant_of(previous), 1.0 / radius, cant_of(radius), Limits); + previous = radius; + } +} + bool tangent_intersection(design const &Design, glm::dvec2 &Point) { auto const start{plan_of(Design.start)}; @@ -1460,7 +1918,7 @@ constexpr double kTraceRadiusMax{50000.0}; constexpr double kTraceRobust{0.25}; // the parameters: the logarithms of the radii, then the weights of the shares of all but the first radius, as logarithms too -vertex traced_shape(vertex const &Base, std::vector const &Parameters, int const Radii) +vertex traced_shape(design const &Design, vertex const &Base, std::vector const &Parameters, int const Radii) { auto shape{Base}; auto const radius = [&](int const Index) { return std::exp(std::clamp(Parameters[Index], std::log(kTraceRadiusMin), std::log(kTraceRadiusMax))); }; @@ -1488,6 +1946,8 @@ vertex traced_shape(vertex const &Base, std::vector const &Parameters, i arc.share = weights[i] / total; shape.arcs.push_back(arc); } + if (Design.norms.checked) + recommend_between(shape, Design.speed, Design.norms); return shape; } @@ -1514,7 +1974,7 @@ trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector< return [&, Count](std::vector const &Parameters, std::vector &Residuals) { auto trial{Design}; trial.norms.checked = false; - trial.vertices[Vertex] = traced_shape(base, Parameters, Count); + trial.vertices[Vertex] = traced_shape(Design, base, Parameters, Count); auto const result{compute(trial)}; if (false == result.valid) return false; @@ -1645,10 +2105,12 @@ trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector< take(3); } - auto shape{traced_shape(base, fits[chosen].parameters, chosen)}; + auto shape{traced_shape(Design, base, fits[chosen].parameters, chosen)}; shape.radius = std::max(kTraceRadiusMin, std::round(shape.radius)); for (auto &arc : shape.arcs) arc.radius = std::max(kTraceRadiusMin, std::round(arc.radius)); + if (Design.norms.checked) + recommend_between(shape, Design.speed, Design.norms); auto trial{Design}; trial.vertices[Vertex] = shape; auto const result{compute(trial)}; diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index eaba18a0..a96c00b8 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -9,6 +9,7 @@ http: #pragma once +#include #include #include #include @@ -38,6 +39,8 @@ struct limits double arc_min{20.0}; double arc_min_factor{0.4}; bool checked{true}; + + bool operator==(limits const &) const = default; }; struct compound_arc @@ -92,6 +95,9 @@ struct recommendation }; recommendation recommend(double const Speed, double const Radius, limits const &Limits); double transition_length(double const Speed, double const Radius, double const Cant, limits const &Limits); +// the transition curve from the curvature and the cant of one arc to those of the next one, 0 for a straight; the curvature positive +// turning one way, the cant with the same sign as the curvature it leans into +double transition_between(double const Speed, double const Curvature0, double const Cant0, double const Curvature1, double const Cant1, limits const &Limits); double unbalanced_acceleration(double const Speed, double const Radius, double const Cant, limits const &Limits); enum class element_kind @@ -217,6 +223,8 @@ struct join_end glm::dvec3 position{0.0}; glm::dvec2 direction{0.0, 1.0}; double roll{0.0}; + + bool operator==(join_end const &) const = default; }; enum class join_kind @@ -259,4 +267,47 @@ struct join_result join_result join(join_request const &Request); +enum class lay_kind +{ + arc_straight, + straight_arc, + arc, + straight +}; + +// a track laid on from an end the way the layout lays it, with the transition curves of the norms: from the curvature the end has +// into the arc, between the arcs of a compound curve where both turn the same way, and out of the arc to the straight +struct lay_request +{ + join_end start; // the roll in degrees, as the track goes on + double curvature{0.0}; // of the end as the track goes on, positive turning from x towards z + glm::dvec3 target{0.0}; + lay_kind kind{lay_kind::arc_straight}; + double radius{500.0}; // of the arc, but the arc through the target + double speed{100.0}; + double piece_length{100.0}; + limits norms; + + bool operator==(lay_request const &) const = default; +}; + +// no pieces where the norms find no room for the transitions, the note says why +struct lay_result +{ + std::vector pieces; + std::string note; + double length{0.0}; + double radius{0.0}; + double cant{0.0}; + double entry{0.0}; // the transition out of the curvature of the end + double exit{0.0}; + double from_radius{0.0}; // of the end, 0 for a straight + bool compound{false}; +}; + +std::optional lay_on(lay_request const &Request); + +// the transitions between the arcs of a compound curve which the norms ask for at the speed, the cant of the vertex on its sharpest arc +void recommend_between(vertex &Vertex, double const Speed, limits const &Limits); + } diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 5c8cf9b0..8fc41f2b 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -178,6 +178,14 @@ class editor_track glm::dvec2 direction{0.0}; std::vector pieces; }; + // the curvature and the roll a track ends with at Joint, as it goes on beyond the joint + struct bend + { + double curvature{0.0}; // positive turning from x towards z + double roll{0.0}; + glm::dvec2 direction{0.0}; + }; + static std::optional bend_at(TTrack const &Neighbour, glm::dvec3 const &Joint); static easing ease_out_of(TTrack const &Neighbour, glm::dvec3 const &Joint, double const Grade, double const Speed, alignment::limits const &Limits); static bool eases_room(easing const &In, easing const &Out, glm::dvec3 const &Start, glm::dvec2 const &Startdirection, glm::dvec3 const &End, glm::dvec2 const &Enddirection); static std::vector eased_pieces(easing const &In, std::vector const &Middle, easing const &Out); diff --git a/editor/editorTrackShape.cpp b/editor/editorTrackShape.cpp index ff0a2e01..0c856517 100644 --- a/editor/editorTrackShape.cpp +++ b/editor/editorTrackShape.cpp @@ -640,33 +640,48 @@ std::vector editor_track::layout_pieces(layout const Kind, glm::dv return straight_then_arc(Point, Direction, Grade, offset, radius, side); } -editor_track::easing editor_track::ease_out_of(TTrack const &Neighbour, glm::dvec3 const &Joint, double const Grade, double const Speed, alignment::limits const &Limits) +std::optional editor_track::bend_at(TTrack const &Neighbour, glm::dvec3 const &Joint) { - easing result; for (auto const &path : Neighbour.m_paths) for (auto const atend : {false, true}) { if (glm::distance(path.points[atend ? segment_data::point::end : segment_data::point::start], Joint) > 0.05) continue; + bend result; + auto const sign{atend ? 1.0 : -1.0}; + result.roll = atend ? static_cast(path.rolls[1]) : -static_cast(path.rolls[0]); if (false == has_controls(path)) + { + auto const chord{plan_of(path.points[segment_data::point::end] - path.points[segment_data::point::start])}; + if (glm::length(chord) < 1e-9) + return std::nullopt; + result.direction = glm::normalize(chord) * sign; return result; + } bezier const curve{path}; auto const first{curve.first(atend ? 1.0 : 0.0)}; auto const second{curve.second(atend ? 1.0 : 0.0)}; auto const speed{std::hypot(first.x, first.z)}; if (speed < 1e-9) - return result; - auto const sign{atend ? 1.0 : -1.0}; - auto const curvature{sign * (first.x * second.z - first.z * second.x) / (speed * speed * speed)}; - glm::dvec2 const direction{sign * first.x / speed, sign * first.z / speed}; - auto const roll{atend ? static_cast(path.rolls[1]) : -static_cast(path.rolls[0])}; - result.curvature = curvature; - if (std::abs(curvature) < 1e-4) - return result; - result.length = std::max(20.0, alignment::recommend(Speed, 1.0 / std::abs(curvature), Limits).transition); - lay_transition(result, Joint, direction, Grade, roll); + return std::nullopt; + result.curvature = sign * (first.x * second.z - first.z * second.x) / (speed * speed * speed); + result.direction = {sign * first.x / speed, sign * first.z / speed}; return result; } + return std::nullopt; +} + +editor_track::easing editor_track::ease_out_of(TTrack const &Neighbour, glm::dvec3 const &Joint, double const Grade, double const Speed, alignment::limits const &Limits) +{ + easing result; + auto const end{bend_at(Neighbour, Joint)}; + if (false == end.has_value()) + return result; + result.curvature = end->curvature; + if (std::abs(end->curvature) < 1e-4) + return result; + result.length = std::max(20.0, alignment::recommend(Speed, 1.0 / std::abs(end->curvature), Limits).transition); + lay_transition(result, Joint, end->direction, Grade, end->roll); return result; } diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 77d020a8..5043132f 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10222,3 +10222,24 @@ msgstr "LPM: punkt na środku toru na zdjęciu. Enter: zastosuj, Esc: porzuć" msgid "Click: one more point, the curve follows. Ctrl+click: take a point out" msgstr "LPM: kolejny punkt, łuk idzie za nimi. Ctrl+LPM: usuń punkt" + +msgid "compound curve on from R %.0f m, a transition of %.0f m between the arcs" +msgstr "łuk koszowy dalej od R %.0f m, krzywa przejściowa %.0f m między łukami" + +msgid "Compound curve: R %.0f m goes on into R %.0f m through a transition curve of %.0f m" +msgstr "Łuk koszowy: R %.0f m przechodzi w R %.0f m przez krzywą przejściową %.0f m" + +msgid "transition curves %.0f / %.0f m" +msgstr "krzywe przejściowe %.0f / %.0f m" + +msgid "No room for the transition curves of the norms, the track is laid without them" +msgstr "Brak miejsca na krzywe przejściowe wg norm, tor ułożony bez nich" + +msgid "No room to ramp the cant of %.0f mm the track ends with, the track is laid without transition curves" +msgstr "Brak miejsca, żeby zejść z przechyłką %.0f mm z końca toru, tor ułożony bez krzywych przejściowych" + +msgid "The transition curves are cut to %.0f%% of what the norms ask for, there's no more room" +msgstr "Krzywe przejściowe skrócone do %.0f%% długości wg norm, nie ma na więcej miejsca" + +msgid "The cant is lowered to %.0f mm, what the transition curves ramp" +msgstr "Przechyłka obniżona do %.0f mm, tyle zdążą podnieść krzywe przejściowe" From 9fedbaf6a843bd9e1d2d6b96cb63d70ef0b0747a Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 20:28:34 +0200 Subject: [PATCH 037/145] feat(editor): the design speed the norms lay for is shown by the cursor with the track it comes from, and can be the one set for the laying Laid to the norms, the readout says the design speed of the transition curves and of the cant: that of the track the laying goes on from or joins, or the one set for the laying. A new option takes the set speed also on from a track with its own, the new tracks get it; Ctrl+Shift+wheel steps it while laying and turns the option on. The panel shows the speed the laying under way is designed for. --- application/editormode.h | 7 +++ application/editormodelay.cpp | 72 ++++++++++++++++++++++---- application/editormodeselftest.cpp | 1 + application/editormodeselftestjoin.cpp | 8 +++ application/editormodetrack.cpp | 11 +++- lang/editor_pl.po | 18 +++++++ 6 files changed, 105 insertions(+), 12 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 2ca12cac..d68ef2fd 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1341,6 +1341,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri double piece_length{100.0}; bool copy_selected{false}; editor_track::path_style style; + bool own_speed{false}; // the speed of the style also where the track goes on from one with its own, the new tracks given it std::string status; double status_time{-100.0}; std::string skew; // what Alt turned the straight to @@ -1501,6 +1502,12 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::string track_shaping(bool const Joined) const; void render_layout_combo(); double design_speed(TTrack const *From, TTrack const *To) const; + // the track whose speed the design takes, none where it's the one set for the laying + TTrack const *speed_track(TTrack const *From, TTrack const *To) const; + // the tracks the laying under way goes on from and to + std::pair laying_ends() const; + std::string design_speed_of(TTrack const *From, TTrack const *To) const; + void give_own_speed(std::vector const &Created) const; std::vector join_pieces(TTrack const *From, editor_track::point_ref const &Frompoint, glm::dvec3 const &Start, glm::dvec2 const &Direction, editor_track::snap_target const &End, double &Length, std::string &Error) const; double m_extend_length{25.0}; diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index f0e7081b..dc173cdd 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -490,20 +490,50 @@ std::vector editor_mode::extend_pieces() const return pieces; } +TTrack const *editor_mode::speed_track(TTrack const *From, TTrack const *To) const +{ + if (m_lay.own_speed && m_lay.style.velocity > 0.0) + return nullptr; + TTrack const *result{nullptr}; + for (auto const *track : {From, To}) + if (track != nullptr && editor_track::velocity(*track) > 0.0 && (result == nullptr || editor_track::velocity(*track) < editor_track::velocity(*result))) + result = track; + return result; +} + double editor_mode::design_speed(TTrack const *From, TTrack const *To) const { - double speed{0.0}; - for (auto const *track : {From, To}) + if (auto const *track{speed_track(From, To)}; track != nullptr) + return editor_track::velocity(*track); + return m_lay.style.velocity > 0.0 ? m_lay.style.velocity : 100.0; +} + +std::pair editor_mode::laying_ends() const +{ + if (m_extend.active) { - if (track == nullptr) - continue; - auto const velocity{editor_track::velocity(*track)}; - if (velocity > 0.0) - speed = speed > 0.0 ? std::min(speed, velocity) : velocity; + editor_track::snap_target target; + return {m_extend.track, extend_snap(target) ? target.track : nullptr}; } - if (speed <= 0.0) - speed = m_lay.style.velocity > 0.0 ? m_lay.style.velocity : 100.0; - return speed; + if (m_lay.active && false == m_lay.points.empty()) + return {m_lay.start.track, m_lay.points.size() == 1 ? m_lay.mouse_snap.track : nullptr}; + return {nullptr, nullptr}; +} + +std::string editor_mode::design_speed_of(TTrack const *From, TTrack const *To) const +{ + auto const speed{design_speed(From, To)}; + if (auto const *track{speed_track(From, To)}; track != nullptr) + return format(STR_C("design speed %.0f km/h, that of the track %s"), speed, name_of(*track).c_str()); + return format(STR_C("design speed %.0f km/h, set for the laying"), speed); +} + +void editor_mode::give_own_speed(std::vector const &Created) const +{ + if (false == m_lay.own_speed || m_lay.style.velocity <= 0.0) + return; + for (auto *track : Created) + editor_track::velocity(*track, m_lay.style.velocity); } std::vector editor_mode::join_pieces(TTrack const *From, editor_track::point_ref const &Frompoint, glm::dvec3 const &Start, glm::dvec2 const &Direction, editor_track::snap_target const &End, double &Length, @@ -601,6 +631,7 @@ void editor_mode::finish_extend() std::vector created; for (auto const &piece : pieces) created.push_back(editor_track::create_path(*style, piece)); + give_own_speed(created); editor_track::commit(created); digitize_drawn(created); push_track_snapshot({}, created); @@ -817,6 +848,21 @@ bool editor_mode::track_wheel(double const Offset) return radii[0]; }; first_step_done(io.KeyCtrl ? first_step::wheel : first_step::shape); + if (io.KeyCtrl && io.KeyShift) + { + static double const speeds[]{20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 120.0, 140.0, 160.0, 200.0, 250.0}; + auto const [from, to]{laying_ends()}; + auto const current{design_speed(from, to)}; + int index{0}; + for (int i = 0; i < static_cast(std::size(speeds)); ++i) + if (speeds[i] <= current + 1e-6) + index = i; + if (step < 0 && speeds[index] < current - 1e-6) + ++index; + m_lay.style.velocity = speeds[std::clamp(index + step, 0, static_cast(std::size(speeds)) - 1)]; + m_lay.own_speed = true; + return true; + } if (io.KeyCtrl) { if (false == joined) @@ -1073,6 +1119,7 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) std::vector created; for (auto const &piece : pieces) created.push_back(style != nullptr ? editor_track::create_path(*style, piece) : editor_track::create_path(tool.style, piece)); + give_own_speed(created); editor_track::commit(created); digitize_drawn(created); push_track_snapshot({}, created); @@ -1470,6 +1517,11 @@ void editor_mode::render_lay_ui() value_chips("speeds", tool.style.velocity, {40.0, 60.0, 80.0, 100.0, 120.0, 160.0}, "%.0f"); tool.style.velocity = std::clamp(tool.style.velocity, 0.0, 400.0); ImGui::PopItemWidth(); + ImGui::Checkbox(STR_C("This speed also on from a track with its own"), &tool.own_speed); + item_tooltip(m_norms_free ? "Off: a track laid on from another one, or joined to it, takes the speed of that track.\nOn: the new tracks get the speed above" + : "Off: a track laid on from another one, or joined to it, is designed for the speed of that track and takes it.\nOn: it's designed for the speed above and the new tracks get it.\nWhile laying: Ctrl+Shift+wheel"); + if (auto const [from, to]{laying_ends()}; false == m_norms_free && (from != nullptr || to != nullptr)) + ImGui::TextDisabled("%s", design_speed_of(from, to).c_str()); if (auto *track{selected_track()}; track != nullptr) { std::string reason; diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 146ff6c9..3617b307 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -122,6 +122,7 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c // join [radius] [variant] [transitions 0|1] the free ends at the points joined, as a drag from one to the other // layto from the free end at the first point towards the second, as a drag in the layout // layclicks ... clicks of the lay tool at the points, a free end within 3 m joined +// layspeed the speed set for the laying, and whether it goes also on from a track with its own // joints ... the joints check over the scenery, the issues around the points logged, every issue with free ends without points // route the route design of the path nearest the point, a curve or the whole line // routevertex , routearc , routeshare , routekm diff --git a/application/editormodeselftestjoin.cpp b/application/editormodeselftestjoin.cpp index 46a2f94e..892e301b 100644 --- a/application/editormodeselftestjoin.cpp +++ b/application/editormodeselftestjoin.cpp @@ -130,6 +130,14 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma WriteLog(std::string{"SELFTEST norms: "} + (m_norms_free ? "free" : "plk")); return selftest_result::handled; } + if (command == "layspeed") + { + int own{0}; + words >> m_lay.style.velocity >> own; + m_lay.own_speed = own != 0; + WriteLog(format("SELFTEST layspeed: %.0f km/h, own %d", m_lay.style.velocity, m_lay.own_speed ? 1 : 0)); + return selftest_result::handled; + } if (command == "tracktab") { std::string key; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 1cbc072e..bdb4d216 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1795,6 +1795,8 @@ std::string editor_mode::track_readout() const auto text{radius > 0.0 ? format("L %.2f m R %.0f m", length, radius) + speed_of(radius, laying_norms()) : format("L %.2f m %s", length, STR_C("straight"))}; if (auto const eased{eased_of(m_eased_last)}; false == eased.empty()) text += "\n" + eased; + if (false == m_norms_free) + text += "\n" + design_speed_of(m_extend.track, snapped ? target.track : nullptr); if (auto const vertical{vertical_of(pieces)}; false == vertical.empty()) text += "\n" + vertical; if (snapped) @@ -1829,6 +1831,11 @@ std::string editor_mode::track_readout() const text = format("L %.1f m %s", lay.preview_length, STR_C("straight")); if (auto const eased{eased_of(m_eased_last)}; false == eased.empty()) text += "\n" + eased; + if (false == m_norms_free) + { + auto const [from, to]{laying_ends()}; + text += "\n" + design_speed_of(from, to); + } if (auto const vertical{vertical_of(lay.preview)}; false == vertical.empty()) text += "\n" + vertical; } @@ -1943,7 +1950,7 @@ std::string editor_mode::track_shaping(bool const Joined) const auto text{std::string{STR_C(names[static_cast(m_track_layout)])}}; if (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) text += format(" R %.0f m", m_lay.radius); - return text + "\n" + STR("Ctrl+wheel: radius, Shift+wheel: arc / straight"); + return text + "\n" + STR("Ctrl+wheel: radius, Shift+wheel: arc / straight") + (m_norms_free ? std::string{} : STR(", Ctrl+Shift+wheel: speed")); } auto const &join{m_join_last}; char const *const kinds[]{"", "straight", "arc", "curve", "reverse curve", "free curve"}; @@ -1958,7 +1965,7 @@ std::string editor_mode::track_shaping(bool const Joined) const text += format(" V %.0f km/h", join.speed); if (join.variants > 1) text += format(" %d/%d", (m_join_variant % join.variants + join.variants) % join.variants + 1, join.variants); - return text + "\n" + STR("Ctrl+wheel: radius, Shift+wheel: another shape"); + return text + "\n" + STR("Ctrl+wheel: radius, Shift+wheel: another shape") + (m_norms_free ? std::string{} : STR(", Ctrl+Shift+wheel: speed")); } void editor_mode::draw_empty_scenery_hint() diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 5043132f..5b6cb1e2 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10243,3 +10243,21 @@ msgstr "Krzywe przejściowe skrócone do %.0f%% długości wg norm, nie ma na wi msgid "The cant is lowered to %.0f mm, what the transition curves ramp" msgstr "Przechyłka obniżona do %.0f mm, tyle zdążą podnieść krzywe przejściowe" + +msgid "design speed %.0f km/h, that of the track %s" +msgstr "prędkość projektowa %.0f km/h, jak tor %s" + +msgid "design speed %.0f km/h, set for the laying" +msgstr "prędkość projektowa %.0f km/h, ustawiona do kładzenia" + +msgid ", Ctrl+Shift+wheel: speed" +msgstr ", Ctrl+Shift+kółko: prędkość" + +msgid "This speed also on from a track with its own" +msgstr "Ta prędkość także od toru z własną" + +msgid "Off: a track laid on from another one, or joined to it, takes the speed of that track.\nOn: the new tracks get the speed above" +msgstr "Wyłączone: tor kładziony od innego toru albo dołączany do niego bierze prędkość tamtego toru.\nWłączone: nowe tory dostają prędkość z pola powyżej" + +msgid "Off: a track laid on from another one, or joined to it, is designed for the speed of that track and takes it.\nOn: it's designed for the speed above and the new tracks get it.\nWhile laying: Ctrl+Shift+wheel" +msgstr "Wyłączone: tor kładziony od innego toru albo dołączany do niego jest projektowany na prędkość tamtego toru i ją dostaje.\nWłączone: jest projektowany na prędkość z pola powyżej i nowe tory ją dostają.\nW trakcie kładzenia: Ctrl+Shift+kółko" From f361bc406bfa515be13ba23491ac45142d3a7519 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 01:48:04 +0200 Subject: [PATCH 038/145] feat(editor): the whole-scenery grade fit keeps the tracks a station shares at one height, lets the switches lie on the vertical curves the regulation allows, runs straight through the double slips and leaves the bridges out of the faults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A switch may lie on a concave vertical curve, and on a convex one no sharper than 2000 m (Dz.U. 2026 poz. 1225 § 58 ust. 16): the fit bends over the switches and their shared timbers where the ground does, a curve on a switch is a warning, and only a sharper convex one an error. A sharp crest of a hump keeps its curve off the switch zones. The zone of a crossing reaches only as far as the path crossed has rails, so the middle of a double slip keeps to itself. A line runs through a switch along its main path, and through the quarters of a double slip straight on, so the long lines go through the slips and the connections between them are fitted after. Rails of the lines fitted already nearer than a sleeper is long are taken at their height, as both lie on the same timbers. A short line between two lines whose end grades can't be joined within the norms keeps only the heights of its ends, and the grade breaks at the joints. The norms speed of a line is the one most of its length is laid for, so a few tracks set much faster don't stiffen the curves of all of it. The fault finder leaves out the places over the terrain model on a bridge or a viaduct, the ground falling away under them by the abutments. A formation modifier may be open at its start or end. --- application/editormode.h | 22 +- application/editormodejoints.cpp | 44 ++- application/editormodenmt.cpp | 581 ++++++++++++++++++++++++------ application/editormodeprofile.cpp | 12 +- editor/editorProfile.cpp | 428 +++++++++++++++++++--- editor/editorProfile.hpp | 33 +- editor/editorTrack.cpp | 55 ++- editor/editorTrack.hpp | 11 + lang/editor_pl.po | 3 + scene/heightmapterrain.cpp | 13 +- scene/heightmapterrain.h | 4 + 11 files changed, 1020 insertions(+), 186 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index d68ef2fd..dfcb1324 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -45,6 +45,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include #include #include @@ -349,8 +350,25 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::vector left; // tracks whose grade line is still to be fitted std::vector done; // tracks of the lines fitted already: the lines fitted later end on them and don't change them // the rail tops of the lines fitted already, in squares of the plan: the tracks of a station lie in one plane, a line laid - // near them takes their height for its ground too, the more closely the nearer it is - std::map, std::vector> laid; + // near them takes their height for its ground too, the more closely the nearer it is, and a line crossing one of them + // on its level its height on the crossing + struct laid_point + { + glm::dvec3 position{0.0}; + glm::dvec2 direction{0.0, 1.0}; + double grade{0.0}; + double was{0.0}; // the height before the line was fitted + double rails{0.0}; // the height of the rails of the track + TTrack const *track{nullptr}; + }; + std::map, std::vector> laid; + // the points of all the paths as they were laid, for the crossings + std::map, std::vector> paths; + // ends which meet in the plan unlinked, laid apart in height, that the joining of the ends couldn't join as neither of + // them can be changed alone, as of two switches: each with the other one, which a line ending there takes over + std::map, std::tuple> loose; + // the points of the lines over a bridge or a viaduct, the model falling away under them: no formation is laid there + std::map, std::vector> bridged; std::size_t lines{0}; // grade lines fitted and applied std::size_t refused{0}; // grade lines the checks didn't let apply std::size_t departures{0}; // grade lines parting from the terrain model by much diff --git a/application/editormodejoints.cpp b/application/editormodejoints.cpp index 5b46320e..dff1cf4b 100644 --- a/application/editormodejoints.cpp +++ b/application/editormodejoints.cpp @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editorprojection.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" +#include "editor/editorProfile.hpp" #include "scene/scene.h" #include "scene/scenelayers.h" @@ -45,6 +46,12 @@ double const kModelStep{5.0}; // m between the places of the tracks compared wit std::size_t const kModelRun{200}; // places of a path whose model is asked for at once double const kModelPoll{0.25}; // s between the looks whether the model is in double const kUnfitted{30.0}; // m; a scenery whose tracks lie this far from the model as a whole isn't fitted to it +// a track over the model by more than the depth goes on a bridge or a viaduct the model leaves out where the model falls away by +// the abutments, steeper than the banks, within the reach along the track both ways; as the grade lines fitted to the model +// tell them. By the abutments the track is over the model by less +double const kBridgeDepth{1.0}; // m +double const kAbutment{100.0}; // per mille +double const kBridgeReach{200.0}; // m using kind = joints_tool::joint_issue::kind; @@ -198,8 +205,8 @@ bool joints_tool::joints_model() for (auto i = Run.first; i < Run.second; ++i) { glm::dvec2 const at{state.places[i].position.x, state.places[i].position.z}; - Min = glm::min(Min, at - 2.0); - Max = glm::max(Max, at + 2.0); + Min = glm::min(Min, at - kBridgeReach); + Max = glm::max(Max, at + kBridgeReach); } }; glm::dvec2 low, high; @@ -235,6 +242,37 @@ bool joints_tool::joints_model() if (std::abs(known[known.size() / 2]) > kUnfitted) shift = known[known.size() / 2]; } + // a place on a bridge or a viaduct the model leaves out isn't off it: the route through its track sampled both ways, the + // stretches over the model as on a bridge found along it + std::map, std::vector>> routes; + auto const on_bridge = [&](nmt_place const &Place) { + auto found{routes.find(Place.track)}; + if (found == routes.end()) + { + auto route{editor_track::run_route(*Place.track, kBridgeReach)}; + auto samples{editor_track::sample_route(route, 1.0)}; + std::vector chainages, heights, ground; + for (auto const &sample : samples) + { + chainages.push_back(sample.chainage); + heights.push_back(sample.position.y - route.spans[sample.span].track->rail_height()); + double height; + ground.push_back(m_ground.terrain_model().height(sample.position.x, sample.position.z, height) ? height + m_ground.terrain_model_offset() + shift : std::numeric_limits::quiet_NaN()); + } + auto spans{profile::bridged(chainages, heights, ground, kBridgeDepth, kAbutment)}; + found = routes.emplace(Place.track, std::make_pair(std::move(samples), std::move(spans))).first; + } + auto const &[samples, spans]{found->second}; + auto nearest{std::numeric_limits::max()}; + double at{0.0}; + for (auto const &sample : samples) + if (auto const distance{geometry::plan_distance(sample.position, Place.position)}; distance < nearest) + { + nearest = distance; + at = sample.chainage; + } + return nearest <= 1.0 && std::any_of(spans.begin(), spans.end(), [&](profile::departure const &Span) { return at >= Span.from - kModelStep && at <= Span.to + kModelStep; }); + }; // the place of each path the farthest from the model std::map, std::size_t> worst; for (std::size_t i = 0; i < state.places.size(); ++i) @@ -242,7 +280,7 @@ bool joints_tool::joints_model() if (false == std::isfinite(differences[i])) continue; differences[i] -= shift; - if (std::abs(differences[i]) <= state.model) + if (std::abs(differences[i]) <= state.model || (differences[i] > 0.0 && on_bridge(state.places[i]))) continue; auto const key{std::make_pair(state.places[i].track, state.places[i].path)}; auto const found{worst.find(key)}; diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index 94d5896a..e0adf534 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -30,6 +30,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include namespace { @@ -140,12 +141,101 @@ bool ground_card(char const *Id, char const *Title, char const *Text, bool const return clicked && Enabled; } +// m, the squares of the plan the points of the lines are kept in +double constexpr kPlanCell{50.0}; + // a bridge or a tunnel keeps the model under it as it is bool shapes_ground(TTrack const &Track) { return Track.eEnvironment != e_bridge && Track.eEnvironment != e_tunnel; } +// the name the formation of a track is known by in the terrain, of a track with no name where it lies: two of the same name +// would be one formation +std::string formation_name(TTrack const &Track) +{ + if (false == Track.name().empty() || Track.m_paths.empty()) + return Track.name(); + auto const &points{Track.m_paths.front().points}; + auto const &start{points[segment_data::point::start]}; + auto const &end{points[segment_data::point::end]}; + return format("@%.2f,%.2f>%.2f,%.2f", start.x, start.z, end.x, end.z); +} + +// an end of a path no other path is linked to +struct loose_end +{ + TTrack *track{nullptr}; + int path{0}; + bool atend{false}; + glm::dvec3 point{0.0}; + glm::dvec3 outward{0.0}; // off the path along it +}; + +loose_end end_of(TTrack &Track, int const Path, bool const Atend) +{ + auto const &points{Track.m_paths[Path].points}; + auto const &point{points[Atend ? segment_data::point::end : segment_data::point::start]}; + auto const &control{points[Atend ? segment_data::point::control2 : segment_data::point::control1]}; + auto const &other{points[Atend ? segment_data::point::start : segment_data::point::end]}; + return {&Track, Path, Atend, point, control != glm::dvec3{} ? -control : point - other}; +} + +// ends which meet in the plan facing each other, laid apart in height by hand, so the game doesn't join them; farther apart +// than a track over another can be, they lie on two levels +std::vector> loose_pairs() +{ + double constexpr kLevel{2.0}; // m + auto const linked = [](TTrack const &Track, int const Path, bool const Atend) { + if (Track.eType == tt_Switch && Track.SwitchExtension) + return (Atend ? Track.SwitchExtension->pNexts[Path & 1] : Track.SwitchExtension->pPrevs[Path & 1]) != nullptr; + return (Atend ? Track.trNext : Track.trPrev) != nullptr; + }; + std::map, std::vector> cells; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == rail(*track) || track->Length() < 0.01) + continue; + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + for (auto const atend : {false, true}) + if (false == linked(*track, i, atend)) + { + auto const end{end_of(*track, i, atend)}; + cells[{static_cast(std::floor(end.point.x)), static_cast(std::floor(end.point.z))}].push_back(end); + } + } + std::vector> result; + std::set> taken; + for (auto const &[cell, ends] : cells) + for (auto const &a : ends) + { + if (taken.contains({a.track, a.path, a.atend})) + continue; + loose_end const *match{nullptr}; + for (int dx = -1; dx <= 1 && match == nullptr; ++dx) + for (int dz = -1; dz <= 1 && match == nullptr; ++dz) + { + auto const found{cells.find({cell.first + dx, cell.second + dz})}; + if (found == cells.end()) + continue; + auto const outward{geometry::plan_of(a.outward)}; + for (auto const &b : found->second) + if (b.track != a.track && false == taken.contains({b.track, b.path, b.atend}) && geometry::plan_distance(a.point, b.point) <= geometry::same_point && + std::abs(a.point.y - b.point.y) <= kLevel && glm::dot(outward, geometry::plan_of(b.outward)) < -0.5 * glm::length(outward) * glm::length(geometry::plan_of(b.outward))) + { + match = &b; + break; + } + } + if (match == nullptr) + continue; + taken.insert({a.track, a.path, a.atend}); + taken.insert({match->track, match->path, match->atend}); + result.emplace_back(a, *match); + } + return result; +} + } // namespace std::vector editor_mode::nmt_track_places(double const Step) const @@ -240,6 +330,27 @@ std::vector editor_mode::nmt_track_places(double const S for (std::size_t i = 0; i < result.size(); ++i) if (over[i] != 0) result[i].shapes = false; + // nor where the grade lines went over the model falling away by the abutments of a bridge or a viaduct + double constexpr bridged_near{1.0}; + double constexpr bridged_level{1.0}; + auto const &bridged{m_nmt_state.bridged}; + if (false == bridged.empty()) + for (auto &place : result) + { + if (false == place.shapes) + continue; + auto const cx{static_cast(std::floor(place.position.x / kPlanCell))}; + auto const cz{static_cast(std::floor(place.position.z / kPlanCell))}; + for (int dx = -1; dx <= 1 && place.shapes; ++dx) + for (int dz = -1; dz <= 1 && place.shapes; ++dz) + if (auto const cell{bridged.find({cx + dx, cz + dz})}; cell != bridged.end()) + for (auto const &point : cell->second) + if (glm::length(glm::dvec2{point.x - place.position.x, point.z - place.position.z}) <= bridged_near && std::abs(point.y - place.position.y) <= bridged_level) + { + place.shapes = false; + break; + } + } return result; } @@ -720,6 +831,7 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector const *Only) double constexpr step{5.0}; std::pair previous{nullptr, -1}; bool broken{false}; + std::vector over; for (auto const &place : nmt_track_places(step)) { if (false == wanted(place.track)) @@ -731,6 +843,7 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector const *Only) if (false == place.shapes) { broken = true; + over.push_back(place); continue; } auto &piece{pieces[path]}; @@ -742,21 +855,31 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector const *Only) auto &modifier{modifiers[{place.track, place.path, piece}]}; if (modifier.name.empty()) { - modifier.name = "nmt " + (place.track->name().empty() ? std::string{"track"} : place.track->name()) + (place.track->m_paths.size() > 1 ? " " + std::to_string(place.path) : std::string{}) + (piece > 0 ? " #" + std::to_string(piece) : std::string{}); + modifier.name = "nmt " + formation_name(*place.track) + (place.track->m_paths.size() > 1 ? " " + std::to_string(place.path) : std::string{}) + (piece > 0 ? " #" + std::to_string(piece) : std::string{}); modifier.slope = state.slope; modifier.reach = state.reach; } modifier.points.push_back({{place.position.x, place.position.z}, place.formation, place.half_width}); } + // a formation which ends by a bridge or a tunnel ends square against its abutment, its portal, whichever path goes on over it + auto const walled = [&](heightmap::corridor_point const &End) { + return std::any_of(over.begin(), over.end(), [&](nmt_place const &Place) { + return glm::distance(glm::dvec2{Place.position.x, Place.position.z}, End.position) <= step * 1.5 && std::abs(Place.formation - End.formation) <= 1.0; + }); + }; std::vector laid; for (auto &[path, modifier] : modifiers) if (modifier.points.size() >= 2) + { + modifier.open_start = walled(modifier.points.front()); + modifier.open_end = walled(modifier.points.back()); laid.push_back(std::move(modifier)); + } if (Only != nullptr) { // the formations as they are already aren't led into the terrain again auto const same = [](heightmap::modifier const &A, heightmap::modifier const &B) { - return A.slope == B.slope && A.reach == B.reach && A.rounding == B.rounding && A.points.size() == B.points.size() && + return A.slope == B.slope && A.reach == B.reach && A.rounding == B.rounding && A.open_start == B.open_start && A.open_end == B.open_end && A.points.size() == B.points.size() && std::equal(A.points.begin(), A.points.end(), B.points.begin(), [](heightmap::corridor_point const &P, heightmap::corridor_point const &Q) { return glm::distance(P.position, Q.position) < 1e-6 && std::abs(P.formation - Q.formation) < 1e-6 && std::abs(P.half_width - Q.half_width) < 1e-6; }); @@ -782,6 +905,17 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector const *Only) for (auto const &name : gone) m_streamer.remove_modifier(name); } + if (Only == nullptr) + { + // the formations of the tracks there are no more, or are on bridges now, and of the tracks with no name laid over each + // other under one name before + std::vector gone; + for (auto const &modifier : m_streamer.modifiers()) + if (modifier.name.starts_with("nmt ") && std::none_of(laid.begin(), laid.end(), [&](heightmap::modifier const &Laid) { return Laid.name == modifier.name; })) + gone.push_back(modifier.name); + for (auto const &name : gone) + m_streamer.remove_modifier(name); + } auto const count{laid.size()}; m_streamer.modifiers(std::move(laid)); forget_ground(); @@ -978,6 +1112,8 @@ void editor_mode::nmt_stop() state.left.clear(); state.done.clear(); state.laid.clear(); + state.paths.clear(); + state.loose.clear(); state.stage = nmt_state::automatic::idle; state.pending.clear(); m_profile.open = false; @@ -1469,43 +1605,10 @@ void editor_mode::nmt_lift_scenery(double const Offset) std::size_t editor_mode::nmt_join_ends() { - // ends which meet in the plan, laid apart in height by hand, so the game doesn't join them, are one joint before the grade - // lines run over them; farther apart than a track over another can be, they lie on two levels - double constexpr kLevel{2.0}; // m - struct end - { - TTrack *track; - int path; - bool atend; - glm::dvec3 point; - glm::dvec2 outward; - }; - auto const linked = [](TTrack const &Track, int const Path, bool const Atend) { - if (Track.eType == tt_Switch && Track.SwitchExtension) - return (Atend ? Track.SwitchExtension->pNexts[Path & 1] : Track.SwitchExtension->pPrevs[Path & 1]) != nullptr; - return (Atend ? Track.trNext : Track.trPrev) != nullptr; - }; - std::map, std::vector> cells; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || false == rail(*track) || track->Length() < 0.01) - continue; - for (int i = 0; i < static_cast(track->m_paths.size()); ++i) - for (auto const atend : {false, true}) - { - if (linked(*track, i, atend)) - continue; - auto const &path{track->m_paths[i]}; - auto const &point{path.points[atend ? segment_data::point::end : segment_data::point::start]}; - auto const &control{path.points[atend ? segment_data::point::control2 : segment_data::point::control1]}; - auto const &other{path.points[atend ? segment_data::point::start : segment_data::point::end]}; - auto const outward{geometry::plan_of(control != glm::dvec3{} ? -control : point - other)}; - cells[{static_cast(std::floor(point.x)), static_cast(std::floor(point.z))}].push_back({track, i, atend, point, outward}); - } - } + // the loose ends are one joint before the grade lines run over them; of two switches, which can't be changed alone, the + // lines over them take each other over as they're fitted std::vector> states; std::vector changed; - std::set> taken; auto const remember = [&](TTrack *Track) { if (std::find(changed.begin(), changed.end(), Track) != changed.end()) return; @@ -1513,39 +1616,32 @@ std::size_t editor_mode::nmt_join_ends() changed.push_back(Track); }; std::size_t joined{0}; - for (auto const &[cell, ends] : cells) - for (auto const &a : ends) + m_nmt_state.loose.clear(); + for (auto pair : loose_pairs()) + { + if (pair.first.track->eType != tt_Normal) + std::swap(pair.first, pair.second); + auto const &a{pair.first}; + auto const &b{pair.second}; + if (a.track->eType != tt_Normal) { - if (a.track->eType != tt_Normal || taken.contains({a.track, a.path, a.atend})) - continue; - end const *match{nullptr}; - for (int dx = -1; dx <= 1 && match == nullptr; ++dx) - for (int dz = -1; dz <= 1 && match == nullptr; ++dz) - { - auto const found{cells.find({cell.first + dx, cell.second + dz})}; - if (found == cells.end()) - continue; - for (auto const &b : found->second) - if (b.track != a.track && false == taken.contains({b.track, b.path, b.atend}) && geometry::plan_distance(a.point, b.point) <= geometry::same_point && - std::abs(a.point.y - b.point.y) <= kLevel && glm::dot(a.outward, b.outward) < -0.5 * glm::length(a.outward) * glm::length(b.outward)) - { - match = &b; - break; - } - } - if (match == nullptr || (match->track->eType != tt_Normal && false == editor_track::is_supported(*match->track))) - continue; - auto const height{match->track->eType == tt_Normal ? (a.point.y + match->point.y) * 0.5 : match->point.y}; - for (auto const *side : {&a, match}) - { - taken.insert({side->track, side->path, side->atend}); - if (side->track->eType != tt_Normal) - continue; - remember(side->track); - side->track->m_paths[side->path].points[side->atend ? segment_data::point::end : segment_data::point::start].y = height; - } - ++joined; + // two switches: the lines over them meet as they're fitted + m_nmt_state.loose[{a.track, a.path, a.atend}] = {b.track, b.path, b.atend}; + m_nmt_state.loose[{b.track, b.path, b.atend}] = {a.track, a.path, a.atend}; + continue; } + if (b.track->eType != tt_Normal && false == editor_track::is_supported(*b.track)) + continue; + auto const height{b.track->eType == tt_Normal ? (a.point.y + b.point.y) * 0.5 : b.point.y}; + for (auto const *side : {&a, &b}) + { + if (side->track->eType != tt_Normal) + continue; + remember(side->track); + side->track->m_paths[side->path].points[side->atend ? segment_data::point::end : segment_data::point::start].y = height; + } + ++joined; + } if (changed.empty()) return 0; editor_track::commit(changed); @@ -1621,22 +1717,52 @@ bool editor_mode::nmt_fit_line(TTrack &Track) }; profile.context.start_joined = profile.context.start_joined && joint(false, profile.context.start_elevation); profile.context.end_joined = profile.context.end_joined && joint(true, profile.context.end_elevation); + auto const on_route = [&](TTrack const *Other) { return std::any_of(profile.route.spans.begin(), profile.route.spans.end(), [&](auto const &Span) { return Span.track == Other; }); }; + // an end on a switch which meets a switch fitted already unlinked, laid apart in height, takes it over as if it went on into it + auto const partner_of = [&](editor_track::route_span const &Span, bool const Atend) -> std::optional { + auto const found{m_nmt_state.loose.find({Span.track, Span.path, Atend})}; + if (found == m_nmt_state.loose.end() || on_route(std::get<0>(found->second))) + return std::nullopt; + auto const end{end_of(*std::get<0>(found->second), std::get<1>(found->second), std::get<2>(found->second))}; + if (glm::length(geometry::plan_of(end.outward)) < 1e-6) + return std::nullopt; + return end; + }; + auto const loose_into = [&](editor_track::route_span const &Span, bool const Atend) -> std::optional { + auto const end{partner_of(Span, Atend)}; + return end.has_value() && fitted(end->track) ? end : std::nullopt; + }; + // rising off the end met, into the route at its start and against it at its end + auto const rising = [](loose_end const &End) { return End.outward.y / glm::length(geometry::plan_of(End.outward)); }; + if (false == profile.context.start_joined) + if (auto const end{loose_into(profile.route.spans.front(), false == profile.route.spans.front().forward)}) + { + profile.context.start_joined = true; + profile.context.start_elevation = end->point.y; + profile.context.start_grade = rising(*end); + } + if (false == profile.context.end_joined) + if (auto const end{loose_into(profile.route.spans.back(), profile.route.spans.back().forward)}) + { + profile.context.end_joined = true; + profile.context.end_elevation = end->point.y; + profile.context.end_grade = -rising(*end); + } // the ground of the line: the terrain model on its axis, the top of the ballast with the rails on it, and the rail tops of // the lines fitted already beside it on the same level as the model here, which tell its height the more closely the // nearer they are. What doesn't agree with the line, a bridge over it, the ground under a viaduct, the fit lets go of by // itself - double constexpr kModel{0.25}; // m, the tolerance of the model + double constexpr kModel{0.15}; // m, the tolerance of the model double constexpr kRail{0.02}; // m, of a rail top next to the line, growing by kSpread a metre of the distance double constexpr kSpread{0.01}; double constexpr kLevel{0.5}; // m, a rail top farther from the model or from the line lies on another level + double constexpr kSameLevel{1.0}; // m, tracks authored closer in height lie on one level, farther one over the other double constexpr kReach{50.0}; - double constexpr kSteepest{40.0}; // per mille, the steepest grade the ground may give a line - double constexpr kCell{50.0}; auto const nearest = [&](glm::dvec3 const &Point, double &Distance) { Distance = std::numeric_limits::max(); double height{0.0}; - auto const cx{static_cast(std::floor(Point.x / kCell))}; - auto const cz{static_cast(std::floor(Point.z / kCell))}; + auto const cx{static_cast(std::floor(Point.x / kPlanCell))}; + auto const cz{static_cast(std::floor(Point.z / kPlanCell))}; for (int dx = -1; dx <= 1; ++dx) for (int dz = -1; dz <= 1; ++dz) { @@ -1644,22 +1770,24 @@ bool editor_mode::nmt_fit_line(TTrack &Track) if (cell == m_nmt_state.laid.end()) continue; for (auto const &laid : cell->second) - if (auto const distance{glm::length(glm::dvec2{laid.x - Point.x, laid.z - Point.z})}; distance < Distance) + if (auto const distance{glm::length(glm::dvec2{laid.position.x - Point.x, laid.position.z - Point.z})}; distance < Distance) { Distance = distance; - height = laid.y; + height = laid.position.y; } } return height; }; // an end of the line, or the other path of a switch of it, which leads through the tracks not fitted yet into a line fitted // already, as at a crossover, ties the two where it leaves the line: there the line keeps to the height of the other, on - // along its grade over the tracks between, give or take what these can climb with curves of the least radius at both ends + // along its grade over the tracks between, give or take what the plain tracks among these can climb with curves of the least + // radius. A switch lies in its plane and climbs nothing. Where they can climb more than a little, the curves of the norms of + // the line come first: what it can't keep to, the tracks between take over double constexpr kJoin{0.002}; // m + double constexpr kClimbing{0.05}; // m double constexpr kFarthest{200.0}; profile::ground_fit options; auto const length_of = [](TTrack const &Track, int const Path) { return Track.eType == tt_Switch ? Track.SwitchExtension->Segments[Path]->GetLength() : Track.Segment->GetLength(); }; - auto const on_route = [&](TTrack const *Other) { return std::any_of(profile.route.spans.begin(), profile.route.spans.end(), [&](auto const &Span) { return Span.track == Other; }); }; std::vector exits; exits.push_back({profile.route.spans.front().track, profile.route.spans.front().path, false == profile.route.spans.front().forward}); exits.push_back({profile.route.spans.back().track, profile.route.spans.back().path, profile.route.spans.back().forward}); @@ -1667,45 +1795,113 @@ bool editor_mode::nmt_fit_line(TTrack &Track) if (span.track->eType == tt_Switch && span.track->m_paths.size() >= 2) for (auto const outward : {true, false}) exits.push_back({span.track, 1 - span.path, outward}); + // a switch entered at the far end of a path has the far end of its other path on its plane as high, when as far along it + auto const frog_beside = [](TTrack const &Switch, int const Path) { + if (Switch.m_paths.size() < 2) + return false; + auto const &entered{Switch.m_paths[Path].points}; + auto const &other{Switch.m_paths[1 - Path].points}; + auto const &origin{entered[segment_data::point::start]}; + auto const &control{entered[segment_data::point::control1]}; + auto const along{geometry::plan_of(control != glm::dvec3{} ? control : entered[segment_data::point::end] - origin)}; + if (glm::length(along) < 1e-6) + return false; + auto const ahead = [&](glm::dvec3 const &Point) { return glm::dot(geometry::plan_of(Point - origin), glm::normalize(along)); }; + return std::abs(ahead(entered[segment_data::point::end]) - ahead(other[segment_data::point::end])) <= 0.5; + }; for (auto const &exiting : exits) { auto const &exit{exiting.track->m_paths[exiting.path].points[exiting.forward ? segment_data::point::end : segment_data::point::start]}; - editor_track::route step; - step.spans.push_back(exiting); - double between{0.0}; - for (int hops = 0; hops < 50 && between < kFarthest; ++hops) + auto const bind = [&](double const Height, double const Between, double const Bending) { + std::size_t nearest{0}; + for (std::size_t i = 1; i < profile.samples.size(); ++i) + if (geometry::plan_distance(profile.samples[i].position, exit) < geometry::plan_distance(profile.samples[nearest].position, exit)) + nearest = i; + auto const climbing{Bending * Between / (4.0 * profile.line.norms.radius_min)}; + options.bounds.push_back({profile.samples[nearest].chainage, Height - kJoin - climbing, Height + kJoin + climbing, climbing >= kClimbing}); + }; + // the tracks it leads into, the nearest first, through both paths of a switch met at its start and across the frog of one + // met at the end of a path, onto the end of its other path + struct walk { - double grade; - TTrack *next{nullptr}; - int path{0}; - bool forward{true}; - if (false == editor_track::adjoining_grade(step, true, grade, &next, &path, &forward) || next == nullptr || on_route(next)) - break; - if (fitted(next)) + editor_track::route_span span; + double between{0.0}; // m, over the tracks not fitted yet + double bending{0.0}; // m, of these the plain tracks + }; + std::vector open{{exiting, 0.0, 0.0}}; + std::set> seen; + while (false == open.empty()) + { + auto const nearest{std::min_element(open.begin(), open.end(), [](walk const &A, walk const &B) { return A.between < B.between; })}; + auto const step{*nearest}; + open.erase(nearest); + if (step.between >= kFarthest || false == seen.insert({step.span.track, step.span.path, step.span.forward}).second) + continue; + auto onward{editor_track::continuations(step.span)}; + onward.erase(std::remove_if(onward.begin(), onward.end(), [&](editor_track::continuation const &Next) { return on_route(Next.span.track); }), onward.end()); + // met unlinked, laid apart in height: on as if linked + if (onward.empty()) + if (auto const end{partner_of(step.span, step.span.forward)}) + onward.push_back({{end->track, end->path, false == end->atend}, -rising(*end)}); + for (auto const &[next, grade] : onward) { - std::size_t nearest{0}; - for (std::size_t i = 1; i < profile.samples.size(); ++i) - if (geometry::plan_distance(profile.samples[i].position, exit) < geometry::plan_distance(profile.samples[nearest].position, exit)) - nearest = i; - auto const &points{next->m_paths[path].points}; - auto const height{points[forward ? segment_data::point::start : segment_data::point::end].y - grade * between}; - auto const leeway{kJoin + between * between / (4.0 * profile.line.norms.radius_min)}; - options.bounds.push_back({profile.samples[nearest].chainage, height - leeway, height + leeway}); - break; + auto const &points{next.track->m_paths[next.path].points}; + auto const entry{points[next.forward ? segment_data::point::start : segment_data::point::end].y}; + if (fitted(next.track)) + { + bind(entry - grade * step.between, step.between, step.bending); + continue; + } + auto const length{length_of(*next.track, next.path)}; + open.push_back({next, step.between + length, step.bending + (next.track->eType == tt_Switch ? 0.0 : length)}); + if (false == next.forward && next.track->eType == tt_Switch && frog_beside(*next.track, next.path)) + open.push_back({{next.track, 1 - next.path, true}, step.between, step.bending}); } - step.spans = {{next, path, forward}}; - between += length_of(*next, path); } } + // the lines fitted already close beside it on its level, as authored, are ground it never lets go of, however far they are + // off: side by side the tracks lie on one formation, in one plane, as near as the curves of the norms let them. The nearest + // on each side. Where they run so near that the timbers carry both, at a switch, a crossover, they are at one height, on + // any vertical curve the timbers lie on + double constexpr kNear{8.0}; // m between the axes + double constexpr kHeld{5.0}; // m, the reach of their rail tops + double constexpr kTimbers{2.65}; // m between the axes, about the length of a sleeper + double constexpr kTimbered{0.005}; // m + auto const alongside = [&](editor_track::route_sample const &Sample) { + std::array, 2> sides{std::pair{kNear, nullptr}, std::pair{kNear, nullptr}}; + auto const along{glm::normalize(Sample.direction)}; + auto const cx{static_cast(std::floor(Sample.position.x / kPlanCell))}; + auto const cz{static_cast(std::floor(Sample.position.z / kPlanCell))}; + for (int dx = -1; dx <= 1; ++dx) + for (int dz = -1; dz <= 1; ++dz) + { + auto const cell{m_nmt_state.laid.find({cx + dx, cz + dz})}; + if (cell == m_nmt_state.laid.end()) + continue; + for (auto const &point : cell->second) + { + glm::dvec2 const offset{point.position.x - Sample.position.x, point.position.z - Sample.position.z}; + auto const across{along.x * offset.y - along.y * offset.x}; + if (std::abs(glm::dot(offset, along)) > 1.0 || std::abs(across) >= kNear || std::abs(point.was - Sample.position.y) > kSameLevel) + continue; + auto &side{sides[across > 0.0 ? 0 : 1]}; + if (std::abs(across) < side.first) + side = {std::abs(across), &point}; + } + } + return sides; + }; auto const count{profile.samples.size()}; std::vector sampled(count), model(count, std::numeric_limits::quiet_NaN()); + std::vector plan(count); std::vector chainages, ground, tolerances, reaches; for (std::size_t i = 0; i < count; ++i) { auto const &sample{profile.samples[i]}; auto const *track{sample.span < profile.route.spans.size() ? profile.route.spans[sample.span].track : nullptr}; sampled[i] = sample.chainage; - if (double height; track != nullptr && m_nmt.height(sample.position.x, sample.position.z, height)) + plan[i] = {sample.position.x, sample.position.z}; + if (double height; track != nullptr && shapes_ground(*track) && m_nmt.height(sample.position.x, sample.position.z, height)) model[i] = height + m_nmt_state.offset + track->rail_height(); chainages.push_back(sample.chainage); ground.push_back(model[i]); @@ -1720,11 +1916,108 @@ bool editor_mode::nmt_fit_line(TTrack &Track) tolerances.push_back(std::max(kRail, kSpread * distance)); reaches.push_back(kLevel); } + if (track != nullptr) + for (auto const &[apart, point] : alongside(sample)) + if (point != nullptr) + { + chainages.push_back(sample.chainage); + ground.push_back(point->position.y + point->rails - track->rail_height()); + tolerances.push_back(apart < kTimbers ? kTimbered : std::max(kRail, kSpread * apart)); + reaches.push_back(kHeld); + } } if (std::none_of(ground.begin(), ground.end(), [](double const Height) { return std::isfinite(Height); })) return false; + // a path which the line crosses on its level, as authored, makes a crossing with it: over it the two lie in one plane, the + // grade doesn't break nor does a vertical curve reach; the line crosses a line fitted already at its height, the rail tops + // of the two at one + double constexpr kCrossing{0.75}; // m, the nearest the samples of the two come, a metre apart + double constexpr kCrossed{0.035}; // the sine of the least angle the axes cross at + double constexpr kGauge{1.435}; // m + struct crossing + { + double chainage; + double height; + double sine; + double distance; + TTrack const *track; + glm::dvec2 at; // where the axes meet + glm::dvec2 along; // the way of the line there + }; + auto const crossings_of = [&](std::map, std::vector> const &Points) { + std::vector found; + auto const across = [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x * B.y - A.y * B.x; }; + for (auto const &sample : profile.samples) + { + auto const *track{profile.route.spans[sample.span].track}; + auto const cx{static_cast(std::floor(sample.position.x / kPlanCell))}; + auto const cz{static_cast(std::floor(sample.position.z / kPlanCell))}; + auto const along{glm::normalize(sample.direction)}; + for (int dx = -1; dx <= 1; ++dx) + for (int dz = -1; dz <= 1; ++dz) + { + auto const cell{Points.find({cx + dx, cz + dz})}; + if (cell == Points.end()) + continue; + for (auto const &point : cell->second) + { + glm::dvec2 const offset{point.position.x - sample.position.x, point.position.z - sample.position.z}; + auto const other{glm::normalize(point.direction)}; + auto const sine{across(along, other)}; + if (point.track == track || glm::length(offset) > kCrossing || std::abs(point.was - sample.position.y) > kSameLevel || std::abs(sine) < kCrossed) + continue; + // where the axes meet + auto const here{across(offset, other) / sine}; + auto const there{across(offset, along) / sine}; + found.push_back({sample.chainage + here, point.position.y + there * point.grade + point.rails - track->rail_height(), std::abs(sine), glm::length(offset), point.track, glm::dvec2{sample.position.x, sample.position.z} + along * here, along}); + } + } + } + // the nearest samples of each crossing tell it + std::sort(found.begin(), found.end(), [](crossing const &A, crossing const &B) { return std::tie(A.track, A.chainage) < std::tie(B.track, B.chainage); }); + std::vector result; + for (auto const &one : found) + if (one.chainage < 0.0 || one.chainage > profile.route.length || on_route(one.track)) + continue; + else if (result.empty() || result.back().track != one.track || one.chainage - result.back().chainage > 5.0) + result.push_back(one); + else if (one.distance < result.back().distance) + result.back() = one; + return result; + }; + // no farther than the path crossed has rails: the middle of a double slip, a few metres crossed at a low angle, keeps to + // itself, the switches about it have zones of their own + for (auto const &found : crossings_of(m_nmt_state.paths)) + { + auto const half{std::min(30.0, kGauge / found.sine + 1.5)}; + auto first{std::numeric_limits::max()}; + auto last{std::numeric_limits::lowest()}; + for (auto const &path : found.track->m_paths) + for (auto const end : {segment_data::point::start, segment_data::point::end}) + { + auto const along{glm::dot(glm::dvec2{path.points[end].x, path.points[end].z} - found.at, found.along)}; + first = std::min(first, along); + last = std::max(last, along); + } + auto const from{std::max(-half, first - 1.5)}; + auto const to{std::min(half, last + 1.5)}; + if (from < to) + profile.context.switches.push_back({found.chainage + from, found.chainage + to, found.track->name()}); + } + // the height of a crossing of a line fitted already is ground the line keeps to as closely as to the switches beside, though + // not past the curves of the norms: where an end fitted another way is too near, as near as they let it + double constexpr kCrossingHeight{0.005}; // m + for (auto const &found : crossings_of(m_nmt_state.laid)) + { + auto const at{static_cast(std::distance(chainages.begin(), std::upper_bound(chainages.begin(), chainages.end(), found.chainage)))}; + chainages.insert(chainages.begin() + at, found.chainage); + ground.insert(ground.begin() + at, found.height); + tolerances.insert(tolerances.begin() + at, kCrossingHeight); + reaches.insert(reaches.begin() + at, kLevel); + } // the switches of the tracks beside, the ones of the crossovers above all, lie on bearers common with this track or close // to it: no break of the grade on them either, so the tracks of the station stay in one plane + double constexpr kShared{7.0}; // m, about the spacing of the tracks for (auto *other : simulation::Paths.sequence()) { if (other == nullptr || other->eType != tt_Switch || other->m_paths.empty() || false == rail(*other)) @@ -1737,7 +2030,7 @@ bool editor_mode::nmt_fit_line(TTrack &Track) for (auto const end : {segment_data::point::start, segment_data::point::end}) { auto const &point{points[end]}; - double best{25.0}; + double best{kShared}; double chainage{0.0}; for (auto const &sample : profile.samples) if (auto const distance{glm::length(glm::dvec2{sample.position.x - point.x, sample.position.z - point.z})}; distance < best) @@ -1745,7 +2038,7 @@ bool editor_mode::nmt_fit_line(TTrack &Track) best = distance; chainage = sample.chainage; } - if (best < 25.0) + if (best < kShared) { from = std::min(from, chainage); to = std::max(to, chainage); @@ -1758,13 +2051,16 @@ bool editor_mode::nmt_fit_line(TTrack &Track) auto settings{profile.line}; settings.join_start = true; settings.join_end = true; - settings.speed = 0.0; - for (auto const &span : profile.route.spans) - settings.speed = std::max(settings.speed, editor_track::velocity(*span.track)); - if (settings.speed <= 0.0) - settings.speed = 100.0; + settings.speed = editor_track::route_speed(profile.route); + // the grades as steep as the ground has them; the vertical curves of the norms, sharper only on a hump of a marshalling yard options.tolerance = kModel; - options.grade_max = kSteepest; + options.grade_max = std::numeric_limits::max(); + options.follow_length = 60.0; + options.follow_height = 4.0; + options.follow_steepest = 100.0; + options.follow_tolerance = 0.05; + options.humps = profile::humps(chainages, ground, profile.context, kModel, tolerances); + options.hump_radius = 300.0; auto line{profile::fit_ground(chainages, ground, profile.context, settings, options, tolerances, reaches)}; if (line.points.size() < 2) return false; @@ -1772,6 +2068,7 @@ bool editor_mode::nmt_fit_line(TTrack &Track) profile.selected = -1; profile_check(); auto const fitted_line{profile.line}; + auto const authored{profile.samples}; profile_apply(); auto const applied{profile.error.empty()}; // where the line parts from the model: under it a gap, a bridge or a viaduct, over it a wall or a structure above the track @@ -1823,13 +2120,37 @@ bool editor_mode::nmt_fit_line(TTrack &Track) } if (applied) for (auto const &sample : profile.samples) - m_nmt_state.laid[{static_cast(std::floor(sample.position.x / kCell)), static_cast(std::floor(sample.position.z / kCell))}].push_back(sample.position); + { + auto const *track{sample.span < profile.route.spans.size() ? profile.route.spans[sample.span].track : nullptr}; + m_nmt_state.laid[{static_cast(std::floor(sample.position.x / kPlanCell)), static_cast(std::floor(sample.position.z / kPlanCell))}].push_back( + {sample.position, sample.direction, sample.grade, editor_track::sampled_elevation(authored, sample.chainage), track != nullptr ? static_cast(track->rail_height()) : 0.0, track}); + } + // where the model falls away under the line by the abutments, the line goes over on a bridge or a viaduct: the formation + // is laid up to it, the ground under it stays as the model has it + double constexpr kBridged{1.0}; // m + double constexpr kAbutment{100.0}; // per mille + std::vector heights(count); + for (std::size_t i = 0; i < count; ++i) + heights[i] = profile::elevation(fitted_line, sampled[i]); + auto const spans{applied ? profile::bridged(sampled, heights, model, kBridged, kAbutment) : std::vector{}}; + for (auto const &stretch : spans) + for (std::size_t i = 0; i < count; ++i) + if (sampled[i] >= stretch.from && sampled[i] <= stretch.to) + m_nmt_state.bridged[{static_cast(std::floor(plan[i].x / kPlanCell)), static_cast(std::floor(plan[i].y / kPlanCell))}].push_back({plan[i].x, heights[i], plan[i].y}); // the line in the log: how much of it lies on the model, where it parts from it, its grades, why it wasn't applied, its // course in the plan std::string text{format("Editor: NMT line %s: %s .. %s, %.0f m, on the model within %.2f m over %.1f%%, parts from it", applied ? "applied" : "refused", from->name().c_str(), to->name().c_str(), profile.route.length, kModel, 100.0 * static_cast(agreeing) / static_cast(std::max(1, modelled)))}; for (auto const &stretch : parted) text += format(" %s %.0f..%.0f %.1f m", stretch.depth < 0.0 ? "under" : "over", stretch.from, stretch.to, std::abs(stretch.depth)); + if (false == spans.empty()) + { + text += ", bridged"; + for (auto const &stretch : spans) + text += format(" %.0f..%.0f", stretch.from, stretch.to); + } + for (auto const &hump : options.humps) + text += format(", a hump %.0f..%.0f, its crest at %.0f %.1f m high", hump.from, hump.to, hump.crest, hump.height); text += ", grades"; for (std::size_t i = 1; i < fitted_line.points.size(); ++i) { @@ -1908,12 +2229,50 @@ void editor_mode::nmt_auto_step() state.split = 0; state.done.clear(); state.laid.clear(); + state.bridged.clear(); + // every path sampled as laid, for the crossings of the lines + state.paths.clear(); + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && rail(*track)) + for (auto const &path : track->m_paths) + { + geometry::bezier const curve{path}; + auto const chord{glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end])}; + auto const count{std::max(1, static_cast(std::ceil(chord * 1.1)))}; + for (int i = 0; i <= count; ++i) + { + auto const t{static_cast(i) / count}; + auto const at{curve.point(t)}; + auto const tangent{curve.first(t)}; + auto const run{glm::length(glm::dvec2{tangent.x, tangent.z})}; + if (run > 1e-9) + state.paths[{static_cast(std::floor(at.x / kPlanCell)), static_cast(std::floor(at.z / kPlanCell))}].push_back( + {at, glm::dvec2{tangent.x, tangent.z} / run, tangent.y / run, at.y, static_cast(track->rail_height()), track}); + } + } state.stage = nmt_state::automatic::fitting; return; } - // a few lines a frame + // a few lines a frame: the longest track next to the lines fitted already, so that a line meets the ones it leads into as + // it's fitted, not when the lines between them come; the longest of all where none is next to them for (int line = 0; line < 2 && false == state.left.empty(); ++line) { + std::unordered_set const fitted(state.done.begin(), state.done.end()); + auto const touches = [&](TTrack *Track) { + for (int path = 0; path < static_cast(Track->m_paths.size()); ++path) + for (auto const forward : {true, false}) + { + auto const onward{editor_track::continuations({Track, path, forward})}; + if (std::any_of(onward.begin(), onward.end(), [&](editor_track::continuation const &Next) { return fitted.contains(Next.span.track); })) + return true; + if (auto const loose{state.loose.find({Track, path, forward})}; loose != state.loose.end() && fitted.contains(std::get<0>(loose->second))) + return true; + } + return false; + }; + auto const next{std::find_if(state.left.begin(), state.left.end(), touches)}; + if (next != state.left.end()) + std::rotate(state.left.begin(), next, next + 1); auto *track{state.left.front()}; auto const name{track->name()}; auto const known{simulation::Paths.sequence().size()}; @@ -1959,6 +2318,8 @@ void editor_mode::nmt_auto_step() m_profile.open = false; state.done.clear(); state.laid.clear(); + state.paths.clear(); + state.loose.clear(); state.stage = nmt_state::automatic::idle; WriteLog("Editor: NMT - " + state.status, logtype::generic); nmt_make(nmt_keys_along_tracks(), true); diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 24e495f6..d7bbab48 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -82,14 +82,6 @@ double nice_step(double const Approximate) return power * 10.0; } -double route_speed(editor_track::route const &Route) -{ - double speed{-1.0}; - for (auto const &span : Route.spans) - speed = std::max(speed, editor_track::velocity(*span.track)); - return speed > 0.0 ? speed : 100.0; -} - std::string const kProfileMark{"//$p"}; profile::line reversed_line(profile::line Line, double const Length) @@ -915,7 +907,7 @@ void editor_mode::profile_recognize() auto const norms{state.line.norms}; state.line = profile::recognize(chainages, elevations, grades, {}); state.line.norms = norms; - state.line.speed = route_speed(state.route); + state.line.speed = editor_track::route_speed(state.route); state.selected = -1; profile_check(); state.status = format(STR_P("Grade line recognized from the track: %zu point, largest departure %.3f m", "Grade line recognized from the track: %zu points, largest departure %.3f m", state.line.points.size()), state.line.points.size(), state.departure); @@ -933,7 +925,7 @@ void editor_mode::profile_fit_ground() return; } auto settings{state.line}; - settings.speed = route_speed(state.route); + settings.speed = editor_track::route_speed(state.route); // the grade line is the rail top: it's fitted so the formation under it, not the rails, follows the ground auto ground{state.terrain}; for (std::size_t i = 0; i < ground.size() && i < state.beds.size(); ++i) diff --git a/editor/editorProfile.cpp b/editor/editorProfile.cpp index b838d13d..9bbabf9b 100644 --- a/editor/editorProfile.cpp +++ b/editor/editorProfile.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include @@ -24,6 +25,9 @@ namespace profile namespace { +// a switch, and the timbers it shares, may lie on a vertical curve: a concave one, or a convex one no sharper than this +double constexpr switch_crest{2000.0}; // m + double polyline(line const &Line, double const Chainage) { auto const &points{Line.points}; @@ -256,9 +260,17 @@ std::vector check(line const &Line, context const &Context) add(true, point, points[c.point].chainage, format(STR_C("Point %zu: the vertical curve reaches past the end of the route"), c.point + 1)); if (i + 1 < list.size() && c.end > list[i + 1].start + 1e-6) add(true, point, c.end, format(STR_C("Vertical curves of points %zu and %zu overlap by %.2f m"), c.point + 1, list[i + 1].point + 1, c.end - list[i + 1].start)); + // a switch may lie on a concave curve, and on a convex one no sharper than switch_crest, though better off them; within + // what the fit rounds the radius to its room by for (auto const &zone : Context.switches) if (overlaps(c.start, c.end, zone)) - add(true, point, points[c.point].chainage, format(STR_C("Point %zu: the vertical curve lies on switch %s"), c.point + 1, zone.name.c_str())); + { + if (c.change < 0.0 && points[c.point].radius < switch_crest * 0.99) + add(true, point, points[c.point].chainage, format(STR_C("Point %zu: the convex vertical curve on switch %s is sharper than %.0f m"), c.point + 1, zone.name.c_str(), switch_crest)); + else + add(false, point, points[c.point].chainage, format(STR_C("Point %zu: the vertical curve lies on switch %s"), c.point + 1, zone.name.c_str())); + break; + } } if (norms.element_min > 0.0) { @@ -499,6 +511,117 @@ line recognize(std::vector const &Chainages, std::vector const & return result; } +std::vector humps(std::vector const &Chainages, std::vector const &Ground, context const &Context, double const Tolerance, std::vector const &Tolerances) +{ + std::vector result; + auto const count{std::min(Chainages.size(), Ground.size())}; + if (count < 2) + return result; + // the ground every few metres, the mean of the model about each, past its noise + double constexpr spacing{5.0}; + auto const origin{Chainages.front()}; + auto const end{Chainages[count - 1]}; + auto const cells{static_cast(std::lround((end - origin) / spacing)) + 1}; + std::vector level(cells, 0.0); + std::vector taken(cells, 0); + for (std::size_t i = 0; i < count; ++i) + { + if (false == std::isfinite(Ground[i]) || (i < Tolerances.size() && Tolerances[i] > 0.0 && Tolerances[i] < Tolerance)) + continue; + auto const k{std::clamp(std::lround((Chainages[i] - origin) / spacing), 0, cells - 1)}; + level[k] += Ground[i]; + ++taken[k]; + } + for (std::ptrdiff_t k = 0; k < cells; ++k) + level[k] = taken[k] > 0 ? level[k] / taken[k] : std::numeric_limits::quiet_NaN(); + auto const at = [&](std::ptrdiff_t const K) { return K >= 0 && K < cells ? level[K] : std::numeric_limits::quiet_NaN(); }; + auto const cells_of = [](double const Distance) { return static_cast(std::lround(Distance / spacing)); }; + // the crest is the highest of the ground about it, the feet where the ground has fallen nearly as far as it falls within + // the reach: some metres lower than the crest, not as many as of a hill or a bridge + double constexpr around{60.0}; // m + double constexpr farthest{300.0}; // m + double constexpr lowest{1.0}; // m + double constexpr highest{6.0}; // m + // the grades just past the rounding of the crest fall away from it both ways, one of them more steeply than lines and + // stations are laid, the two together a change of grade over some 50 m which no curve of 1250 m or more makes + double constexpr inner{10.0}; // m + double constexpr outer{40.0}; // m + double constexpr falling{5.0}; // per mille + double constexpr steep{25.0}; // per mille + double constexpr sharp{40.0}; // per mille + // nowhere about the crest as steep as the banks of a bridge, the walls of a cutting + double constexpr wall{80.0}; // per mille + // no switch on the crest; past it the fan, switches of their own length, not the shared timbers of the ones beside + double constexpr clear{15.0}; // m + double constexpr fan{150.0}; // m + double constexpr body{15.0}; // m + std::size_t constexpr fanning{2}; + auto const crest_of = [&](std::ptrdiff_t const C) -> std::optional { + auto const top{at(C)}; + if (false == std::isfinite(top)) + return std::nullopt; + for (auto k = C - cells_of(around); k <= C + cells_of(around); ++k) + if (auto const other{at(k)}; std::isfinite(other) && (other > top || (other == top && k < C))) + return std::nullopt; + hump found; + found.crest = origin + spacing * static_cast(C); + for (auto const &zone : Context.switches) + if (zone.from < found.crest + clear && zone.to > found.crest - clear) + return std::nullopt; + double grades{0.0}; + double steepest{0.0}; + bool fanned{false}; + for (int const side : {-1, 1}) + { + auto low{top}; + std::ptrdiff_t seen{0}; + auto const reach{cells_of(farthest)}; + for (std::ptrdiff_t d = 1; d <= reach; ++d) + if (auto const height{at(C + side * d)}; std::isfinite(height)) + { + low = std::min(low, height); + ++seen; + } + if (seen < cells_of(around) || top - low < lowest || top - low > highest) + return std::nullopt; + found.height = std::max(found.height, top - low); + auto foot{reach}; + for (std::ptrdiff_t d = 1; d <= reach; ++d) + if (auto const height{at(C + side * d)}; std::isfinite(height) && top - height >= 0.9 * (top - low)) + { + foot = d; + break; + } + // and on past the foot as far as the curve into the grade beyond + (side < 0 ? found.from : found.to) = std::clamp(found.crest + side * spacing * static_cast(foot + cells_of(25.0)), origin, end); + auto const off{at(C + side * cells_of(inner))}; + auto const on{at(C + side * cells_of(outer))}; + if (false == std::isfinite(off) || false == std::isfinite(on)) + return std::nullopt; + auto const grade{(off - on) / (outer - inner) * 1000.0}; + if (grade < falling) + return std::nullopt; + grades += grade; + steepest = std::max(steepest, grade); + for (std::ptrdiff_t d = 0; d + 2 <= cells_of(around); ++d) + if (auto const a{at(C + side * d)}, b{at(C + side * (d + 2))}; std::isfinite(a) && std::isfinite(b) && std::abs(a - b) / (2.0 * spacing) * 1000.0 > wall) + return std::nullopt; + std::size_t past{0}; + for (auto const &zone : Context.switches) + if (auto const distance{side > 0 ? zone.from - found.crest : found.crest - zone.to}; zone.to - zone.from >= body && distance >= clear && distance <= fan) + ++past; + fanned = fanned || past >= fanning; + } + if (steepest < steep || grades < sharp || false == fanned) + return std::nullopt; + return found; + }; + for (std::ptrdiff_t c = 0; c < cells; ++c) + if (auto const found{crest_of(c)}; found && (result.empty() || found->crest > result.back().to)) + result.push_back(*found); + return result; +} + line fit_ground(std::vector const &Chainages, std::vector const &Ground, context const &Context, line const &Settings, ground_fit const &Options, std::vector const &Tolerances, std::vector const &Reaches) { @@ -527,27 +650,60 @@ line fit_ground(std::vector const &Chainages, std::vector const part[i] = at - static_cast(cell[i]); } auto const node = [&](std::size_t const K) { return origin + step * static_cast(K); }; - // a change of grade at a node is the curve of the required radius around it, which keeps off the switches: none on a - // switch, and beside one no more than a curve reaching up to it + // a change of grade at a node is the curve of the required radius around it. On a hump the least radius is the one of the + // hump, though a convex curve over a switch is no sharper than switch_crest auto const required{std::ceil(required_radius(result) / 10.0) * 10.0}; + auto const on_hump = [&](double const Chainage) { return std::any_of(Options.humps.begin(), Options.humps.end(), [&](hump const &Hump) { return Chainage >= Hump.from && Chainage <= Hump.to; }); }; + auto const least = [&](double const Chainage) { return Options.hump_radius > 0.0 && on_hump(Chainage) ? std::min(required, std::ceil(Options.hump_radius / 10.0) * 10.0) : required; }; + auto const on_switch = [&](double const From, double const To) { return std::any_of(Context.switches.begin(), Context.switches.end(), [&](auto const &Zone) { return Zone.to > From && Zone.from < To; }); }; auto const sharpest{step / (required * 1.001)}; - std::vector bend_max(nodes, sharpest); - for (std::size_t k = 1; k + 1 < nodes; ++k) - for (auto const &zone : Context.switches) - bend_max[k] = std::min(bend_max[k], std::max(0.0, 2.0 * std::max(zone.from - node(k), node(k) - zone.to) / (required * 1.001))); + std::vector sag_max(nodes), crest_max(nodes); + for (std::size_t k = 0; k < nodes; ++k) + { + auto const radius{least(node(k))}; + sag_max[k] = crest_max[k] = step / (radius * 1.001); + // a sharper one keeps its curve off the switches + if (radius < switch_crest) + for (auto const &zone : Context.switches) + crest_max[k] = std::min(crest_max[k], std::max(step / (switch_crest * 1.001), 2.0 * std::max(zone.from - node(k), node(k) - zone.to) / (radius * 1.001))); + } struct held { std::size_t cell; double part; double low; double high; + bool yielding; }; std::vector bounds; for (auto const &bound : Options.bounds) { auto const at{std::clamp((bound.chainage - origin) / step, 0.0, static_cast(nodes - 1))}; auto const cell{std::min(nodes - 2, static_cast(at))}; - bounds.push_back({cell, at - static_cast(cell), std::min(bound.low, bound.high), std::max(bound.low, bound.high)}); + bounds.push_back({cell, at - static_cast(cell), std::min(bound.low, bound.high), std::max(bound.low, bound.high), bound.yielding}); + } + // the bounds which contradict each other about one place, as of two crossovers into lines fitted apart in height, hold as far + // as both can: the line keeps between them. Held both, it would tear itself apart over them + std::sort(bounds.begin(), bounds.end(), [](held const &A, held const &B) { return A.cell + A.part < B.cell + B.part; }); + for (std::size_t first = 0; first < bounds.size();) + { + auto last{first + 1}; + while (last < bounds.size() && bounds[last].cell + bounds[last].part - bounds[first].cell - bounds[first].part <= 1.0) + ++last; + auto low{-std::numeric_limits::max()}; + auto high{std::numeric_limits::max()}; + for (auto i = first; i < last; ++i) + { + low = std::max(low, bounds[i].low); + high = std::min(high, bounds[i].high); + } + if (low > high) + for (auto i = first; i < last; ++i) + { + bounds[i].low = high; + bounds[i].high = low; + } + first = last; } bool const start_fixed{Settings.join_start && Context.start_joined}; bool const end_fixed{Settings.join_end && Context.end_joined}; @@ -557,14 +713,24 @@ line fit_ground(std::vector const &Chainages, std::vector const // what parts from the line, a bridge, a viaduct, a wall, a track on another level, is gone past rather than followed. The // bends are no sharper than the radius of the norms and the grades no steeper than the limit auto const tolerance{std::max(0.01, Options.tolerance)}; - auto const own = [&](std::size_t const I) { return I < Tolerances.size() && Tolerances[I] > 0.0 ? std::max(0.005, Tolerances[I]) : tolerance; }; - auto const reach = [&](std::size_t const I) { return I < Reaches.size() && Reaches[I] > 0.0 ? std::max(0.005, Reaches[I]) : own(I); }; + auto const told = [&](std::size_t const I) { return I < Tolerances.size() && Tolerances[I] > 0.0 ? std::max(0.005, Tolerances[I]) : tolerance; }; + // the ground the line follows however far it parts, and as closely as asked, where it parted longer than a structure would + std::vector followed(count, 0.0); + auto const close_to{Options.follow_tolerance > 0.0 ? std::max(0.005, Options.follow_tolerance) : tolerance}; + auto const own = [&](std::size_t const I) { return followed[I] > 0.0 ? close_to : told(I); }; + auto const reach = [&](std::size_t const I) { return followed[I] > 0.0 ? followed[I] : I < Reaches.size() && Reaches[I] > 0.0 ? std::max(0.005, Reaches[I]) : told(I); }; + for (auto const &hump : Options.humps) + for (std::size_t i = 0; i < count; ++i) + if (std::isfinite(Ground[i]) && told(i) >= tolerance && Chainages[i] >= hump.from && Chainages[i] <= hump.to) + followed[i] = hump.height + tolerance; auto const price{24000.0 * tolerance * tolerance}; auto const grade_max{(Options.grade_max > 0.0 ? Options.grade_max : result.norms.grade_max) / 1000.0}; - // what has to hold, the heights of the joined ends and the switches in one plane, holds before the grades of the ends, and - // these before the limits of the norms + // what has to hold, the heights of the joined ends, holds before the grades of the ends, and these before the limits of the + // norms. A yielding bound counts as much as a kilometre of the ground off as far, and gives + // way to the norms double constexpr stiff{1e11}; double constexpr firm{1e9}; + double constexpr leaning{1e3}; std::vector x(nodes); { @@ -628,15 +794,10 @@ line fit_ground(std::vector const &Chainages, std::vector const { auto const terms = {std::pair{k - 1, 1.0 / step}, std::pair{k, -2.0 / step}, std::pair{k + 1, 1.0 / step}}; auto const bend{(Line[k - 1] - 2.0 * Line[k] + Line[k + 1]) / step}; - if (bend_max[k] <= 0.0) - { - square(terms, bend, stiff); - continue; - } auto const root{std::sqrt(bend * bend + flat * flat)}; add(terms, price * root, price * bend / root, price * flat * flat / (root * root * root)); - if (std::abs(bend) > bend_max[k]) - square(terms, bend - std::copysign(bend_max[k], bend), firm); + if (auto const most{bend > 0.0 ? sag_max[k] : crest_max[k]}; std::abs(bend) > most) + square(terms, bend - std::copysign(most, bend), firm); } for (std::size_t k = 0; k + 1 < nodes; ++k) { @@ -647,18 +808,20 @@ line fit_ground(std::vector const &Chainages, std::vector const if (start_fixed) { square({{0, 1.0}}, Line[0] - Context.start_elevation, stiff); - square({{0, -1.0 / step}, {1, 1.0 / step}}, (Line[1] - Line[0]) / step - Context.start_grade, firm * 10.0); + if (Options.hold_grades) + square({{0, -1.0 / step}, {1, 1.0 / step}}, (Line[1] - Line[0]) / step - Context.start_grade, firm * 10.0); } if (end_fixed) { square({{nodes - 1, 1.0}}, Line[nodes - 1] - Context.end_elevation, stiff); - square({{nodes - 2, -1.0 / step}, {nodes - 1, 1.0 / step}}, (Line[nodes - 1] - Line[nodes - 2]) / step - Context.end_grade, firm * 10.0); + if (Options.hold_grades) + square({{nodes - 2, -1.0 / step}, {nodes - 1, 1.0 / step}}, (Line[nodes - 1] - Line[nodes - 2]) / step - Context.end_grade, firm * 10.0); } for (auto const &bound : bounds) { auto const height{Line[bound.cell] * (1.0 - bound.part) + Line[bound.cell + 1] * bound.part}; if (height < bound.low || height > bound.high) - square({{bound.cell, 1.0 - bound.part}, {bound.cell + 1, bound.part}}, height - std::clamp(height, bound.low, bound.high), firm * 10.0); + square({{bound.cell, 1.0 - bound.part}, {bound.cell + 1, bound.part}}, height - std::clamp(height, bound.low, bound.high), bound.yielding ? leaning : firm * 10.0); } return total; }; @@ -683,36 +846,105 @@ line fit_ground(std::vector const &Chainages, std::vector const }; // the spread and the flat narrow down level by level, each level going on from the line of the one before: from nearly // all the ground agreeing and changes of grade costing their square, to the tolerance and their size - for (;;) - { - auto current{cost(x, true)}; - for (int pass = 0; pass < 100; ++pass) + auto const solve = [&]() { + spread = 256.0; + flat = sharpest; + for (;;) { - auto const direction{newton()}; - auto descent{0.0}; - for (std::size_t k = 0; k < nodes; ++k) - descent += gradient[k] * direction[k]; - std::vector trial(nodes); - auto length{1.0}; - for (;; length *= 0.5) + auto current{cost(x, true)}; + for (int pass = 0; pass < 100; ++pass) { + auto const direction{newton()}; + auto descent{0.0}; for (std::size_t k = 0; k < nodes; ++k) - trial[k] = x[k] + length * direction[k]; - if (cost(trial, false) <= current + 1e-4 * length * descent || length < 1e-6) + descent += gradient[k] * direction[k]; + std::vector trial(nodes); + auto length{1.0}; + for (;; length *= 0.5) + { + for (std::size_t k = 0; k < nodes; ++k) + trial[k] = x[k] + length * direction[k]; + if (cost(trial, false) <= current + 1e-4 * length * descent || length < 1e-6) + break; + } + auto moved{0.0}; + for (std::size_t k = 0; k < nodes; ++k) + moved = std::max(moved, std::abs(length * direction[k])); + x = std::move(trial); + current = cost(x, true); + if (moved < 1e-5) break; } - auto moved{0.0}; - for (std::size_t k = 0; k < nodes; ++k) - moved = std::max(moved, std::abs(length * direction[k])); - x = std::move(trial); - current = cost(x, true); - if (moved < 1e-5) + if (spread <= 1.0 && flat <= 1e-6) break; + spread = std::max(1.0, spread * 0.5); + flat = std::max(1e-6, flat * 0.5); } - if (spread <= 1.0 && flat <= 1e-6) + }; + solve(); + // the ground parted from the line farther along than a structure is long, not as far as a viaduct or a hill over a tunnel + // keep it, and nowhere as steep as the banks and the walls under a bridge, is none: a hump, a rise, a hollow the line goes + // over. It agrees with the line however far it is, and the line is sought again. The ground which tells the height no more + // closely than the tolerance counts, the model, not the rail tops beside; its steepness over a few metres, past the noise + // of the model + double constexpr chord{5.0}; + for (int round = 0; round < 3 && Options.follow_length > 0.0; ++round) + { + bool more{false}; + std::vector run; + std::size_t back{0}; + std::size_t previous{count}; + int side{0}; + double farthest{0.0}; + double steepest{0.0}; + auto const close = [&]() { + if (side != 0 && Chainages[run.back()] - Chainages[run.front()] > Options.follow_length && farthest <= Options.follow_height && steepest <= Options.follow_steepest / 1000.0) + { + for (auto const i : run) + if (followed[i] <= 0.0) + followed[i] = farthest + tolerance; + more = true; + } + side = 0; + run.clear(); + }; + auto const extend = [&](std::size_t const I) { + while (back + 1 < run.size() && Chainages[I] - Chainages[run[back + 1]] >= chord) + ++back; + if (false == run.empty() && Chainages[I] - Chainages[run[back]] >= chord) + steepest = std::max(steepest, std::abs(Ground[I] - Ground[run[back]]) / (Chainages[I] - Chainages[run[back]])); + run.push_back(I); + }; + for (std::size_t i = 0; i < count; ++i) + { + if (false == std::isfinite(Ground[i]) || told(i) < tolerance) + continue; + auto const off{x[cell[i]] * (1.0 - part[i]) + x[cell[i] + 1] * part[i] - Ground[i]}; + auto const now{followed[i] > 0.0 ? 0 : off > reach(i) ? 1 : off < -reach(i) ? -1 : 0}; + if (now != side) + { + close(); + if (now != 0) + { + side = now; + back = 0; + farthest = 0.0; + steepest = 0.0; + if (previous < count) + run.push_back(previous); + } + } + if (now != 0) + { + farthest = std::max(farthest, std::abs(off)); + extend(i); + } + previous = i; + } + close(); + if (false == more) break; - spread = std::max(1.0, spread * 0.5); - flat = std::max(1e-6, flat * 0.5); + solve(); } // the design: the changes of grade along a bend, one after another the same way, are one point of intersection where @@ -720,10 +952,7 @@ line fit_ground(std::vector const &Chainages, std::vector const // radius stays within the nodes of its bend; where the ends held the line sharper, the curve is as sharp as the line auto const change = [&](std::size_t const K) { return (x[K - 1] - 2.0 * x[K] + x[K + 1]) / step; }; auto const significant{sharpest * 0.01}; - auto const between = [&](std::size_t const K, std::size_t const L) { - return std::any_of(Context.switches.begin(), Context.switches.end(), [&](auto const &Zone) { return Zone.to > node(K) && Zone.from < node(L); }); - }; - auto const bends = [&](std::size_t const K) { return bend_max[K] > 0.0 && std::abs(change(K)) >= significant; }; + auto const bends = [&](std::size_t const K) { return std::abs(change(K)) >= significant; }; // an end whose grade the line couldn't take over keeps only its height, the grade breaks at the joint if (start_fixed && std::abs((x[1] - x[0]) / step - Context.start_grade) > significant) result.join_start = false; @@ -731,6 +960,7 @@ line fit_ground(std::vector const &Chainages, std::vector const result.join_end = false; result.points.push_back({origin, x[0]}); std::vector rooms{0.0}; + std::vector lengths{0.0}; for (std::size_t k = 1; k + 1 < nodes;) { if (false == bends(k)) @@ -741,31 +971,50 @@ line fit_ground(std::vector const &Chainages, std::vector const auto const first{k}; auto const sign{change(k) > 0.0}; double sum{0.0}, moment{0.0}; - for (; k + 1 < nodes && bends(k) && (change(k) > 0.0) == sign && (k == first || false == between(k - 1, k)); ++k) + for (; k + 1 < nodes && bends(k) && (change(k) > 0.0) == sign; ++k) { sum += change(k); moment += change(k) * node(k); } auto const at{moment / sum}; pvi point{at, x[first] + (x[first] - x[first - 1]) / step * (at - node(first))}; - auto room{std::min(at - node(first) + step * 0.5, node(k - 1) + step * 0.5 - at)}; - for (auto const &zone : Context.switches) - room = std::min(room, std::max(0.0, std::max(zone.from - at, at - zone.to))); + auto const room{std::min(at - node(first) + step * 0.5, node(k - 1) + step * 0.5 - at)}; result.points.push_back(point); rooms.push_back(room); + lengths.push_back(node(k - 1) - node(first) + step); } result.points.push_back({node(nodes - 1), x[nodes - 1]}); rooms.push_back(0.0); - // the curve of the required radius where it has room, off the switches and the curves beside; where it hasn't, as sharp as - // the room lets it be + lengths.push_back(0.0); + // the curve of the required radius where it has room, off the curves beside; where it hasn't, as sharp as the room lets it + // be. On a hump as long as the bend of the line, no sharper than the radius of the hump, nor than a switch lets a convex one for (std::size_t i = 1; i + 1 < result.points.size(); ++i) { auto const turn{std::abs(grade_after(result, i) - grade_after(result, i - 1))}; - if (turn * 1000.0 > result.norms.curve_threshold + 1e-6 && required * turn * 0.5 > rooms[i] - 1e-4) + if (turn * 1000.0 <= result.norms.curve_threshold + 1e-6) + { + // a change no curve needs breaks bare, but not on a switch + if (auto const at{result.points[i].chainage}; turn > 1e-9 && on_switch(at, at)) + { + result.points[i].radius = std::min(required, 2.0 * std::max(0.0, rooms[i] - 1e-4) / turn); + result.points[i].automatic = false; + } + continue; + } + auto const radius{least(result.points[i].chainage)}; + auto bent{radius < required ? std::clamp(std::ceil(lengths[i] / turn / 10.0) * 10.0, radius, required) : required}; + if (auto const at{result.points[i].chainage}; bent < switch_crest && grade_after(result, i) < grade_after(result, i - 1) && on_switch(at - bent * turn * 0.5, at + bent * turn * 0.5)) + bent = switch_crest; + if (bent * turn * 0.5 > rooms[i] - 1e-4) { result.points[i].radius = 2.0 * std::max(0.0, rooms[i] - 1e-4) / turn; result.points[i].automatic = false; } + else if (bent < required) + { + result.points[i].radius = bent; + result.points[i].automatic = false; + } } assign_radii(result); @@ -793,6 +1042,24 @@ line fit_ground(std::vector const &Chainages, std::vector const return worst; }; auto faults{errors(result)}; + // a convex curve over a switch which the room of its bend cut sharper than a switch may lie on takes the radius it may have, + // as far as the curves beside leave it room + for (std::size_t i = 1; faults > 0 && i + 1 < result.points.size(); ++i) + { + auto const turn{grade_after(result, i) - grade_after(result, i - 1)}; + auto const radius{result.points[i].radius}; + auto const at{result.points[i].chainage}; + if (radius <= 0.0 || radius >= switch_crest || turn >= 0.0 || false == on_switch(at + radius * turn * 0.5, at - radius * turn * 0.5)) + continue; + auto wider{result}; + wider.points[i].radius = switch_crest; + wider.points[i].automatic = false; + if (auto const fewer{errors(wider)}; fewer < faults) + { + result = std::move(wider); + faults = fewer; + } + } // an end whose grade the curve of the joint can't take over without a fault breaks at the joint for (auto *join : {&result.join_start, &result.join_end}) if (faults > 0 && *join) @@ -803,6 +1070,15 @@ line fit_ground(std::vector const &Chainages, std::vector const else *join = true; } + // where the line can't bend from the grades of its ends into the ground within the norms, as a short piece between two + // lines, the grades break at the joints: the line keeps only the heights of the ends + if (faults > 0 && Options.hold_grades && (start_fixed || end_fixed)) + { + auto loose{Options}; + loose.hold_grades = false; + if (auto again{fit_ground(Chainages, Ground, Context, Settings, loose, Tolerances, Reaches)}; errors(again) < faults) + return again; + } for (;;) { auto const &points{result.points}; @@ -865,6 +1141,48 @@ std::vector departures(line const &Line, std::vector const &C return result; } +std::vector bridged(std::vector const &Chainages, std::vector const &Heights, std::vector const &Ground, double const Depth, double const Steepest) +{ + std::vector result; + auto const count{std::min({Chainages.size(), Heights.size(), Ground.size()})}; + auto const under = [&](std::size_t const I) { return std::isfinite(Ground[I]) && Heights[I] - Ground[I] > Depth; }; + // the steepness over a few metres, past the noise of the model, in and about the stretch + double constexpr chord{5.0}; + double constexpr about{10.0}; + auto const steepest = [&](double const From, double const To) { + double most{0.0}; + for (std::size_t i = 0, j = 0; i < count; ++i) + { + if (Chainages[i] < From - about || Chainages[i] > To + about || false == std::isfinite(Ground[i])) + continue; + j = std::max(j, i); + while (j + 1 < count && Chainages[j] - Chainages[i] < chord) + ++j; + if (auto const run{Chainages[j] - Chainages[i]}; run >= chord && run <= 2.0 * chord && std::isfinite(Ground[j])) + most = std::max(most, std::abs(Ground[j] - Ground[i]) / run); + } + return most * 1000.0; + }; + for (std::size_t i = 0; i < count;) + { + if (false == under(i)) + { + ++i; + continue; + } + departure stretch; + stretch.from = Chainages[i]; + for (; i < count && under(i); ++i) + { + stretch.depth = std::min(stretch.depth, Ground[i] - Heights[i]); + stretch.to = Chainages[i]; + } + if (steepest(stretch.from, stretch.to) > Steepest) + result.push_back(stretch); + } + return result; +} + std::vector breaks(line const &Line) { std::vector result; diff --git a/editor/editorProfile.hpp b/editor/editorProfile.hpp index b2314086..5f1c42f7 100644 --- a/editor/editorProfile.hpp +++ b/editor/editorProfile.hpp @@ -115,19 +115,47 @@ struct recognition // grade line of existing track, from its elevations and grades sampled along the chainage line recognize(std::vector const &Chainages, std::vector const &Elevations, std::vector const &Grades, recognition const &Options); +// the hump of a marshalling yard: a crest with the fan of the switches of the sorting tracks past it, the ground falling away from +// it both ways, one of them more steeply than a line or a station is laid, and the grade changing across it more sharply than +// any vertical curve of the norms bends. Nothing else has all of it +struct hump +{ + double from{0.0}; + double crest{0.0}; + double to{0.0}; + double height{0.0}; // m, of the crest over the lower of its feet +}; +// the humps of the ground; the samples which tell the height more closely than Tolerance, the rail tops beside, don't count +std::vector humps(std::vector const &Chainages, std::vector const &Ground, context const &Context, double const Tolerance, std::vector const &Tolerances = {}); + struct ground_fit { double tolerance{1.0}; // m, the ground this near the line agrees with it; farther, the line doesn't lie on it there double grade_max{0.0}; // per mille, the steepest grade the line may take; 0: the limit of the line + // the ground parting from the line longer than this, by less than the height and nowhere steeper than the steepest, is no + // structure: no bridge or viaduct with the banks and the walls under it, no hill over a tunnel, but a hump, a rise, a + // hollow. The line follows it, as closely as the tolerance. 0: the line goes past whatever it parts from + double follow_length{0.0}; // m + double follow_height{0.0}; // m + double follow_steepest{0.0}; // per mille + double follow_tolerance{0.0}; // m, 0: the tolerance + // on the humps the line follows the ground all along, as closely as the follow tolerance, and its vertical curves are as sharp + // as the ground bends, down to the hump radius. Nowhere else is a curve sharper than the norms require + std::vector humps; + double hump_radius{0.0}; // m, 0: the required one // heights the line keeps between before it lies on the ground, as where the tracks off a switch of it lead into another line: - // they can't climb more than their length lets them + // they can't climb more than their length lets them. A yielding one, told over tracks which still bend, gives way to the + // curves of the norms, if to no ground struct bound { double chainage{0.0}; double low{0.0}; double high{0.0}; + bool yielding{false}; }; std::vector bounds; + // the joined ends keep the grade of the adjoining track; false: their height only, the grade breaks at the joint + bool hold_grades{true}; }; // grade line which lies on the ground where it agrees, within the limits of Settings, with the fewest and least changes of // grade; where the ground parts from it, at a bridge, a viaduct, a cutting wall, the line goes on past it. Chainages ascend, @@ -145,6 +173,9 @@ struct departure double depth{0.0}; // m, the ground against the line where they part the most }; std::vector departures(line const &Line, std::vector const &Chainages, std::vector const &Ground, double const Tolerance); +// stretches a track goes over on a bridge or a viaduct the ground doesn't have: the ground under the heights of the track by over +// Depth, with a bank about them steeper than Steepest (per mille), as it falls by the abutments into what the bridge spans +std::vector bridged(std::vector const &Chainages, std::vector const &Heights, std::vector const &Ground, double const Depth, double const Steepest); // chainages where the paths have to end so that each of them carries a single grade or a single curve std::vector breaks(line const &Line); diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index ba5a65be..01b33559 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -2192,6 +2192,20 @@ editor_track::route editor_track::run_route(TTrack &Track, double const Maximum) route result; if (false == is_supported(Track)) return result; + // through a switch the run goes along its first path, the main one; but where that one bends and the other runs straight, + // as in the quarters of a double slip, straight on + auto const bends = [](TTrack const &Of, int const Path) { + bezier const curve{Of.m_paths[Path]}; + auto const from{plan_of(curve.first(0.0))}; + auto const to{plan_of(curve.first(1.0))}; + return glm::length(from) > 1e-9 && glm::length(to) > 1e-9 && std::abs(std::atan2(from.x * to.y - from.y * to.x, glm::dot(from, to))) > 0.002; + }; + auto const main_of = [&](TTrack const &Of) { + for (int i = 1; i < static_cast(Of.m_paths.size()); ++i) + if (bends(Of, 0) && false == bends(Of, i)) + return i; + return 0; + }; auto const walk = [&](route_node Node) { std::vector nodes; double length{0.0}; @@ -2202,7 +2216,7 @@ editor_track::route editor_track::run_route(TTrack &Track, double const Maximum) break; auto chosen{candidates.front()}; for (auto const &candidate : candidates) - if (std::get<1>(candidate) == 0) + if (std::get<1>(candidate) == main_of(*std::get<0>(candidate))) chosen = candidate; auto *track{std::get<0>(chosen)}; if (track == &Track || std::any_of(nodes.begin(), nodes.end(), [&](route_node const &Other) { return std::get<0>(Other) == track; })) @@ -2213,12 +2227,13 @@ editor_track::route editor_track::run_route(TTrack &Track, double const Maximum) } return nodes; }; - auto const ahead{walk({&Track, 0, true})}; - auto const behind{walk({&Track, 0, false})}; + auto const own{main_of(Track)}; + auto const ahead{walk({&Track, own, true})}; + auto const behind{walk({&Track, own, false})}; for (auto node = behind.rbegin(); node != behind.rend(); ++node) if (std::none_of(ahead.begin(), ahead.end(), [&](route_node const &Other) { return std::get<0>(Other) == std::get<0>(*node); })) result.spans.push_back({std::get<0>(*node), std::get<1>(*node), false == std::get<2>(*node)}); - result.spans.push_back({&Track, 0, true}); + result.spans.push_back({&Track, own, true}); for (auto const &node : ahead) result.spans.push_back({std::get<0>(node), std::get<1>(node), std::get<2>(node)}); return result; @@ -2232,6 +2247,24 @@ editor_track::route editor_track::route_of(chain const &Chain) return result; } +double editor_track::route_speed(route const &Route) +{ + std::vector> speeds; + auto total{0.0}; + for (auto const &span : Route.spans) + if (auto const speed{velocity(*span.track)}; speed > 0.0) + { + speeds.emplace_back(speed, std::abs(span.to - span.from)); + total += speeds.back().second; + } + std::sort(speeds.begin(), speeds.end()); + auto passed{0.0}; + for (auto const &[speed, length] : speeds) + if ((passed += length) >= total * 0.5) + return speed; + return 100.0; +} + std::vector editor_track::sample_route(route &Route, double const Step) { std::vector samples; @@ -2448,6 +2481,20 @@ bool editor_track::adjoining_grade(route const &Route, bool const Atend, double return true; } +std::vector editor_track::continuations(route_span const &Span) +{ + std::vector result; + for (auto const &[track, path, forward] : route_next({Span.track, Span.path, Span.forward})) + { + auto const tangent{path_tangent(track->m_paths[path], false == forward)}; + auto const plan{std::hypot(tangent.x, tangent.z)}; + if (plan < 1e-9) + continue; + result.push_back({{track, path, forward}, (forward ? 1.0 : -1.0) * tangent.y / plan}); + } + return result; +} + std::vector editor_track::apply_profile(route &Route, std::function const &Elevation, std::function const &Grade, std::vector const &Breaks, std::vector> &States, std::vector &Created) { double constexpr margin{0.25}; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 8fc41f2b..b32de786 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -377,6 +377,9 @@ class editor_track // continues both ways from the path, through switches along their path 0 static route run_route(TTrack &Track, double const Maximum); static route route_of(chain const &Chain); + // the speed most of the sampled route is laid for, by the length: a few tracks set faster or slower don't set the curves of + // all of it. 100 where none is set + static double route_speed(route const &Route); struct route_sample { double chainage{0.0}; @@ -394,6 +397,14 @@ class editor_track // grade of the path which adjoins the end of the route, positive rising along the route // Adjoining, Path, Entered: the path met, and whether it's met at its start static bool adjoining_grade(route const &Route, bool const Atend, double &Grade, TTrack **Adjoining = nullptr, int *Path = nullptr, bool *Entered = nullptr); + // the paths which go on from the end of the span the way it runs, both of a switch met at its start, with their grade there, + // positive rising onward + struct continuation + { + route_span span; + double grade{0.0}; + }; + static std::vector continuations(route_span const &Span); // path off the route whose end no longer met the adjoining path in height struct height_gap { diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 5b6cb1e2..54968b17 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -8846,6 +8846,9 @@ msgstr "Łuki pionowe punktów %zu i %zu nachodzą na siebie o %.2f m" msgid "Point %zu: the vertical curve lies on switch %s" msgstr "Punkt %zu: łuk pionowy leży na rozjeździe %s" +msgid "Point %zu: the convex vertical curve on switch %s is sharper than %.0f m" +msgstr "Punkt %zu: wypukły łuk pionowy na rozjeździe %s jest ostrzejszy niż %.0f m" + msgid "Constant grade between points %zu and %zu is %.1f m long, less than %.0f m" msgstr "Stałe pochylenie między punktami %zu i %zu ma %.1f m, mniej niż %.0f m" diff --git a/scene/heightmapterrain.cpp b/scene/heightmapterrain.cpp index e7b8f491..036db219 100644 --- a/scene/heightmapterrain.cpp +++ b/scene/heightmapterrain.cpp @@ -514,7 +514,10 @@ std::optional modifier_shape::evaluate(double const X, d auto const &b{points[i + 1].position}; auto const run{b - a}; auto const length{glm::dot(run, run)}; - auto const t{length > 1e-12 ? std::clamp(glm::dot(point - a, run) / length, 0.0, 1.0) : 0.0}; + auto const along{length > 1e-12 ? glm::dot(point - a, run) / length : 0.0}; + if ((m_modifier.open_start && i == 0 && along < 0.0) || (m_modifier.open_end && i + 2 == points.size() && along > 1.0)) + continue; + auto const t{std::clamp(along, 0.0, 1.0)}; auto const offset{glm::length(point - (a + run * t))}; if (offset < distance) { @@ -608,6 +611,10 @@ bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Err item.reach = reach.as(); if (auto const rounding{entry["rounding"]}) item.rounding = rounding.as(); + if (auto const open{entry["open_start"]}) + item.open_start = open.as(); + if (auto const open{entry["open_end"]}) + item.open_end = open.as(); if (auto const points{entry["points"]}; points && points.IsSequence()) for (auto const &point : points) if (point.IsSequence() && point.size() >= 4) @@ -681,6 +688,10 @@ bool save_manifest(std::string const &Path, manifest const &Manifest, std::strin out << YAML::Key << "slope" << YAML::Value << item.slope; out << YAML::Key << "reach" << YAML::Value << item.reach; out << YAML::Key << "rounding" << YAML::Value << item.rounding; + if (item.open_start) + out << YAML::Key << "open_start" << YAML::Value << true; + if (item.open_end) + out << YAML::Key << "open_end" << YAML::Value << true; out << YAML::Key << "points" << YAML::Value << YAML::BeginSeq; for (auto const &point : item.points) out << YAML::Flow << YAML::BeginSeq << point.position.x << point.position.y << point.formation << point.half_width << YAML::EndSeq; diff --git a/scene/heightmapterrain.h b/scene/heightmapterrain.h index 707af808..95ca6316 100644 --- a/scene/heightmapterrain.h +++ b/scene/heightmapterrain.h @@ -130,6 +130,10 @@ struct modifier double slope{1.5}; // horizontal run of the slopes per metre of height double reach{60.0}; // from the edge of the formation, where the slopes end even if they don't get to the ground double rounding{1.5}; // over which the slopes bend into the formation and into the ground + // the formation and its slopes end square across the first or the last point, as against the abutment of a bridge or the + // portal of a tunnel; otherwise they go round it + bool open_start{false}; + bool open_end{false}; std::vector points; // rectangle (x, z) the modifier can change the ground in From 0d9fa6337d8700b561580c5161f68af4e234dd35 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 02:45:19 +0200 Subject: [PATCH 039/145] feat(editor): the whole-scenery grade fit sets the tracks over a bridge or a viaduct to the bridge environment Where the terrain model falls away under a fitted line by the abutments, the paths are cut off at the ends of the stretch as the line is applied and set to the bridge, in the same step of the history: they sound as on a bridge, get no formation and the terrain under them stays as the model has it, also after the scenery is saved and opened again. Tunnels and tracks set to the bridge already are left as they are. The status says how many paths went on a bridge. --- application/editormode.h | 5 +++-- application/editormodenmt.cpp | 19 ++++++++++--------- application/editormodeprofile.cpp | 20 ++++++++++++++++++-- lang/editor_pl.po | 6 ++++++ 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index dfcb1324..3fb5bf59 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -2288,8 +2288,9 @@ class editor_mode : public application_mode, private editor_track::observer, pri void profile_restore_ground(); void render_profile_earthworks(); void profile_check(); - // Terrain: the heightmap terrain follows the tracks changed - void profile_apply(bool const Terrain = false); + // Terrain: the heightmap terrain follows the tracks changed. Bridges: stretches of the route over a bridge or a viaduct, the + // paths over them cut off at their ends and set to the bridge environment + void profile_apply(bool const Terrain = false, std::vector const &Bridges = {}); // the terrain under the tracks changed by the last Edits steps of the history which changed tracks void profile_follow_terrain(std::size_t const Edits); void profile_fit_view(); diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index e0adf534..02992d4b 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -2069,7 +2069,15 @@ bool editor_mode::nmt_fit_line(TTrack &Track) profile_check(); auto const fitted_line{profile.line}; auto const authored{profile.samples}; - profile_apply(); + // where the model falls away under the line by the abutments, the line goes over on a bridge or a viaduct: its paths there + // are cut off and set to the bridge, the formation is laid up to it, the ground under it stays as the model has it + double constexpr kBridged{1.0}; // m + double constexpr kAbutment{100.0}; // per mille + std::vector heights(count); + for (std::size_t i = 0; i < count; ++i) + heights[i] = profile::elevation(fitted_line, sampled[i]); + auto const bridges{profile::bridged(sampled, heights, model, kBridged, kAbutment)}; + profile_apply(false, bridges); auto const applied{profile.error.empty()}; // where the line parts from the model: under it a gap, a bridge or a viaduct, over it a wall or a structure above the track auto const parted{profile::departures(fitted_line, sampled, model, 2.0 * kModel)}; @@ -2125,14 +2133,7 @@ bool editor_mode::nmt_fit_line(TTrack &Track) m_nmt_state.laid[{static_cast(std::floor(sample.position.x / kPlanCell)), static_cast(std::floor(sample.position.z / kPlanCell))}].push_back( {sample.position, sample.direction, sample.grade, editor_track::sampled_elevation(authored, sample.chainage), track != nullptr ? static_cast(track->rail_height()) : 0.0, track}); } - // where the model falls away under the line by the abutments, the line goes over on a bridge or a viaduct: the formation - // is laid up to it, the ground under it stays as the model has it - double constexpr kBridged{1.0}; // m - double constexpr kAbutment{100.0}; // per mille - std::vector heights(count); - for (std::size_t i = 0; i < count; ++i) - heights[i] = profile::elevation(fitted_line, sampled[i]); - auto const spans{applied ? profile::bridged(sampled, heights, model, kBridged, kAbutment) : std::vector{}}; + auto const spans{applied ? bridges : std::vector{}}; for (auto const &stretch : spans) for (std::size_t i = 0; i < count; ++i) if (sampled[i] >= stretch.from && sampled[i] <= stretch.to) diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index d7bbab48..d14dcf70 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -1450,7 +1450,7 @@ void editor_mode::profile_frame_view() fly_to(centre - right * (shift.x * pixel) + above * (shift.y * pixel), look, distance, 0.35); } -void editor_mode::profile_apply(bool const Terrain) +void editor_mode::profile_apply(bool const Terrain, std::vector const &Bridges) { auto &state{m_profile}; state.status.clear(); @@ -1475,8 +1475,22 @@ void editor_mode::profile_apply(bool const Terrain) auto const before{state.samples}; std::vector> states; std::vector created; + auto cuts{profile::breaks(line)}; + for (auto const &bridge : Bridges) + cuts.insert(cuts.end(), {bridge.from, bridge.to}); + std::sort(cuts.begin(), cuts.end()); auto const gaps{editor_track::apply_profile( - state.route, [&](double const Chainage) { return profile::elevation(line, Chainage); }, [&](double const Chainage) { return profile::grade(line, Chainage); }, profile::breaks(line), states, created)}; + state.route, [&](double const Chainage) { return profile::elevation(line, Chainage); }, [&](double const Chainage) { return profile::grade(line, Chainage); }, cuts, states, created)}; + // the paths over a bridge sound and light as on one, and get no formation; within what a cut too near an end leaves on + double constexpr kBridgeEnd{0.5}; // m + std::size_t bridged{0}; + for (auto const &span : state.route.spans) + if (span.track->eEnvironment != e_bridge && span.track->eEnvironment != e_tunnel + && std::any_of(Bridges.begin(), Bridges.end(), [&](profile::departure const &Bridge) { return span.from >= Bridge.from - kBridgeEnd && span.to <= Bridge.to + kBridgeEnd; })) + { + span.track->eEnvironment = e_bridge; + ++bridged; + } auto const changed{states.size()}; auto const added{created.size()}; state.created = created; @@ -1499,6 +1513,8 @@ void editor_mode::profile_apply(bool const Terrain) state.status = format(STR_P("Grade line applied: %zu path changed, %zu added, largest departure %.3f m", "Grade line applied: %zu paths changed, %zu added, largest departure %.3f m", changed), changed, added, state.departure); if (false == scene::Layers.empty()) state.status += STR_C("\nThe paths and the design (//$p lines) go to the scenery files on save"); + if (bridged > 0) + state.status += format(STR_P("\nOver a bridge or a viaduct: %zu path", "\nOver a bridge or a viaduct: %zu paths", bridged), bridged); auto const name = [](TTrack const *Track) { return name_of(*Track); }; auto const closed{std::count_if(gaps.begin(), gaps.end(), [](auto const &Gap) { return Gap.closed; })}; if (closed > 0) diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 54968b17..4a2b9083 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -8960,6 +8960,12 @@ msgstr "Serwis nie zwrócił modelu terenu" msgid "\nThe paths and the design (//$p lines) go to the scenery files on save" msgstr "\nTory i projekt (wiersze //$p) trafią do plików scenerii przy zapisie" +msgid "\nOver a bridge or a viaduct: %zu path" +msgid_plural "\nOver a bridge or a viaduct: %zu paths" +msgstr[0] "\nNa moście lub wiadukcie: %zu tor" +msgstr[1] "\nNa moście lub wiadukcie: %zu tory" +msgstr[2] "\nNa moście lub wiadukcie: %zu torów" + msgid "\nPaths off the route brought to the height of the switches:" msgstr "\nTory spoza trasy dociągnięte do wysokości rozjazdów:" From 7fb9d2d4580f317154a6b7839e3907112e6f3232 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 05:56:05 +0200 Subject: [PATCH 040/145] fix(editor): the whole-scenery grade fit takes spans a few metres apart for one bridge, leaves the culverts on the embankment and ends a bridge at a joint by its abutment Bridged stretches less than 5 m apart are merged into one bridge on its piers, and those shorter than 3 m, a ditch or a culvert under the embankment, are no bridge. A bridge end within 1 m of a joint of the route or a break of the line moves to it, so the cut leaves no sliver of a path by the abutment. Against the OpenStreetMap railway bridges the marked share stays the same (lk91 60 of 66, TG 90 of 104); the places with nothing under them in the map fall from 6 to 4 on lk91 and from 18 to 12 on TG, and the bridge paths shorter than 1 m from 9 to 0 and from 11 to 1. --- application/editormodenmt.cpp | 14 +++++++++++++- application/editormodeprofile.cpp | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index 02992d4b..7760a619 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -2073,10 +2073,22 @@ bool editor_mode::nmt_fit_line(TTrack &Track) // are cut off and set to the bridge, the formation is laid up to it, the ground under it stays as the model has it double constexpr kBridged{1.0}; // m double constexpr kAbutment{100.0}; // per mille + // spans a few metres apart are one bridge on its piers; what is shorter than that is a culvert under the embankment + double constexpr kPier{5.0}; // m + double constexpr kCulvert{3.0}; // m std::vector heights(count); for (std::size_t i = 0; i < count; ++i) heights[i] = profile::elevation(fitted_line, sampled[i]); - auto const bridges{profile::bridged(sampled, heights, model, kBridged, kAbutment)}; + std::vector bridges; + for (auto const &stretch : profile::bridged(sampled, heights, model, kBridged, kAbutment)) + if (false == bridges.empty() && stretch.from - bridges.back().to < kPier) + { + bridges.back().to = stretch.to; + bridges.back().depth = std::min(bridges.back().depth, stretch.depth); + } + else + bridges.push_back(stretch); + bridges.erase(std::remove_if(bridges.begin(), bridges.end(), [&](profile::departure const &Bridge) { return Bridge.to - Bridge.from < kCulvert; }), bridges.end()); profile_apply(false, bridges); auto const applied{profile.error.empty()}; // where the line parts from the model: under it a gap, a bridge or a viaduct, over it a wall or a structure above the track diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index d14dcf70..0d0add80 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -1476,7 +1476,18 @@ void editor_mode::profile_apply(bool const Terrain, std::vector> states; std::vector created; auto cuts{profile::breaks(line)}; - for (auto const &bridge : Bridges) + // a bridge ends at a joint or a break of the line by its abutment rather than leave a sliver of a path there + double constexpr kBridgeSnap{1.0}; // m + auto joints{cuts}; + for (auto const &span : state.route.spans) + joints.insert(joints.end(), {span.from, span.to}); + auto bridges{Bridges}; + for (auto &bridge : bridges) + for (auto *end : {&bridge.from, &bridge.to}) + if (auto const nearest{std::min_element(joints.begin(), joints.end(), [&](double const A, double const B) { return std::abs(A - *end) < std::abs(B - *end); })}; + nearest != joints.end() && std::abs(*nearest - *end) <= kBridgeSnap) + *end = *nearest; + for (auto const &bridge : bridges) cuts.insert(cuts.end(), {bridge.from, bridge.to}); std::sort(cuts.begin(), cuts.end()); auto const gaps{editor_track::apply_profile( @@ -1486,7 +1497,7 @@ void editor_mode::profile_apply(bool const Terrain, std::vectoreEnvironment != e_bridge && span.track->eEnvironment != e_tunnel - && std::any_of(Bridges.begin(), Bridges.end(), [&](profile::departure const &Bridge) { return span.from >= Bridge.from - kBridgeEnd && span.to <= Bridge.to + kBridgeEnd; })) + && std::any_of(bridges.begin(), bridges.end(), [&](profile::departure const &Bridge) { return span.from >= Bridge.from - kBridgeEnd && span.to <= Bridge.to + kBridgeEnd; })) { span.track->eEnvironment = e_bridge; ++bridged; From 8c5795348f7d6c5e794e828de66adf56f4cd69ec Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 07:35:31 +0200 Subject: [PATCH 041/145] feat(editor): the signalling mode finds the station of a switch, lays it out as a schematic along its main track and keeps its name, code and line in the scenery --- CMakeLists.txt | 2 + application/editormode.cpp | 3 + application/editormode.h | 6 +- application/editormodesrk.cpp | 642 +++++++++++++++++++++++++++++ application/editormodesrk.h | 81 ++++ application/editormodetrack.cpp | 9 +- application/editormodetrackux.cpp | 7 + application/editormodeviewport.cpp | 5 +- application/editortool.h | 2 +- editor/editorIcons.hpp | 1 + editor/editorStation.cpp | 399 ++++++++++++++++++ editor/editorStation.hpp | 65 +++ editor/icons/editor_icons.png | Bin 46348 -> 47042 bytes editor/icons/editor_icons.py | 11 +- lang/editor_pl.po | 99 ++++- scene/scenelayers.cpp | 5 +- 16 files changed, 1325 insertions(+), 12 deletions(-) create mode 100644 application/editormodesrk.cpp create mode 100644 application/editormodesrk.h create mode 100644 editor/editorStation.cpp create mode 100644 editor/editorStation.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d85d027..4e3430f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,6 +191,7 @@ set(SOURCES "editor/editorGroundFollow.cpp" "editor/editorAlignmentStore.cpp" "editor/editorSwitchStore.cpp" +"editor/editorStation.cpp" "editor/editorSwitchDesign.cpp" "editor/editorConnections.cpp" "application/editormodeconnection.cpp" @@ -230,6 +231,7 @@ set(SOURCES "application/editormodegauge.cpp" "application/editormodeinfra.cpp" "application/editormodespeed.cpp" +"application/editormodesrk.cpp" "application/editormodejoints.cpp" "application/editormodeparallel.cpp" "application/editormodelineside.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 7ed6c292..3de5f4a5 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1505,12 +1505,14 @@ bool editor_mode::update() draw_tape(); m_speed.update(); m_joints.update(); + m_srk.update(); draw_track_hints(); draw_empty_scenery_hint(); draw_profile_overlay(); draw_infra_overlay(); m_speed.draw_overlay(); m_joints.draw_overlay(); + m_srk.draw_overlay(); } else { @@ -2978,6 +2980,7 @@ bool editor_mode::save() switch_store::tidy(); switch_store::settle(); infra_store(); + m_srk.store(); if (false == m_saved_camera.has_value() || glm::distance(m_saved_camera->first, glm::dvec3{Camera.Pos}) > 0.5 || glm::distance(m_saved_camera->second, glm::vec3{Camera.Angle}) > 0.01f) { m_saved_camera = std::make_pair(glm::dvec3{Camera.Pos}, glm::vec3{Camera.Angle}); diff --git a/application/editormode.h b/application/editormode.h index 3fb5bf59..e2692fc8 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormodelineside.h" #include "application/editormodeparallel.h" #include "application/editormodespeed.h" +#include "application/editormodesrk.h" #include "application/editormodevehicle.h" #include "application/editortool.h" #include "scene/sceneeditor.h" @@ -1544,7 +1545,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri editor_icons::icon picture; int family; // 0 builds the track, 1 equips it, 2 checks it }; - static std::array const &track_modes(); + static std::array const &track_modes(); bool track_plan_tab() const { return m_track_tab == track_tab::straights || m_track_tab == track_tab::route; } track_tab plan_tab_for(TTrack const *Track) const; // the objects along the track: a tab for each tool @@ -2380,7 +2381,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri hekto_tool m_hekto{*this, *this, *this, *this, *this, *this}; fouling_tool m_fouling{*this, *this, *this, *this, *this}; parallel_tool m_parallel{*this, *this, *this, *this}; - std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle, &m_hekto, &m_fouling, &m_parallel}; + srk_tool m_srk{*this, *this, *this}; + std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle, &m_hekto, &m_fouling, &m_parallel, &m_srk}; void infra_rebase(); void render_infra_search(); diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp new file mode 100644 index 00000000..db2a8598 --- /dev/null +++ b/application/editormodesrk.cpp @@ -0,0 +1,642 @@ +/* +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/editormodesrk.h" +#include "application/editorgestures.h" +#include "application/editormodeshared.h" +#include "application/editorprojection.h" +#include "application/editoruilayer.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorSwitchStore.hpp" + +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include +#include + +namespace +{ + +char const *const kMark{"//$s"}; +double constexpr kOverlayRange{4000.0}; + +ImU32 constexpr kPlain{IM_COL32(170, 176, 188, 255)}; +ImU32 constexpr kSwitch{IM_COL32(240, 200, 90, 255)}; +ImU32 constexpr kApproach{IM_COL32(95, 100, 112, 255)}; +ImU32 constexpr kHovered{IM_COL32(255, 255, 255, 255)}; + +bool is_switch(TTrack const &Track) +{ + return Track.eType != tt_Normal; +} + +// a word of the record line: blanks taken out, - for none +std::string word_of(std::string const &Text) +{ + std::string result; + for (auto const c : Text) + if (false == std::isspace(static_cast(c))) + result += c; + return result.empty() ? std::string{"-"} : result; +} + +std::string shown(std::string const &Word) +{ + return Word == "-" ? std::string{} : Word; +} + +} // namespace + +void srk_tool::load() +{ + if (m_loaded) + return; + m_loaded = true; + m_records.clear(); + if (scene::Layers.empty()) + return; + for (auto const layer : scene::Layers.marked_layers(kMark)) + for (auto const &text : scene::Layers.marked(layer, kMark)) + { + std::istringstream words{text}; + std::string word; + if ((words >> word).fail() || word != "station") + continue; + record entry; + entry.layer = layer; + words >> entry.code; + while (words >> word) + { + if (word == "line") + words >> entry.line; + else if (word == "anchor") + words >> entry.anchor; + else if (word == "name") + { + std::getline(words, entry.name); + entry.name.erase(0, entry.name.find_first_not_of(' ')); + break; + } + } + entry.code = shown(entry.code); + entry.line = shown(entry.line); + if (false == entry.anchor.empty()) + m_records.push_back(std::move(entry)); + } +} + +void srk_tool::store() +{ + if (false == m_changed || scene::Layers.empty()) + return; + m_changed = false; + std::vector layers{scene::Layers.marked_layers(kMark)}; + for (auto const &entry : m_records) + if (std::find(layers.begin(), layers.end(), entry.layer) == layers.end()) + layers.push_back(entry.layer); + for (auto const layer : layers) + { + std::vector lines; + for (auto const &entry : m_records) + { + if (entry.layer != layer) + continue; + if (lines.empty()) + lines.emplace_back("# stations described in the track editor, the simulation skips these lines"); + lines.push_back(format("station %s line %s anchor %s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), entry.anchor.c_str(), entry.name.c_str())); + } + scene::Layers.mark(layer, kMark, std::move(lines)); + } +} + +void srk_tool::open(TTrack &Track) +{ + m_status.clear(); + auto found{station::find(Track)}; + if (false == found.has_value()) + { + m_status = STR_C("No switch near this track: it isn't a part of a station"); + return; + } + m_layout = std::move(found); + m_hover = -1; + m_view_state.exaggeration = 0.0f; + m_history_size = m_history.history_size(); + // the record of the station, by any of its switches + m_record = -1; + for (int index = 0; index < static_cast(m_records.size()) && m_record < 0; ++index) + if (std::any_of(m_layout->switches.begin(), m_layout->switches.end(), [&](TTrack const *Switch) { return Switch->name() == m_records[index].anchor; })) + m_record = index; + m_anchor = m_record >= 0 ? m_records[m_record].anchor : m_layout->switches.front()->name(); +} + +void srk_tool::refresh() +{ + m_history_size = m_history.history_size(); + auto *anchor{m_anchor.empty() ? nullptr : simulation::Paths.find(m_anchor)}; + if (anchor == nullptr || anchor->m_editorremoved) + { + m_layout.reset(); + return; + } + auto const view{m_view_state}; + open(*anchor); + if (m_layout.has_value()) + m_view_state = view; +} + +srk_tool::record &srk_tool::record_of_open() +{ + if (m_record < 0) + { + record entry; + auto const *first{m_layout->switches.front()}; + entry.anchor = first->name(); + entry.layer = scene::Layers.resolve(first->layer()); + if (false == scene::Layers.valid(entry.layer) || false == scene::Layers.accepts(entry.layer)) + entry.layer = scene::Layers.active(); + if (false == scene::Layers.valid(entry.layer)) + entry.layer = 1; + m_records.push_back(std::move(entry)); + m_record = static_cast(m_records.size()) - 1; + m_anchor = m_records.back().anchor; + } + return m_records[m_record]; +} + +std::string srk_tool::title() const +{ + if (m_record >= 0) + { + auto const &entry{m_records[m_record]}; + if (false == entry.name.empty()) + return entry.code.empty() ? entry.name : entry.name + " (" + entry.code + ")"; + if (false == entry.code.empty()) + return entry.code; + } + return format(STR_C("Station of %s"), m_anchor.c_str()); +} + +void srk_tool::find_all() +{ + m_found.clear(); + for (auto *track : station::stations()) + m_found.emplace_back(track->name(), station::switches_of(*track).size()); + std::sort(m_found.begin(), m_found.end(), [](auto const &A, auto const &B) { return A.second > B.second; }); +} + +void srk_tool::update() +{ + if (false == m_open) + return; + load(); + if (m_layout.has_value() && m_history.history_size() != m_history_size) + refresh(); + else if (false == m_layout.has_value() && false == m_anchor.empty() && m_history.history_size() != m_history_size) + refresh(); + // a track of another station selected opens it + auto *track{m_selection.selected_track()}; + if (track == nullptr || track == m_picked) + return; + m_picked = track; + if (m_layout.has_value() && std::any_of(m_layout->paths.begin(), m_layout->paths.end(), [&](station::path const &Path) { return Path.track == track && false == Path.approach; })) + return; + open(*track); +} + +void srk_tool::forget(scene::basic_node const *Node) +{ + if (Node == m_picked) + m_picked = nullptr; + if (m_layout.has_value() && std::any_of(m_layout->paths.begin(), m_layout->paths.end(), [&](station::path const &Path) { return Path.track == Node; })) + { + // laid out again from its anchor at the next update + m_layout.reset(); + m_hover = -1; + m_history_size = 0; + } +} + +void srk_tool::draw_overlay() const +{ + if (false == m_open || false == m_layout.has_value()) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + glm::dvec3 const camera{Global.pCamera.Pos}; + if (m_hover >= 0 && m_hover < static_cast(m_layout->paths.size())) + { + auto const &points{m_layout->paths[m_hover].points}; + for (std::size_t i = 1; i < points.size(); ++i) + projection.line(drawlist, points[i - 1], points[i], kHovered, 4.0f); + } + for (auto const *track : m_layout->switches) + { + auto const start{track->m_paths.front().points[segment_data::point::start] + glm::dvec3{0.0, 1.0, 0.0}}; + ImVec2 screen; + if ((false == Global.EditorOrtho && glm::distance(start, camera) > kOverlayRange) || false == projection.project(start, screen)) + continue; + auto const text{track->name()}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + drawlist->AddRectFilled(ImVec2(screen.x - 3.0f, screen.y - size.y - 4.0f), ImVec2(screen.x + size.x + 3.0f, screen.y), IM_COL32(0, 0, 0, 160), 3.0f); + drawlist->AddText(ImVec2(screen.x, screen.y - size.y - 2.0f), kSwitch, text.c_str()); + } +} + +void srk_tool::render_found() +{ + if (main_button(STR_C("Find the stations of the scenery"), ImVec2(-1.0f, tall_button_height()))) + find_all(); + item_tooltip("Each group of switches joined by tracks shorter than 1.5 km is a station"); + if (m_found.empty()) + return; + ImGui::TextDisabled("%s", format(STR_P("%zu station", "%zu stations", m_found.size()), m_found.size()).c_str()); + if (false == ImGui::BeginChild("##srkfound", ImVec2(0.0f, std::min(300.0f, ImGui::GetTextLineHeightWithSpacing() * (m_found.size() + 1.0f))), true)) + { + ImGui::EndChild(); + return; + } + for (std::size_t index = 0; index < m_found.size(); ++index) + { + auto const &[name, count]{m_found[index]}; + auto const described{std::find_if(m_records.begin(), m_records.end(), [&](record const &Entry) { return Entry.anchor == name; })}; + auto label{format(STR_P("%s: %zu switch", "%s: %zu switches", count), name.c_str(), count)}; + if (described != m_records.end() && false == described->name.empty()) + label = described->name + " - " + label; + if (ImGui::Selectable((label + "##srkfound" + std::to_string(index)).c_str(), false)) + if (auto *track{simulation::Paths.find(name)}; track != nullptr) + { + open(*track); + m_selection.set_current_node(track); + m_picked = track; + m_view.focus_track(*track, 0, track->m_paths.front().points[segment_data::point::start]); + } + } + ImGui::EndChild(); +} + +void srk_tool::render_options() +{ + if (false == m_layout.has_value()) + { + ImGui::TextDisabled("%s", STR_C("The signalling of a station: its schematic, the numbers of the switches and the tracks, the track circuits and the signals")); + ImGui::Spacing(); + gesture_rows({{"LMB on a switch", "opens its station"}}); + if (false == m_status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); + ImGui::Spacing(); + render_found(); + return; + } + auto const &layout{*m_layout}; + ImGui::TextUnformatted(title().c_str()); + std::unordered_set tracks; + for (auto const &path : layout.paths) + if (false == path.approach) + tracks.insert(path.track); + metric_tiles({{format("%zu", layout.switches.size()), STR_C("switches"), kSwitch}, + {format("%zu", tracks.size() - layout.switches.size()), STR_C("tracks"), kPlain}, + {format("%.0f m", layout.to - layout.from), STR_C("length"), ImGui::GetColorU32(ImGuiCol_Text)}}); + if (begin_fields("##srkstation")) + { + auto const text = [&](char const *Label, char const *Id, std::string const &Value, char const *Hint, auto const &Set) { + field_row(Label); + std::array buffer{}; + Value.copy(buffer.data(), buffer.size() - 1); + if (ImGui::InputTextWithHint(Id, Hint, buffer.data(), buffer.size())) + { + Set(std::string{buffer.data()}); + m_changed = true; + } + }; + auto const *entry{m_record >= 0 ? &m_records[m_record] : nullptr}; + text("Name", "##srkname", entry != nullptr ? entry->name : std::string{}, "Biała Podlaska", [&](std::string const &Value) { record_of_open().name = Value; }); + text("Code", "##srkcode", entry != nullptr ? entry->code : std::string{}, "BPd", [&](std::string const &Value) { record_of_open().code = word_of(Value) == "-" ? std::string{} : word_of(Value); }); + item_tooltip("The abbreviation of the station: the names of its switches, track circuits and signals begin with it"); + text("Line", "##srkline", entry != nullptr ? entry->line : std::string{}, "2", [&](std::string const &Value) { record_of_open().line = word_of(Value) == "-" ? std::string{} : word_of(Value); }); + item_tooltip("The number of the railway line the station lies on, as the block sections between the stations are named after it"); + ImGui::EndTable(); + } + ImGui::Spacing(); + if (ImGui::CollapsingHeader(format("%s (%zu)###srkswitches", STR_C("Switches"), layout.switches.size()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + { + if (ImGui::BeginTable("##srkswitchlist", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(320.0f, ImGui::GetTextLineHeightWithSpacing() * (layout.switches.size() + 1.5f))))) + { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn(STR_C("Switch"), ImGuiTableColumnFlags_WidthStretch, 0.35f); + ImGui::TableSetupColumn(STR_C("Type"), ImGuiTableColumnFlags_WidthStretch, 0.45f); + ImGui::TableSetupColumn(STR_C("km"), ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableHeadersRow(); + auto *selected{m_selection.selected_track()}; + for (std::size_t index = 0; index < layout.switches.size(); ++index) + { + auto *track{layout.switches[index]}; + auto const start{track->m_paths.front().points[segment_data::point::start]}; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable((track->name() + "##srksw" + std::to_string(index)).c_str(), track == selected, ImGuiSelectableFlags_SpanAllColumns)) + { + m_selection.set_current_node(track); + m_picked = track; + m_view.focus_track(*track, 0, start); + } + ImGui::TableNextColumn(); + auto const record{switch_store::named(*track)}; + ImGui::TextUnformatted(record ? switch_store::label(*record).c_str() : track->eType == tt_Switch ? STR_C("switch") : track->eType == tt_Cross ? STR_C("crossing") : STR_C("turntable")); + ImGui::TableNextColumn(); + auto const along{station::project(layout, start).x}; + if (layout.kilometrage) + ImGui::Text("%.3f", station::km_of(layout, along)); + else + ImGui::Text("%.0f m", along); + } + ImGui::EndTable(); + } + } + ImGui::Spacing(); + if (false == m_status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); + if (ImGui::Button(STR_C("Another station"), ImVec2(-1.0f, 0.0f))) + { + m_layout.reset(); + m_anchor.clear(); + m_record = -1; + m_hover = -1; + } + item_tooltip("Leaves this station; LMB on a switch opens another one"); +} + +void srk_tool::fit_view(float const Width, float const Height) +{ + auto const &layout{*m_layout}; + auto &view{m_view_state}; + auto const span{std::max(50.0, layout.to - layout.from)}; + view.from = layout.from - span * 0.08 - 30.0; + view.to = layout.to + span * 0.08 + 30.0; + view.centre = (layout.low + layout.high) * 0.5; + auto const across{(view.to - view.from) / std::max(1.0f, Width)}; + auto const vertical{(layout.high - layout.low + 20.0) / std::max(1.0f, Height * 0.75f)}; + view.exaggeration = static_cast(std::clamp(across / vertical, 1.0, 500.0)); +} + +void srk_tool::render_canvas(float const Height) +{ + auto const &layout{*m_layout}; + auto &view{m_view_state}; + auto const &io{ImGui::GetIO()}; + ImVec2 const size{std::max(200.0f, ImGui::GetContentRegionAvail().x), std::max(80.0f, Height)}; + ImVec2 const corner{ImGui::GetCursorScreenPos()}; + ImGui::InvisibleButton("##srkcanvas", size, ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonMiddle); + bool const hovered{ImGui::IsItemHovered()}; + if (view.exaggeration <= 0.0f) + fit_view(size.x, size.y); + // the wheel zooms about the mouse, with Ctrl it exaggerates the offsets; a drag moves the plot + if (hovered && io.MouseWheel != 0.0f) + { + if (io.KeyCtrl) + view.exaggeration = std::clamp(view.exaggeration * std::pow(1.2f, io.MouseWheel), 1.0f, 500.0f); + else + { + auto const at{view.from + (io.MousePos.x - corner.x) / size.x * (view.to - view.from)}; + auto const factor{std::pow(0.8, static_cast(io.MouseWheel))}; + view.from = at - (at - view.from) * factor; + view.to = at + (view.to - at) * factor; + } + } + auto const across{(view.to - view.from) / size.x}; + auto const vertical{across / view.exaggeration}; + if (ImGui::IsItemActive() && (ImGui::IsMouseDragging(ImGuiMouseButton_Left, kDragPixels) || ImGui::IsMouseDragging(ImGuiMouseButton_Middle, kDragPixels))) + { + m_dragging = true; + view.from -= io.MouseDelta.x * across; + view.to -= io.MouseDelta.x * across; + view.centre += io.MouseDelta.y * vertical; + } + auto const x_of = [&](double const Chainage) { return corner.x + static_cast((Chainage - view.from) / across); }; + auto const y_of = [&](double const Offset) { return corner.y + size.y * 0.5f - static_cast((Offset - view.centre) / vertical); }; + auto const screen_of = [&](glm::dvec2 const &Point) { return ImVec2(x_of(Point.x), y_of(Point.y)); }; + + // the path under the mouse + if (hovered && false == m_dragging) + { + m_hover = -1; + auto nearest{7.0f * 7.0f}; + auto const &mouse{io.MousePos}; + for (int index = 0; index < static_cast(layout.paths.size()); ++index) + { + auto const &points{layout.paths[index].schematic}; + for (std::size_t i = 1; i < points.size(); ++i) + { + auto const a{screen_of(points[i - 1])}; + auto const b{screen_of(points[i])}; + auto const dx{b.x - a.x}; + auto const dy{b.y - a.y}; + auto const length2{dx * dx + dy * dy}; + auto const t{length2 > 0.0f ? std::clamp(((mouse.x - a.x) * dx + (mouse.y - a.y) * dy) / length2, 0.0f, 1.0f) : 0.0f}; + auto const ex{a.x + t * dx - mouse.x}; + auto const ey{a.y + t * dy - mouse.y}; + if (ex * ex + ey * ey < nearest) + { + nearest = ex * ex + ey * ey; + m_hover = index; + } + } + } + } + else if (false == hovered && false == ImGui::IsItemActive()) + m_hover = -1; + if (ImGui::IsItemDeactivated()) + { + if (false == m_dragging && m_hover >= 0) + { + auto const &path{layout.paths[m_hover]}; + m_selection.set_current_node(path.track); + m_picked = path.track; + m_view.focus_track(*path.track, path.index, path.points[path.points.size() / 2]); + } + m_dragging = false; + } + + auto *draw{ImGui::GetWindowDrawList()}; + ImVec2 const corner_end{corner.x + size.x, corner.y + size.y}; + draw->AddRectFilled(corner, corner_end, IM_COL32(22, 24, 28, 255)); + draw->PushClipRect(corner, corner_end, true); + // the km, or the chainage, along the top + { + auto const metres{across * 90.0}; + double step{10.0}; + for (auto const candidate : {10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 2000.0, 5000.0}) + if ((step = candidate) >= metres) + break; + auto const origin{layout.kilometrage ? layout.km * 1000.0 : 0.0}; + for (auto mark{std::ceil((view.from + origin) / step) * step}; mark <= view.to + origin; mark += step) + { + auto const x{x_of(mark - origin)}; + draw->AddLine(ImVec2(x, corner.y), ImVec2(x, corner_end.y), IM_COL32(255, 255, 255, 16)); + auto const label{layout.kilometrage ? format("%.3f", mark / 1000.0) : format("%.0f m", mark)}; + draw->AddText(ImVec2(x + 3.0f, corner.y + 2.0f), IM_COL32(150, 155, 165, 255), label.c_str()); + } + } + auto const *selected{m_selection.selected_track()}; + auto const draw_path = [&](station::path const &Path, ImU32 const Colour, float const Width) { + for (std::size_t i = 1; i < Path.schematic.size(); ++i) + draw->AddLine(screen_of(Path.schematic[i - 1]), screen_of(Path.schematic[i]), Colour, Width); + }; + for (auto const &path : layout.paths) + if (path.approach) + draw_path(path, kApproach, 2.0f); + for (auto const &path : layout.paths) + if (false == path.approach) + draw_path(path, is_switch(*path.track) ? kSwitch : kPlain, 2.5f); + for (auto const &path : layout.paths) + if (path.track == selected) + draw_path(path, overlay_color::selected, 4.0f); + if (m_hover >= 0) + draw_path(layout.paths[m_hover], kHovered, 4.0f); + // the switches: a dot at the start, the name over or under it in turn + for (std::size_t index = 0; index < layout.switches.size(); ++index) + { + auto const *track{layout.switches[index]}; + auto const at{screen_of(station::project(layout, track->m_paths.front().points[segment_data::point::start]))}; + draw->AddCircleFilled(at, 3.5f, kSwitch); + auto const text{track->name()}; + auto const extent{ImGui::CalcTextSize(text.c_str())}; + ImVec2 const place{at.x - extent.x * 0.5f, index % 2 == 0 ? at.y - extent.y - 6.0f : at.y + 6.0f}; + draw->AddRectFilled(ImVec2(place.x - 2.0f, place.y - 1.0f), ImVec2(place.x + extent.x + 2.0f, place.y + extent.y + 1.0f), IM_COL32(22, 24, 28, 200), 2.0f); + draw->AddText(place, kSwitch, text.c_str()); + } + draw->PopClipRect(); + if (hovered && m_hover >= 0 && false == m_dragging) + { + auto const &path{layout.paths[m_hover]}; + auto const along{view.from + (io.MousePos.x - corner.x) * across}; + ImGui::BeginTooltip(); + ImGui::TextUnformatted(path.track->name().empty() ? STR_C("(no name)") : path.track->name().c_str()); + ImGui::TextDisabled("%s, %.1f m", is_switch(*path.track) ? STR_C("switch") : path.approach ? STR_C("line out of the station") : STR_C("track"), path.length); + if (layout.kilometrage) + ImGui::TextDisabled("km %.3f", station::km_of(layout, along)); + ImGui::TextDisabled("%s", STR_C("LMB: select it")); + ImGui::EndTooltip(); + } +} + +void srk_tool::render_windows() +{ + if (false == m_open || false == m_layout.has_value()) + return; + auto const &io{ImGui::GetIO()}; + // along the bottom of the 3d view, as the vertical profile + auto const low{editor_ui::view_min()}; + auto const high{editor_ui::view_max()}; + auto const width{std::max(480.0f, high.x - low.x)}; + auto const height{std::clamp((high.y - low.y) * 0.4f, 220.0f, std::max(220.0f, high.y - low.y - 80.0f))}; + ImGui::SetNextWindowPos(ImVec2(low.x, high.y - height), ImGuiCond_Appearing); + ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Appearing); + auto const name{std::string{STR_C("Station schematic")} + " " + title() + "###srkstrip"}; + if (ImGui::Begin(name.c_str(), nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) + { + auto const position{ImGui::GetWindowPos()}; + auto const size{ImGui::GetWindowSize()}; + if (false == ImGui::IsWindowDocked() && (position.y + size.y > io.DisplaySize.y + 1.0f || position.x + 80.0f > io.DisplaySize.x || position.x + size.x < 80.0f)) + ImGui::SetWindowPos(ImVec2(std::clamp(position.x, 0.0f, std::max(0.0f, io.DisplaySize.x - size.x)), std::max(0.0f, std::min(position.y, io.DisplaySize.y - size.y)))); + if (ImGui::Button(STR_C("Fit"))) + m_view_state.exaggeration = 0.0f; + item_tooltip("Shows the whole station"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(140.0f); + ImGui::SliderFloat("##srkexaggeration", &m_view_state.exaggeration, 1.0f, 500.0f, STR_C("offsets x%.0f"), ImGuiSliderFlags_Logarithmic); + item_tooltip("How much the offsets of the tracks are stretched against the lengths, also Ctrl+wheel over the plot"); + ImGui::SameLine(); + ImGui::TextDisabled("%s", STR_C("Wheel: zoom, drag: move, LMB on a track: select it")); + render_canvas(ImGui::GetContentRegionAvail().y); + } + ImGui::End(); +} + +editor_selftest_result srk_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + if (Command == "srkopen") + { + // srkopen track=: the station of the track, as a click on it opens it + auto const values{selftest_values(Words)}; + auto const found{values.find("track")}; + auto *track{found != values.end() ? simulation::Paths.find(found->second) : nullptr}; + m_open = true; + load(); + if (track == nullptr) + { + WriteLog("SELFTEST srkopen: no such track"); + return editor_selftest_result::handled; + } + open(*track); + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkopen: " + m_status); + return editor_selftest_result::handled; + } + auto const &layout{*m_layout}; + std::unordered_set tracks; + std::size_t approach{0}; + for (auto const &path : layout.paths) + if (path.approach) + ++approach; + else + tracks.insert(path.track); + WriteLog(format("SELFTEST srkopen %s: %zu switches, %zu tracks, %zu paths out, axis %.0f m, switches over %.0f..%.0f, offsets %.1f..%.1f, km %s, record %d", m_anchor.c_str(), layout.switches.size(), tracks.size(), approach, + layout.chainage.back(), layout.from, layout.to, layout.low, layout.high, layout.kilometrage ? format("%.3f", layout.km).c_str() : "-", m_record)); + for (auto const *track : layout.switches) + { + auto const at{station::project(layout, track->m_paths.front().points[segment_data::point::start])}; + WriteLog(format("SELFTEST srkopen switch %s at %.1f offset %.1f", track->name().c_str(), at.x, at.y)); + } + return editor_selftest_result::handled; + } + if (Command == "srkstations") + { + load(); + find_all(); + WriteLog(format("SELFTEST srkstations: %zu", m_found.size())); + for (auto const &[name, count] : m_found) + WriteLog(format("SELFTEST srkstations %s: %zu switches", name.c_str(), count)); + return editor_selftest_result::handled; + } + if (Command == "srkset") + { + // srkset code= line= name= + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkset: no station open"); + return editor_selftest_result::handled; + } + auto const values{selftest_values(Words)}; + auto &entry{record_of_open()}; + if (auto const code{values.find("code")}; code != values.end()) + entry.code = code->second; + if (auto const line{values.find("line")}; line != values.end()) + entry.line = line->second; + if (auto const name{values.find("name")}; name != values.end()) + { + entry.name = name->second; + std::replace(entry.name.begin(), entry.name.end(), '_', ' '); + } + m_changed = true; + WriteLog("SELFTEST srkset: " + title()); + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} diff --git a/application/editormodesrk.h b/application/editormodesrk.h new file mode 100644 index 00000000..2460bda5 --- /dev/null +++ b/application/editormodesrk.h @@ -0,0 +1,81 @@ +/* +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/editortool.h" +#include "editor/editorStation.hpp" +#include "scene/scenelayers.h" + +#include +#include +#include +#include + +// the signalling of a station (SRK): the station found from a switch of it, its schematic, its name and code +class srk_tool final : public editor_tool +{ + public: + srk_tool(editor_selection &Selection, editor_view &View, editor_history &History) : m_selection{Selection}, m_view{View}, m_history{History} {} + void update() override; + void draw_overlay() const override; + void render_options() override; + void render_windows() override; + void forget(scene::basic_node const *Node) override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + void set_open(bool const Open) { m_open = Open; } + // the stations described go to the files of their layers, as "//$s" lines + void store(); + + private: + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + // a station described: found again by a switch of it + struct record + { + scene::layer_handle layer{null_handle}; + std::string anchor; // the name of a switch of the station + std::string code; // the abbreviation of the station, as MMz + std::string name; + std::string line; // the number of the railway line + }; + // the schematic: the chainage across, the offsets upwards, exaggerated + struct view + { + double from{0.0}; + double to{1000.0}; + double centre{0.0}; + float exaggeration{0.0f}; // 0: fit at the next draw + }; + bool m_open{false}; + bool m_loaded{false}; + bool m_changed{false}; + std::vector m_records; + std::optional m_layout; + int m_record{-1}; // of the open station, -1 while it has none + std::string m_anchor; // the switch the open station was found from + std::size_t m_history_size{0}; + TTrack const *m_picked{nullptr}; // the track selected when the station was looked for + view m_view_state; + int m_hover{-1}; // path of the layout under the mouse in the schematic + bool m_dragging{false}; + std::vector> m_found; // the stations of the scenery: a switch and the count of them + std::string m_status; + + void load(); + void open(TTrack &Track); + void refresh(); + record &record_of_open(); + std::string title() const; + void render_found(); + void render_canvas(float const Height); + void fit_view(float const Width, float const Height); + void find_all(); +}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index bdb4d216..7da24b60 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -411,9 +411,9 @@ 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", editor_icons::icon::select, 0}, {"Lay track", "L", track_tab::lay, "Lays new track through the clicked points, on the ground or from a free end of a track", editor_icons::icon::lay_track, 0}, {"Plan", "G", track_tab::straights, "The plan of the line laid: LMB on a straight drags its ends or the middle, on a curve its vertex and radius. C works too", editor_icons::icon::curve, 0}, @@ -421,6 +421,7 @@ std::array const &editor_mode::track_modes() {"Parallel", "N", track_tab::parallel, "A second track beside a track: LMB on it, then the side and the spacing", editor_icons::icon::parallel, 0}, {"Turntable", "U", track_tab::turntable, "Places a turntable, leads tracks out of it and fits the tracks around to it", editor_icons::icon::turntable, 1}, {"Signals", "H", track_tab::signals, "Signals by the track, from the templates of the scenery, with the event the train reads them by", editor_icons::icon::signal, 1}, + {"Signalling", "I", track_tab::srk, "The signalling of a station: LMB on a switch lays its station out as a schematic", editor_icons::icon::station, 1}, {"Objects", "B", track_tab::lineside, "Along the track: hectometre posts, fouling point markers, trainsets", editor_icons::icon::objects, 1}, {"Profile", "P", track_tab::profile, "Vertical profile (grade line) along the line", editor_icons::icon::profile, 2}, {"Speed", "V", track_tab::speed, "Speed limits of the paths against the speed their geometry allows", editor_icons::icon::speed, 2}, @@ -565,6 +566,7 @@ std::string editor_mode::track_mode_name() const case track_tab::infra: return STR_C("Infrastructure"); case track_tab::parallel: return STR_C("Parallel track"); case track_tab::signals: return STR_C("Signal"); + case track_tab::srk: return STR_C("Signalling"); case track_tab::turntable: return STR_C("Turntable"); default: return STR_C("Select"); } @@ -577,6 +579,7 @@ void editor_mode::render_track_inspector() m_speed.set_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.set_open(m_track_window_open && m_track_tab == track_tab::joints); + m_srk.set_open(m_track_window_open && m_track_tab == track_tab::srk); if (false == m_track_window_open || m_track_tab != track_tab::lineside) { m_sweep.open = false; @@ -618,6 +621,7 @@ void editor_mode::render_track_inspector() m_profile.strip_top = -1.0f; if (m_track_tab == track_tab::profile && m_track_window_open) render_profile_strip(); + m_srk.render_windows(); } void editor_mode::render_track_tool_options() @@ -659,6 +663,7 @@ void editor_mode::render_track_tool_options() break; case track_tab::lineside: render_lineside_ui(); break; case track_tab::signals: render_signal_ui(); break; + case track_tab::srk: m_srk.render_options(); break; case track_tab::turntable: render_turntable_ui(); break; default: render_track_set_ui(); diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index f40348d3..1c562ed1 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -458,6 +458,13 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) } return intent; } + if (m_track_tab == track_tab::srk && m_hover.track != nullptr) + { + intent.track = m_hover.track; + intent.path = m_hover.path; + intent.position = m_hover.point; + return make(kind::select, m_hover.track->eType != tt_Normal ? "Click: the station of this switch" : "Click: the station of the switch nearest along this track"); + } if (m_track_style.picking && (m_track_tab == track_tab::path || m_track_tab == track_tab::turnout)) { intent.track = m_hover.track; diff --git a/application/editormodeviewport.cpp b/application/editormodeviewport.cpp index 1bbed3fa..3f9887f7 100644 --- a/application/editormodeviewport.cpp +++ b/application/editormodeviewport.cpp @@ -177,10 +177,10 @@ void editor_mode::work_area_tools(std::vector &Buttons) add(track_tab::straights, false); add(track_tab::turnout, false); add(track_tab::parallel, false); - auto const advanced{m_track_tab == track_tab::turntable || m_track_tab == track_tab::signals || m_track_tab == track_tab::lineside || m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || + auto const advanced{m_track_tab == track_tab::turntable || m_track_tab == track_tab::signals || m_track_tab == track_tab::srk || m_track_tab == track_tab::lineside || m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || m_track_tab == track_tab::joints || m_gauge.is_open()}; auto const more{EditorSettings.track_tools_more()}; - Buttons.push_back({icon::more, more ? STR_C("Less") : STR_C("More"), nullptr, STR_C("Turntable, signals, objects, trainset, profile, speed, joints, gauge"), more, [more]() { + Buttons.push_back({icon::more, more ? STR_C("Less") : STR_C("More"), nullptr, STR_C("Turntable, signals, signalling, objects, trainset, profile, speed, joints, gauge"), more, [more]() { EditorSettings.track_tools_more(false == more); EditorSettings.save(); }, true}); @@ -188,6 +188,7 @@ void editor_mode::work_area_tools(std::vector &Buttons) break; add(track_tab::turntable, true); add(track_tab::signals, false); + add(track_tab::srk, false); add(track_tab::lineside, false); Buttons.push_back({icon::vehicle, STR_C("Trainset"), nullptr, STR_C("A trainset on the selected track: written to the scenery on save, and to drive at once"), vehicle, [this, vehicle]() { show_track_tab(vehicle ? track_tab::path : track_tab::lineside); diff --git a/application/editortool.h b/application/editortool.h index f93503f8..744a7646 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -36,7 +36,7 @@ enum class editor_selftest_result enum class editor_first_step { tracks, lay, laid, wheel, join, turnout, undo, save, drive, look, top, select, shape, profile, profile_point, profile_apply, terrain, nmt, plan, plan_band, plan_apply, signal, posts, count }; -enum class editor_track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals, parallel }; +enum class editor_track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals, parallel, srk }; struct track_hover { diff --git a/editor/editorIcons.hpp b/editor/editorIcons.hpp index eec9f4a8..8b583b40 100644 --- a/editor/editorIcons.hpp +++ b/editor/editorIcons.hpp @@ -51,6 +51,7 @@ enum class icon gauge, turntable, parallel, + station, more, // the fields of work work_surroundings, diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp new file mode 100644 index 00000000..c60aa4a0 --- /dev/null +++ b/editor/editorStation.cpp @@ -0,0 +1,399 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorStation.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorSwitchStore.hpp" +#include "editor/editorTrack.hpp" +#include "simulation/simulation.h" +#include "world/Track.h" + +#include +#include +#include +#include +#include + +namespace station +{ + +using geometry::bezier; +using geometry::plan_distance; +using geometry::same_point; + +namespace +{ + +bool rail(TTrack const &Track) +{ + return (Track.iCategoryFlag & 15) == 1 && false == Track.m_editorremoved && false == Track.m_paths.empty(); +} + +// switches, crossings and turntables: where the plain track between them ends +bool node(TTrack const &Track) +{ + return Track.eType != tt_Normal; +} + +glm::dvec3 end_of(TTrack const &Track, int const Path, bool const Atend) +{ + return Track.m_paths[Path].points[Atend ? segment_data::point::end : segment_data::point::start]; +} + +TTrack *neighbour(TTrack const &Track, int const Path, bool const Atend) +{ + if (Track.SwitchExtension != nullptr && (Track.eType == tt_Switch || Track.eType == tt_Cross)) + return Atend ? Track.SwitchExtension->pNexts[Path & 1] : Track.SwitchExtension->pPrevs[Path & 1]; // NOSONAR + return Atend ? Track.trNext : Track.trPrev; +} + +// the end of the track at the point +bool end_at(TTrack const &Track, glm::dvec3 const &Point, int &Path, bool &Atend) +{ + for (int path = 0; path < static_cast(Track.m_paths.size()); ++path) + for (auto const atend : {false, true}) + if (glm::distance(end_of(Track, path, atend), Point) < same_point) + { + Path = path; + Atend = atend; + return true; + } + return false; +} + +// the ends of the paths of a switch, each place once: its start, the ends of its paths +std::vector> ends_of(TTrack const &Track) +{ + std::vector> result; + for (int path = 0; path < static_cast(Track.m_paths.size()); ++path) + for (auto const atend : {false, true}) + { + auto const point{end_of(Track, path, atend)}; + if (std::none_of(result.begin(), result.end(), [&](auto const &End) { return glm::distance(end_of(Track, End.first, End.second), point) < same_point; })) + result.emplace_back(path, atend); + } + return result; +} + +double plan_length(TTrack const &Track, int const Path) +{ + return bezier{Track.m_paths[Path]}.plan_length(16); +} + +// plain track from an end of a switch on, up to the next switch, a free end or past the limit +struct chain +{ + std::vector tracks; + double length{0.0}; + TTrack *reached{nullptr}; // the switch met at its other end + bool ended{false}; // at a free end +}; + +chain follow(TTrack const &From, int const Path, bool const Atend, double const Limit) +{ + chain result; + auto point{end_of(From, Path, Atend)}; + auto *next{neighbour(From, Path, Atend)}; + while (next != nullptr && result.length <= Limit) + { + if (false == rail(*next)) + break; + if (node(*next)) + { + result.reached = next; + return result; + } + int path{0}; + bool atend{false}; + if (false == end_at(*next, point, path, atend) || std::find(result.tracks.begin(), result.tracks.end(), next) != result.tracks.end()) + break; + result.tracks.push_back(next); + result.length += plan_length(*next, 0); + point = end_of(*next, 0, false == atend); + next = neighbour(*next, 0, false == atend); + } + result.ended = next == nullptr || false == rail(*next); + return result; +} + +// the switches and the plain tracks of the station, and the lines leading out of it +struct area +{ + std::vector switches; + std::vector plain; + std::vector exits; +}; + +area collect(TTrack &From, options const &Options) +{ + area result; + if (false == rail(From)) + return result; + auto *first{node(From) ? &From : nullptr}; + if (first == nullptr) + { + auto nearest{Options.link}; + for (auto const atend : {false, true}) + if (auto const line{follow(From, 0, atend, Options.link)}; line.reached != nullptr && line.length < nearest) + { + nearest = line.length; + first = line.reached; + } + } + if (first == nullptr) + return result; + std::unordered_set known{first}; + result.switches.push_back(first); + for (std::size_t i = 0; i < result.switches.size(); ++i) + { + auto const &from{*result.switches[i]}; + for (auto const &[path, atend] : ends_of(from)) + { + auto line{follow(from, path, atend, Options.link)}; + if (line.reached == nullptr && false == line.ended) + { + result.exits.push_back(std::move(line)); + continue; + } + for (auto *track : line.tracks) + if (known.insert(track).second) + result.plain.push_back(track); + if (line.reached != nullptr && known.insert(line.reached).second) + result.switches.push_back(line.reached); + } + } + return result; +} + +std::vector points_of(TTrack const &Track, int const Path, double &Length) +{ + bezier const curve{Track.m_paths[Path]}; + Length = curve.plan_length(16); + auto const count{std::clamp(static_cast(Length / 2.0), 2, 64)}; + std::vector result; + for (int i = 0; i <= count; ++i) + result.push_back(curve.point(static_cast(i) / count)); + return result; +} + +// the map shows the east to the right and the north up: the x of the scenery runs to the west +glm::dvec2 map_of(glm::dvec3 const &Point) +{ + return {-Point.x, Point.z}; +} + +} // namespace + +std::optional find(TTrack &From, options const &Options) +{ + auto const found{collect(From, Options)}; + if (found.switches.empty()) + return std::nullopt; + std::unordered_set switches(found.switches.begin(), found.switches.end()); + // the axis: the run through the station along the main paths of its switches which passes the most of them + std::vector starts; + for (auto const &line : found.exits) + if (false == line.tracks.empty()) + starts.push_back(line.tracks.front()); + if (starts.empty()) + starts.push_back(found.switches.front()); + editor_track::route best; + std::size_t most{0}; + double longest{0.0}; + for (auto *start : starts) + { + auto route{editor_track::run_route(*start, 20000.0)}; + std::size_t passed{0}; + double length{0.0}; + for (auto const &span : route.spans) + { + passed += switches.count(span.track); + length += plan_length(*span.track, span.path); + } + if (passed > most || (passed == most && length > longest)) + { + most = passed; + longest = length; + best = std::move(route); + } + } + layout result; + for (auto const &span : best.spans) + { + double length{0.0}; + auto points{points_of(*span.track, span.path, length)}; + if (false == span.forward) + std::reverse(points.begin(), points.end()); + for (auto const &point : points) + if (result.axis.empty() || plan_distance(result.axis.back(), point) > 0.01) + result.axis.push_back(point); + } + if (result.axis.size() < 2) + return std::nullopt; + auto const measure = [&]() { + result.chainage.assign(1, 0.0); + for (std::size_t i = 1; i < result.axis.size(); ++i) + result.chainage.push_back(result.chainage.back() + plan_distance(result.axis[i - 1], result.axis[i])); + }; + measure(); + // the chainage runs with the km of the switches, when they have one and most of them agree on it + double constexpr kAgree{50.0}; // m + std::vector> marks; // the chainage and the km, in m + for (auto *track : found.switches) + { + auto const start{end_of(*track, 0, false)}; + std::optional km; + if (auto const record{switch_store::named(*track)}; record && record->km) + km = record->km; + else + km = alignment_store::chainage_at(start); + if (km) + marks.emplace_back(project(result, start).x, *km); + } + auto const length{result.chainage.back()}; + struct fit + { + std::size_t agree{0}; + double origin{0.0}; + }; + auto const fit_of = [&](bool const Reversed) { + std::vector origins; + for (auto const &[chainage, km] : marks) + origins.push_back(km - (Reversed ? length - chainage : chainage)); + std::nth_element(origins.begin(), origins.begin() + origins.size() / 2, origins.end()); + fit result{0, origins[origins.size() / 2]}; + for (auto const origin : origins) + result.agree += std::abs(origin - result.origin) <= kAgree ? 1 : 0; + return result; + }; + if (false == marks.empty()) + { + auto const along{fit_of(false)}; + auto const against{fit_of(true)}; + auto const reversed{against.agree > along.agree}; + auto const &best{reversed ? against : along}; + if (best.agree * 2 >= marks.size()) + { + if (reversed) + { + std::reverse(result.axis.begin(), result.axis.end()); + measure(); + } + result.kilometrage = true; + result.km = best.origin / 1000.0; + } + } + // the paths, laid out along the axis + auto const add = [&](TTrack &Track, bool const Approach) { + for (int index = 0; index < static_cast(Track.m_paths.size()); ++index) + { + path entry; + entry.track = &Track; + entry.index = index; + entry.approach = Approach; + entry.points = points_of(Track, index, entry.length); + for (auto const &point : entry.points) + entry.schematic.push_back(project(result, point)); + result.paths.push_back(std::move(entry)); + } + }; + for (auto *track : found.switches) + add(*track, false); + for (auto *track : found.plain) + add(*track, false); + for (auto const &line : found.exits) + { + double length{0.0}; + for (auto *track : line.tracks) + { + if (length >= Options.approach) + break; + add(*track, true); + length += plan_length(*track, 0); + } + } + result.from = std::numeric_limits::max(); + result.to = std::numeric_limits::lowest(); + result.low = std::numeric_limits::max(); + result.high = std::numeric_limits::lowest(); + for (auto const &entry : result.paths) + { + if (entry.approach) + continue; + for (auto const &point : entry.schematic) + { + if (switches.count(entry.track) > 0) + { + result.from = std::min(result.from, point.x); + result.to = std::max(result.to, point.x); + } + result.low = std::min(result.low, point.y); + result.high = std::max(result.high, point.y); + } + } + result.switches = found.switches; + std::sort(result.switches.begin(), result.switches.end(), [&](TTrack const *A, TTrack const *B) { return project(result, end_of(*A, 0, false)).x < project(result, end_of(*B, 0, false)).x; }); + return result; +} + +glm::dvec2 project(layout const &Layout, glm::dvec3 const &Point) +{ + auto const point{map_of(Point)}; + auto best{std::numeric_limits::max()}; + glm::dvec2 result{0.0}; + for (std::size_t i = 1; i < Layout.axis.size(); ++i) + { + auto const a{map_of(Layout.axis[i - 1])}; + auto const b{map_of(Layout.axis[i])}; + auto const along{b - a}; + auto const length{glm::length(along)}; + if (length < 1e-9) + continue; + auto const unit{along / length}; + auto const t{std::clamp(glm::dot(point - a, unit), 0.0, length)}; + auto const foot{a + unit * t}; + auto const distance{glm::distance(point, foot)}; + if (distance >= best) + continue; + best = distance; + auto const side{point - foot}; + result = {Layout.chainage[i - 1] + t, unit.x * side.y - unit.y * side.x}; + } + return result; +} + +double km_of(layout const &Layout, double const Chainage) +{ + return Layout.km + Chainage / 1000.0; +} + +std::vector stations(options const &Options) +{ + std::vector result; + std::unordered_set taken; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == rail(*track) || false == node(*track) || taken.count(track) > 0) + continue; + auto const found{collect(*track, Options)}; + for (auto const *member : found.switches) + taken.insert(member); + result.push_back(track); + } + return result; +} + +std::vector switches_of(TTrack &Switch, options const &Options) +{ + return collect(Switch, Options).switches; +} + +} // namespace station diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp new file mode 100644 index 00000000..96c283a0 --- /dev/null +++ b/editor/editorStation.hpp @@ -0,0 +1,65 @@ +/* +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 + +class TTrack; + +// a station as the signalling sees it: its switches, the tracks between them and the lines leading out, and its schematic, +// the tracks laid out along the main track through it +namespace station +{ + +struct options +{ + double link{1500.0}; // m, switches joined by plain track no longer than this belong to the same station + double approach{400.0}; // m, of each line leading out, taken beyond the outermost switch +}; + +struct path +{ + TTrack *track{nullptr}; + int index{0}; // of the path of the track + std::vector points; // along the path, world space + std::vector schematic; // the chainage along the axis and the offset to the left of it, as the map shows it + double length{0.0}; // in the plan + bool approach{false}; // on a line leading out of the station +}; + +struct layout +{ + std::vector switches; // switches, crossings and turntables, by the chainage of their start + std::vector paths; + std::vector axis; // the main route through the station + std::vector chainage; // of the points of the axis + double from{0.0}; // the chainage over which the switches lie + double to{0.0}; + double low{0.0}; // the offsets of the outermost tracks of the station + double high{0.0}; + bool kilometrage{false}; // the chainage runs with the km of the switches + double km{0.0}; // at chainage 0, when it does +}; + +// the station of the switch, or of the plain track near one +std::optional find(TTrack &From, options const &Options = {}); +// the chainage along the axis and the offset to the left of it, as the map shows it +glm::dvec2 project(layout const &Layout, glm::dvec3 const &Point); +// km at the chainage, when the station knows its kilometrage +double km_of(layout const &Layout, double const Chainage); +// one switch of each station of the scenery +std::vector stations(options const &Options = {}); +// the switches of the station of the switch, without laying it out +std::vector switches_of(TTrack &Switch, options const &Options = {}); + +} // namespace station diff --git a/editor/icons/editor_icons.png b/editor/icons/editor_icons.png index affe75b5021dd99818f350ec3adb81cf0c483fa3..c720314949e1c43ffdcc2d39690640ffc635a039 100644 GIT binary patch literal 47042 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

+94g;`Ri|;SNhUC#~ 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 JYSgU5{tsfq-rxWL literal 46348 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 z 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 z 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 diff --git a/editor/icons/editor_icons.py b/editor/icons/editor_icons.py index a1756b34..3015a1a0 100644 --- a/editor/icons/editor_icons.py +++ b/editor/icons/editor_icons.py @@ -331,6 +331,15 @@ def parallel(d): arrowhead(d, 40, 32, 0, 10) +def station(d): + # two tracks of a station with a crossover between them, and the insulated joints of its track circuits + line(d, [(4, 22), (60, 22)], W - 6) + line(d, [(4, 46), (60, 46)], W - 6) + line(d, [(14, 46), (32, 22)], W - 6) + line(d, [(46, 12), (46, 32)], W - 10) + line(d, [(46, 36), (46, 56)], W - 10) + + def more(d): for x in (12, 32, 52): circle(d, x, 32, 6) @@ -381,7 +390,7 @@ def work_terrain(d): ICONS = [select, insert, brush, area_fill, copy_to_bank, translate, rotate, scale, local_space, ortho, perspective, sculpt, smooth, chunks, orthophoto, road, place, lanes, lay_track, switch, straight, curve, signal, objects, - vehicle, profile, speed, joints, infra, gauge, turntable, parallel, more, work_surroundings, + vehicle, profile, speed, joints, infra, gauge, turntable, parallel, station, more, work_surroundings, work_tracks, work_roads, work_terrain] diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 4a2b9083..58b5fc97 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5981,8 +5981,8 @@ msgstr "Rozjazd krzyżowy pojedynczy (Rkp) albo podwójny (Rkpd) na skrzyżowani msgid "Crossover, double, scissors, ladder" msgstr "Przejście rozjazdowe, podwójne przejście rozjazdowe, nożyce, ulica zwrotnicowa" -msgid "Turntable, signals, objects, trainset, profile, speed, joints, gauge" -msgstr "Obrotnica, sygnalizatory, obiekty, skład, profil, prędkość, styki, skrajnia" +msgid "Turntable, signals, signalling, objects, trainset, profile, speed, joints, gauge" +msgstr "Obrotnica, sygnalizatory, SRK, obiekty, skład, profil, prędkość, styki, skrajnia" msgid "No switch drive models" msgstr "Brak modeli napędów" @@ -10270,3 +10270,98 @@ msgstr "Wyłączone: tor kładziony od innego toru albo dołączany do niego bie msgid "Off: a track laid on from another one, or joined to it, is designed for the speed of that track and takes it.\nOn: it's designed for the speed above and the new tracks get it.\nWhile laying: Ctrl+Shift+wheel" msgstr "Wyłączone: tor kładziony od innego toru albo dołączany do niego jest projektowany na prędkość tamtego toru i ją dostaje.\nWłączone: jest projektowany na prędkość z pola powyżej i nowe tory ją dostają.\nW trakcie kładzenia: Ctrl+Shift+kółko" + +# scenery editor: the signalling of a station (SRK) +msgid "Signalling" +msgstr "SRK" + +msgid "The signalling of a station: LMB on a switch lays its station out as a schematic" +msgstr "Sterowanie ruchem na stacji: LPM na rozjazd rozkłada jego stację w schemat" + +msgid "Click: the station of this switch" +msgstr "Klik: stacja tego rozjazdu" + +msgid "Click: the station of the switch nearest along this track" +msgstr "Klik: stacja najbliższego rozjazdu wzdłuż tego toru" + +msgid "No switch near this track: it isn't a part of a station" +msgstr "Przy tym torze nie ma rozjazdu: nie należy do żadnej stacji" + +msgid "Station of %s" +msgstr "Stacja rozjazdu %s" + +msgid "Find the stations of the scenery" +msgstr "Znajdź stacje w scenerii" + +msgid "Each group of switches joined by tracks shorter than 1.5 km is a station" +msgstr "Stacją jest każda grupa rozjazdów połączonych torami krótszymi niż 1.5 km" + +msgid "The signalling of a station: its schematic, the numbers of the switches and the tracks, the track circuits and the signals" +msgstr "SRK stacji: jej schemat, numery rozjazdów i torów, izolacje i semafory" + +msgid "LMB on a switch" +msgstr "LPM na rozjazd" + +msgid "opens its station" +msgstr "otwiera jego stację" + +msgid "switches" +msgstr "rozjazdy" + +msgid "Code" +msgstr "Skrót" + +msgid "The abbreviation of the station: the names of its switches, track circuits and signals begin with it" +msgstr "Skrót nazwy stacji: od niego zaczynają się nazwy jej rozjazdów, izolacji i semaforów" + +msgid "Line" +msgstr "Linia" + +msgid "The number of the railway line the station lies on, as the block sections between the stations are named after it" +msgstr "Numer linii kolejowej, na której leży stacja: od niego biorą nazwy odstępy blokady między stacjami" + +msgid "Type" +msgstr "Typ" + +msgid "km" +msgstr "km" + +msgid "Another station" +msgstr "Inna stacja" + +msgid "Leaves this station; LMB on a switch opens another one" +msgstr "Zamyka tę stację; LPM na rozjazd otwiera inną" + +msgid "Station schematic" +msgstr "Schemat stacji" + +msgid "Shows the whole station" +msgstr "Pokazuje całą stację" + +msgid "offsets x%.0f" +msgstr "rozstawy x%.0f" + +msgid "How much the offsets of the tracks are stretched against the lengths, also Ctrl+wheel over the plot" +msgstr "Ile razy rozstawy torów są rozciągnięte względem długości, także Ctrl+kółko nad schematem" + +msgid "Wheel: zoom, drag: move, LMB on a track: select it" +msgstr "Kółko: przybliżenie, przeciąganie: przesunięcie, LPM na tor: zaznacza go" + +msgid "line out of the station" +msgstr "szlak wychodzący ze stacji" + +msgid "LMB: select it" +msgstr "LPM: zaznacza go" + +msgid "%zu station" +msgid_plural "%zu stations" +msgstr[0] "%zu stacja" +msgstr[1] "%zu stacje" +msgstr[2] "%zu stacji" + +msgid "%s: %zu switch" +msgid_plural "%s: %zu switches" +msgstr[0] "%s: %zu rozjazd" +msgstr[1] "%s: %zu rozjazdy" +msgstr[2] "%s: %zu rozjazdów" + diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index 561ccdd0..c7163477 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -843,16 +843,17 @@ bool node_layers::pending() const namespace { // marks of the lines the editor keeps track of -std::array const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c", "//$o"}; +std::array const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c", "//$o", "//$s"}; // other tools put lines of their own after some of these marks: Rainsted its includes after //$r, the launcher the descriptions // of the trainsets after //$o. The editor's lines of them are told by their first word bool editor_line(char const *Mark, std::string const &Text) { - static std::array>, 3> const words{{ + static std::array>, 4> const words{{ {"//$h", {"#", "alignment", "source", "ends", "norms", "pieces", "vertex", "element", "end"}}, {"//$r", {"#", "switch", "slip", "crossing", "double", "connection"}}, {"//$o", {"on"}}, + {"//$s", {"#", "station"}}, }}; auto const lookup{std::find_if(words.begin(), words.end(), [&](auto const &Entry) { return Entry.first == Mark; })}; if (lookup == words.end()) From 5044beb3807b0e1bd0273444d93da0a07975c6dc Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 07:50:59 +0200 Subject: [PATCH 042/145] feat(editor): the signalling mode numbers the switches of a station along it and names them after its code, the drives and the switch records follow, and an undo of names passed round among the tracks leaves the drives right --- application/editormodesrk.cpp | 181 ++++++++++++++++++++++++++++---- application/editormodesrk.h | 6 +- application/editormodetrack.cpp | 11 ++ editor/editorStation.cpp | 50 +++++++++ editor/editorStation.hpp | 13 +++ editor/editorSwitchStore.cpp | 36 +++++++ editor/editorSwitchStore.hpp | 6 ++ editor/editorTrack.cpp | 4 +- editor/editorTrack.hpp | 3 +- lang/editor_pl.po | 45 ++++++++ 10 files changed, 332 insertions(+), 23 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index db2a8598..3c128de2 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -16,6 +16,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorSwitchStore.hpp" +#include "editor/editorTrack.hpp" #include "simulation/simulation.h" #include "utilities/Globals.h" @@ -27,6 +28,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include namespace @@ -60,6 +62,11 @@ std::string shown(std::string const &Word) return Word == "-" ? std::string{} : Word; } +template std::string anchor_of(Record_ const &Entry) +{ + return Entry.track != nullptr && false == Entry.track->m_editorremoved ? Entry.track->name() : Entry.anchor; +} + } // namespace void srk_tool::load() @@ -95,6 +102,7 @@ void srk_tool::load() } entry.code = shown(entry.code); entry.line = shown(entry.line); + entry.track = simulation::Paths.find(entry.anchor); if (false == entry.anchor.empty()) m_records.push_back(std::move(entry)); } @@ -118,7 +126,7 @@ void srk_tool::store() continue; if (lines.empty()) lines.emplace_back("# stations described in the track editor, the simulation skips these lines"); - lines.push_back(format("station %s line %s anchor %s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), entry.anchor.c_str(), entry.name.c_str())); + lines.push_back(format("station %s line %s anchor %s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), anchor_of(entry).c_str(), entry.name.c_str())); } scene::Layers.mark(layer, kMark, std::move(lines)); } @@ -140,24 +148,32 @@ void srk_tool::open(TTrack &Track) // the record of the station, by any of its switches m_record = -1; for (int index = 0; index < static_cast(m_records.size()) && m_record < 0; ++index) - if (std::any_of(m_layout->switches.begin(), m_layout->switches.end(), [&](TTrack const *Switch) { return Switch->name() == m_records[index].anchor; })) + { + auto &entry{m_records[index]}; + if (entry.track == nullptr) + entry.track = simulation::Paths.find(entry.anchor); + if (std::find(m_layout->switches.begin(), m_layout->switches.end(), entry.track) != m_layout->switches.end()) m_record = index; - m_anchor = m_record >= 0 ? m_records[m_record].anchor : m_layout->switches.front()->name(); + } + m_anchor = m_record >= 0 ? m_records[m_record].track : m_layout->switches.front(); } void srk_tool::refresh() { m_history_size = m_history.history_size(); - auto *anchor{m_anchor.empty() ? nullptr : simulation::Paths.find(m_anchor)}; - if (anchor == nullptr || anchor->m_editorremoved) + if (m_anchor == nullptr || m_anchor->m_editorremoved) { m_layout.reset(); return; } auto const view{m_view_state}; - open(*anchor); + auto const status{m_status}; + open(*m_anchor); if (m_layout.has_value()) + { m_view_state = view; + m_status = status; + } } srk_tool::record &srk_tool::record_of_open() @@ -165,8 +181,9 @@ srk_tool::record &srk_tool::record_of_open() if (m_record < 0) { record entry; - auto const *first{m_layout->switches.front()}; + auto *first{m_layout->switches.front()}; entry.anchor = first->name(); + entry.track = first; entry.layer = scene::Layers.resolve(first->layer()); if (false == scene::Layers.valid(entry.layer) || false == scene::Layers.accepts(entry.layer)) entry.layer = scene::Layers.active(); @@ -174,7 +191,7 @@ srk_tool::record &srk_tool::record_of_open() entry.layer = 1; m_records.push_back(std::move(entry)); m_record = static_cast(m_records.size()) - 1; - m_anchor = m_records.back().anchor; + m_anchor = first; } return m_records[m_record]; } @@ -189,7 +206,7 @@ std::string srk_tool::title() const if (false == entry.code.empty()) return entry.code; } - return format(STR_C("Station of %s"), m_anchor.c_str()); + return format(STR_C("Station of %s"), m_anchor != nullptr ? m_anchor->name().c_str() : ""); } void srk_tool::find_all() @@ -205,9 +222,14 @@ void srk_tool::update() if (false == m_open) return; load(); - if (m_layout.has_value() && m_history.history_size() != m_history_size) - refresh(); - else if (false == m_layout.has_value() && false == m_anchor.empty() && m_history.history_size() != m_history_size) + // the stations follow their anchors renamed + for (auto &entry : m_records) + if (auto const name{anchor_of(entry)}; name != entry.anchor) + { + entry.anchor = name; + m_changed = true; + } + if ((m_layout.has_value() || m_anchor != nullptr) && m_history.history_size() != m_history_size) refresh(); // a track of another station selected opens it auto *track{m_selection.selected_track()}; @@ -223,6 +245,20 @@ void srk_tool::forget(scene::basic_node const *Node) { if (Node == m_picked) m_picked = nullptr; + for (auto &entry : m_records) + if (entry.track == Node) + entry.track = nullptr; + if (Node == m_anchor) + { + m_anchor = nullptr; + if (m_layout.has_value()) + for (auto *track : m_layout->switches) + if (track != Node) + { + m_anchor = track; + break; + } + } if (m_layout.has_value() && std::any_of(m_layout->paths.begin(), m_layout->paths.end(), [&](station::path const &Path) { return Path.track == Node; })) { // laid out again from its anchor at the next update @@ -274,7 +310,7 @@ void srk_tool::render_found() for (std::size_t index = 0; index < m_found.size(); ++index) { auto const &[name, count]{m_found[index]}; - auto const described{std::find_if(m_records.begin(), m_records.end(), [&](record const &Entry) { return Entry.anchor == name; })}; + auto const described{std::find_if(m_records.begin(), m_records.end(), [&](record const &Entry) { return anchor_of(Entry) == name; })}; auto label{format(STR_P("%s: %zu switch", "%s: %zu switches", count), name.c_str(), count)}; if (described != m_records.end() && false == described->name.empty()) label = described->name + " - " + label; @@ -332,6 +368,18 @@ void srk_tool::render_options() item_tooltip("The number of the railway line the station lies on, as the block sections between the stations are named after it"); ImGui::EndTable(); } + step_heading(1, STR_C("Switch numbers")); + int from{m_reversed ? 1 : 0}; + if (segmented("##srkfrom", from, + {{0, STR_C("From the left"), layout.kilometrage ? "From the lower km, as PKP PLK numbers them" : "From the west end of the station"}, + {1, STR_C("From the right"), layout.kilometrage ? "From the higher km" : "From the east end of the station"}})) + m_reversed = from == 1; + if (main_button(STR_C("Number the switches"), ImVec2(-1.0f, tall_button_height()))) + number_switches(); + item_tooltip("Numbers the switches 1, 2, 3... along the station and names them after its code, as on the lines of PKP PLK: bpd_1, " + "the parts of a slip bpd_15a to bpd_15d. The drives follow their switches, events written by hand keep the old names. One step of undo"); + if (false == m_status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); ImGui::Spacing(); if (ImGui::CollapsingHeader(format("%s (%zu)###srkswitches", STR_C("Switches"), layout.switches.size()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { @@ -369,18 +417,86 @@ void srk_tool::render_options() } } ImGui::Spacing(); - if (false == m_status.empty()) - ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); if (ImGui::Button(STR_C("Another station"), ImVec2(-1.0f, 0.0f))) { m_layout.reset(); - m_anchor.clear(); + m_anchor = nullptr; m_record = -1; m_hover = -1; } item_tooltip("Leaves this station; LMB on a switch opens another one"); } +void srk_tool::number_switches() +{ + m_status.clear(); + auto const code{m_record >= 0 ? m_records[m_record].code : std::string{}}; + if (code.empty()) + { + m_status = STR_C("Give the station its code first: the switches are named after it"); + return; + } + auto const units{station::numbering(*m_layout, m_reversed)}; + std::vector> names; + std::unordered_set own; + for (std::size_t index = 0; index < units.size(); ++index) + for (std::size_t part = 0; part < units[index].parts.size(); ++part) + { + names.emplace_back(units[index].parts[part], station::switch_name(code, static_cast(index) + 1, part, units[index].parts.size())); + own.insert(units[index].parts[part]); + } + for (auto &[track, name] : names) + { + std::string reason; + TTrack *owner{nullptr}; + if (false == scene::Layers.editable(track, &reason)) + { + m_status = format(STR_C("%s can't be renamed: %s"), track->name().c_str(), reason.c_str()); + return; + } + if (false == editor_track::name_valid(*track, name, reason, &owner) && (owner == nullptr || own.count(owner) == 0)) + { + m_status = format(STR_C("%s can't be named %s: %s"), track->name().c_str(), name.c_str(), reason.c_str()); + return; + } + } + std::vector> before; + std::map parts; + std::map bases; + for (auto const &[track, name] : names) + if (track->name() != name) + { + before.emplace_back(track, editor_track::capture(*track)); + parts[track->name()] = name; + } + for (std::size_t index = 0; index < units.size(); ++index) + if (false == units[index].record.empty()) + bases[units[index].record] = station::switch_name(code, static_cast(index) + 1, 0, 1); + switch_store::rename(parts, bases); + // the names go round among the switches: a switch whose name another one gets gives it up first + for (auto const &entry : names) + if (entry.first->name() != entry.second && std::any_of(names.begin(), names.end(), [&](auto const &Other) { return Other.first != entry.first && Other.second == entry.first->name(); })) + editor_track::rename(*entry.first, entry.first->name() + "~"); + for (auto const &[track, name] : names) + editor_track::rename(*track, name); + std::size_t numbered{0}; + for (std::size_t index = 0; index < units.size(); ++index) + if (auto const record{switch_store::named(*units[index].parts.front())}; record && record->number != std::to_string(index + 1)) + { + switch_store::number(record->name, std::to_string(index + 1)); + ++numbered; + } + if (before.empty() && numbered == 0) + { + m_status = STR_C("The switches have these numbers already"); + return; + } + if (before.empty()) + before.emplace_back(units.front().parts.front(), editor_track::capture(*units.front().parts.front())); + m_history.push_track_snapshot(std::move(before)); + m_status = format(STR_P("%zu switch numbered", "%zu switches numbered", units.size()), units.size()); +} + void srk_tool::fit_view(float const Width, float const Height) { auto const &layout{*m_layout}; @@ -508,13 +624,17 @@ void srk_tool::render_canvas(float const Height) draw_path(path, overlay_color::selected, 4.0f); if (m_hover >= 0) draw_path(layout.paths[m_hover], kHovered, 4.0f); - // the switches: a dot at the start, the name over or under it in turn + // the switches: a dot at the start, the name over or under it in turn; the number alone, once named after the station + auto const prefix{m_record >= 0 && false == m_records[m_record].code.empty() ? station::switch_name(m_records[m_record].code, 0, 0, 1) : std::string{}}; + auto const stem{prefix.empty() ? std::string{} : prefix.substr(0, prefix.size() - 1)}; for (std::size_t index = 0; index < layout.switches.size(); ++index) { auto const *track{layout.switches[index]}; auto const at{screen_of(station::project(layout, track->m_paths.front().points[segment_data::point::start]))}; draw->AddCircleFilled(at, 3.5f, kSwitch); - auto const text{track->name()}; + auto text{track->name()}; + if (false == stem.empty() && text.size() > stem.size() && text.compare(0, stem.size(), stem) == 0) + text.erase(0, stem.size()); auto const extent{ImGui::CalcTextSize(text.c_str())}; ImVec2 const place{at.x - extent.x * 0.5f, index % 2 == 0 ? at.y - extent.y - 6.0f : at.y + 6.0f}; draw->AddRectFilled(ImVec2(place.x - 2.0f, place.y - 1.0f), ImVec2(place.x + extent.x + 2.0f, place.y + extent.y + 1.0f), IM_COL32(22, 24, 28, 200), 2.0f); @@ -597,7 +717,7 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri ++approach; else tracks.insert(path.track); - WriteLog(format("SELFTEST srkopen %s: %zu switches, %zu tracks, %zu paths out, axis %.0f m, switches over %.0f..%.0f, offsets %.1f..%.1f, km %s, record %d", m_anchor.c_str(), layout.switches.size(), tracks.size(), approach, + WriteLog(format("SELFTEST srkopen %s: %zu switches, %zu tracks, %zu paths out, axis %.0f m, switches over %.0f..%.0f, offsets %.1f..%.1f, km %s, record %d", m_anchor->name().c_str(), layout.switches.size(), tracks.size(), approach, layout.chainage.back(), layout.from, layout.to, layout.low, layout.high, layout.kilometrage ? format("%.3f", layout.km).c_str() : "-", m_record)); for (auto const *track : layout.switches) { @@ -606,6 +726,29 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri } return editor_selftest_result::handled; } + if (Command == "srknumber") + { + // srknumber [from=right]: the switches of the open station numbered + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srknumber: no station open"); + return editor_selftest_result::handled; + } + auto const values{selftest_values(Words)}; + auto const from{values.find("from")}; + m_reversed = from != values.end() && from->second == "right"; + number_switches(); + std::string list; + for (auto const &unit : station::numbering(*m_layout, m_reversed)) + for (auto const *part : unit.parts) + { + auto const record{switch_store::named(*part)}; + list += ' ' + part->name() + '/' + (record ? record->number : std::string{"?"}); + } + WriteLog("SELFTEST srknumber: " + m_status); + WriteLog("SELFTEST srknumber names:" + list); + return editor_selftest_result::handled; + } if (Command == "srkstations") { load(); diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 2460bda5..557c4711 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -42,6 +42,7 @@ class srk_tool final : public editor_tool { scene::layer_handle layer{null_handle}; std::string anchor; // the name of a switch of the station + TTrack *track{nullptr}; // the switch, once found: it stays the anchor when it gets renamed std::string code; // the abbreviation of the station, as MMz std::string name; std::string line; // the number of the railway line @@ -60,7 +61,8 @@ class srk_tool final : public editor_tool std::vector m_records; std::optional m_layout; int m_record{-1}; // of the open station, -1 while it has none - std::string m_anchor; // the switch the open station was found from + TTrack *m_anchor{nullptr}; // the switch the open station was found from + bool m_reversed{false}; // the switches numbered from the right end of the schematic std::size_t m_history_size{0}; TTrack const *m_picked{nullptr}; // the track selected when the station was looked for view m_view_state; @@ -78,4 +80,6 @@ class srk_tool final : public editor_tool void render_canvas(float const Height); void fit_view(float const Width, float const Height); void find_all(); + // the switches numbered along the station and named after its code; one step of the history + void number_switches(); }; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 7da24b60..919a1b93 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -374,6 +374,17 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve for (auto *track : Snapshot.created) editor_track::revive(*track); } + // names passed round among the tracks: a track whose name another one gets back gives it up first. The drives the step + // put back have the names of their switches already + std::vector restored; + for (auto const &entry : Snapshot.directives) + restored.push_back(entry.first); + for (auto const &entry : Snapshot.tracks) + if (entry.second.name != entry.first->name() && std::any_of(Snapshot.tracks.begin(), Snapshot.tracks.end(), [&](auto const &Other) { return Other.first != entry.first && Other.second.name == entry.first->name(); })) + editor_track::rename(*entry.first, entry.first->name() + "~", restored); + for (auto const &entry : Snapshot.tracks) + if (entry.second.name != entry.first->name()) + editor_track::rename(*entry.first, entry.second.name, restored); for (auto const &entry : Snapshot.tracks) editor_track::apply(*entry.first, entry.second); editor_track::commit(tracks); diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index c60aa4a0..31e3d245 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include "world/Track.h" #include +#include #include #include #include @@ -291,6 +292,12 @@ std::optional find(TTrack &From, options const &Options) result.km = best.origin / 1000.0; } } + // without the km the station runs from the west to the east, as the map shows it + if (false == result.kilometrage && map_of(result.axis.back()).x < map_of(result.axis.front()).x) + { + std::reverse(result.axis.begin(), result.axis.end()); + measure(); + } // the paths, laid out along the axis auto const add = [&](TTrack &Track, bool const Approach) { for (int index = 0; index < static_cast(Track.m_paths.size()); ++index) @@ -391,6 +398,49 @@ std::vector stations(options const &Options) return result; } +std::vector numbering(layout const &Layout, bool const Reversed) +{ + std::unordered_set members; + for (auto const &entry : Layout.paths) + if (false == entry.approach) + members.insert(entry.track); + std::unordered_set taken; + std::vector result; + for (auto *track : Layout.switches) + { + if (track->eType == tt_Table || taken.count(track) > 0) + continue; + unit entry; + if (auto const record{switch_store::named(*track)}) + { + entry.record = record->name; + if (record->kind != "switch") + for (auto const &name : record->parts) + if (auto *part{simulation::Paths.find(name)}; part != nullptr && members.count(part) > 0 && taken.insert(part).second) + entry.parts.push_back(part); + } + if (taken.insert(track).second) + entry.parts.push_back(track); + entry.chainage = std::numeric_limits::max(); + for (auto const *part : entry.parts) + entry.chainage = std::min(entry.chainage, project(Layout, end_of(*part, 0, false)).x); + result.push_back(std::move(entry)); + } + std::stable_sort(result.begin(), result.end(), [&](unit const &A, unit const &B) { return Reversed ? A.chainage > B.chainage : A.chainage < B.chainage; }); + return result; +} + +std::string switch_name(std::string const &Code, int const Number, std::size_t const Part, std::size_t const Parts) +{ + std::string result; + for (auto const c : Code) + result += static_cast(std::tolower(static_cast(c))); + result += '_' + std::to_string(Number); + if (Parts > 1) + result += static_cast('a' + std::min(Part, 25)); + return result; +} + std::vector switches_of(TTrack &Switch, options const &Options) { return collect(Switch, Options).switches; diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index 96c283a0..a6a85ec1 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #pragma once #include +#include #include #include @@ -51,6 +52,14 @@ struct layout double km{0.0}; // at chainage 0, when it does }; +// what shares a number of the station: a switch, or the switches of a slip, the two of a double switch, the paths of a crossing +struct unit +{ + std::vector parts; + std::string record; // the name of its record of the switches, empty without one + double chainage{0.0}; // of the start of its first part along the station +}; + // the station of the switch, or of the plain track near one std::optional find(TTrack &From, options const &Options = {}); // the chainage along the axis and the offset to the left of it, as the map shows it @@ -59,6 +68,10 @@ glm::dvec2 project(layout const &Layout, glm::dvec3 const &Point); double km_of(layout const &Layout, double const Chainage); // one switch of each station of the scenery std::vector stations(options const &Options = {}); +// the switches of the station by their numbers: along the chainage, or against it when Reversed; turntables have none +std::vector numbering(layout const &Layout, bool const Reversed); +// _, each part of a unit of several with a letter of its own: bpd_15a +std::string switch_name(std::string const &Code, int const Number, std::size_t const Part, std::size_t const Parts); // the switches of the station of the switch, without laying it out std::vector switches_of(TTrack &Switch, options const &Options = {}); diff --git a/editor/editorSwitchStore.cpp b/editor/editorSwitchStore.cpp index 02a04a3b..f8a2cedb 100644 --- a/editor/editorSwitchStore.cpp +++ b/editor/editorSwitchStore.cpp @@ -330,6 +330,42 @@ void forget(std::string const &Name) mark(layer); } +void rename(std::map const &Parts, std::map const &Bases) +{ + load(); + auto const renamed = [](std::map const &Names, std::string &Name) { + auto const found{Names.find(Name)}; + if (found == Names.end() || found->second == Name) + return false; + Name = found->second; + return true; + }; + std::set layers; + for (auto &entry : g_records) + { + auto changed{renamed(entry.kind == "slip" || entry.kind == "crossing" ? Bases : Parts, entry.name)}; + for (auto &part : entry.parts) + changed = renamed(Parts, part) || changed; + if (changed) + layers.insert(entry.layer); + } + for (auto const layer : layers) + mark(layer); + g_changed = g_changed || false == layers.empty(); +} + +void number(std::string const &Name, std::string const &Number) +{ + load(); + for (auto &entry : g_records) + if (entry.kind != "connection" && entry.name == Name && entry.number != Number) + { + entry.number = Number; + mark(entry.layer); + g_changed = true; + } +} + void tidy() { load(); diff --git a/editor/editorSwitchStore.hpp b/editor/editorSwitchStore.hpp index 7b5f29f4..69e90c14 100644 --- a/editor/editorSwitchStore.hpp +++ b/editor/editorSwitchStore.hpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include #include #include #include @@ -72,6 +73,11 @@ void put(record Entry, scene::layer_handle const Layer); void place(record &Entry, TTrack const &First); void classify(record &Entry, TTrack const &Switch); void forget(std::string const &Name); +// the records follow renamed switches: their parts and the records named after them; Bases renames the records of slips and +// crossings, whose names aren't the names of their parts +void rename(std::map const &Parts, std::map const &Bases); +// the number of the switch, slip or crossing of the record of the name, as the lists of the switches give it +void number(std::string const &Name, std::string const &Number); // on save: the records follow the renamed switches, the ones whose paths are gone are dropped void tidy(); // "Rz 60E1-500-1:12 L", as the type is written on the lists of the switches diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 01b33559..c2aa39ea 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -367,7 +367,7 @@ editor_track::name_uses editor_track::uses_of_name(TTrack const &Track) return result; } -void editor_track::rename(TTrack &Track, std::string const &Name) +void editor_track::rename(TTrack &Track, std::string const &Name, std::vector const &Kept) { auto const old{Track.name()}; if (old == Name) @@ -388,7 +388,7 @@ void editor_track::rename(TTrack &Track, std::string const &Name) std::string file; std::vector values; auto const index{track_parameter(handle, file, values)}; - if (index < 0 || lower_name(values[index]) != old) + if (index < 0 || lower_name(values[index]) != old || std::find(Kept.begin(), Kept.end(), handle) != Kept.end()) continue; values[index] = Name; scene::Layers.modify(handle, editor_includes::compose_directive(file, values)); diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index b32de786..7e69cc1c 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -86,7 +86,8 @@ class editor_track std::vector loose; }; static name_uses uses_of_name(TTrack const &Track); - static void rename(TTrack &Track, std::string const &Name); + // the includes of Kept keep their directives: an undo step puts them back on its own + static void rename(TTrack &Track, std::string const &Name, std::vector const &Kept = {}); // 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); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 58b5fc97..a9d74cd5 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10365,3 +10365,48 @@ msgstr[0] "%s: %zu rozjazd" msgstr[1] "%s: %zu rozjazdy" msgstr[2] "%s: %zu rozjazdów" +msgid "Switch numbers" +msgstr "Numery rozjazdów" + +msgid "From the left" +msgstr "Od lewej" + +msgid "From the right" +msgstr "Od prawej" + +msgid "From the lower km, as PKP PLK numbers them" +msgstr "Od niższego km, jak numeruje PKP PLK" + +msgid "From the west end of the station" +msgstr "Od zachodniego końca stacji" + +msgid "From the higher km" +msgstr "Od wyższego km" + +msgid "From the east end of the station" +msgstr "Od wschodniego końca stacji" + +msgid "Number the switches" +msgstr "Ponumeruj rozjazdy" + +msgid "Numbers the switches 1, 2, 3... along the station and names them after its code, as on the lines of PKP PLK: bpd_1, the parts of a slip bpd_15a to bpd_15d. The drives follow their switches, events written by hand keep the old names. One step of undo" +msgstr "Numeruje rozjazdy 1, 2, 3... wzdłuż stacji i nazywa je od jej skrótu, jak na liniach PKP PLK: bpd_1, części rozjazdu krzyżowego bpd_15a do bpd_15d. Napędy idą za swoimi rozjazdami, eventy pisane ręcznie zostają przy starych nazwach. Jeden krok cofania" + +msgid "Give the station its code first: the switches are named after it" +msgstr "Najpierw wpisz skrót stacji: od niego biorą się nazwy rozjazdów" + +msgid "%s can't be renamed: %s" +msgstr "Nie można zmienić nazwy %s: %s" + +msgid "%s can't be named %s: %s" +msgstr "%s nie może dostać nazwy %s: %s" + +msgid "The switches have these numbers already" +msgstr "Rozjazdy mają już te numery" + +msgid "%zu switch numbered" +msgid_plural "%zu switches numbered" +msgstr[0] "Ponumerowano %zu rozjazd" +msgstr[1] "Ponumerowano %zu rozjazdy" +msgstr[2] "Ponumerowano %zu rozjazdów" + From c8f3039a8cdb31e1a3e58bc4034cb9d23c8a53bd Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 08:16:39 +0200 Subject: [PATCH 043/145] feat(editor): the signalling mode cuts a station at the joints of its track circuits and names them, a circuit for each switch and one for each stretch of a station track, the tracks numbered as PKP PLK does --- application/editormodesrk.cpp | 227 +++++++++++++++- application/editormodesrk.h | 15 ++ editor/editorStation.cpp | 485 +++++++++++++++++++++++++++++++++- editor/editorStation.hpp | 32 +++ lang/editor_pl.po | 33 +++ 5 files changed, 776 insertions(+), 16 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 3c128de2..2cf93026 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -27,8 +27,10 @@ http://mozilla.org/MPL/2.0/. #include "utilities/translation.h" #include #include +#include #include #include +#include #include namespace @@ -62,6 +64,29 @@ std::string shown(std::string const &Word) return Word == "-" ? std::string{} : Word; } +// the number of the switch of a unit: of its name once named after the station, of its record, else of its place +std::string unit_number(station::unit const &Unit, std::string const &Stem, std::size_t const Index) +{ + auto const &name{Unit.parts.front()->name()}; + if (name.size() > Stem.size() && name.compare(0, Stem.size(), Stem) == 0) + { + auto digits{name.substr(Stem.size())}; + while (false == digits.empty() && std::isalpha(static_cast(digits.back()))) + digits.pop_back(); + if (false == digits.empty() && std::all_of(digits.begin(), digits.end(), [](char const Digit) { return std::isdigit(static_cast(Digit)) != 0; })) + return digits; + } + if (auto const record{switch_store::named(*Unit.parts.front())}; record && record->number != "-") + return record->number; + return std::to_string(Index + 1); +} + +ImU32 circuit_colour(std::string const &Name) +{ + static std::array const palette{IM_COL32(110, 200, 250, 255), IM_COL32(250, 150, 90, 255), IM_COL32(150, 220, 120, 255), IM_COL32(230, 120, 200, 255), IM_COL32(240, 220, 100, 255), IM_COL32(150, 140, 250, 255)}; + return palette[std::hash{}(Name) % palette.size()]; +} + template std::string anchor_of(Record_ const &Entry) { return Entry.track != nullptr && false == Entry.track->m_editorremoved ? Entry.track->name() : Entry.anchor; @@ -156,6 +181,7 @@ void srk_tool::open(TTrack &Track) m_record = index; } m_anchor = m_record >= 0 ? m_records[m_record].track : m_layout->switches.front(); + survey(); } void srk_tool::refresh() @@ -374,10 +400,17 @@ void srk_tool::render_options() {{0, STR_C("From the left"), layout.kilometrage ? "From the lower km, as PKP PLK numbers them" : "From the west end of the station"}, {1, STR_C("From the right"), layout.kilometrage ? "From the higher km" : "From the east end of the station"}})) m_reversed = from == 1; - if (main_button(STR_C("Number the switches"), ImVec2(-1.0f, tall_button_height()))) + if (ImGui::Button(STR_C("Number the switches"), ImVec2(-1.0f, 0.0f))) number_switches(); item_tooltip("Numbers the switches 1, 2, 3... along the station and names them after its code, as on the lines of PKP PLK: bpd_1, " "the parts of a slip bpd_15a to bpd_15d. The drives follow their switches, events written by hand keep the old names. One step of undo"); + step_heading(2, STR_C("Track circuits")); + if (main_button(STR_C("Set the track circuits"), ImVec2(-1.0f, tall_button_height()))) + set_circuits(); + item_tooltip("Cuts the tracks of the station at the joints of the track circuits and names the circuits, as on the lines of PKP PLK: " + "a joint 1.5 m before the start of a switch, beyond the fouling point of each branch where the axes are 4 m apart, in the middle " + "of a link too short for both. Each switch gets bpd_iz, each station track bpd_it: 1 and 2 the main tracks, " + "track 1 on the right along the km, the odd ones beyond it, the even ones on the other side. One step of undo"); if (false == m_status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); ImGui::Spacing(); @@ -497,6 +530,150 @@ void srk_tool::number_switches() m_status = format(STR_P("%zu switch numbered", "%zu switches numbered", units.size()), units.size()); } +void srk_tool::set_circuits() +{ + m_status.clear(); + auto const code{m_record >= 0 ? m_records[m_record].code : std::string{}}; + if (code.empty()) + { + m_status = STR_C("Give the station its code first: the track circuits are named after it"); + return; + } + auto const units{station::numbering(*m_layout, m_reversed)}; + auto const planned{station::joints(*m_layout, units)}; + for (auto const &entry : planned) + if (std::string reason; entry.track != nullptr && false == editor_track::can_edit_geometry(*entry.track, reason)) + { + m_status = format(STR_C("%s can't be cut: %s"), name_of(*entry.track).c_str(), reason.c_str()); + return; + } + for (auto const &path : m_layout->paths) + if (std::string reason; false == path.approach && false == scene::Layers.editable(path.track, &reason)) + { + m_status = format(STR_C("%s can't be changed: %s"), name_of(*path.track).c_str(), reason.c_str()); + return; + } + // the cuts: each track from its end back, so the pieces left to cut stay in the track + std::vector> before; + std::vector created; + std::vector points; + std::vector>> cuts; + for (auto const &entry : planned) + { + points.push_back(entry.point); + if (entry.track == nullptr) + continue; + auto found{std::find_if(cuts.begin(), cuts.end(), [&](auto const &Cut) { return Cut.first == entry.track; })}; + if (found == cuts.end()) + found = cuts.insert(cuts.end(), {entry.track, {}}); + found->second.push_back(entry.point); + } + for (auto &[track, at] : cuts) + { + before.emplace_back(track, editor_track::capture(*track)); + auto *const piece{track}; + std::sort(at.begin(), at.end(), [&](glm::dvec3 const &A, glm::dvec3 const &B) { return editor_track::nearest_parameter(*piece, A) > editor_track::nearest_parameter(*piece, B); }); + for (auto const &point : at) + if (auto *cut{editor_track::split_path(*track, editor_track::nearest_parameter(*track, point))}) + created.push_back(cut); + } + // the circuits of the station as cut + std::size_t count{0}; + if (auto const found{station::find(*m_anchor)}) + { + auto const circuits{station::circuits(*found, units, points)}; + auto const numbers{station::track_numbers(*found, circuits)}; + auto const stem{station::prefix(code)}; + std::unordered_set captured(created.begin(), created.end()); + for (auto const &[track, state] : before) + captured.insert(track); + for (std::size_t index = 0; index < circuits.size(); ++index) + { + auto const &circuit{circuits[index]}; + auto const name{circuit.unit >= 0 ? stem + "iz" + unit_number(units[circuit.unit], stem, circuit.unit) : stem + "it" + numbers[index]}; + for (auto *track : circuit.tracks) + { + if (track->Isolated.size() == 1 && track->Isolated.front()->asName == name) + continue; + if (captured.insert(track).second) + before.emplace_back(track, editor_track::capture(*track)); + editor_track::isolated(*track, {name}); + } + } + count = circuits.size(); + } + if (before.empty() && created.empty()) + { + m_status = STR_C("The track circuits are set already"); + return; + } + m_history.push_track_snapshot(std::move(before), created); + m_status = format(STR_P("%zu track circuit", "%zu track circuits", count), count) + ", " + format(STR_P("%zu cut", "%zu cuts", created.size()), created.size()); +} + +void srk_tool::survey() +{ + m_circuit_of.assign(m_layout.has_value() ? m_layout->paths.size() : 0, -1); + m_circuits.clear(); + m_ticks.clear(); + if (false == m_layout.has_value()) + return; + auto const &layout{*m_layout}; + auto const stem{m_record >= 0 && false == m_records[m_record].code.empty() ? station::prefix(m_records[m_record].code) : std::string{}}; + std::vector weights; + for (std::size_t index = 0; index < layout.paths.size(); ++index) + { + auto const &path{layout.paths[index]}; + if (path.track->Isolated.empty()) + continue; + // of the lines leading out only the stretches up to the joints of the station + auto const &name{path.track->Isolated.front()->asName}; + if (path.approach && (stem.empty() || name.compare(0, stem.size(), stem) != 0)) + continue; + auto found{std::find_if(m_circuits.begin(), m_circuits.end(), [&](circuit_mark const &Mark) { return Mark.text == name; })}; + if (found == m_circuits.end()) + { + found = m_circuits.insert(m_circuits.end(), {glm::dvec2{0.0}, name, circuit_colour(name)}); + weights.push_back(0.0); + } + auto const circuit{static_cast(found - m_circuits.begin())}; + m_circuit_of[index] = static_cast(circuit); + for (auto const &point : path.schematic) + { + found->at += point; + weights[circuit] += 1.0; + } + } + for (std::size_t index = 0; index < m_circuits.size(); ++index) + { + auto &mark{m_circuits[index]}; + mark.at /= std::max(1.0, weights[index]); + // bpd_it3 shown as It3 + if (false == stem.empty() && mark.text.compare(0, stem.size(), stem) == 0) + mark.text.erase(0, stem.size()); + if (false == mark.text.empty()) + mark.text.front() = static_cast(std::toupper(static_cast(mark.text.front()))); + } + // the joints: the ends where paths of different circuits meet + std::vector> ends; + for (std::size_t index = 0; index < layout.paths.size(); ++index) + { + auto const &path{layout.paths[index]}; + if (path.approach && m_circuit_of[index] < 0) + { + for (auto const &point : {path.points.front(), path.points.back()}) + ends.emplace_back(point, -1); + continue; + } + for (auto const &point : {path.points.front(), path.points.back()}) + ends.emplace_back(point, m_circuit_of[index]); + } + for (std::size_t i = 0; i < ends.size(); ++i) + for (std::size_t j = i + 1; j < ends.size(); ++j) + if (ends[i].second != ends[j].second && (ends[i].second >= 0 || ends[j].second >= 0) && glm::distance(ends[i].first, ends[j].first) < 0.05) + m_ticks.push_back(station::project(layout, ends[i].first)); +} + void srk_tool::fit_view(float const Width, float const Height) { auto const &layout{*m_layout}; @@ -616,17 +793,21 @@ void srk_tool::render_canvas(float const Height) for (auto const &path : layout.paths) if (path.approach) draw_path(path, kApproach, 2.0f); - for (auto const &path : layout.paths) - if (false == path.approach) - draw_path(path, is_switch(*path.track) ? kSwitch : kPlain, 2.5f); + for (std::size_t index = 0; index < layout.paths.size(); ++index) + if (auto const &path{layout.paths[index]}; false == path.approach || m_circuit_of[index] >= 0) + draw_path(path, m_circuit_of[index] >= 0 ? m_circuits[m_circuit_of[index]].colour : is_switch(*path.track) ? kSwitch : kPlain, 2.5f); + for (auto const &tick : m_ticks) + { + auto const at{screen_of(tick)}; + draw->AddLine(ImVec2(at.x, at.y - 5.0f), ImVec2(at.x, at.y + 5.0f), IM_COL32(255, 255, 255, 220), 2.0f); + } for (auto const &path : layout.paths) if (path.track == selected) draw_path(path, overlay_color::selected, 4.0f); if (m_hover >= 0) draw_path(layout.paths[m_hover], kHovered, 4.0f); // the switches: a dot at the start, the name over or under it in turn; the number alone, once named after the station - auto const prefix{m_record >= 0 && false == m_records[m_record].code.empty() ? station::switch_name(m_records[m_record].code, 0, 0, 1) : std::string{}}; - auto const stem{prefix.empty() ? std::string{} : prefix.substr(0, prefix.size() - 1)}; + auto const stem{m_record >= 0 && false == m_records[m_record].code.empty() ? station::prefix(m_records[m_record].code) : std::string{}}; for (std::size_t index = 0; index < layout.switches.size(); ++index) { auto const *track{layout.switches[index]}; @@ -640,6 +821,12 @@ void srk_tool::render_canvas(float const Height) draw->AddRectFilled(ImVec2(place.x - 2.0f, place.y - 1.0f), ImVec2(place.x + extent.x + 2.0f, place.y + extent.y + 1.0f), IM_COL32(22, 24, 28, 200), 2.0f); draw->AddText(place, kSwitch, text.c_str()); } + for (auto const &mark : m_circuits) + { + auto const at{screen_of(mark.at)}; + auto const extent{ImGui::CalcTextSize(mark.text.c_str())}; + draw->AddText(ImVec2(at.x - extent.x * 0.5f, at.y + 4.0f), mark.colour, mark.text.c_str()); + } draw->PopClipRect(); if (hovered && m_hover >= 0 && false == m_dragging) { @@ -650,6 +837,8 @@ void srk_tool::render_canvas(float const Height) ImGui::TextDisabled("%s, %.1f m", is_switch(*path.track) ? STR_C("switch") : path.approach ? STR_C("line out of the station") : STR_C("track"), path.length); if (layout.kilometrage) ImGui::TextDisabled("km %.3f", station::km_of(layout, along)); + if (false == path.track->Isolated.empty()) + ImGui::TextDisabled(STR_C("track circuit %s"), path.track->Isolated.front()->asName.c_str()); ImGui::TextDisabled("%s", STR_C("LMB: select it")); ImGui::EndTooltip(); } @@ -719,6 +908,7 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri tracks.insert(path.track); WriteLog(format("SELFTEST srkopen %s: %zu switches, %zu tracks, %zu paths out, axis %.0f m, switches over %.0f..%.0f, offsets %.1f..%.1f, km %s, record %d", m_anchor->name().c_str(), layout.switches.size(), tracks.size(), approach, layout.chainage.back(), layout.from, layout.to, layout.low, layout.high, layout.kilometrage ? format("%.3f", layout.km).c_str() : "-", m_record)); + WriteLog(format("SELFTEST srkopen lines %zu, main %zu tracks, second %zu tracks", layout.lines.size(), layout.main.size(), layout.second.size())); for (auto const *track : layout.switches) { auto const at{station::project(layout, track->m_paths.front().points[segment_data::point::start])}; @@ -749,6 +939,31 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri WriteLog("SELFTEST srknumber names:" + list); return editor_selftest_result::handled; } + if (Command == "srkcircuits") + { + // srkcircuits: the open station cut into its track circuits + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkcircuits: no station open"); + return editor_selftest_result::handled; + } + set_circuits(); + WriteLog("SELFTEST srkcircuits: " + m_status); + refresh(); + std::map> found; + for (auto const &path : m_layout->paths) + if (false == path.track->Isolated.empty()) + { + auto &entry{found[path.track->Isolated.front()->asName]}; + ++entry.first; + entry.second += path.length; + } + std::string list; + for (auto const &[name, entry] : found) + list += format(" %s:%zu/%.0f", name.c_str(), entry.first, entry.second); + WriteLog(format("SELFTEST srkcircuits %zu named, %zu joints:", found.size(), m_ticks.size()) + list); + return editor_selftest_result::handled; + } if (Command == "srkstations") { load(); diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 557c4711..0ef8fc8b 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -13,6 +13,8 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorStation.hpp" #include "scene/scenelayers.h" +#include "imgui/imgui.h" + #include #include #include @@ -69,6 +71,15 @@ class srk_tool final : public editor_tool int m_hover{-1}; // path of the layout under the mouse in the schematic bool m_dragging{false}; std::vector> m_found; // the stations of the scenery: a switch and the count of them + struct circuit_mark + { + glm::dvec2 at{0.0}; // in the schematic + std::string text; + ImU32 colour{0}; + }; + std::vector m_circuit_of; // of each path of the layout, an index into m_circuits, -1 for none + std::vector m_circuits; + std::vector m_ticks; // the joints between the circuits, in the schematic std::string m_status; void load(); @@ -82,4 +93,8 @@ class srk_tool final : public editor_tool void find_all(); // the switches numbered along the station and named after its code; one step of the history void number_switches(); + // the station cut at the joints of its track circuits and the circuits named; one step of the history + void set_circuits(); + // the track circuits as the tracks have them, for the schematic + void survey(); }; diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index 31e3d245..a35ccc79 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -20,6 +20,8 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include +#include #include #include @@ -96,11 +98,17 @@ struct chain double length{0.0}; TTrack *reached{nullptr}; // the switch met at its other end bool ended{false}; // at a free end + TTrack const *from{nullptr}; // the switch it leads out of, with the end of it + int path{0}; + bool atend{false}; }; chain follow(TTrack const &From, int const Path, bool const Atend, double const Limit) { chain result; + result.from = &From; + result.path = Path; + result.atend = Atend; auto point{end_of(From, Path, Atend)}; auto *next{neighbour(From, Path, Atend)}; while (next != nullptr && result.length <= Limit) @@ -131,6 +139,7 @@ struct area std::vector switches; std::vector plain; std::vector exits; + std::vector ends; // tracks ending free: sidings, or lines cut off by the edge of the scenery }; area collect(TTrack &From, options const &Options) @@ -159,9 +168,9 @@ area collect(TTrack &From, options const &Options) for (auto const &[path, atend] : ends_of(from)) { auto line{follow(from, path, atend, Options.link)}; - if (line.reached == nullptr && false == line.ended) + if (line.reached == nullptr) { - result.exits.push_back(std::move(line)); + (line.ended ? result.ends : result.exits).push_back(std::move(line)); continue; } for (auto *track : line.tracks) @@ -171,6 +180,22 @@ area collect(TTrack &From, options const &Options) result.switches.push_back(line.reached); } } + // a link longer than the limit between two switches of the station is a track of it, not two lines out + auto exits{std::move(result.exits)}; + result.exits.clear(); + for (auto &line : exits) + { + if (false == line.ended) + if (auto const whole{follow(*line.from, line.path, line.atend, std::numeric_limits::max())}; whole.reached != nullptr && known.count(whole.reached) > 0) + { + for (auto *track : whole.tracks) + if (known.insert(track).second) + result.plain.push_back(track); + continue; + } + if (std::none_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return known.count(Track) > 0; })) + result.exits.push_back(std::move(line)); + } return result; } @@ -185,12 +210,143 @@ std::vector points_of(TTrack const &Track, int const Path, double &L return result; } +// the points of the path a metre or so apart, for the joints to come out the same however the tracks are cut +std::vector dense_points(TTrack const &Track, int const Path, double &Length) +{ + bezier const curve{Track.m_paths[Path]}; + Length = curve.plan_length(64); + auto const count{std::clamp(static_cast(std::ceil(Length)), 2, 4000)}; + std::vector result; + for (int i = 0; i <= count; ++i) + result.push_back(curve.point(static_cast(i) / count)); + return result; +} + // the map shows the east to the right and the north up: the x of the scenery runs to the west glm::dvec2 map_of(glm::dvec3 const &Point) { return {-Point.x, Point.z}; } +double constexpr kStart{1.5}; // m, a joint before the start of a switch +double constexpr kClear{4.0}; // m, between the axes at a joint beyond a fouling point +double constexpr kReach{120.0}; // m, along a branch looked for its fouling point +double constexpr kSection{30.0}; // m, the shortest stretch between two joints made a track circuit of its own +double constexpr kSnap{0.3}; // m, a joint this close to the end of a track lies at the end + +// the points of the way out of an end of the track, as a train runs: along the main path of a switch run into from its start, +// back along the path of one run into from the end of it +std::vector ahead(TTrack const &From, int const Path, bool const Atend, double const Length) +{ + std::vector result{end_of(From, Path, Atend)}; + auto *next{neighbour(From, Path, Atend)}; + std::unordered_set seen{&From}; + double length{0.0}; + while (next != nullptr && length < Length && rail(*next) && seen.insert(next).second) + { + int path{0}; + bool atend{false}; + if (false == end_at(*next, result.back(), path, atend)) + break; + if (next->eType == tt_Switch && false == atend) + path = 0; + double plan{0.0}; + auto points{dense_points(*next, path, plan)}; + if (atend) + std::reverse(points.begin(), points.end()); + result.insert(result.end(), points.begin() + 1, points.end()); + length += plan; + auto const *track{next}; + next = neighbour(*track, path, false == atend); + } + return result; +} + +// the path of a switch a train run into from its start goes on along: the one turning the least +int straighter(TTrack const &Switch) +{ + if (Switch.eType != tt_Switch || Switch.m_paths.size() < 2) + return 0; + auto const turn = [&](int const Path) { + bezier const curve{Switch.m_paths[Path]}; + auto const from{curve.first(0.0)}; + auto const to{curve.first(1.0)}; + return std::abs(std::atan2(from.x * to.z - from.z * to.x, from.x * to.x + from.z * to.z)); + }; + return turn(1) < turn(0) ? 1 : 0; +} + +// the tracks of the run from the end of the track at the point on, through the switches along their straighter paths +std::vector straight_run(TTrack &Track, glm::dvec3 Point, double const Limit) +{ + std::vector result{&Track}; + std::unordered_set seen{&Track}; + int path{0}; + bool atend{false}; + if (false == end_at(Track, Point, path, atend)) + return result; + auto *next{neighbour(Track, path, atend)}; + double length{0.0}; + while (next != nullptr && length < Limit && rail(*next) && seen.insert(next).second) + { + if (false == end_at(*next, Point, path, atend)) + break; + if (next->eType == tt_Switch && false == atend) + path = straighter(*next); + result.push_back(next); + length += plan_length(*next, path); + Point = end_of(*next, path, false == atend); + auto const *track{next}; + next = neighbour(*track, path, false == atend); + } + return result; +} + +double plan_gap(glm::dvec3 const &Point, std::vector const &Line) +{ + glm::dvec2 const point{Point.x, Point.z}; + auto best{glm::distance(point, glm::dvec2{Line.front().x, Line.front().z})}; + for (std::size_t i = 1; i < Line.size(); ++i) + { + glm::dvec2 const a{Line[i - 1].x, Line[i - 1].z}; + glm::dvec2 const along{glm::dvec2{Line[i].x, Line[i].z} - a}; + auto const length2{glm::dot(along, along)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, along) / length2, 0.0, 1.0) : 0.0}; + best = std::min(best, glm::distance(point, a + along * t)); + } + return best; +} + +// how far from the end of the switch its joint lies: beyond the fouling point at a branch, before the start elsewhere +double clearance(TTrack const &Switch, int const Path, bool const Atend) +{ + auto const point{end_of(Switch, Path, Atend)}; + std::optional> other; + auto nearest{kClear}; + for (auto const &[path, atend] : ends_of(Switch)) + if (auto const distance{plan_distance(end_of(Switch, path, atend), point)}; distance > same_point && distance < nearest) + { + nearest = distance; + other = std::make_pair(path, atend); + } + if (false == other.has_value()) + return kStart; + auto const mine{ahead(Switch, Path, Atend, kReach)}; + auto const theirs{ahead(Switch, other->first, other->second, kReach + 50.0)}; + double along{0.0}; + auto gap{plan_gap(mine.front(), theirs)}; + for (std::size_t i = 1; i < mine.size(); ++i) + { + auto const step{plan_distance(mine[i - 1], mine[i])}; + auto const next{plan_gap(mine[i], theirs)}; + if (next >= kClear) + return along + (next > gap ? step * std::clamp((kClear - gap) / (next - gap), 0.0, 1.0) : step); + along += step; + gap = next; + } + return along; +} + } // namespace std::optional find(TTrack &From, options const &Options) @@ -201,9 +357,10 @@ std::optional find(TTrack &From, options const &Options) std::unordered_set switches(found.switches.begin(), found.switches.end()); // the axis: the run through the station along the main paths of its switches which passes the most of them std::vector starts; - for (auto const &line : found.exits) - if (false == line.tracks.empty()) - starts.push_back(line.tracks.front()); + for (auto const *lines : {&found.exits, &found.ends}) + for (auto const &line : *lines) + if (false == line.tracks.empty()) + starts.push_back(line.tracks.front()); if (starts.empty()) starts.push_back(found.switches.front()); editor_track::route best; @@ -298,6 +455,33 @@ std::optional find(TTrack &From, options const &Options) std::reverse(result.axis.begin(), result.axis.end()); measure(); } + // a long track ending free far beyond the switches is a line cut off by the edge of the scenery, else a siding + std::vector exits{found.exits}; + std::vector plain{found.plain}; + { + auto low{std::numeric_limits::max()}; + auto high{std::numeric_limits::lowest()}; + for (auto const *track : found.switches) + { + auto const along{project(result, end_of(*track, 0, false)).x}; + low = std::min(low, along); + high = std::max(high, along); + } + for (auto const &line : found.ends) + { + auto beyond{0.0}; + for (auto const *track : line.tracks) + for (auto const atend : {false, true}) + { + auto const at{project(result, end_of(*track, 0, atend))}; + beyond = std::max({beyond, low - at.x, at.x - high}); + } + if (beyond > Options.approach * 0.5 && line.length > Options.approach) + exits.push_back(line); + else + plain.insert(plain.end(), line.tracks.begin(), line.tracks.end()); + } + } // the paths, laid out along the axis auto const add = [&](TTrack &Track, bool const Approach) { for (int index = 0; index < static_cast(Track.m_paths.size()); ++index) @@ -314,16 +498,18 @@ std::optional find(TTrack &From, options const &Options) }; for (auto *track : found.switches) add(*track, false); - for (auto *track : found.plain) + for (auto *track : plain) add(*track, false); - for (auto const &line : found.exits) + for (auto const &line : exits) { double length{0.0}; + result.lines.emplace_back(); for (auto *track : line.tracks) { if (length >= Options.approach) break; add(*track, true); + result.lines.back().push_back(track); length += plan_length(*track, 0); } } @@ -348,6 +534,40 @@ std::optional find(TTrack &From, options const &Options) } result.switches = found.switches; std::sort(result.switches.begin(), result.switches.end(), [&](TTrack const *A, TTrack const *B) { return project(result, end_of(*A, 0, false)).x < project(result, end_of(*B, 0, false)).x; }); + // a double-track line: another line leaves the station by the one along the axis a few metres aside; the run from it along + // the main paths of the switches is the other main track + for (auto const &span : best.spans) + result.main.push_back(span.track); + std::unordered_set const axis(result.main.begin(), result.main.end()); + std::vector> leaving; + for (std::size_t index = 0; index < result.lines.size(); ++index) + { + std::vector points; + for (auto const &path : result.paths) + if (path.approach && std::find(result.lines[index].begin(), result.lines[index].end(), path.track) != result.lines[index].end()) + points.insert(points.end(), path.schematic.begin(), path.schematic.end()); + auto const beyond = [&](glm::dvec2 const &Point) { return std::abs(std::max(result.from - Point.x, Point.x - result.to) - 60.0); }; + if (false == points.empty()) + leaving.emplace_back(index, *std::min_element(points.begin(), points.end(), [&](glm::dvec2 const &A, glm::dvec2 const &B) { return beyond(A) < beyond(B); })); + } + for (auto const &[along, at] : leaving) + { + if (axis.count(result.lines[along].front()) == 0 || false == result.second.empty()) + continue; + for (auto const &[aside, there] : leaving) + { + if (aside == along || axis.count(result.lines[aside].front()) > 0 || (at.x < result.from) != (there.x < result.from) || std::abs(there.y - at.y) < 3.0 || std::abs(there.y - at.y) > 7.0) + continue; + // the run goes in from the end of the line at the station + auto &first{*result.lines[aside].front()}; + auto const *start{neighbour(first, 0, false)}; + auto const inwards{start != nullptr && switches.count(start) > 0 ? false : true}; + for (auto *track : straight_run(first, end_of(first, 0, inwards), result.chainage.back() + 2.0 * Options.approach)) + if (axis.count(track) == 0 && std::find(result.second.begin(), result.second.end(), track) == result.second.end()) + result.second.push_back(track); + break; + } + } return result; } @@ -365,7 +585,8 @@ glm::dvec2 project(layout const &Layout, glm::dvec3 const &Point) if (length < 1e-9) continue; auto const unit{along / length}; - auto const t{std::clamp(glm::dot(point - a, unit), 0.0, length)}; + // beyond the ends of the axis it goes on straight + auto const t{std::clamp(glm::dot(point - a, unit), i == 1 ? std::numeric_limits::lowest() : 0.0, i + 1 == Layout.axis.size() ? std::numeric_limits::max() : length)}; auto const foot{a + unit * t}; auto const distance{glm::distance(point, foot)}; if (distance >= best) @@ -430,17 +651,261 @@ std::vector numbering(layout const &Layout, bool const Reversed) return result; } -std::string switch_name(std::string const &Code, int const Number, std::size_t const Part, std::size_t const Parts) +std::string prefix(std::string const &Code) { std::string result; for (auto const c : Code) result += static_cast(std::tolower(static_cast(c))); - result += '_' + std::to_string(Number); + return result + '_'; +} + +std::string switch_name(std::string const &Code, int const Number, std::size_t const Part, std::size_t const Parts) +{ + auto result{prefix(Code) + std::to_string(Number)}; if (Parts > 1) result += static_cast('a' + std::min(Part, 25)); return result; } +std::vector joints(layout const &Layout, std::vector const &Units, options const &Options) +{ + std::unordered_map unit_of; + for (std::size_t index = 0; index < Units.size(); ++index) + for (auto const *part : Units[index].parts) + unit_of[part] = index; + auto const one_unit = [&](TTrack const *A, TTrack const *B) { + auto const a{unit_of.find(A)}; + auto const b{unit_of.find(B)}; + return a != unit_of.end() && b != unit_of.end() && a->second == b->second; + }; + std::unordered_set through; + std::vector meets; + std::vector result; + for (auto *from : Layout.switches) + for (auto const &[path, atend] : ends_of(*from)) + { + auto line{follow(*from, path, atend, std::numeric_limits::max())}; + auto const start{end_of(*from, path, atend)}; + // out of the station: to a switch of another one, along a line cut off by the edge of the scenery + if ((line.reached != nullptr && std::find(Layout.switches.begin(), Layout.switches.end(), line.reached) == Layout.switches.end()) || + (line.ended && false == line.tracks.empty() && std::any_of(Layout.lines.begin(), Layout.lines.end(), [&](std::vector const &Line) { return false == Line.empty() && Line.front() == line.tracks.front(); }))) + { + line.reached = nullptr; + line.ended = false; + } + if (line.reached == nullptr && false == line.ended) + { + double length{0.0}; + auto const kept{std::find_if(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return (length += plan_length(*Track, 0)) > kReach * 2.0; })}; + line.tracks.erase(kept == line.tracks.end() ? kept : std::next(kept), line.tracks.end()); + } + if (line.tracks.empty()) + { + // two switches meeting: the joint lies where they meet, unless they're parts of one + if (line.reached == nullptr || one_unit(from, line.reached) || std::any_of(meets.begin(), meets.end(), [&](glm::dvec3 const &Meet) { return glm::distance(Meet, start) < same_point; })) + continue; + meets.push_back(start); + result.push_back({nullptr, start}); + continue; + } + if (through.count(line.tracks.front()) > 0) + continue; + through.insert(line.tracks.begin(), line.tracks.end()); + // the link as one line from the switch on, and where each of its tracks ends along it + std::vector points{start}; + std::vector lengths{0.0}; + std::vector> pieces; + for (auto *track : line.tracks) + { + double plan{0.0}; + auto samples{dense_points(*track, 0, plan)}; + if (plan_distance(samples.back(), points.back()) < plan_distance(samples.front(), points.back())) + std::reverse(samples.begin(), samples.end()); + for (std::size_t i = 1; i < samples.size(); ++i) + { + lengths.push_back(lengths.back() + plan_distance(points.back(), samples[i])); + points.push_back(samples[i]); + } + pieces.emplace_back(lengths.back(), track); + } + auto const length{lengths.back()}; + auto const own{clearance(*from, path, atend)}; + std::vector places; + if (line.reached != nullptr) + { + int far_path{0}; + bool far_atend{false}; + if (one_unit(from, line.reached) || false == end_at(*line.reached, points.back(), far_path, far_atend)) + continue; + auto const theirs{clearance(*line.reached, far_path, far_atend)}; + if (length - own - theirs >= kSection) + places = {own, length - theirs}; + else + places = {(own + length - theirs) * 0.5}; + } + else if (false == line.ended || length - own >= kSection) + places = {own}; + for (auto const place : places) + { + auto distance{std::clamp(place, 0.0, length)}; + TTrack *track{nullptr}; + auto previous{0.0}; + for (auto const &[end, piece] : pieces) + { + if (std::abs(distance - previous) <= kSnap || std::abs(distance - end) <= kSnap) + { + distance = std::abs(distance - previous) <= kSnap ? previous : end; + track = nullptr; + break; + } + if (distance < end) + { + track = piece; + break; + } + previous = end; + } + auto const after{std::min(std::upper_bound(lengths.begin(), lengths.end(), distance) - lengths.begin(), lengths.size() - 1)}; + auto const before{after > 0 ? after - 1 : 0}; + auto const span{lengths[after] - lengths[before]}; + result.push_back({track, glm::mix(points[before], points[after], span > 1e-9 ? (distance - lengths[before]) / span : 0.0)}); + } + } + return result; +} + +std::vector circuits(layout const &Layout, std::vector const &Units, std::vector const &Joints) +{ + std::unordered_map unit_of; + for (std::size_t index = 0; index < Units.size(); ++index) + for (auto const *part : Units[index].parts) + unit_of[part] = index; + std::vector tracks{Layout.switches}; + std::unordered_set members(tracks.begin(), tracks.end()); + std::unordered_set approach; + for (auto const &entry : Layout.paths) + { + if (members.insert(entry.track).second) + tracks.push_back(entry.track); + if (entry.approach) + approach.insert(entry.track); + } + auto const barrier = [&](glm::dvec3 const &Point) { return std::any_of(Joints.begin(), Joints.end(), [&](glm::dvec3 const &Joint) { return glm::distance(Joint, Point) < 0.05; }); }; + std::unordered_set taken; + std::vector result; + for (auto *seed : tracks) + { + if (taken.count(seed) > 0) + continue; + taken.insert(seed); + circuit entry; + entry.tracks.push_back(seed); + for (std::size_t i = 0; i < entry.tracks.size(); ++i) + { + auto const &track{*entry.tracks[i]}; + auto const ends{node(track) ? ends_of(track) : std::vector>{{0, false}, {0, true}}}; + for (auto const &[path, atend] : ends) + { + if (barrier(end_of(track, path, atend))) + continue; + auto *next{neighbour(track, path, atend)}; + if (next != nullptr && members.count(next) > 0 && taken.insert(next).second) + entry.tracks.push_back(next); + } + } + for (auto const *track : entry.tracks) + if (auto const found{unit_of.find(track)}; found != unit_of.end()) + { + entry.unit = static_cast(found->second); + break; + } + if (entry.unit < 0 && std::any_of(entry.tracks.begin(), entry.tracks.end(), [&](TTrack const *Track) { return approach.count(Track) > 0; })) + continue; + entry.from = std::numeric_limits::max(); + entry.to = std::numeric_limits::lowest(); + double weighted{0.0}; + std::unordered_set own(entry.tracks.begin(), entry.tracks.end()); + for (auto const &path : Layout.paths) + { + if (own.count(path.track) == 0) + continue; + for (std::size_t i = 1; i < path.schematic.size(); ++i) + { + auto const step{glm::distance(path.points[i - 1], path.points[i])}; + weighted += step * (path.schematic[i - 1].y + path.schematic[i].y) * 0.5; + entry.length += step; + } + for (auto const &point : path.schematic) + { + entry.from = std::min(entry.from, point.x); + entry.to = std::max(entry.to, point.x); + } + } + entry.offset = entry.length > 0.0 ? weighted / entry.length : 0.0; + result.push_back(std::move(entry)); + } + return result; +} + +std::vector track_numbers(layout const &Layout, std::vector const &Circuits) +{ + std::unordered_set const main(Layout.main.begin(), Layout.main.end()); + std::unordered_set const second(Layout.second.begin(), Layout.second.end()); + auto const on = [&](circuit const &Circuit, std::unordered_set const &Tracks) { return std::any_of(Circuit.tracks.begin(), Circuit.tracks.end(), [&](TTrack const *Track) { return Tracks.count(Track) > 0; }); }; + std::vector axis; + std::vector other; + std::vector right; + std::vector left; + for (std::size_t index = 0; index < Circuits.size(); ++index) + if (Circuits[index].unit >= 0) + continue; + else if (on(Circuits[index], main)) + axis.push_back(index); + else if (on(Circuits[index], second)) + other.push_back(index); + else + (Circuits[index].offset < 0.0 ? right : left).push_back(index); + auto const mean = [&](std::vector const &Group) { + double weighted{0.0}; + double length{0.0}; + for (auto const index : Group) + { + weighted += Circuits[index].offset * Circuits[index].length; + length += Circuits[index].length; + } + return length > 0.0 ? weighted / length : 0.0; + }; + // track 1 on the right along the chainage: the other main track of a double-track line when it lies to the right of the axis + auto const swapped{false == other.empty() && mean(other) < mean(axis)}; + std::vector, int>> tracks{{axis, swapped ? 2 : 1}}; + if (false == other.empty()) + tracks.emplace_back(other, swapped ? 1 : 2); + // beyond the main tracks the circuits side by side, the nearest first, the odd numbers to the right, the even ones to the left + for (auto *side : {&right, &left}) + { + std::sort(side->begin(), side->end(), [&](std::size_t A, std::size_t B) { return std::abs(Circuits[A].offset) < std::abs(Circuits[B].offset); }); + auto number{side == &right ? 3 : other.empty() ? 2 : 4}; + for (std::size_t index = 0; index < side->size(); ++index) + { + if (index == 0 || std::abs(Circuits[(*side)[index]].offset) - std::abs(Circuits[(*side)[index - 1]].offset) > 2.0) + { + if (index > 0) + number += 2; + tracks.emplace_back(std::vector{}, number); + } + tracks.back().first.push_back((*side)[index]); + } + } + std::vector result(Circuits.size()); + for (auto &[members, number] : tracks) + { + std::sort(members.begin(), members.end(), [&](std::size_t A, std::size_t B) { return Circuits[A].from < Circuits[B].from; }); + for (std::size_t index = 0; index < members.size(); ++index) + result[members[index]] = std::to_string(number) + (members.size() > 1 ? std::string(1, static_cast('a' + std::min(index, 25))) : std::string{}); + } + return result; +} + std::vector switches_of(TTrack &Switch, options const &Options) { return collect(Switch, Options).switches; diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index a6a85ec1..0efda3b0 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -50,6 +50,9 @@ struct layout double high{0.0}; bool kilometrage{false}; // the chainage runs with the km of the switches double km{0.0}; // at chainage 0, when it does + std::vector> lines; // the lines leading out, their tracks from the station on, as far as the approach + std::vector main; // the tracks of the axis + std::vector second; // of the other main track of a double-track line, none on a single-track one }; // what shares a number of the station: a switch, or the switches of a slip, the two of a double switch, the paths of a crossing @@ -60,6 +63,24 @@ struct unit double chainage{0.0}; // of the start of its first part along the station }; +// a joint of the track circuits; Track is the plain track to be cut there, none where two tracks meet already +struct joint +{ + TTrack *track{nullptr}; + glm::dvec3 point{0.0}; +}; + +// a track circuit of the station, once its tracks are cut at the joints: of a switch, or of a station track +struct circuit +{ + std::vector tracks; + int unit{-1}; // of the switch, -1 for a station track + double offset{0.0}; // the mean one of a station track, in the schematic + double from{0.0}; // the chainage it spans + double to{0.0}; + double length{0.0}; +}; + // the station of the switch, or of the plain track near one std::optional find(TTrack &From, options const &Options = {}); // the chainage along the axis and the offset to the left of it, as the map shows it @@ -70,8 +91,19 @@ double km_of(layout const &Layout, double const Chainage); std::vector stations(options const &Options = {}); // the switches of the station by their numbers: along the chainage, or against it when Reversed; turntables have none std::vector numbering(layout const &Layout, bool const Reversed); +// the names of the switches and the track circuits of the station begin with it: bpd_ +std::string prefix(std::string const &Code); // _, each part of a unit of several with a letter of its own: bpd_15a std::string switch_name(std::string const &Code, int const Number, std::size_t const Part, std::size_t const Parts); +// the joints of the track circuits: 1.5 m before the start of a switch, beyond the fouling points of its branches where the axes +// are 4 m apart, in the middle of the links between switches too short for both, and so where the lines leave the station +std::vector joints(layout const &Layout, std::vector const &Units, options const &Options = {}); +// the track circuits the joints part the station into; the tracks of the lines beyond the joints belong to none +std::vector circuits(layout const &Layout, std::vector const &Units, std::vector const &Joints); +// the numbers of the station tracks as PKP PLK gives them, one for each circuit, empty for those of switches: 1 and 2 the main +// tracks, track 1 on the right along the chainage, the odd ones beyond it on its side, the even ones on the other; a track of +// several circuits has them lettered along the chainage: 5a, 5b +std::vector track_numbers(layout const &Layout, std::vector const &Circuits); // the switches of the station of the switch, without laying it out std::vector switches_of(TTrack &Switch, options const &Options = {}); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index a9d74cd5..b725c4c0 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10410,3 +10410,36 @@ msgstr[0] "Ponumerowano %zu rozjazd" msgstr[1] "Ponumerowano %zu rozjazdy" msgstr[2] "Ponumerowano %zu rozjazdów" +msgid "Set the track circuits" +msgstr "Ustaw izolacje" + +msgid "Cuts the tracks of the station at the joints of the track circuits and names the circuits, as on the lines of PKP PLK: a joint 1.5 m before the start of a switch, beyond the fouling point of each branch where the axes are 4 m apart, in the middle of a link too short for both. Each switch gets bpd_iz, each station track bpd_it: 1 and 2 the main tracks, track 1 on the right along the km, the odd ones beyond it, the even ones on the other side. One step of undo" +msgstr "Tnie tory stacji na stykach izolowanych i nazywa odcinki jak na liniach PKP PLK: styk 1,5 m przed początkiem rozjazdu, za ukresem każdej gałęzi tam, gdzie osie są 4 m od siebie, w połowie łącznika za krótkiego na oba. Każdy rozjazd dostaje bpd_iz, każdy tor stacyjny bpd_it: 1 i 2 to tory główne, tor 1 po prawej w stronę rosnącego km, nieparzyste za nim, parzyste po drugiej stronie. Jeden krok cofania" + +msgid "Give the station its code first: the track circuits are named after it" +msgstr "Najpierw wpisz skrót stacji: od niego biorą się nazwy odcinków izolowanych" + +msgid "%s can't be cut: %s" +msgstr "Nie można przeciąć %s: %s" + +msgid "%s can't be changed: %s" +msgstr "Nie można zmienić %s: %s" + +msgid "The track circuits are set already" +msgstr "Izolacje są już ustawione" + +msgid "%zu track circuit" +msgid_plural "%zu track circuits" +msgstr[0] "%zu odcinek izolowany" +msgstr[1] "%zu odcinki izolowane" +msgstr[2] "%zu odcinków izolowanych" + +msgid "%zu cut" +msgid_plural "%zu cuts" +msgstr[0] "%zu cięcie" +msgstr[1] "%zu cięcia" +msgstr[2] "%zu cięć" + +msgid "track circuit %s" +msgstr "odcinek izolowany %s" + From f5d2ae756924ac8cf9d03ef8ecf7e3ef205e0524 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 10:41:11 +0200 Subject: [PATCH 044/145] feat(editor): the signalling mode numbers a station track as a whole through the switches along it, keeps the numbers the tracks have when set again, renumbers the selected track and labels the schematic without one label over another --- application/editormodesrk.cpp | 186 +++++++++++++++++++++++++++++++--- application/editormodesrk.h | 6 ++ editor/editorStation.cpp | 92 ++++++++++++++--- editor/editorStation.hpp | 4 +- lang/editor_pl.po | 16 ++- 5 files changed, 275 insertions(+), 29 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 2cf93026..26b92ce0 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -29,6 +29,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include #include @@ -81,6 +82,18 @@ std::string unit_number(station::unit const &Unit, std::string const &Stem, std: return std::to_string(Index + 1); } +// the number of the station track a circuit is named after: bpd_it5b of track 5; empty for the other names +std::string track_of(std::string const &Name, std::string const &Stem) +{ + auto const head{Stem + "it"}; + if (Stem.empty() || Name.size() <= head.size() || Name.compare(0, head.size(), head) != 0) + return {}; + auto number{Name.substr(head.size())}; + if (number.size() > 1 && std::isalpha(static_cast(number.back())) && std::isdigit(static_cast(number[number.size() - 2]))) + number.pop_back(); + return number; +} + ImU32 circuit_colour(std::string const &Name) { static std::array const palette{IM_COL32(110, 200, 250, 255), IM_COL32(250, 150, 90, 255), IM_COL32(150, 220, 120, 255), IM_COL32(230, 120, 200, 255), IM_COL32(240, 220, 100, 255), IM_COL32(150, 140, 250, 255)}; @@ -410,7 +423,19 @@ void srk_tool::render_options() item_tooltip("Cuts the tracks of the station at the joints of the track circuits and names the circuits, as on the lines of PKP PLK: " "a joint 1.5 m before the start of a switch, beyond the fouling point of each branch where the axes are 4 m apart, in the middle " "of a link too short for both. Each switch gets bpd_iz, each station track bpd_it: 1 and 2 the main tracks, " - "track 1 on the right along the km, the odd ones beyond it, the even ones on the other side. One step of undo"); + "track 1 on the right along the km, the odd ones beyond it, the even ones on the other side. Run again after changes, it keeps " + "the numbers the tracks have. One step of undo"); + if (auto const *selected{m_selection.selected_track()}; selected != nullptr && false == selected->Isolated.empty()) + if (auto const number{track_of(selected->Isolated.front()->asName, stem_of_open())}; false == number.empty() && begin_fields("##srktrack")) + { + field_row("Track number"); + std::array buffer{}; + number.copy(buffer.data(), buffer.size() - 1); + if (ImGui::InputText("##srktracknumber", buffer.data(), buffer.size(), ImGuiInputTextFlags_EnterReturnsTrue) || ImGui::IsItemDeactivatedAfterEdit()) + renumber_track(*selected, buffer.data()); + item_tooltip("The number of the selected station track: type another one and its track circuits are renamed, bpd_it7a, bpd_it7b. One step of undo"); + ImGui::EndTable(); + } if (false == m_status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); ImGui::Spacing(); @@ -587,10 +612,48 @@ void srk_tool::set_circuits() std::unordered_set captured(created.begin(), created.end()); for (auto const &[track, state] : before) captured.insert(track); + // a station track keeps the name most of it has, given by hand or so far, unless another one has it too; the others take + // the numbers left on their side + std::vector names(circuits.size()); + std::map held; for (std::size_t index = 0; index < circuits.size(); ++index) { auto const &circuit{circuits[index]}; - auto const name{circuit.unit >= 0 ? stem + "iz" + unit_number(units[circuit.unit], stem, circuit.unit) : stem + "it" + numbers[index]}; + if (circuit.unit >= 0) + continue; + std::map counts; + for (auto const *track : circuit.tracks) + if (track->Isolated.size() == 1 && false == track_of(track->Isolated.front()->asName, stem).empty()) + ++counts[track->Isolated.front()->asName]; + if (auto const most{std::max_element(counts.begin(), counts.end(), [](auto const &A, auto const &B) { return A.second < B.second; })}; most != counts.end()) + ++held[names[index] = most->first]; + } + std::unordered_set taken; + for (auto &name : names) + if (false == name.empty() && held[name] > 1) + name.clear(); + else if (false == name.empty()) + taken.insert(name); + for (std::size_t index = 0; index < circuits.size(); ++index) + { + auto const &circuit{circuits[index]}; + auto &name{names[index]}; + if (circuit.unit >= 0) + name = stem + "iz" + unit_number(units[circuit.unit], stem, circuit.unit); + else if (name.empty()) + { + auto number{numbers[index]}; + while (taken.count(stem + "it" + number) > 0) + { + std::size_t digits{0}; + while (digits < number.size() && std::isdigit(static_cast(number[digits]))) + ++digits; + if (digits == 0) + break; + number = std::to_string(std::stoi(number.substr(0, digits)) + 2) + number.substr(digits); + } + taken.insert(name = stem + "it" + number); + } for (auto *track : circuit.tracks) { if (track->Isolated.size() == 1 && track->Isolated.front()->asName == name) @@ -611,6 +674,48 @@ void srk_tool::set_circuits() m_status = format(STR_P("%zu track circuit", "%zu track circuits", count), count) + ", " + format(STR_P("%zu cut", "%zu cuts", created.size()), created.size()); } +std::string srk_tool::stem_of_open() const +{ + return m_record >= 0 && false == m_records[m_record].code.empty() ? station::prefix(m_records[m_record].code) : std::string{}; +} + +void srk_tool::renumber_track(TTrack const &Track, std::string const &Number) +{ + m_status.clear(); + auto const stem{stem_of_open()}; + auto const from{Track.Isolated.empty() ? std::string{} : track_of(Track.Isolated.front()->asName, stem)}; + std::string to; + for (auto const letter : Number) + if (std::isalnum(static_cast(letter))) + to += static_cast(std::tolower(static_cast(letter))); + if (from.empty() || to.empty() || to == from) + return; + std::vector> before; + std::unordered_set seen; + for (auto const &path : m_layout->paths) + { + if (path.track->Isolated.empty() || false == seen.insert(path.track).second) + continue; + auto const number{track_of(path.track->Isolated.front()->asName, stem)}; + if (number == to) + { + m_status = format(STR_C("The station has a track %s already"), to.c_str()); + return; + } + if (number == from) + before.emplace_back(path.track, editor_track::capture(*path.track)); + } + for (auto &[track, state] : before) + { + auto const &name{track->Isolated.front()->asName}; + editor_track::isolated(*track, {stem + "it" + to + name.substr(stem.size() + 2 + from.size())}); + } + if (before.empty()) + return; + m_history.push_track_snapshot(std::move(before)); + m_status = format(STR_C("Track %s is track %s now"), from.c_str(), to.c_str()); +} + void srk_tool::survey() { m_circuit_of.assign(m_layout.has_value() ? m_layout->paths.size() : 0, -1); @@ -619,7 +724,7 @@ void srk_tool::survey() if (false == m_layout.has_value()) return; auto const &layout{*m_layout}; - auto const stem{m_record >= 0 && false == m_records[m_record].code.empty() ? station::prefix(m_records[m_record].code) : std::string{}}; + auto const stem{stem_of_open()}; std::vector weights; for (std::size_t index = 0; index < layout.paths.size(); ++index) { @@ -634,6 +739,7 @@ void srk_tool::survey() if (found == m_circuits.end()) { found = m_circuits.insert(m_circuits.end(), {glm::dvec2{0.0}, name, circuit_colour(name)}); + found->labelled = stem.empty() || name.compare(0, stem.size() + 2, stem + "iz") != 0; weights.push_back(0.0); } auto const circuit{static_cast(found - m_circuits.begin())}; @@ -643,11 +749,24 @@ void srk_tool::survey() found->at += point; weights[circuit] += 1.0; } + found->length += path.length; } for (std::size_t index = 0; index < m_circuits.size(); ++index) { auto &mark{m_circuits[index]}; mark.at /= std::max(1.0, weights[index]); + // on its track, nearest the middle of its points + auto nearest{std::numeric_limits::max()}; + glm::dvec2 on{mark.at}; + for (std::size_t path = 0; path < layout.paths.size(); ++path) + if (m_circuit_of[path] == static_cast(index)) + for (auto const &point : layout.paths[path].schematic) + if (auto const distance{glm::distance(point, mark.at)}; distance < nearest) + { + nearest = distance; + on = point; + } + mark.at = on; // bpd_it3 shown as It3 if (false == stem.empty() && mark.text.compare(0, stem.size(), stem) == 0) mark.text.erase(0, stem.size()); @@ -806,8 +925,24 @@ void srk_tool::render_canvas(float const Height) draw_path(path, overlay_color::selected, 4.0f); if (m_hover >= 0) draw_path(layout.paths[m_hover], kHovered, 4.0f); - // the switches: a dot at the start, the name over or under it in turn; the number alone, once named after the station - auto const stem{m_record >= 0 && false == m_records[m_record].code.empty() ? station::prefix(m_records[m_record].code) : std::string{}}; + // the labels, none over another: the switches first, a dot at the start and the name over or under it in turn, the number + // alone once named after the station; then the station tracks under or over them, the longest first + std::vector placed; + auto const label = [&](std::string const &Text, ImU32 const Colour, std::initializer_list Places) { + auto const extent{ImGui::CalcTextSize(Text.c_str())}; + for (auto const &place : Places) + { + ImVec4 const box{place.x - extent.x * 0.5f - 2.0f, place.y - 1.0f, place.x + extent.x * 0.5f + 2.0f, place.y + extent.y + 1.0f}; + if (std::any_of(placed.begin(), placed.end(), [&](ImVec4 const &Other) { return box.x < Other.z && Other.x < box.z && box.y < Other.w && Other.y < box.w; })) + continue; + placed.push_back(box); + draw->AddRectFilled(ImVec2(box.x, box.y), ImVec2(box.z, box.w), IM_COL32(22, 24, 28, 200), 2.0f); + draw->AddText(ImVec2(place.x - extent.x * 0.5f, place.y), Colour, Text.c_str()); + return; + } + }; + auto const stem{stem_of_open()}; + auto const height{ImGui::GetTextLineHeight()}; for (std::size_t index = 0; index < layout.switches.size(); ++index) { auto const *track{layout.switches[index]}; @@ -816,16 +951,23 @@ void srk_tool::render_canvas(float const Height) auto text{track->name()}; if (false == stem.empty() && text.size() > stem.size() && text.compare(0, stem.size(), stem) == 0) text.erase(0, stem.size()); - auto const extent{ImGui::CalcTextSize(text.c_str())}; - ImVec2 const place{at.x - extent.x * 0.5f, index % 2 == 0 ? at.y - extent.y - 6.0f : at.y + 6.0f}; - draw->AddRectFilled(ImVec2(place.x - 2.0f, place.y - 1.0f), ImVec2(place.x + extent.x + 2.0f, place.y + extent.y + 1.0f), IM_COL32(22, 24, 28, 200), 2.0f); - draw->AddText(place, kSwitch, text.c_str()); + ImVec2 const over{at.x, at.y - height - 6.0f}; + ImVec2 const under{at.x, at.y + 6.0f}; + if (index % 2 == 0) + label(text, kSwitch, {over, under}); + else + label(text, kSwitch, {under, over}); } - for (auto const &mark : m_circuits) + std::vector order; + for (std::size_t index = 0; index < m_circuits.size(); ++index) + if (m_circuits[index].labelled) + order.push_back(index); + std::sort(order.begin(), order.end(), [&](std::size_t A, std::size_t B) { return m_circuits[A].length > m_circuits[B].length; }); + for (auto const index : order) { + auto const &mark{m_circuits[index]}; auto const at{screen_of(mark.at)}; - auto const extent{ImGui::CalcTextSize(mark.text.c_str())}; - draw->AddText(ImVec2(at.x - extent.x * 0.5f, at.y + 4.0f), mark.colour, mark.text.c_str()); + label(mark.text, mark.colour, {ImVec2(at.x, at.y + 4.0f), ImVec2(at.x, at.y - height - 4.0f)}); } draw->PopClipRect(); if (hovered && m_hover >= 0 && false == m_dragging) @@ -964,6 +1106,26 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri WriteLog(format("SELFTEST srkcircuits %zu named, %zu joints:", found.size(), m_ticks.size()) + list); return editor_selftest_result::handled; } + if (Command == "srktrack") + { + // srktrack circuit= number=: the station track of the circuit renumbered + auto const values{selftest_values(Words)}; + auto const circuit{values.find("circuit")}; + auto const number{values.find("number")}; + TTrack const *found{nullptr}; + if (m_layout.has_value() && circuit != values.end()) + for (auto const &path : m_layout->paths) + if (false == path.track->Isolated.empty() && path.track->Isolated.front()->asName == circuit->second) + found = path.track; + if (found == nullptr || number == values.end()) + { + WriteLog("SELFTEST srktrack: no station open, or no such circuit"); + return editor_selftest_result::handled; + } + renumber_track(*found, number->second); + WriteLog("SELFTEST srktrack: " + m_status); + return editor_selftest_result::handled; + } if (Command == "srkstations") { load(); diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 0ef8fc8b..2201e790 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -76,6 +76,8 @@ class srk_tool final : public editor_tool glm::dvec2 at{0.0}; // in the schematic std::string text; ImU32 colour{0}; + double length{0.0}; + bool labelled{true}; // the circuits of the switches go without, named after them }; std::vector m_circuit_of; // of each path of the layout, an index into m_circuits, -1 for none std::vector m_circuits; @@ -97,4 +99,8 @@ class srk_tool final : public editor_tool void set_circuits(); // the track circuits as the tracks have them, for the schematic void survey(); + // the names of the switches and the circuits of the open station begin with it, empty while it has no code + std::string stem_of_open() const; + // the circuits of the station track of the track renamed after another number; one step of the history + void renumber_track(TTrack const &Track, std::string const &Number); }; diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index a35ccc79..ef2c78f1 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -20,6 +20,8 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include +#include #include #include #include @@ -233,6 +235,7 @@ double constexpr kClear{4.0}; // m, between the axes at a joint beyond a fouling double constexpr kReach{120.0}; // m, along a branch looked for its fouling point double constexpr kSection{30.0}; // m, the shortest stretch between two joints made a track circuit of its own double constexpr kSnap{0.3}; // m, a joint this close to the end of a track lies at the end +double constexpr kThrough{400.0}; // m, of the switches a station track is followed through for its circuits beyond // the points of the way out of an end of the track, as a train runs: along the main path of a switch run into from its start, // back along the path of one run into from the end of it @@ -854,9 +857,12 @@ std::vector track_numbers(layout const &Layout, std::vector const &Tracks) { return std::any_of(Circuit.tracks.begin(), Circuit.tracks.end(), [&](TTrack const *Track) { return Tracks.count(Track) > 0; }); }; std::vector axis; std::vector other; - std::vector right; - std::vector left; + std::vector rest; + std::unordered_map circuit_of; for (std::size_t index = 0; index < Circuits.size(); ++index) + { + for (auto const *track : Circuits[index].tracks) + circuit_of[track] = index; if (Circuits[index].unit >= 0) continue; else if (on(Circuits[index], main)) @@ -864,7 +870,72 @@ std::vector track_numbers(layout const &Layout, std::vector group(Circuits.size()); + std::iota(group.begin(), group.end(), 0); + auto const root = [&](std::size_t Index) { + while (group[Index] != Index) + Index = group[Index] = group[group[Index]]; + return Index; + }; + std::unordered_set const plain(rest.begin(), rest.end()); + for (auto const index : rest) + for (auto const *track : Circuits[index].tracks) + for (auto const atend : {false, true}) + { + if (node(*track)) + break; + auto point{end_of(*track, 0, atend)}; + auto *next{neighbour(*track, 0, atend)}; + double length{0.0}; + while (next != nullptr && length < kThrough && rail(*next)) + { + auto const there{circuit_of.find(next)}; + if (there == circuit_of.end() || there->second == index) + break; + if (Circuits[there->second].unit < 0) + { + if (plain.count(there->second) > 0) + group[root(there->second)] = root(index); + break; + } + int path{0}; + bool end{false}; + if (false == end_at(*next, point, path, end)) + break; + if (next->eType == tt_Switch) + { + if (end && path != straighter(*next)) + break; + path = straighter(*next); + } + length += plan_length(*next, path); + point = end_of(*next, path, false == end); + auto const *through{next}; + next = neighbour(*through, path, false == end); + } + } + std::vector, double>> right; + std::vector, double>> left; + { + std::map> groups; + for (auto const index : rest) + groups[root(index)].push_back(index); + for (auto &[first, members] : groups) + { + double weighted{0.0}; + double length{0.0}; + for (auto const index : members) + { + weighted += Circuits[index].offset * Circuits[index].length; + length += Circuits[index].length; + } + auto const offset{length > 0.0 ? weighted / length : Circuits[first].offset}; + (offset < 0.0 ? right : left).emplace_back(std::move(members), offset); + } + } auto const mean = [&](std::vector const &Group) { double weighted{0.0}; double length{0.0}; @@ -880,20 +951,15 @@ std::vector track_numbers(layout const &Layout, std::vector, int>> tracks{{axis, swapped ? 2 : 1}}; if (false == other.empty()) tracks.emplace_back(other, swapped ? 1 : 2); - // beyond the main tracks the circuits side by side, the nearest first, the odd numbers to the right, the even ones to the left + // beyond the main tracks the others side by side, the nearest first, the odd numbers to the right, the even ones to the left for (auto *side : {&right, &left}) { - std::sort(side->begin(), side->end(), [&](std::size_t A, std::size_t B) { return std::abs(Circuits[A].offset) < std::abs(Circuits[B].offset); }); + std::sort(side->begin(), side->end(), [&](auto const &A, auto const &B) { return std::abs(A.second) < std::abs(B.second); }); auto number{side == &right ? 3 : other.empty() ? 2 : 4}; - for (std::size_t index = 0; index < side->size(); ++index) + for (auto &[members, offset] : *side) { - if (index == 0 || std::abs(Circuits[(*side)[index]].offset) - std::abs(Circuits[(*side)[index - 1]].offset) > 2.0) - { - if (index > 0) - number += 2; - tracks.emplace_back(std::vector{}, number); - } - tracks.back().first.push_back((*side)[index]); + tracks.emplace_back(std::move(members), number); + number += 2; } } std::vector result(Circuits.size()); diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index 0efda3b0..89ffe9fd 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -101,8 +101,8 @@ std::vector joints(layout const &Layout, std::vector const &Units, // the track circuits the joints part the station into; the tracks of the lines beyond the joints belong to none std::vector circuits(layout const &Layout, std::vector const &Units, std::vector const &Joints); // the numbers of the station tracks as PKP PLK gives them, one for each circuit, empty for those of switches: 1 and 2 the main -// tracks, track 1 on the right along the chainage, the odd ones beyond it on its side, the even ones on the other; a track of -// several circuits has them lettered along the chainage: 5a, 5b +// tracks, track 1 on the right along the chainage, the odd ones beyond it on its side, the even ones on the other; a track goes +// on through the switches along their straighter paths, its circuits lettered along the chainage: 5a, 5b std::vector track_numbers(layout const &Layout, std::vector const &Circuits); // the switches of the station of the switch, without laying it out std::vector switches_of(TTrack &Switch, options const &Options = {}); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index b725c4c0..e7f9adf4 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10413,8 +10413,8 @@ msgstr[2] "Ponumerowano %zu rozjazdów" msgid "Set the track circuits" msgstr "Ustaw izolacje" -msgid "Cuts the tracks of the station at the joints of the track circuits and names the circuits, as on the lines of PKP PLK: a joint 1.5 m before the start of a switch, beyond the fouling point of each branch where the axes are 4 m apart, in the middle of a link too short for both. Each switch gets bpd_iz, each station track bpd_it: 1 and 2 the main tracks, track 1 on the right along the km, the odd ones beyond it, the even ones on the other side. One step of undo" -msgstr "Tnie tory stacji na stykach izolowanych i nazywa odcinki jak na liniach PKP PLK: styk 1,5 m przed początkiem rozjazdu, za ukresem każdej gałęzi tam, gdzie osie są 4 m od siebie, w połowie łącznika za krótkiego na oba. Każdy rozjazd dostaje bpd_iz, każdy tor stacyjny bpd_it: 1 i 2 to tory główne, tor 1 po prawej w stronę rosnącego km, nieparzyste za nim, parzyste po drugiej stronie. Jeden krok cofania" +msgid "Cuts the tracks of the station at the joints of the track circuits and names the circuits, as on the lines of PKP PLK: a joint 1.5 m before the start of a switch, beyond the fouling point of each branch where the axes are 4 m apart, in the middle of a link too short for both. Each switch gets bpd_iz, each station track bpd_it: 1 and 2 the main tracks, track 1 on the right along the km, the odd ones beyond it, the even ones on the other side. Run again after changes, it keeps the numbers the tracks have. One step of undo" +msgstr "Tnie tory stacji na stykach izolowanych i nazywa odcinki jak na liniach PKP PLK: styk 1,5 m przed początkiem rozjazdu, za ukresem każdej gałęzi tam, gdzie osie są 4 m od siebie, w połowie łącznika za krótkiego na oba. Każdy rozjazd dostaje bpd_iz, każdy tor stacyjny bpd_it: 1 i 2 to tory główne, tor 1 po prawej w stronę rosnącego km, nieparzyste za nim, parzyste po drugiej stronie. Kliknięte ponownie po zmianach zostawia numery, które tory już mają. Jeden krok cofania" msgid "Give the station its code first: the track circuits are named after it" msgstr "Najpierw wpisz skrót stacji: od niego biorą się nazwy odcinków izolowanych" @@ -10443,3 +10443,15 @@ msgstr[2] "%zu cięć" msgid "track circuit %s" msgstr "odcinek izolowany %s" +msgid "Track number" +msgstr "Numer toru" + +msgid "The number of the selected station track: type another one and its track circuits are renamed, bpd_it7a, bpd_it7b. One step of undo" +msgstr "Numer zaznaczonego toru stacyjnego: wpisz inny, a jego odcinki izolowane dostaną nowe nazwy, bpd_it7a, bpd_it7b. Jeden krok cofania" + +msgid "The station has a track %s already" +msgstr "Stacja ma już tor %s" + +msgid "Track %s is track %s now" +msgstr "Tor %s ma teraz numer %s" + From 21d7a0e94aa3837ebd3bb9cf96abfd3e2193ed09 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 11:07:58 +0200 Subject: [PATCH 045/145] fix(editor): a track with its track circuits changed is marked for the save, the circuits the signalling mode set without cutting the track were lost from the saved scenery --- editor/editorTrack.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index c2aa39ea..db98f6b2 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -215,6 +215,7 @@ void editor_track::isolated(TTrack &Track, std::vector const &Names isolated->AssignEvents(); Track.Isolated.push_back(isolated); } + Track.mark_dirty(); } void editor_track::overhead(TTrack &Track, float const Overhead) From 8f8a1518b7ede01cc887c08752341fe6ca8ffa61 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 11:07:58 +0200 Subject: [PATCH 046/145] feat(editor): the signalling mode places the signals of a station and names them as PKP PLK does, the entry ones out on the lines with the circuits before them, the exit ones at the ends of the station tracks, the shunting ones at the shorter tracks, and renames the signals standing in their places --- application/editormode.h | 4 +- application/editormodesignal.cpp | 114 +++++++++++ application/editormodesrk.cpp | 323 ++++++++++++++++++++++++++++++- application/editormodesrk.h | 16 +- application/editortool.h | 42 +++- editor/editorStation.cpp | 277 ++++++++++++++++++++++++++ editor/editorStation.hpp | 38 ++++ lang/editor_pl.po | 33 ++++ 8 files changed, 840 insertions(+), 7 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index e2692fc8..dd42ffba 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1890,6 +1890,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::size_t move_templates(std::vector> const &Moves) override; // takes them out, as one step of the undo std::size_t remove_templates(std::vector const &Instances) override; + bool standing_signals(std::vector &Signals, bool const Wait) override; + std::size_t signal_batch(std::vector const &Placed, std::vector const &Renamed, std::vector> States, std::vector Created, std::string &Error) override; struct signal_template { @@ -2381,7 +2383,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri hekto_tool m_hekto{*this, *this, *this, *this, *this, *this}; fouling_tool m_fouling{*this, *this, *this, *this, *this}; parallel_tool m_parallel{*this, *this, *this, *this}; - srk_tool m_srk{*this, *this, *this}; + srk_tool m_srk{*this, *this, *this, *this}; std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle, &m_hekto, &m_fouling, &m_parallel, &m_srk}; void infra_rebase(); diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 5366b5fa..55ad4950 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -31,6 +31,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include namespace { @@ -874,6 +875,119 @@ void editor_mode::signal_renamed(std::string const &Before, std::string const &A Snapshot.node_name = Snapshot.tracks.front().first->name(); } +bool editor_mode::standing_signals(std::vector &Signals, bool const Wait) +{ + Signals.clear(); + signal_scan(Wait); + if (m_signal.templates.empty()) + return false; + signal_refresh(true); + for (auto const &standing : m_signal.standing) + if (standing.kind >= 0) + Signals.push_back({standing.include.instance, standing.name, kKinds[standing.kind], standing.include.location, standing.include.yaw}); + return true; +} + +std::size_t editor_mode::signal_batch(std::vector const &Placed, std::vector const &Renamed, std::vector> States, std::vector Created, std::string &Error) +{ + auto &tool{m_signal}; + signal_scan(); + signal_refresh(true); + std::vector items; + std::vector> reads; + for (auto const &order : Placed) + { + auto const kind{kind_index(order.kind)}; + auto const *item{kind >= 0 && tool.chosen[kind] >= 0 && tool.chosen[kind] < static_cast(tool.templates.size()) ? &tool.templates[tool.chosen[kind]] : nullptr}; + if (item == nullptr) + { + Error = format(STR_C("no template of the kind %s"), STR(kKindLabels[std::max(0, kind)]).c_str()); + break; + } + signal_spot spot; + if (order.track == nullptr || false == signal_spot_at(*order.track, order.path, order.point, order.point, order.facing, order.side, *item, spot)) + continue; + template_item placing; + placing.file = signal_variant(*item, spot); + placing.location = spot.position; + placing.yaw = spot.yaw; + placing.described = true; + placing.values["name"] = order.name; + auto const *variant{signal_template_of(placing.file)}; + if (variant == nullptr) + variant = item; + if (variant->plate) + placing.values["#6"] = plate_for(std::string{}, order.plate); + if (variant->linked) + placing.values["#7"] = "none"; + items.push_back(std::move(placing)); + reads.emplace_back(spot, variant->read.empty() ? std::string{} : editor_includes::substitute(variant->read, {order.name})); + } + // the names passed round among the signals go through ones of their own first, or a read event renamed would be renamed again + EditorSnapshot step; + step.tracks = std::move(States); + std::vector> directives; + for (auto const temporary : {true, false}) + for (auto const &rename : Renamed) + { + auto const *standing{signal_standing_of(rename.instance)}; + auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr}; + auto const before{scene::Layers.directive(rename.instance)}; + std::string file; + std::vector values; + if (item == nullptr || false == editor_includes::parse_directive(before, file, values) || values.empty()) + continue; + auto const read_before{item->read.empty() ? std::string{} : editor_includes::substitute(item->read, values)}; + values[0] = rename.name + (temporary ? "~" : ""); + if (item->plate && values.size() > 5) + values[5] = plate_for(std::string{}, rename.plate); + auto const read_after{item->read.empty() ? std::string{} : editor_includes::substitute(item->read, values)}; + auto const directive{editor_includes::compose_directive(file, values)}; + if (directive == before) + continue; + if (temporary) + directives.emplace_back(rename.instance, before); + set_include_directive(rename.instance, directive); + if (read_before != read_after) + signal_renamed(read_before, read_after, step); + } + std::vector instances; + if (Error.empty() && false == items.empty()) + { + if (place_templates(items, Error) == 0) + reads.clear(); + else + { + auto placed{std::move(m_history.back())}; + m_history.pop_back(); + instances = std::move(placed.instances); + } + } + else + reads.clear(); + std::unordered_set const created(Created.begin(), Created.end()); + for (auto const &[spot, read] : reads) + { + if (read.empty() || false == spot.track->Dynamics.empty() || false == scene::Layers.editable(spot.track)) + continue; + if (created.count(spot.track) == 0 && std::none_of(step.tracks.begin(), step.tracks.end(), [&](auto const &State) { return State.first == spot.track; })) + step.tracks.emplace_back(spot.track, editor_track::capture(*spot.track)); + editor_track::events(*spot.track, spot.list).emplace_back(ToLower(read), nullptr); + std::string missing; + editor_track::bind_events(*spot.track, missing); + spot.track->mark_dirty(); + } + auto const count{instances.size()}; + if (step.tracks.empty() && Created.empty() && instances.empty() && directives.empty()) + return 0; + push_track_snapshot(std::move(step.tracks), std::move(Created)); + auto &pushed{m_history.back()}; + pushed.instances = std::move(instances); + pushed.directives.insert(pushed.directives.end(), directives.begin(), directives.end()); + signal_refresh(true); + return count; +} + void editor_mode::signal_select(scene::instance_handle const Instance) { m_signal.selected = Instance; diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 26b92ce0..cddcd5aa 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "utilities/Globals.h" #include "utilities/Logs.h" +#include "utilities/utilities.h" #include "world/Track.h" #include "imgui/imgui.h" @@ -31,6 +32,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include @@ -44,6 +46,7 @@ ImU32 constexpr kPlain{IM_COL32(170, 176, 188, 255)}; ImU32 constexpr kSwitch{IM_COL32(240, 200, 90, 255)}; ImU32 constexpr kApproach{IM_COL32(95, 100, 112, 255)}; ImU32 constexpr kHovered{IM_COL32(255, 255, 255, 255)}; +ImU32 constexpr kSignal{IM_COL32(240, 95, 85, 255)}; bool is_switch(TTrack const &Track) { @@ -82,18 +85,56 @@ std::string unit_number(station::unit const &Unit, std::string const &Stem, std: return std::to_string(Index + 1); } -// the number of the station track a circuit is named after: bpd_it5b of track 5; empty for the other names +// the number of the station track a circuit is named after: bpd_it5b of track 5; empty for the other names, bpd_ita of the +// circuit before the entry signal A among them std::string track_of(std::string const &Name, std::string const &Stem) { auto const head{Stem + "it"}; if (Stem.empty() || Name.size() <= head.size() || Name.compare(0, head.size(), head) != 0) return {}; auto number{Name.substr(head.size())}; + if (std::none_of(number.begin(), number.end(), [](char const Letter) { return std::isdigit(static_cast(Letter)) != 0; })) + return {}; if (number.size() > 1 && std::isalpha(static_cast(number.back())) && std::isdigit(static_cast(number[number.size() - 2]))) number.pop_back(); return number; } +// the circuit between an entry signal and the station: bpd_ita +bool entry_circuit(std::string const &Name, std::string const &Stem) +{ + auto const head{Stem + "it"}; + return false == Stem.empty() && Name.size() > head.size() && Name.compare(0, head.size(), head) == 0 && + std::all_of(Name.begin() + head.size(), Name.end(), [](char const Letter) { return std::isalpha(static_cast(Letter)) != 0; }); +} + +// the name of a signal as its plate and the schematic show it: bpd_tm3 as Tm3, bpd_f1 as F1 +std::string signal_shown(std::string const &Name, std::string const &Stem) +{ + auto text{false == Stem.empty() && Name.size() > Stem.size() && ToLower(Name).compare(0, Stem.size(), Stem) == 0 ? Name.substr(Stem.size()) : Name}; + std::transform(text.begin(), text.end(), text.begin(), [](char const Letter) { return static_cast(std::toupper(static_cast(Letter))); }); + if (text.size() > 2 && text[0] == 'T' && (text[1] == 'M' || text[1] == 'O')) + text[1] = static_cast(std::tolower(static_cast(text[1]))); + return text; +} + +// the way the trains a signal faces run, in the plan: it stands turned to them +glm::dvec2 travel_of(double const Yaw) +{ + auto const yaw{glm::radians(Yaw + 180.0)}; + return {std::sin(yaw), std::cos(yaw)}; +} + +// how far the point lies from the tracks of the station, in the plan +double gap_to(station::layout const &Layout, glm::dvec3 const &Point) +{ + auto best{std::numeric_limits::max()}; + for (auto const &path : Layout.paths) + for (auto const &point : path.points) + best = std::min(best, glm::distance(glm::dvec2{point.x, point.z}, glm::dvec2{Point.x, Point.z})); + return best; +} + ImU32 circuit_colour(std::string const &Name) { static std::array const palette{IM_COL32(110, 200, 250, 255), IM_COL32(250, 150, 90, 255), IM_COL32(150, 220, 120, 255), IM_COL32(230, 120, 200, 255), IM_COL32(240, 220, 100, 255), IM_COL32(150, 140, 250, 255)}; @@ -270,6 +311,8 @@ void srk_tool::update() } if ((m_layout.has_value() || m_anchor != nullptr) && m_history.history_size() != m_history_size) refresh(); + else if (m_layout.has_value() && false == m_signals_known) + survey_signals(); // a track of another station selected opens it auto *track{m_selection.selected_track()}; if (track == nullptr || track == m_picked) @@ -425,6 +468,14 @@ void srk_tool::render_options() "of a link too short for both. Each switch gets bpd_iz, each station track bpd_it: 1 and 2 the main tracks, " "track 1 on the right along the km, the odd ones beyond it, the even ones on the other side. Run again after changes, it keeps " "the numbers the tracks have. One step of undo"); + step_heading(3, STR_C("Signals")); + if (ImGui::Button(STR_C("Set the signals"), ImVec2(-1.0f, 0.0f))) + set_signals(); + item_tooltip("Places the signals of the station, as on the lines of PKP PLK, and names them after its code: an entry signal 300 m out on " + "each line, A, B... from the start of the km, Z, Y... from the end, the circuit from it to the station bpd_ita; an exit " + "signal at the end of each station track a train is sent out of, F1, G2 towards the start, R1, P2 towards the end; a " + "shunting signal Tm1, Tm2... at the end of a shorter track. A signal standing by the place of one gets its name, none is " + "doubled. The kinds of signals chosen in the Signals tool. One step of undo"); if (auto const *selected{m_selection.selected_track()}; selected != nullptr && false == selected->Isolated.empty()) if (auto const number{track_of(selected->Isolated.front()->asName, stem_of_open())}; false == number.empty() && begin_fields("##srktrack")) { @@ -716,6 +767,232 @@ void srk_tool::renumber_track(TTrack const &Track, std::string const &Number) m_status = format(STR_C("Track %s is track %s now"), from.c_str(), to.c_str()); } +void srk_tool::set_signals() +{ + m_status.clear(); + auto const stem{stem_of_open()}; + if (stem.empty()) + { + m_status = STR_C("Give the station its code first: the signals are named after it"); + return; + } + auto const &layout{*m_layout}; + // the station tracks by their numbers, the circuits as the tracks have them + std::map>> numbers; + std::unordered_set inside; + for (auto const &path : layout.paths) + { + if (path.track->Isolated.empty() || inside.count(path.track) > 0) + continue; + auto const &name{path.track->Isolated.front()->asName}; + if (name.size() <= stem.size() || name.compare(0, stem.size(), stem) != 0 || entry_circuit(name, stem)) + continue; + inside.insert(path.track); + if (auto const number{track_of(name, stem)}; false == number.empty()) + numbers[number][name].push_back(path.track); + } + if (numbers.empty()) + { + m_status = STR_C("Set the track circuits first: the signals stand at their joints"); + return; + } + std::vector tracks; + for (auto &[number, circuits] : numbers) + { + station::numbered entry; + entry.number = number; + for (auto &[name, members] : circuits) + entry.circuits.push_back(std::move(members)); + tracks.push_back(std::move(entry)); + } + auto plan{station::signals(layout, tracks, [&](TTrack const &Track) { return inside.count(&Track) > 0; })}; + std::vector standing; + m_signals.standing_signals(standing, true); + standing.erase(std::remove_if(standing.begin(), standing.end(), [&](standing_signal const &Signal) { return gap_to(layout, Signal.position) > 8.0; }), standing.end()); + auto const full = [&](station::signal const &Signal) { return stem + ToLower(Signal.name); }; + // the standing ones by their places, then by their names the ones moved away + std::vector match(plan.size(), -1); + std::vector used(standing.size(), 0); + std::vector> candidates; + for (std::size_t i = 0; i < plan.size(); ++i) + { + auto const &planned{plan[i]}; + glm::dvec2 const travel{glm::normalize(glm::dvec2{planned.travel.x, planned.travel.z} + glm::dvec2{1e-12, 0.0})}; + auto const reach{planned.kind == station::signal::role::entry ? 400.0 : planned.kind == station::signal::role::exit ? 60.0 : 40.0}; + for (std::size_t k = 0; k < standing.size(); ++k) + { + auto const &signal{standing[k]}; + if ((signal.kind == "shunt") != (planned.kind == station::signal::role::shunting) || (signal.kind != "shunt" && signal.kind != "main") || glm::dot(travel_of(signal.yaw), travel) < 0.87) + continue; + glm::dvec2 const offset{signal.position.x - planned.point.x, signal.position.z - planned.point.z}; + auto const along{glm::dot(offset, travel)}; + auto const across{std::abs(offset.x * travel.y - offset.y * travel.x)}; + if (std::abs(along) < reach && across < 3.0) + candidates.emplace_back(std::abs(along) + across * 10.0, i, k); + } + } + std::sort(candidates.begin(), candidates.end()); + for (auto const &[score, i, k] : candidates) + if (match[i] < 0 && 0 == used[k]) + { + match[i] = static_cast(k); + used[k] = 1; + } + for (std::size_t i = 0; i < plan.size(); ++i) + for (std::size_t k = 0; k < standing.size() && match[i] < 0; ++k) + if (0 == used[k] && ToLower(standing[k].name) == full(plan[i])) + { + match[i] = static_cast(k); + used[k] = 1; + } + // the circuits from the entry signals to the station: the line cut by each one, all of it checked before any is + struct cut + { + std::size_t signal{0}; + TTrack *track{nullptr}; + double parameter{0.0}; + glm::dvec3 point{0.0}; + bool outward{false}; + std::vector named; // between the station and the cut + }; + std::vector cuts; + for (std::size_t i = 0; i < plan.size(); ++i) + { + auto const &planned{plan[i]}; + if (planned.kind != station::signal::role::entry || planned.run.empty()) + continue; + auto const at{match[i] >= 0 ? standing[match[i]].position : planned.point}; + cut entry; + entry.signal = i; + auto best{std::numeric_limits::max()}; + std::size_t nearest{0}; + for (std::size_t k = 0; k < planned.run.size(); ++k) + { + auto const t{editor_track::nearest_parameter(*planned.run[k].first, at)}; + auto const point{editor_track::point_at(*planned.run[k].first, t)}; + if (auto const distance{glm::distance(glm::dvec2{point.x, point.z}, glm::dvec2{at.x, at.z})}; distance < best) + { + best = distance; + nearest = k; + entry.parameter = t; + entry.point = point; + } + } + for (std::size_t k = 0; k < nearest; ++k) + entry.named.push_back(planned.run[k].first); + entry.track = planned.run[nearest].first; + entry.outward = planned.run[nearest].second; + auto const &ends{entry.track->m_paths.front().points}; + auto const to_start{glm::distance(entry.point, ends[segment_data::point::start])}; + auto const to_end{glm::distance(entry.point, ends[segment_data::point::end])}; + std::string reason; + if (std::min(to_start, to_end) <= 0.3) + { + // at an end already + if ((to_end < to_start) == entry.outward) + entry.named.push_back(entry.track); + entry.track = nullptr; + } + else if (false == editor_track::can_edit_geometry(*entry.track, reason)) + { + m_status = format(STR_C("%s can't be cut: %s"), name_of(*entry.track).c_str(), reason.c_str()); + return; + } + for (auto *part : entry.named) + if (false == scene::Layers.editable(part, &reason)) + { + m_status = format(STR_C("%s can't be changed: %s"), name_of(*part).c_str(), reason.c_str()); + return; + } + cuts.push_back(std::move(entry)); + } + std::vector> before; + std::vector created; + std::unordered_set captured; + for (auto &entry : cuts) + { + auto &planned{plan[entry.signal]}; + if (entry.track != nullptr) + { + captured.insert(entry.track); + before.emplace_back(entry.track, editor_track::capture(*entry.track)); + if (auto *piece{editor_track::split_path(*entry.track, entry.parameter)}) + { + created.push_back(piece); + captured.insert(piece); + entry.named.push_back(entry.outward ? entry.track : piece); + planned.track = entry.outward ? piece : entry.track; + planned.point = entry.point; + } + } + auto const name{stem + "it" + ToLower(planned.name)}; + for (auto *part : entry.named) + { + if (part->Isolated.size() == 1 && part->Isolated.front()->asName == name) + continue; + if (captured.insert(part).second) + before.emplace_back(part, editor_track::capture(*part)); + editor_track::isolated(*part, {name}); + } + } + std::vector placed; + std::vector renamed; + for (std::size_t i = 0; i < plan.size(); ++i) + { + auto const &planned{plan[i]}; + auto const plate{ToLower(planned.name)}; + if (match[i] >= 0) + { + if (ToLower(standing[match[i]].name) != full(planned)) + renamed.push_back({standing[match[i]].instance, full(planned), plate}); + continue; + } + signal_order order; + order.track = planned.track; + order.point = planned.point; + order.facing = planned.facing; + order.side = planned.side; + order.kind = planned.kind == station::signal::role::shunting ? "shunt" : "main"; + order.name = full(planned); + order.plate = plate; + placed.push_back(std::move(order)); + } + // the others keep their names, unless one of these takes it + for (std::size_t k = 0; k < standing.size(); ++k) + if (0 == used[k] && std::any_of(plan.begin(), plan.end(), [&](station::signal const &Signal) { return full(Signal) == ToLower(standing[k].name); })) + renamed.push_back({standing[k].instance, ToLower(standing[k].name) + "_old", signal_shown(standing[k].name, stem) + "_old"}); + if (placed.empty() && renamed.empty() && before.empty() && created.empty()) + { + m_status = STR_C("The signals are set already"); + return; + } + std::string error; + auto const count{m_signals.signal_batch(placed, renamed, std::move(before), std::move(created), error)}; + if (false == error.empty()) + m_status = format(STR_C("The signals couldn't be placed: %s"), error.c_str()); + else + m_status = format(STR_P("%zu signal placed", "%zu signals placed", count), count) + ", " + format(STR_P("%zu renamed", "%zu renamed", renamed.size()), renamed.size()); +} + +void srk_tool::survey_signals() +{ + m_signal_marks.clear(); + std::vector standing; + m_signals_known = m_layout.has_value() && m_signals.standing_signals(standing, false); + if (false == m_signals_known) + return; + auto const stem{stem_of_open()}; + for (auto const &signal : standing) + { + if (gap_to(*m_layout, signal.position) > 8.0) + continue; + auto const travel{travel_of(signal.yaw)}; + auto const at{station::project(*m_layout, signal.position)}; + auto const ahead{station::project(*m_layout, signal.position + glm::dvec3{travel.x, 0.0, travel.y} * 5.0)}; + m_signal_marks.push_back({at, ahead.x > at.x, signal_shown(signal.name, stem)}); + } +} + void srk_tool::survey() { m_circuit_of.assign(m_layout.has_value() ? m_layout->paths.size() : 0, -1); @@ -767,11 +1044,15 @@ void srk_tool::survey() on = point; } mark.at = on; + auto const entry{entry_circuit(mark.text, stem)}; // bpd_it3 shown as It3 if (false == stem.empty() && mark.text.compare(0, stem.size(), stem) == 0) mark.text.erase(0, stem.size()); if (false == mark.text.empty()) mark.text.front() = static_cast(std::toupper(static_cast(mark.text.front()))); + // bpd_ita, before the entry signal A, as ItA + if (entry && mark.text.size() > 2) + std::transform(mark.text.begin() + 2, mark.text.end(), mark.text.begin() + 2, [](char const Letter) { return static_cast(std::toupper(static_cast(Letter))); }); } // the joints: the ends where paths of different circuits meet std::vector> ends; @@ -791,15 +1072,24 @@ void srk_tool::survey() for (std::size_t j = i + 1; j < ends.size(); ++j) if (ends[i].second != ends[j].second && (ends[i].second >= 0 || ends[j].second >= 0) && glm::distance(ends[i].first, ends[j].first) < 0.05) m_ticks.push_back(station::project(layout, ends[i].first)); + survey_signals(); } void srk_tool::fit_view(float const Width, float const Height) { auto const &layout{*m_layout}; auto &view{m_view_state}; - auto const span{std::max(50.0, layout.to - layout.from)}; - view.from = layout.from - span * 0.08 - 30.0; - view.to = layout.to + span * 0.08 + 30.0; + // the switches and the signals out on the lines + auto from{layout.from}; + auto to{layout.to}; + for (auto const &mark : m_signal_marks) + { + from = std::min(from, mark.at.x); + to = std::max(to, mark.at.x); + } + auto const span{std::max(50.0, to - from)}; + view.from = from - span * 0.04 - 30.0; + view.to = to + span * 0.04 + 30.0; view.centre = (layout.low + layout.high) * 0.5; auto const across{(view.to - view.from) / std::max(1.0f, Width)}; auto const vertical{(layout.high - layout.low + 20.0) / std::max(1.0f, Height * 0.75f)}; @@ -958,6 +1248,14 @@ void srk_tool::render_canvas(float const Height) else label(text, kSwitch, {under, over}); } + // the signals: a flag pointing the way of the trains they face + for (auto const &mark : m_signal_marks) + { + auto const at{screen_of(mark.at)}; + auto const way{mark.ahead ? 1.0f : -1.0f}; + draw->AddTriangleFilled(ImVec2(at.x - 4.0f * way, at.y - 4.0f), ImVec2(at.x - 4.0f * way, at.y + 4.0f), ImVec2(at.x + 4.0f * way, at.y), kSignal); + label(mark.text, kSignal, {ImVec2(at.x, at.y - height - 5.0f), ImVec2(at.x, at.y + 5.0f)}); + } std::vector order; for (std::size_t index = 0; index < m_circuits.size(); ++index) if (m_circuits[index].labelled) @@ -1126,6 +1424,23 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri WriteLog("SELFTEST srktrack: " + m_status); return editor_selftest_result::handled; } + if (Command == "srksignals") + { + // srksignals: the signals of the open station set + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srksignals: no station open"); + return editor_selftest_result::handled; + } + set_signals(); + WriteLog("SELFTEST srksignals: " + m_status); + refresh(); + std::string list; + for (auto const &mark : m_signal_marks) + list += format(" %s@%.0f/%.1f%s", mark.text.c_str(), mark.at.x, mark.at.y, mark.ahead ? ">" : "<"); + WriteLog(format("SELFTEST srksignals %zu:", m_signal_marks.size()) + list); + return editor_selftest_result::handled; + } if (Command == "srkstations") { load(); diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 2201e790..240babd4 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -24,7 +24,7 @@ http://mozilla.org/MPL/2.0/. class srk_tool final : public editor_tool { public: - srk_tool(editor_selection &Selection, editor_view &View, editor_history &History) : m_selection{Selection}, m_view{View}, m_history{History} {} + srk_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_signals &Signals) : m_selection{Selection}, m_view{View}, m_history{History}, m_signals{Signals} {} void update() override; void draw_overlay() const override; void render_options() override; @@ -39,6 +39,7 @@ class srk_tool final : public editor_tool editor_selection &m_selection; editor_view &m_view; editor_history &m_history; + editor_signals &m_signals; // a station described: found again by a switch of it struct record { @@ -82,6 +83,14 @@ class srk_tool final : public editor_tool std::vector m_circuit_of; // of each path of the layout, an index into m_circuits, -1 for none std::vector m_circuits; std::vector m_ticks; // the joints between the circuits, in the schematic + struct signal_mark + { + glm::dvec2 at{0.0}; // in the schematic + bool ahead{true}; // the trains it faces run along the chainage + std::string text; + }; + std::vector m_signal_marks; + bool m_signals_known{false}; // the signals standing surveyed, once their templates are found std::string m_status; void load(); @@ -99,6 +108,11 @@ class srk_tool final : public editor_tool void set_circuits(); // the track circuits as the tracks have them, for the schematic void survey(); + // the signals placed where the station has none and the standing ones named, the circuits before the entry signals set too; one + // step of the history + void set_signals(); + // the signals standing by the station, for the schematic + void survey_signals(); // the names of the switches and the circuits of the open station begin with it, empty while it has no code std::string stem_of_open() const; // the circuits of the station track of the track renamed after another number; one step of the history diff --git a/application/editortool.h b/application/editortool.h index 744a7646..bbe506da 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -73,6 +73,33 @@ struct standing_template std::vector values; }; +// a signal to stand by a track, of the kind of the signal templates: main, shunt... +struct signal_order +{ + TTrack *track{nullptr}; + int path{0}; + glm::dvec3 point{0.0}; // on the axis + int facing{1}; // the trains it faces run 1: towards the end of the path, -1: towards its start + int side{1}; // 1: on the right of the path, -1: on the left + std::string kind; + std::string name; + std::string plate; // the name on the plate under the head, when the template has one +}; +struct signal_rename +{ + scene::instance_handle instance{0}; + std::string name; + std::string plate; +}; +struct standing_signal +{ + scene::instance_handle instance{0}; + std::string name; + std::string kind; + glm::dvec3 position{0.0}; + double yaw{0.0}; +}; + struct nmt_place { glm::dvec3 position{0.0}; // on the axis of the path @@ -145,6 +172,19 @@ class editor_templates ~editor_templates() = default; }; +class editor_signals +{ + public: + // false while the signal templates are still being looked for + virtual bool standing_signals(std::vector &Signals, bool const Wait) = 0; + // the signals placed by the templates chosen for their kinds and the standing ones renamed, with the tracks changed before: + // one step of the history. returns: how many were placed + virtual std::size_t signal_batch(std::vector const &Placed, std::vector const &Renamed, std::vector> States, std::vector Created, std::string &Error) = 0; + + protected: + ~editor_signals() = default; +}; + class editor_ground { public: @@ -180,7 +220,7 @@ class editor_guide ~editor_guide() = default; }; -class editor_services : public editor_selection, public editor_view, public editor_history, public editor_templates, public editor_ground, public editor_track_design, public editor_guide +class editor_services : public editor_selection, public editor_view, public editor_history, public editor_templates, public editor_signals, public editor_ground, public editor_track_design, public editor_guide { protected: ~editor_services() = default; diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index ef2c78f1..66c8016a 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include #include @@ -236,6 +237,11 @@ double constexpr kReach{120.0}; // m, along a branch looked for its fouling poin double constexpr kSection{30.0}; // m, the shortest stretch between two joints made a track circuit of its own double constexpr kSnap{0.3}; // m, a joint this close to the end of a track lies at the end double constexpr kThrough{400.0}; // m, of the switches a station track is followed through for its circuits beyond +double constexpr kEntry{300.0}; // m, from the joint of the station out to its entry signal +double constexpr kJoint{3.5}; // m, from a signal to the joint of the circuit it guards +double constexpr kStopping{200.0}; // m, the shortest circuit of a station track a train is sent out of by an exit signal +double constexpr kSignalSide{2.25}; // m, from the axis to a signal +double constexpr kSignalClear{2.5}; // m, the least from a signal to the axis of another track // the points of the way out of an end of the track, as a train runs: along the main path of a switch run into from its start, // back along the path of one run into from the end of it @@ -977,4 +983,275 @@ std::vector switches_of(TTrack &Switch, options const &Options) return collect(Switch, Options).switches; } +namespace +{ + +// the point the distance in from the end of the plain track, and the way to the end there +std::pair in_from(TTrack const &Track, bool const Atend, double const Distance) +{ + double length{0.0}; + auto points{dense_points(Track, 0, length)}; + if (Atend) + std::reverse(points.begin(), points.end()); + double along{0.0}; + for (std::size_t i = 1; i < points.size(); ++i) + { + auto const step{plan_distance(points[i - 1], points[i])}; + if (along + step >= Distance || i + 1 == points.size()) + { + auto const t{step > 1e-9 ? std::clamp((Distance - along) / step, 0.0, 1.0) : 0.0}; + return {glm::mix(points[i - 1], points[i], t), points[i - 1] - points[i]}; + } + along += step; + } + return {points.front(), glm::dvec3{0.0, 0.0, 1.0}}; +} + +// how far a signal on the side of the track at the point keeps from the other tracks; Way: of the path there +double side_gap(layout const &Layout, glm::dvec3 const &Point, glm::dvec3 const &Way, int const Side) +{ + glm::dvec2 const way{glm::normalize(glm::dvec2{Way.x, Way.z} + glm::dvec2{1e-12, 0.0})}; + glm::dvec2 const right{-way.y, way.x}; + auto const spot{glm::dvec2{Point.x, Point.z} + right * (Side * kSignalSide)}; + glm::dvec3 const at{spot.x, Point.y, spot.y}; + auto gap{std::numeric_limits::max()}; + for (auto const &path : Layout.paths) + if (plan_gap(Point, path.points) > 1.0) + gap = std::min(gap, plan_gap(at, path.points)); + return gap; +} + +// on the right of the trains, unless another track runs too close by there and less so on the left +int side_for(layout const &Layout, glm::dvec3 const &Point, glm::dvec3 const &Way, int const Facing) +{ + auto const right{side_gap(Layout, Point, Way, Facing)}; + if (right >= kSignalClear) + return Facing; + auto const left{side_gap(Layout, Point, Way, -Facing)}; + return left >= kSignalClear || left > right ? -Facing : Facing; +} + +// the ends of the tracks out of the set, Free where no track goes on +struct track_end +{ + TTrack *track{nullptr}; + bool atend{false}; + glm::dvec3 point{0.0}; + bool free{false}; +}; + +std::vector ends_out(std::vector const &Tracks) +{ + std::unordered_set const own(Tracks.begin(), Tracks.end()); + std::vector result; + for (auto *track : Tracks) + { + if (node(*track)) + continue; + for (auto const atend : {false, true}) + { + auto const *next{neighbour(*track, 0, atend)}; + if (next != nullptr && rail(*next) && own.count(next) > 0) + continue; + result.push_back({track, atend, end_of(*track, 0, atend), next == nullptr || false == rail(*next)}); + } + } + return result; +} + +char next_letter(char Letter, int const Step) +{ + do + Letter = static_cast(Letter + Step); + while (Letter == 'I' || Letter == 'Q'); + return Letter; +} + +} // namespace + +std::vector signals(layout const &Layout, std::vector const &Tracks, std::function const &Inside) +{ + std::vector result; + auto const middle{(Layout.from + Layout.to) * 0.5}; + // the entry signals: out along each line from where it leaves the circuits of the station + struct entry + { + signal placed; + double offset{0.0}; + bool start{false}; + }; + std::vector entries; + for (auto const &line : Layout.lines) + { + auto const outside{std::find_if(line.begin(), line.end(), [&](TTrack const *Track) { return false == Inside(*Track); })}; + if (outside == line.end() || node(**outside)) + continue; + // the joint: the end of the first track out of the station by the track before it + auto *track{*outside}; + bool atend{false}; + if (outside != line.begin()) + { + int path{0}; + bool end{false}; + atend = false == (end_at(**std::prev(outside), end_of(*track, 0, false), path, end)); + } + else + { + auto const start{end_of(*track, 0, false)}; + atend = std::none_of(Layout.switches.begin(), Layout.switches.end(), [&](TTrack const *Switch) { + return std::any_of(Switch->m_paths.begin(), Switch->m_paths.end(), [&](auto const &Path) { return glm::distance(Path.points[segment_data::point::start], start) < same_point || glm::distance(Path.points[segment_data::point::end], start) < same_point; }); + }); + } + // out from the joint: along the path when the joint is its start + entry found; + auto const joint{end_of(*track, 0, atend)}; + found.offset = project(Layout, joint).y; + found.start = project(Layout, joint).x < middle; + double along{0.0}; + auto point{joint}; + auto *next{track}; + std::unordered_set seen; + while (next != nullptr && rail(*next) && false == node(*next) && seen.insert(next).second) + { + int path{0}; + bool end{false}; + if (false == end_at(*next, point, path, end)) + break; + auto const outward{false == end}; + auto const length{plan_length(*next, 0)}; + found.placed.run.emplace_back(next, outward); + if (along + length >= kEntry) + { + auto const [at, way] = in_from(*next, end, kEntry - along); + found.placed.track = next; + found.placed.point = at; + found.placed.facing = outward ? -1 : 1; + found.placed.side = side_for(Layout, at, outward ? -way : way, found.placed.facing); + found.placed.travel = way; + break; + } + along += length; + point = end_of(*next, 0, outward); + next = neighbour(*next, 0, outward); + } + if (found.placed.track == nullptr && false == found.placed.run.empty() && along > 30.0) + { + // the line ends sooner, by the edge of the scenery: as far out as it goes + auto const &[last, outward] = found.placed.run.back(); + auto const length{plan_length(*last, 0)}; + auto const [at, way] = in_from(*last, outward, std::min(10.0, length * 0.5)); + found.placed.track = last; + found.placed.point = at; + found.placed.facing = outward ? -1 : 1; + found.placed.side = side_for(Layout, at, outward ? way : -way, found.placed.facing); + found.placed.travel = -way; + } + if (found.placed.track != nullptr) + entries.push_back(std::move(found)); + } + std::sort(entries.begin(), entries.end(), [](entry const &A, entry const &B) { return A.offset > B.offset; }); + char from_start{'A'}; + char from_end{'Z'}; + char last_start{'@'}; + char first_end{'['}; + for (auto &found : entries) + { + auto &letter{found.start ? from_start : from_end}; + found.placed.kind = signal::role::entry; + found.placed.name = std::string(1, letter); + (found.start ? last_start : first_end) = letter; + letter = next_letter(letter, found.start ? 1 : -1); + result.push_back(std::move(found.placed)); + } + // the exit signals, a letter for the odd tracks and one for the even ones at each end, clear of the entry ones + auto letter_after = [](char Letter, char const Limit, int const Step) { + while (Step > 0 ? Letter <= Limit : Letter >= Limit) + Letter = next_letter(Letter, Step); + return Letter; + }; + auto const odd_start{letter_after('F', last_start, 1)}; + auto const even_start{next_letter(odd_start, 1)}; + auto const odd_end{letter_after('R', first_end, -1)}; + auto const even_end{next_letter(odd_end, -1)}; + auto const lines_at = [&](bool const Start) { return std::any_of(entries.begin(), entries.end(), [&](entry const &Entry) { return Entry.start == Start; }); }; + auto const starts{lines_at(true)}; + auto const ends{lines_at(false)}; + std::vector shunting; + for (auto const &track : Tracks) + { + auto const digit{std::find_if(track.number.rbegin(), track.number.rend(), [](char const Letter) { return std::isdigit(static_cast(Letter)) != 0; })}; + auto const odd{digit == track.number.rend() || (*digit - '0') % 2 == 1}; + auto upper{track.number}; + std::transform(upper.begin(), upper.end(), upper.begin(), [](char const Letter) { return static_cast(std::toupper(static_cast(Letter))); }); + // the outer ends of the outermost circuits long enough, else of the outermost ones + struct reach + { + track_end low; + track_end high; + double low_x{std::numeric_limits::max()}; + double high_x{std::numeric_limits::lowest()}; + }; + reach longest; + reach any; + for (auto const &circuit : track.circuits) + { + double length{0.0}; + for (auto const *part : circuit) + length += node(*part) ? 0.0 : plan_length(*part, 0); + for (auto const &end : ends_out(circuit)) + { + auto const x{project(Layout, end.point).x}; + for (auto *found : {&any, length >= kStopping ? &longest : static_cast(nullptr)}) + { + if (found == nullptr) + continue; + if (x < found->low_x) + { + found->low_x = x; + found->low = end; + } + if (x > found->high_x) + { + found->high_x = x; + found->high = end; + } + } + } + } + auto const stopping{longest.low.track != nullptr}; + auto const &chosen{stopping ? longest : any}; + for (auto const start : {true, false}) + { + auto const &end{start ? chosen.low : chosen.high}; + if (end.track == nullptr || end.free) + continue; + signal placed; + auto const [at, way] = in_from(*end.track, end.atend, std::min(kJoint, plan_length(*end.track, 0) * 0.5)); + placed.track = end.track; + placed.point = at; + placed.facing = end.atend ? 1 : -1; + placed.side = side_for(Layout, at, end.atend ? way : -way, placed.facing); + placed.travel = way; + if (stopping && (start ? starts : ends)) + { + placed.kind = signal::role::exit; + placed.name = std::string(1, start ? (odd ? odd_start : even_start) : (odd ? odd_end : even_end)) + upper; + result.push_back(std::move(placed)); + } + else + { + placed.kind = signal::role::shunting; + shunting.push_back(std::move(placed)); + } + } + } + std::sort(shunting.begin(), shunting.end(), [&](signal const &A, signal const &B) { return project(Layout, A.point).x < project(Layout, B.point).x; }); + for (std::size_t index = 0; index < shunting.size(); ++index) + { + shunting[index].name = "Tm" + std::to_string(index + 1); + result.push_back(std::move(shunting[index])); + } + return result; +} + } // namespace station diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index 89ffe9fd..f8fcceeb 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include #include #include #include @@ -107,4 +108,41 @@ std::vector track_numbers(layout const &Layout, std::vector switches_of(TTrack &Switch, options const &Options = {}); +// a station track by its number, with its track circuits +struct numbered +{ + std::string number; + std::vector> circuits; +}; + +// a signal of the station, as PKP PLK places and names them +struct signal +{ + enum class role + { + entry, + exit, + shunting + }; + role kind{role::entry}; + std::string name; // without the code of the station: A, F1, Tm3 + TTrack *track{nullptr}; + int path{0}; + glm::dvec3 point{0.0}; // on the axis + int facing{1}; // the trains it faces run 1: towards the end of the path, -1: towards its start + int side{1}; // 1: on the right of the path, -1: on the left + glm::dvec3 travel{0.0}; // the way the trains it faces run, in the plan + // of an entry signal: the tracks of its line from the joint of the station out to it, each with the way out along it, true + // towards the end of the path; a circuit named after the signal runs between the two + std::vector> run; +}; + +// the signals of the station: the entry ones 300 m out along the lines beyond the circuits of the station, A, B... from the start +// of the chainage, Z, Y... from its end, the leftmost first; the exit ones facing out at the outer ends of the outermost circuits +// at least 200 m long of the station tracks, a letter for the odd tracks and one for the even ones at each end, with the number: +// F1, G2 towards the start, R1, P2 towards the end; the shunting ones at the ends of the shorter tracks, Tm1, Tm2... along the +// chainage. Each on the right of the trains, unless another track runs too close by there. Inside: a track of the circuits of the +// station +std::vector signals(layout const &Layout, std::vector const &Tracks, std::function const &Inside); + } // namespace station diff --git a/lang/editor_pl.po b/lang/editor_pl.po index e7f9adf4..79ca1635 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10455,3 +10455,36 @@ msgstr "Stacja ma już tor %s" msgid "Track %s is track %s now" msgstr "Tor %s ma teraz numer %s" +msgid "Set the signals" +msgstr "Ustaw semafory" + +msgid "Places the signals of the station, as on the lines of PKP PLK, and names them after its code: an entry signal 300 m out on each line, A, B... from the start of the km, Z, Y... from the end, the circuit from it to the station bpd_ita; an exit signal at the end of each station track a train is sent out of, F1, G2 towards the start, R1, P2 towards the end; a shunting signal Tm1, Tm2... at the end of a shorter track. A signal standing by the place of one gets its name, none is doubled. The kinds of signals chosen in the Signals tool. One step of undo" +msgstr "Stawia semafory stacji jak na liniach PKP PLK i nazywa je od jej skrótu: wjazdowy 300 m przed stacją na każdej linii, A, B... od początku km, Z, Y... od końca, a odcinek od niego do stacji to bpd_ita; wyjazdowy na końcu każdego toru, z którego wyprawia się pociągi, F1, G2 w stronę początku, R1, P2 w stronę końca; tarcza manewrowa Tm1, Tm2... na końcu krótszego toru. Semafor, który już stoi w miejscu któregoś z nich, dostaje tylko jego nazwę, nic się nie dubluje. Modele semaforów te wybrane w narzędziu Sygnalizatory. Jeden krok cofania" + +msgid "Give the station its code first: the signals are named after it" +msgstr "Najpierw wpisz skrót stacji: od niego biorą się nazwy semaforów" + +msgid "Set the track circuits first: the signals stand at their joints" +msgstr "Najpierw ustaw izolacje: semafory stają przy stykach" + +msgid "The signals are set already" +msgstr "Semafory są już ustawione" + +msgid "The signals couldn't be placed: %s" +msgstr "Nie udało się postawić semaforów: %s" + +msgid "no template of the kind %s" +msgstr "brak szablonu rodzaju %s" + +msgid "%zu signal placed" +msgid_plural "%zu signals placed" +msgstr[0] "Postawiono %zu semafor" +msgstr[1] "Postawiono %zu semafory" +msgstr[2] "Postawiono %zu semaforów" + +msgid "%zu renamed" +msgid_plural "%zu renamed" +msgstr[0] "%zu przemianowany" +msgstr[1] "%zu przemianowane" +msgstr[2] "%zu przemianowanych" + From c50d412bc4979457b97642e8d2597e7a0af6fe65 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 11:21:59 +0200 Subject: [PATCH 047/145] feat(editor): the signalling mode shows the track circuits in the scenery, the tracks in their colours, a bar across them with a post at each joint and the names of the station tracks --- application/editormodesrk.cpp | 76 ++++++++++++++++++++++++++++------- application/editormodesrk.h | 7 ++++ 2 files changed, 68 insertions(+), 15 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index cddcd5aa..640c40c2 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -41,12 +41,15 @@ namespace char const *const kMark{"//$s"}; double constexpr kOverlayRange{4000.0}; +double constexpr kJointRange{1500.0}; // m, of the joints and the names of the circuits shown in the scenery +double constexpr kRailHead{0.18}; // m, above the axis of a path ImU32 constexpr kPlain{IM_COL32(170, 176, 188, 255)}; ImU32 constexpr kSwitch{IM_COL32(240, 200, 90, 255)}; ImU32 constexpr kApproach{IM_COL32(95, 100, 112, 255)}; ImU32 constexpr kHovered{IM_COL32(255, 255, 255, 255)}; ImU32 constexpr kSignal{IM_COL32(240, 95, 85, 255)}; +ImU32 constexpr kJoint{IM_COL32(255, 255, 255, 235)}; bool is_switch(TTrack const &Track) { @@ -357,6 +360,39 @@ void srk_tool::draw_overlay() const screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; glm::dvec3 const camera{Global.pCamera.Pos}; + auto const near_enough = [&](glm::dvec3 const &Point, double const Range) { return Global.EditorOrtho || glm::distance(Point, camera) < Range; }; + glm::dvec3 const lift{0.0, kRailHead, 0.0}; + // the track circuits: the tracks in their colours, a bar across them at each joint with a post, the station tracks named + for (std::size_t index = 0; index < m_layout->paths.size() && index < m_circuit_of.size(); ++index) + { + if (m_circuit_of[index] < 0) + continue; + auto const colour{(m_circuits[m_circuit_of[index]].colour & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 200)}; + auto const &points{m_layout->paths[index].points}; + for (std::size_t i = 1; i < points.size(); ++i) + if (near_enough(points[i], kOverlayRange)) + projection.line(drawlist, points[i - 1] + lift, points[i] + lift, colour, 3.0f); + } + for (auto const &joint : m_joints) + { + if (false == near_enough(joint.at, kJointRange)) + continue; + auto const centre{joint.at + lift}; + projection.line(drawlist, centre - joint.across * 1.7, centre + joint.across * 1.7, kJoint, 4.0f); + projection.line(drawlist, centre + joint.across * 1.7, centre + joint.across * 1.7 + glm::dvec3{0.0, 1.2, 0.0}, kJoint, 2.0f); + ImVec2 top; + if (projection.project(centre + joint.across * 1.7 + glm::dvec3{0.0, 1.2, 0.0}, top)) + drawlist->AddCircleFilled(top, 3.0f, kJoint); + } + for (auto const &mark : m_circuits) + { + ImVec2 screen; + if (false == mark.labelled || false == near_enough(mark.world, kJointRange) || false == projection.project(mark.world + glm::dvec3{0.0, 0.8, 0.0}, screen)) + continue; + auto const size{ImGui::CalcTextSize(mark.text.c_str())}; + drawlist->AddRectFilled(ImVec2(screen.x - size.x * 0.5f - 3.0f, screen.y - size.y - 2.0f), ImVec2(screen.x + size.x * 0.5f + 3.0f, screen.y + 2.0f), IM_COL32(0, 0, 0, 160), 3.0f); + drawlist->AddText(ImVec2(screen.x - size.x * 0.5f, screen.y - size.y), mark.colour, mark.text.c_str()); + } if (m_hover >= 0 && m_hover < static_cast(m_layout->paths.size())) { auto const &points{m_layout->paths[m_hover].points}; @@ -1015,7 +1051,7 @@ void srk_tool::survey() auto found{std::find_if(m_circuits.begin(), m_circuits.end(), [&](circuit_mark const &Mark) { return Mark.text == name; })}; if (found == m_circuits.end()) { - found = m_circuits.insert(m_circuits.end(), {glm::dvec2{0.0}, name, circuit_colour(name)}); + found = m_circuits.insert(m_circuits.end(), {glm::dvec2{0.0}, glm::dvec3{0.0}, name, circuit_colour(name)}); found->labelled = stem.empty() || name.compare(0, stem.size() + 2, stem + "iz") != 0; weights.push_back(0.0); } @@ -1037,11 +1073,12 @@ void srk_tool::survey() glm::dvec2 on{mark.at}; for (std::size_t path = 0; path < layout.paths.size(); ++path) if (m_circuit_of[path] == static_cast(index)) - for (auto const &point : layout.paths[path].schematic) - if (auto const distance{glm::distance(point, mark.at)}; distance < nearest) + for (std::size_t k = 0; k < layout.paths[path].schematic.size(); ++k) + if (auto const distance{glm::distance(layout.paths[path].schematic[k], mark.at)}; distance < nearest) { nearest = distance; - on = point; + on = layout.paths[path].schematic[k]; + mark.world = layout.paths[path].points[k]; } mark.at = on; auto const entry{entry_circuit(mark.text, stem)}; @@ -1055,23 +1092,32 @@ void srk_tool::survey() std::transform(mark.text.begin() + 2, mark.text.end(), mark.text.begin() + 2, [](char const Letter) { return static_cast(std::toupper(static_cast(Letter))); }); } // the joints: the ends where paths of different circuits meet - std::vector> ends; + struct end + { + glm::dvec3 point; + glm::dvec3 way; + int circuit; + }; + std::vector ends; for (std::size_t index = 0; index < layout.paths.size(); ++index) { - auto const &path{layout.paths[index]}; - if (path.approach && m_circuit_of[index] < 0) - { - for (auto const &point : {path.points.front(), path.points.back()}) - ends.emplace_back(point, -1); + auto const &points{layout.paths[index].points}; + if (points.size() < 2) continue; - } - for (auto const &point : {path.points.front(), path.points.back()}) - ends.emplace_back(point, m_circuit_of[index]); + auto const circuit{m_circuit_of[index]}; + ends.push_back({points.front(), points[1] - points.front(), circuit}); + ends.push_back({points.back(), points.back() - points[points.size() - 2], circuit}); } + m_joints.clear(); for (std::size_t i = 0; i < ends.size(); ++i) for (std::size_t j = i + 1; j < ends.size(); ++j) - if (ends[i].second != ends[j].second && (ends[i].second >= 0 || ends[j].second >= 0) && glm::distance(ends[i].first, ends[j].first) < 0.05) - m_ticks.push_back(station::project(layout, ends[i].first)); + if (ends[i].circuit != ends[j].circuit && (ends[i].circuit >= 0 || ends[j].circuit >= 0) && glm::distance(ends[i].point, ends[j].point) < 0.05 && + std::none_of(m_joints.begin(), m_joints.end(), [&](joint_mark const &Joint) { return glm::distance(Joint.at, ends[i].point) < 0.05; })) + { + m_ticks.push_back(station::project(layout, ends[i].point)); + glm::dvec2 const way{glm::normalize(glm::dvec2{ends[i].way.x, ends[i].way.z} + glm::dvec2{1e-12, 0.0})}; + m_joints.push_back({ends[i].point, glm::dvec3{-way.y, 0.0, way.x}}); + } survey_signals(); } diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 240babd4..a832dbcd 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -75,6 +75,7 @@ class srk_tool final : public editor_tool struct circuit_mark { glm::dvec2 at{0.0}; // in the schematic + glm::dvec3 world{0.0}; std::string text; ImU32 colour{0}; double length{0.0}; @@ -83,6 +84,12 @@ class srk_tool final : public editor_tool std::vector m_circuit_of; // of each path of the layout, an index into m_circuits, -1 for none std::vector m_circuits; std::vector m_ticks; // the joints between the circuits, in the schematic + struct joint_mark + { + glm::dvec3 at{0.0}; + glm::dvec3 across{0.0}; // of the track there, in the plan + }; + std::vector m_joints; // the same in the scenery struct signal_mark { glm::dvec2 at{0.0}; // in the schematic From e2305449873257163b9d392fbca00ec4754fa9e9 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 11:40:38 +0200 Subject: [PATCH 048/145] feat(editor): the signalling mode names the signals in the scenery above their lamps, the names shown within 700 m only, and draws the track circuits wider in the top view --- application/editormodesrk.cpp | 30 ++++++++++++++++++++++++------ application/editormodesrk.h | 2 ++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 640c40c2..eb11b7f1 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -41,7 +41,8 @@ namespace char const *const kMark{"//$s"}; double constexpr kOverlayRange{4000.0}; -double constexpr kJointRange{1500.0}; // m, of the joints and the names of the circuits shown in the scenery +double constexpr kJointRange{1500.0}; // m, of the joints shown in the scenery +double constexpr kLabelRange{700.0}; // m, of the names shown there, farther ones crowd the horizon double constexpr kRailHead{0.18}; // m, above the axis of a path ImU32 constexpr kPlain{IM_COL32(170, 176, 188, 255)}; @@ -371,7 +372,7 @@ void srk_tool::draw_overlay() const auto const &points{m_layout->paths[index].points}; for (std::size_t i = 1; i < points.size(); ++i) if (near_enough(points[i], kOverlayRange)) - projection.line(drawlist, points[i - 1] + lift, points[i] + lift, colour, 3.0f); + projection.line(drawlist, points[i - 1] + lift, points[i] + lift, colour, Global.EditorOrtho ? 6.0f : 3.0f); } for (auto const &joint : m_joints) { @@ -387,7 +388,7 @@ void srk_tool::draw_overlay() const for (auto const &mark : m_circuits) { ImVec2 screen; - if (false == mark.labelled || false == near_enough(mark.world, kJointRange) || false == projection.project(mark.world + glm::dvec3{0.0, 0.8, 0.0}, screen)) + if (false == mark.labelled || false == near_enough(mark.world, kLabelRange) || false == projection.project(mark.world + glm::dvec3{0.0, 0.8, 0.0}, screen)) continue; auto const size{ImGui::CalcTextSize(mark.text.c_str())}; drawlist->AddRectFilled(ImVec2(screen.x - size.x * 0.5f - 3.0f, screen.y - size.y - 2.0f), ImVec2(screen.x + size.x * 0.5f + 3.0f, screen.y + 2.0f), IM_COL32(0, 0, 0, 160), 3.0f); @@ -403,13 +404,22 @@ void srk_tool::draw_overlay() const { auto const start{track->m_paths.front().points[segment_data::point::start] + glm::dvec3{0.0, 1.0, 0.0}}; ImVec2 screen; - if ((false == Global.EditorOrtho && glm::distance(start, camera) > kOverlayRange) || false == projection.project(start, screen)) + if (false == near_enough(start, kLabelRange) || false == projection.project(start, screen)) continue; auto const text{track->name()}; auto const size{ImGui::CalcTextSize(text.c_str())}; drawlist->AddRectFilled(ImVec2(screen.x - 3.0f, screen.y - size.y - 4.0f), ImVec2(screen.x + size.x + 3.0f, screen.y), IM_COL32(0, 0, 0, 160), 3.0f); drawlist->AddText(ImVec2(screen.x, screen.y - size.y - 2.0f), kSwitch, text.c_str()); } + for (auto const &mark : m_signal_marks) + { + ImVec2 screen; + if (false == near_enough(mark.head, kLabelRange) || false == projection.project(mark.head, screen)) + continue; + auto const size{ImGui::CalcTextSize(mark.text.c_str())}; + drawlist->AddRectFilled(ImVec2(screen.x - size.x * 0.5f - 4.0f, screen.y - size.y - 4.0f), ImVec2(screen.x + size.x * 0.5f + 4.0f, screen.y), kSignal, 3.0f); + drawlist->AddText(ImVec2(screen.x - size.x * 0.5f, screen.y - size.y - 2.0f), IM_COL32(255, 255, 255, 255), mark.text.c_str()); + } } void srk_tool::render_found() @@ -1025,7 +1035,7 @@ void srk_tool::survey_signals() auto const travel{travel_of(signal.yaw)}; auto const at{station::project(*m_layout, signal.position)}; auto const ahead{station::project(*m_layout, signal.position + glm::dvec3{travel.x, 0.0, travel.y} * 5.0)}; - m_signal_marks.push_back({at, ahead.x > at.x, signal_shown(signal.name, stem)}); + m_signal_marks.push_back({at, ahead.x > at.x, signal_shown(signal.name, stem), signal.position + glm::dvec3{0.0, signal.kind == "shunt" ? 3.0 : 7.0, 0.0}}); } } @@ -1332,7 +1342,7 @@ void srk_tool::render_canvas(float const Height) void srk_tool::render_windows() { - if (false == m_open || false == m_layout.has_value()) + if (false == m_open || false == m_strip || false == m_layout.has_value()) return; auto const &io{ImGui::GetIO()}; // along the bottom of the 3d view, as the vertical profile @@ -1487,6 +1497,14 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri WriteLog(format("SELFTEST srksignals %zu:", m_signal_marks.size()) + list); return editor_selftest_result::handled; } + if (Command == "srkstrip") + { + // srkstrip shown=0: the schematic hidden, the 3d view whole for a screenshot + auto const values{selftest_values(Words)}; + auto const shown{values.find("shown")}; + m_strip = shown == values.end() || shown->second != "0"; + return editor_selftest_result::handled; + } if (Command == "srkstations") { load(); diff --git a/application/editormodesrk.h b/application/editormodesrk.h index a832dbcd..9400f61f 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -59,6 +59,7 @@ class srk_tool final : public editor_tool float exaggeration{0.0f}; // 0: fit at the next draw }; bool m_open{false}; + bool m_strip{true}; // the schematic shown, off for the screenshots of the self-tests bool m_loaded{false}; bool m_changed{false}; std::vector m_records; @@ -95,6 +96,7 @@ class srk_tool final : public editor_tool glm::dvec2 at{0.0}; // in the schematic bool ahead{true}; // the trains it faces run along the chainage std::string text; + glm::dvec3 head{0.0}; // above its lamps, in the scenery }; std::vector m_signal_marks; bool m_signals_known{false}; // the signals standing surveyed, once their templates are found From 46e665c46802a003cdc74f072e90079ee4874c53 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 12:13:22 +0200 Subject: [PATCH 049/145] feat(editor): the signalling mode sets the signals of the lines out of a station, the automatic block signals and the block sections to the next station on a double-track line, a warning disc before the entry signal on a single-track one The station keeps its km and the direction of its numbering in its record. The block signals are named after their km, odd on track 1 and even on track 2, the sections after the signals at their ends, and each signal gets the control of its section. --- application/editormode.h | 4 +- application/editormodesignal.cpp | 47 ++- application/editormodesrk.cpp | 624 +++++++++++++++++++++++++++++-- application/editormodesrk.h | 17 +- application/editortool.h | 18 +- editor/editorStation.cpp | 164 ++++++++ editor/editorStation.hpp | 38 +- lang/editor_pl.po | 48 +++ 8 files changed, 919 insertions(+), 41 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index dd42ffba..4b58cad9 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1891,7 +1891,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri // takes them out, as one step of the undo std::size_t remove_templates(std::vector const &Instances) override; bool standing_signals(std::vector &Signals, bool const Wait) override; - std::size_t signal_batch(std::vector const &Placed, std::vector const &Renamed, std::vector> States, std::vector Created, std::string &Error) override; + std::size_t signal_batch(signal_changes const &Changes, std::vector> States, std::vector Created, std::string &Error) override; struct signal_template { @@ -2383,7 +2383,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri hekto_tool m_hekto{*this, *this, *this, *this, *this, *this}; fouling_tool m_fouling{*this, *this, *this, *this, *this}; parallel_tool m_parallel{*this, *this, *this, *this}; - srk_tool m_srk{*this, *this, *this, *this}; + srk_tool m_srk{*this, *this, *this, *this, *this}; std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle, &m_hekto, &m_fouling, &m_parallel, &m_srk}; void infra_rebase(); diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 55ad4950..596f8fea 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -883,19 +883,26 @@ bool editor_mode::standing_signals(std::vector &Signals, bool c return false; signal_refresh(true); for (auto const &standing : m_signal.standing) - if (standing.kind >= 0) - Signals.push_back({standing.include.instance, standing.name, kKinds[standing.kind], standing.include.location, standing.include.yaw}); + { + if (standing.kind < 0) + continue; + std::string file; + std::vector values; + auto const *item{signal_template_of(standing.include.file)}; + auto const linked{item != nullptr && item->linked && editor_includes::parse_directive(scene::Layers.directive(standing.include.instance), file, values) && values.size() > 6}; + Signals.push_back({standing.include.instance, standing.name, kKinds[standing.kind], standing.include.location, standing.include.yaw, linked ? values[6] : std::string{}}); + } return true; } -std::size_t editor_mode::signal_batch(std::vector const &Placed, std::vector const &Renamed, std::vector> States, std::vector Created, std::string &Error) +std::size_t editor_mode::signal_batch(signal_changes const &Changes, std::vector> States, std::vector Created, std::string &Error) { auto &tool{m_signal}; signal_scan(); signal_refresh(true); std::vector items; std::vector> reads; - for (auto const &order : Placed) + for (auto const &order : Changes.placed) { auto const kind{kind_index(order.kind)}; auto const *item{kind >= 0 && tool.chosen[kind] >= 0 && tool.chosen[kind] < static_cast(tool.templates.size()) ? &tool.templates[tool.chosen[kind]] : nullptr}; @@ -919,16 +926,22 @@ std::size_t editor_mode::signal_batch(std::vector const &Placed, s if (variant->plate) placing.values["#6"] = plate_for(std::string{}, order.plate); if (variant->linked) - placing.values["#7"] = "none"; + placing.values["#7"] = order.linked.empty() ? std::string{"none"} : order.linked; items.push_back(std::move(placing)); reads.emplace_back(spot, variant->read.empty() ? std::string{} : editor_includes::substitute(variant->read, {order.name})); } // the names passed round among the signals go through ones of their own first, or a read event renamed would be renamed again + items.insert(items.end(), Changes.others.begin(), Changes.others.end()); EditorSnapshot step; step.tracks = std::move(States); std::vector> directives; + std::unordered_set changed; + auto const record = [&](scene::instance_handle const Instance, std::string const &Before) { + if (changed.insert(Instance).second) + directives.emplace_back(Instance, Before); + }; for (auto const temporary : {true, false}) - for (auto const &rename : Renamed) + for (auto const &rename : Changes.renamed) { auto const *standing{signal_standing_of(rename.instance)}; auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr}; @@ -937,20 +950,36 @@ std::size_t editor_mode::signal_batch(std::vector const &Placed, s std::vector values; if (item == nullptr || false == editor_includes::parse_directive(before, file, values) || values.empty()) continue; + if (temporary && ToLower(values[0]) == ToLower(rename.name)) + continue; auto const read_before{item->read.empty() ? std::string{} : editor_includes::substitute(item->read, values)}; values[0] = rename.name + (temporary ? "~" : ""); - if (item->plate && values.size() > 5) + if (item->plate && values.size() > 5 && false == rename.plate.empty()) values[5] = plate_for(std::string{}, rename.plate); + if (item->linked && values.size() > 6 && false == rename.linked.empty()) + values[6] = rename.linked; auto const read_after{item->read.empty() ? std::string{} : editor_includes::substitute(item->read, values)}; auto const directive{editor_includes::compose_directive(file, values)}; if (directive == before) continue; - if (temporary) - directives.emplace_back(rename.instance, before); + record(rename.instance, before); set_include_directive(rename.instance, directive); if (read_before != read_after) signal_renamed(read_before, read_after, step); } + for (auto const &[instance, values] : Changes.values) + { + auto const before{scene::Layers.directive(instance)}; + std::string file; + std::vector standing; + if (false == editor_includes::parse_directive(before, file, standing)) + continue; + if (auto const directive{editor_includes::compose_directive(file, values)}; directive != before) + { + record(instance, before); + set_include_directive(instance, directive); + } + } std::vector instances; if (Error.empty() && false == items.empty()) { diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index eb11b7f1..90ad37a1 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editoruilayer.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" #include "editor/editorSwitchStore.hpp" #include "editor/editorTrack.hpp" @@ -34,6 +35,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include namespace @@ -45,6 +47,13 @@ double constexpr kJointRange{1500.0}; // m, of the joints shown in the scenery double constexpr kLabelRange{700.0}; // m, of the names shown there, farther ones crowd the horizon double constexpr kRailHead{0.18}; // m, above the axis of a path +double constexpr kLineLimit{30000.0}; // m, of a line followed out to the next station +double constexpr kBlock{1400.0}; // m, of a block section of the automatic block +double constexpr kPredicted{305.0}; // m, out from the next station to its entry signal, while it has none +double constexpr kWarning{1000.0}; // m, from a warning disc to its entry signal, the braking distance +double constexpr kWarningFast{1300.0}; // m, the same on lines faster than 120 km/h +char const *const kControl{"sbl_odstep.inc"}; // the events of a block section, switching the block signal guarding it + ImU32 constexpr kPlain{IM_COL32(170, 176, 188, 255)}; ImU32 constexpr kSwitch{IM_COL32(240, 200, 90, 255)}; ImU32 constexpr kApproach{IM_COL32(95, 100, 112, 255)}; @@ -122,6 +131,16 @@ std::string signal_shown(std::string const &Name, std::string const &Stem) return text; } +// a signal in the names of the block sections: a block signal by its number, a station one by its letter, with the number of the +// track: 2411, A2 +std::string section_tag(std::string const &Name, int const Track) +{ + auto const cut{Name.rfind('_')}; + auto tag{cut == std::string::npos ? Name : Name.substr(cut + 1)}; + std::transform(tag.begin(), tag.end(), tag.begin(), [](char const Letter) { return static_cast(std::toupper(static_cast(Letter))); }); + return tag + std::to_string(Track); +} + // the way the trains a signal faces run, in the plan: it stands turned to them glm::dvec2 travel_of(double const Yaw) { @@ -174,6 +193,10 @@ void srk_tool::load() { if (word == "line") words >> entry.line; + else if (word == "km") + words >> entry.km; + else if (word == "from") + entry.reversed = (words >> word) && word == "right"; else if (word == "anchor") words >> entry.anchor; else if (word == "name") @@ -185,6 +208,7 @@ void srk_tool::load() } entry.code = shown(entry.code); entry.line = shown(entry.line); + entry.km = shown(entry.km); entry.track = simulation::Paths.find(entry.anchor); if (false == entry.anchor.empty()) m_records.push_back(std::move(entry)); @@ -209,7 +233,7 @@ void srk_tool::store() continue; if (lines.empty()) lines.emplace_back("# stations described in the track editor, the simulation skips these lines"); - lines.push_back(format("station %s line %s anchor %s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), anchor_of(entry).c_str(), entry.name.c_str())); + lines.push_back(format("station %s line %s km %s from %s anchor %s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), word_of(entry.km).c_str(), entry.reversed ? "right" : "left", anchor_of(entry).c_str(), entry.name.c_str())); } scene::Layers.mark(layer, kMark, std::move(lines)); } @@ -239,6 +263,7 @@ void srk_tool::open(TTrack &Track) m_record = index; } m_anchor = m_record >= 0 ? m_records[m_record].track : m_layout->switches.front(); + m_reversed = m_record >= 0 && m_records[m_record].reversed; survey(); } @@ -494,6 +519,14 @@ void srk_tool::render_options() item_tooltip("The abbreviation of the station: the names of its switches, track circuits and signals begin with it"); text("Line", "##srkline", entry != nullptr ? entry->line : std::string{}, "2", [&](std::string const &Value) { record_of_open().line = word_of(Value) == "-" ? std::string{} : word_of(Value); }); item_tooltip("The number of the railway line the station lies on, as the block sections between the stations are named after it"); + double known{0.0}; + auto const hint{layout.kilometrage && km_of_open((layout.from + layout.to) * 0.5, known) ? format("%.3f", known) : std::string{"189.563"}}; + text("Km", "##srkkm", entry != nullptr ? entry->km : std::string{}, hint.c_str(), [&](std::string const &Value) { + auto km{word_of(Value)}; + std::replace(km.begin(), km.end(), ',', '.'); + record_of_open().km = km == "-" ? std::string{} : km; + }); + item_tooltip("The km of the middle of the station along the line, rising the way the switches are numbered: the block signals of the lines are numbered after the km"); ImGui::EndTable(); } step_heading(1, STR_C("Switch numbers")); @@ -501,7 +534,11 @@ void srk_tool::render_options() if (segmented("##srkfrom", from, {{0, STR_C("From the left"), layout.kilometrage ? "From the lower km, as PKP PLK numbers them" : "From the west end of the station"}, {1, STR_C("From the right"), layout.kilometrage ? "From the higher km" : "From the east end of the station"}})) + { m_reversed = from == 1; + record_of_open().reversed = m_reversed; + m_changed = true; + } if (ImGui::Button(STR_C("Number the switches"), ImVec2(-1.0f, 0.0f))) number_switches(); item_tooltip("Numbers the switches 1, 2, 3... along the station and names them after its code, as on the lines of PKP PLK: bpd_1, " @@ -522,6 +559,14 @@ void srk_tool::render_options() "signal at the end of each station track a train is sent out of, F1, G2 towards the start, R1, P2 towards the end; a " "shunting signal Tm1, Tm2... at the end of a shorter track. A signal standing by the place of one gets its name, none is " "doubled. The kinds of signals chosen in the Signals tool. One step of undo"); + step_heading(4, STR_C("Lines")); + if (ImGui::Button(STR_C("Set the line signals"), ImVec2(-1.0f, 0.0f))) + set_lines(); + item_tooltip("Places the signals of the lines out of the station, from its entry signals on. A double-track line gets the automatic " + "block: block signals evenly to the entry signals of the next station, about 1.4 km apart, numbered after the km, 241 on " + "track 1, 242 on track 2, and the block sections between them named as on the lines of PKP PLK, 2411-2571@LK2.1. A " + "single-track line gets a warning disc ToA 1000 m before the entry signal, 1300 m on a line faster than 120 km/h. Needs " + "the line and the km of the station. Run it at the next station too, it keeps what stands. One step of undo"); if (auto const *selected{m_selection.selected_track()}; selected != nullptr && false == selected->Isolated.empty()) if (auto const number{track_of(selected->Isolated.front()->asName, stem_of_open())}; false == number.empty() && begin_fields("##srktrack")) { @@ -591,6 +636,11 @@ void srk_tool::number_switches() m_status = STR_C("Give the station its code first: the switches are named after it"); return; } + if (m_records[m_record].reversed != m_reversed) + { + m_records[m_record].reversed = m_reversed; + m_changed = true; + } auto const units{station::numbering(*m_layout, m_reversed)}; std::vector> names; std::unordered_set own; @@ -776,6 +826,57 @@ std::string srk_tool::stem_of_open() const return m_record >= 0 && false == m_records[m_record].code.empty() ? station::prefix(m_records[m_record].code) : std::string{}; } +bool srk_tool::km_of_open(double const Chainage, double &Km) const +{ + if (false == m_layout.has_value()) + return false; + if (m_record >= 0 && false == m_records[m_record].km.empty()) + { + char *end{nullptr}; + auto const &text{m_records[m_record].km}; + auto const middle{std::strtod(text.c_str(), &end)}; + if (end != text.c_str()) + { + Km = middle + (Chainage - (m_layout->from + m_layout->to) * 0.5) / 1000.0 * (m_reversed ? -1.0 : 1.0); + return true; + } + } + if (false == m_layout->kilometrage) + return false; + Km = station::km_of(*m_layout, Chainage); + return true; +} + +bool srk_tool::station_tracks(std::vector &Tracks, std::unordered_set &Inside) const +{ + Tracks.clear(); + Inside.clear(); + auto const stem{stem_of_open()}; + if (false == m_layout.has_value() || stem.empty()) + return false; + std::map>> numbers; + for (auto const &path : m_layout->paths) + { + if (path.track->Isolated.empty() || Inside.count(path.track) > 0) + continue; + auto const &name{path.track->Isolated.front()->asName}; + if (name.size() <= stem.size() || name.compare(0, stem.size(), stem) != 0 || entry_circuit(name, stem)) + continue; + Inside.insert(path.track); + if (auto const number{track_of(name, stem)}; false == number.empty()) + numbers[number][name].push_back(path.track); + } + for (auto &[number, circuits] : numbers) + { + station::numbered entry; + entry.number = number; + for (auto &[name, members] : circuits) + entry.circuits.push_back(std::move(members)); + Tracks.push_back(std::move(entry)); + } + return false == Tracks.empty(); +} + void srk_tool::renumber_track(TTrack const &Track, std::string const &Number) { m_status.clear(); @@ -823,34 +924,13 @@ void srk_tool::set_signals() return; } auto const &layout{*m_layout}; - // the station tracks by their numbers, the circuits as the tracks have them - std::map>> numbers; + std::vector tracks; std::unordered_set inside; - for (auto const &path : layout.paths) - { - if (path.track->Isolated.empty() || inside.count(path.track) > 0) - continue; - auto const &name{path.track->Isolated.front()->asName}; - if (name.size() <= stem.size() || name.compare(0, stem.size(), stem) != 0 || entry_circuit(name, stem)) - continue; - inside.insert(path.track); - if (auto const number{track_of(name, stem)}; false == number.empty()) - numbers[number][name].push_back(path.track); - } - if (numbers.empty()) + if (false == station_tracks(tracks, inside)) { m_status = STR_C("Set the track circuits first: the signals stand at their joints"); return; } - std::vector tracks; - for (auto &[number, circuits] : numbers) - { - station::numbered entry; - entry.number = number; - for (auto &[name, members] : circuits) - entry.circuits.push_back(std::move(members)); - tracks.push_back(std::move(entry)); - } auto plan{station::signals(layout, tracks, [&](TTrack const &Track) { return inside.count(&Track) > 0; })}; std::vector standing; m_signals.standing_signals(standing, true); @@ -1013,13 +1093,460 @@ void srk_tool::set_signals() return; } std::string error; - auto const count{m_signals.signal_batch(placed, renamed, std::move(before), std::move(created), error)}; + auto const count{m_signals.signal_batch({placed, renamed, {}, {}}, std::move(before), std::move(created), error)}; if (false == error.empty()) m_status = format(STR_C("The signals couldn't be placed: %s"), error.c_str()); else m_status = format(STR_P("%zu signal placed", "%zu signals placed", count), count) + ", " + format(STR_P("%zu renamed", "%zu renamed", renamed.size()), renamed.size()); } +void srk_tool::set_lines() +{ + m_status.clear(); + auto const stem{stem_of_open()}; + if (stem.empty()) + { + m_status = STR_C("Give the station its code first: the signals are named after it"); + return; + } + auto const &layout{*m_layout}; + std::vector tracks; + std::unordered_set inside; + if (false == station_tracks(tracks, inside)) + { + m_status = STR_C("Set the track circuits first: the signals stand at their joints"); + return; + } + auto const plan{station::signals(layout, tracks, [&](TTrack const &Track) { return inside.count(&Track) > 0; })}; + std::vector standing; + m_signals.standing_signals(standing, true); + // the lines begin at the entry signals standing + std::vector entries; + std::vector entry_standing; + for (auto const &planned : plan) + { + if (planned.kind != station::signal::role::entry) + continue; + auto const found{std::find_if(standing.begin(), standing.end(), [&](standing_signal const &Signal) { return Signal.kind == "main" && ToLower(Signal.name) == stem + ToLower(planned.name); })}; + if (found == standing.end()) + continue; + entries.push_back(planned); + entry_standing.push_back(static_cast(found - standing.begin())); + } + if (entries.empty()) + { + m_status = STR_C("Set the signals first: the lines begin at the entry signals"); + return; + } + auto const lines{station::lines_out(layout, entries, kLineLimit)}; + if (lines.empty()) + { + m_status = STR_C("No line leads out of the station beyond its entry signals"); + return; + } + auto const &described{m_records[m_record]}; + double known{0.0}; + if (std::any_of(lines.begin(), lines.end(), [](station::line const &Line) { return Line.number > 0; }) && (false == km_of_open(0.0, known) || described.line.empty())) + { + m_status = STR_C("Give the station its line and its km first: the block signals and the block sections are named after them"); + return; + } + auto const middle{(layout.from + layout.to) * 0.5}; + struct planned_signal + { + station::signal::role kind{station::signal::role::block}; + std::string name; + std::string plate; + std::size_t line{0}; + double distance{0.0}; // out along the line + std::string linked{"none"}; + int match{-1}; // of the standing signals + }; + struct far_end + { + int signal{-1}; // the entry signal of the next station standing, of the standing signals + double distance{0.0}; // out to it, or to where the signals of the line end + }; + std::vector planned; + std::vector ends(lines.size()); + auto const facing_out = [&](station::line const &Line, standing_signal const &Signal, double const Distance) { + auto const way{station::point_on(Line, Distance).way}; + return glm::dot(travel_of(Signal.yaw), glm::dvec2{way.x, way.z}); + }; + auto const km_on = [&](std::size_t const Index, double const Distance) { + auto const chainage{station::project(layout, lines[Index].points.front()).x}; + double km{0.0}; + km_of_open(chainage < middle ? chainage - Distance : chainage + Distance, km); + return km; + }; + for (std::size_t index = 0; index < lines.size(); ++index) + { + auto const &line{lines[index]}; + auto &end{ends[index]}; + // the entry signal of the next station: the first main signal along the line facing out + for (std::size_t k = 0; k < standing.size(); ++k) + { + if (standing[k].kind != "main") + continue; + double gap{0.0}; + auto const distance{station::distance_on(line, standing[k].position, gap)}; + if (gap > 4.5 || distance < 100.0 || distance > line.length() - 1.0 || facing_out(line, standing[k], distance) < 0.87) + continue; + if (end.signal < 0 || distance < end.distance) + { + end.signal = static_cast(k); + end.distance = distance; + } + } + if (end.signal < 0) + end.distance = line.station ? line.length() - kPredicted : line.length(); + } + for (std::size_t index = 0; index < lines.size(); ++index) + { + auto const &line{lines[index]}; + auto const &end{ends[index]}; + auto const &entry{entries[line.entry]}; + if (line.number > 0) + { + // the block signals: the line to the next station parted evenly, towards the end of the tracks every 1.4 km + std::vector places; + if (line.station || end.signal >= 0) + { + // as many on both tracks of the line + auto const reach{line.partner >= 0 ? (end.distance + ends[line.partner].distance) * 0.5 : end.distance}; + auto const sections{std::max(1L, std::lround(reach / kBlock))}; + for (long k = 1; k < sections; ++k) + places.push_back(end.distance * static_cast(k) / static_cast(sections)); + } + else + for (auto distance{kBlock}; distance <= end.distance - 100.0; distance += kBlock) + places.push_back(distance); + for (auto const distance : places) + { + planned_signal signal; + signal.kind = station::signal::role::block; + signal.name = station::block_number(km_on(index, distance), line.number); + signal.plate = signal.name; + signal.line = index; + signal.distance = distance; + planned.push_back(std::move(signal)); + } + } + else + { + // the warning disc of the entry signal, the braking distance before it + auto const fast{entry.track != nullptr && entry.track->VelocityGet() > 120.0}; + auto const distance{std::min(fast ? kWarningFast : kWarning, end.distance - 100.0)}; + if (distance >= 400.0) + { + planned_signal signal; + signal.kind = station::signal::role::warning; + signal.name = stem + "to" + ToLower(entry.name); + signal.plate = "to" + ToLower(entry.name); + signal.line = index; + signal.distance = distance; + planned.push_back(std::move(signal)); + } + } + } + // the standing ones by their places, then by their names + std::vector used(standing.size(), 0); + for (auto const index : entry_standing) + used[index] = 1; + for (auto const &end : ends) + if (end.signal >= 0) + used[end.signal] = 1; + std::vector> candidates; + std::vector standing_at(standing.size(), 0.0); + for (std::size_t i = 0; i < planned.size(); ++i) + { + auto const &signal{planned[i]}; + auto const &line{lines[signal.line]}; + auto const kind{signal.kind == station::signal::role::block ? "block" : "warning"}; + auto const reach{signal.kind == station::signal::role::block ? 300.0 : 400.0}; + for (std::size_t k = 0; k < standing.size(); ++k) + { + if (standing[k].kind != kind) + continue; + double gap{0.0}; + auto const distance{station::distance_on(line, standing[k].position, gap)}; + auto const facing{facing_out(line, standing[k], distance)}; + if (gap < 4.5 && std::abs(distance - signal.distance) < reach && (line.inbound ? -facing : facing) > 0.87) + candidates.emplace_back(std::abs(distance - signal.distance) + gap * 10.0, i, k); + } + } + std::sort(candidates.begin(), candidates.end()); + for (auto const &[score, i, k] : candidates) + if (planned[i].match < 0 && 0 == used[k]) + { + planned[i].match = static_cast(k); + used[k] = 1; + double gap{0.0}; + planned[i].distance = station::distance_on(lines[planned[i].line], standing[k].position, gap); + } + for (auto &signal : planned) + for (std::size_t k = 0; k < standing.size() && signal.match < 0; ++k) + if (0 == used[k] && ToLower(standing[k].name) == ToLower(signal.name)) + { + signal.match = static_cast(k); + used[k] = 1; + } + // a number another signal of the scenery has already gets a letter + std::unordered_set taken; + for (std::size_t k = 0; k < standing.size(); ++k) + if (0 == used[k]) + taken.insert(ToLower(standing[k].name)); + for (auto &signal : planned) + { + if (signal.kind != station::signal::role::block) + continue; + auto const base{signal.name}; + for (char letter = 'a'; taken.count(ToLower(signal.name)) > 0 && letter <= 'z'; ++letter) + signal.name = base + letter; + taken.insert(ToLower(signal.name)); + signal.plate = signal.name; + } + // the block sections of each track of a double-track line, between the signals in the order the trains pass them + struct section + { + std::string name; + std::size_t line{0}; + double from{0.0}; + double to{0.0}; + std::string guard; // the block signal at its start, empty for a station one + glm::dvec3 start{0.0}; + }; + std::vector
sections; + std::vector> relinked; // standing station signals, the block signal before each + for (std::size_t index = 0; index < lines.size(); ++index) + { + auto const &line{lines[index]}; + if (line.number == 0) + continue; + struct stop + { + double distance{0.0}; + std::string name; + int signal{-1}; // of the planned ones, -1 for a station signal + int standing{-1}; + }; + std::vector order{{0.0, standing[entry_standing[line.entry]].name, -1, static_cast(entry_standing[line.entry])}}; + for (std::size_t i = 0; i < planned.size(); ++i) + if (planned[i].line == index) + order.push_back({planned[i].distance, planned[i].name, static_cast(i), planned[i].match}); + if (ends[index].signal >= 0) + order.push_back({ends[index].distance, standing[ends[index].signal].name, -1, ends[index].signal}); + std::sort(order.begin(), order.end(), [](stop const &A, stop const &B) { return A.distance < B.distance; }); + if (line.inbound) + std::reverse(order.begin(), order.end()); + for (std::size_t i = 0; i < order.size(); ++i) + { + auto const &here{order[i]}; + auto const block_before{i > 0 && order[i - 1].signal >= 0}; + auto const linked{block_before ? order[i - 1].name : std::string{"none"}}; + if (here.signal >= 0) + planned[here.signal].linked = linked; + else if (block_before && here.standing >= 0 && false == standing[here.standing].linked.empty() && ToLower(standing[here.standing].linked) != ToLower(linked)) + relinked.emplace_back(here.standing, linked); + if (i == 0) + continue; + auto const &before{order[i - 1]}; + // the lower km first, as on the lines of PKP PLK: 2411-2571@LK2.1 + auto const ascending{km_on(index, before.distance) <= km_on(index, here.distance)}; + auto const &low{ascending ? before : here}; + auto const &high{ascending ? here : before}; + section found; + found.name = ToLower(section_tag(low.name, line.number) + "-" + section_tag(high.name, line.number) + "@lk" + described.line + "." + std::to_string(line.number)); + found.line = index; + found.from = std::min(before.distance, here.distance); + found.to = std::max(before.distance, here.distance); + found.guard = before.signal >= 0 ? before.name : std::string{}; + found.start = station::point_on(line, before.distance).point; + sections.push_back(std::move(found)); + } + } + // the tracks cut where the sections meet, all of it checked before any is + struct cut + { + TTrack *track{nullptr}; + glm::dvec3 point{0.0}; + }; + std::vector cuts; + auto const add_cut = [&](station::line const &Line, double const Distance) -> bool { + auto const at{station::point_on(Line, Distance)}; + if (at.track == nullptr || std::any_of(cuts.begin(), cuts.end(), [&](cut const &Cut) { return glm::distance(Cut.point, at.point) < 0.5; })) + return true; + auto const &ends_of{at.track->m_paths.front().points}; + if (std::min(glm::distance(at.point, ends_of[segment_data::point::start]), glm::distance(at.point, ends_of[segment_data::point::end])) <= 0.3) + return true; + std::string reason; + if (false == editor_track::can_edit_geometry(*at.track, reason)) + { + m_status = format(STR_C("%s can't be cut: %s"), name_of(*at.track).c_str(), reason.c_str()); + return false; + } + cuts.push_back({at.track, at.point}); + return true; + }; + for (auto const &found : sections) + if (false == add_cut(lines[found.line], found.from) || false == add_cut(lines[found.line], found.to)) + return; + for (auto const &line : lines) + for (auto *part : line.tracks) + if (std::string reason; false == scene::Layers.editable(part, &reason) && std::any_of(sections.begin(), sections.end(), [&](section const &Found) { return &lines[Found.line] == &line; })) + { + m_status = format(STR_C("%s can't be changed: %s"), name_of(*part).c_str(), reason.c_str()); + return; + } + std::vector> before; + std::vector created; + std::unordered_set captured; + std::unordered_map> pieces; // of each track cut, itself and the pieces cut off it + auto const piece_at = [&](TTrack *Track, glm::dvec3 const &Point) { + auto found{pieces.find(Track)}; + if (found == pieces.end()) + return Track; + TTrack *best{Track}; + auto nearest{std::numeric_limits::max()}; + for (auto *piece : found->second) + { + auto const point{editor_track::point_at(*piece, editor_track::nearest_parameter(*piece, Point))}; + if (auto const distance{glm::distance(point, Point)}; distance < nearest) + { + nearest = distance; + best = piece; + } + } + return best; + }; + for (auto const &entry : cuts) + { + auto *track{piece_at(entry.track, entry.point)}; + auto const parameter{editor_track::nearest_parameter(*track, entry.point)}; + auto const &ends_of{track->m_paths.front().points}; + if (std::min(glm::distance(entry.point, ends_of[segment_data::point::start]), glm::distance(entry.point, ends_of[segment_data::point::end])) <= 0.3) + continue; + if (captured.insert(track).second) + before.emplace_back(track, editor_track::capture(*track)); + if (auto *piece{editor_track::split_path(*track, parameter)}) + { + created.push_back(piece); + captured.insert(piece); + auto &list{pieces[entry.track]}; + if (list.empty()) + list.push_back(entry.track); + list.push_back(piece); + } + } + // the sections named, each track by its middle + for (std::size_t index = 0; index < lines.size(); ++index) + { + std::vector parts; + for (auto *track : lines[index].tracks) + { + auto const found{pieces.find(track)}; + if (found == pieces.end()) + parts.push_back(track); + else + parts.insert(parts.end(), found->second.begin(), found->second.end()); + } + for (auto *part : parts) + { + double gap{0.0}; + auto const distance{station::distance_on(lines[index], editor_track::point_at(*part, 0.5), gap)}; + if (gap > 1.0) + continue; + auto const found{std::find_if(sections.begin(), sections.end(), [&](section const &Section) { return Section.line == index && distance > Section.from && distance < Section.to; })}; + if (found == sections.end() || (part->Isolated.size() == 1 && ToLower(part->Isolated.front()->asName) == found->name)) + continue; + if (captured.insert(part).second) + before.emplace_back(part, editor_track::capture(*part)); + editor_track::isolated(*part, {found->name}); + } + } + // the signals placed on the track the trains run on up to them + signal_changes changes; + std::size_t blocks{0}; + std::size_t discs{0}; + for (auto const &signal : planned) + { + auto const block{signal.kind == station::signal::role::block}; + if (signal.match >= 0) + { + auto const &found{standing[signal.match]}; + auto const relink{block && false == found.linked.empty() && ToLower(found.linked) != ToLower(signal.linked)}; + if (ToLower(found.name) != ToLower(signal.name) || relink) + changes.renamed.push_back({found.instance, signal.name, signal.plate, relink ? signal.linked : std::string{}}); + continue; + } + auto const &line{lines[signal.line]}; + auto const inbound{line.inbound}; + auto const at{station::point_on(line, signal.distance)}; + auto const approach{station::point_on(line, signal.distance + (inbound ? 1.0 : -1.0))}; + signal_order order; + order.track = piece_at(approach.track, approach.point); + order.point = at.point; + order.facing = inbound == approach.outward ? -1 : 1; + order.side = order.facing; + order.kind = block ? "block" : "warning"; + order.name = signal.name; + order.plate = signal.plate; + order.linked = signal.linked; + changes.placed.push_back(std::move(order)); + ++(block ? blocks : discs); + } + for (auto const &[index, linked] : relinked) + changes.renamed.push_back({standing[index].instance, standing[index].name, std::string{}, linked}); + // the block signals switched by the sections they guard + include_info control; + std::string missing; + auto const controlled{editor_includes::load(kControl, control, missing)}; + if (controlled) + { + auto const standing_controls{m_templates.standing_templates([](std::string const &File) { return File == kControl; })}; + for (auto const &found : sections) + { + if (found.guard.empty()) + continue; + auto const already{std::find_if(standing_controls.begin(), standing_controls.end(), [&](standing_template const &Control) { return false == Control.values.empty() && ToLower(Control.values[0]) == found.name; })}; + if (already != standing_controls.end()) + { + if (already->values.size() > 5 && ToLower(already->values[5]) != ToLower(found.guard)) + { + auto values{already->values}; + values[5] = found.guard; + changes.values.emplace_back(already->instance, std::move(values)); + } + continue; + } + template_item item; + item.file = kControl; + item.location = found.start; + item.described = true; + item.values["name"] = found.name; + item.values["#6"] = found.guard; + changes.others.push_back(std::move(item)); + } + } + if (changes.placed.empty() && changes.renamed.empty() && changes.others.empty() && changes.values.empty() && before.empty() && created.empty()) + { + m_status = STR_C("The lines are set already"); + return; + } + std::string error; + auto const renamed{changes.renamed.size()}; + m_signals.signal_batch(changes, std::move(before), std::move(created), error); + if (false == error.empty()) + { + m_status = format(STR_C("The signals couldn't be placed: %s"), error.c_str()); + return; + } + m_status = format(STR_P("%zu block signal placed", "%zu block signals placed", blocks), blocks) + ", " + format(STR_P("%zu warning disc", "%zu warning discs", discs), discs) + ", " + + format(STR_P("%zu renamed", "%zu renamed", renamed), renamed) + ", " + format(STR_P("%zu block section", "%zu block sections", sections.size()), sections.size()); + if (false == controlled && std::any_of(sections.begin(), sections.end(), [](section const &Found) { return false == Found.guard.empty(); })) + m_status += "\n" + format(STR_C("%s isn't in the scenery folder: the block signals won't change their lights"), kControl); +} + void srk_tool::survey_signals() { m_signal_marks.clear(); @@ -1497,6 +2024,51 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri WriteLog(format("SELFTEST srksignals %zu:", m_signal_marks.size()) + list); return editor_selftest_result::handled; } + if (Command == "srklines") + { + // srklines: the signals of the lines out of the open station set + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srklines: no station open"); + return editor_selftest_result::handled; + } + set_lines(); + WriteLog("SELFTEST srklines: " + m_status); + refresh(); + std::vector tracks; + std::unordered_set inside; + if (false == station_tracks(tracks, inside)) + return editor_selftest_result::handled; + auto const plan{station::signals(*m_layout, tracks, [&](TTrack const &Track) { return inside.count(&Track) > 0; })}; + std::vector standing; + m_signals.standing_signals(standing, true); + for (auto const &line : station::lines_out(*m_layout, plan, kLineLimit)) + { + std::vector> along; + for (auto const &signal : standing) + { + double gap{0.0}; + auto const distance{station::distance_on(line, signal.position, gap)}; + if (gap < 4.5 && distance < line.length()) + along.emplace_back(distance, format("%s(%s,%s)", signal.name.c_str(), signal.kind.c_str(), signal.linked.c_str())); + } + std::string last; + for (std::size_t i = 1; i < line.points.size(); i += 20) + { + auto const *track{line.tracks[line.owners[i]]}; + auto const name{track->Isolated.empty() ? std::string{"-"} : track->Isolated.front()->asName}; + if (name != last) + along.emplace_back(line.distances[i], "[" + name + "]"); + last = name; + } + std::sort(along.begin(), along.end()); + std::string list; + for (auto const &[distance, text] : along) + list += format(" %.0f:%s", distance, text.c_str()); + WriteLog(format("SELFTEST srklines %s track %d %s %.0f m%s:", plan[line.entry].name.c_str(), line.number, line.inbound ? "in" : "out", line.length(), line.station ? " to a station" : "") + list); + } + return editor_selftest_result::handled; + } if (Command == "srkstrip") { // srkstrip shown=0: the schematic hidden, the 3d view whole for a screenshot @@ -1533,6 +2105,8 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri entry.name = name->second; std::replace(entry.name.begin(), entry.name.end(), '_', ' '); } + if (auto const km{values.find("km")}; km != values.end()) + entry.km = km->second; m_changed = true; WriteLog("SELFTEST srkset: " + title()); return editor_selftest_result::handled; diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 9400f61f..8d0f2be2 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include #include @@ -24,7 +25,10 @@ http://mozilla.org/MPL/2.0/. class srk_tool final : public editor_tool { public: - srk_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_signals &Signals) : m_selection{Selection}, m_view{View}, m_history{History}, m_signals{Signals} {} + srk_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_templates &Templates, editor_signals &Signals) + : m_selection{Selection}, m_view{View}, m_history{History}, m_templates{Templates}, m_signals{Signals} + { + } void update() override; void draw_overlay() const override; void render_options() override; @@ -39,6 +43,7 @@ class srk_tool final : public editor_tool editor_selection &m_selection; editor_view &m_view; editor_history &m_history; + editor_templates &m_templates; editor_signals &m_signals; // a station described: found again by a switch of it struct record @@ -49,6 +54,8 @@ class srk_tool final : public editor_tool std::string code; // the abbreviation of the station, as MMz std::string name; std::string line; // the number of the railway line + std::string km; // of the axis of the station along the line, empty while unknown + bool reversed{false}; // the km rise to the left of the schematic, the switches numbered from there }; // the schematic: the chainage across, the offsets upwards, exaggerated struct view @@ -122,8 +129,16 @@ class srk_tool final : public editor_tool void set_signals(); // the signals standing by the station, for the schematic void survey_signals(); + // the signals of the lines out of the station: on a double-track line the block signals to the next station and the block + // sections between them, on a single-track one a warning disc before the entry signal; one step of the history + void set_lines(); // the names of the switches and the circuits of the open station begin with it, empty while it has no code std::string stem_of_open() const; + // the km at the chainage of the open station: of its record, else of its switches; false while unknown + bool km_of_open(double const Chainage, double &Km) const; + // the station tracks by their numbers with their circuits as the tracks have them, Inside: the tracks of the circuits; false + // while it has none + bool station_tracks(std::vector &Tracks, std::unordered_set &Inside) const; // the circuits of the station track of the track renamed after another number; one step of the history void renumber_track(TTrack const &Track, std::string const &Number); }; diff --git a/application/editortool.h b/application/editortool.h index bbe506da..340899c8 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -84,12 +84,14 @@ struct signal_order std::string kind; std::string name; std::string plate; // the name on the plate under the head, when the template has one + std::string linked{"none"}; // the signal before it, when the template takes one: a block signal shows its aspects by it }; struct signal_rename { scene::instance_handle instance{0}; std::string name; std::string plate; + std::string linked; // the same, empty to keep the one it has }; struct standing_signal { @@ -98,6 +100,16 @@ struct standing_signal std::string kind; glm::dvec3 position{0.0}; double yaw{0.0}; + std::string linked; // of a template that takes one, else empty +}; +// one step of the history of the signalling: signals placed, standing ones renamed, other templates placed with them, the values +// of standing ones replaced +struct signal_changes +{ + std::vector placed; + std::vector renamed; + std::vector others; + std::vector>> values; }; struct nmt_place @@ -177,9 +189,9 @@ class editor_signals public: // false while the signal templates are still being looked for virtual bool standing_signals(std::vector &Signals, bool const Wait) = 0; - // the signals placed by the templates chosen for their kinds and the standing ones renamed, with the tracks changed before: - // one step of the history. returns: how many were placed - virtual std::size_t signal_batch(std::vector const &Placed, std::vector const &Renamed, std::vector> States, std::vector Created, std::string &Error) = 0; + // the signals placed by the templates chosen for their kinds and the rest of the changes, with the tracks changed before: one + // step of the history. returns: how many signals were placed + virtual std::size_t signal_batch(signal_changes const &Changes, std::vector> States, std::vector Created, std::string &Error) = 0; protected: ~editor_signals() = default; diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index 66c8016a..ce6b598b 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -1254,4 +1254,168 @@ std::vector signals(layout const &Layout, std::vector const &T return result; } +std::vector lines_out(layout const &Layout, std::vector const &Signals, double const Limit) +{ + std::vector result; + for (std::size_t index = 0; index < Signals.size(); ++index) + { + auto const &entry{Signals[index]}; + if (entry.kind != signal::role::entry || entry.track == nullptr || node(*entry.track)) + continue; + line found; + found.entry = index; + // out where the trains it faces come from + auto *track{entry.track}; + auto outward{entry.facing < 0}; + auto first{true}; + std::unordered_set seen; + while (true) + { + double length{0.0}; + auto points{dense_points(*track, 0, length)}; + if (false == outward) + std::reverse(points.begin(), points.end()); + if (first) + { + auto const nearest{std::min_element(points.begin(), points.end(), [&](glm::dvec3 const &A, glm::dvec3 const &B) { return plan_distance(A, entry.point) < plan_distance(B, entry.point); })}; + points.erase(points.begin(), nearest); + points.front() = entry.point; + } + else + points.erase(points.begin()); + seen.insert(track); + found.tracks.push_back(track); + found.outward.push_back(outward); + for (auto const &point : points) + { + found.distances.push_back(found.points.empty() ? 0.0 : found.distances.back() + plan_distance(found.points.back(), point)); + found.points.push_back(point); + found.owners.push_back(found.tracks.size() - 1); + } + first = false; + if (found.length() > Limit) + break; + auto const end{end_of(*track, 0, outward)}; + auto *next{neighbour(*track, 0, outward)}; + if (next == nullptr || false == rail(*next) || seen.count(next) > 0) + break; + if (node(*next)) + { + found.station = true; + break; + } + int path{0}; + bool atend{false}; + if (false == end_at(*next, end, path, atend)) + break; + track = next; + outward = false == atend; + } + if (found.length() > 1.0) + result.push_back(std::move(found)); + } + // a double-track line: two lines out of the same end 3 to 6.5 m apart all along their first kilometre + auto const middle{(Layout.from + Layout.to) * 0.5}; + auto const start = [&](line const &Line) { return project(Layout, Line.points.front()).x < middle; }; + auto const gap_between = [&](line const &A, line const &B) { + auto const reach{std::min({A.length(), B.length(), 1000.0})}; + auto worst{0.0}; + for (auto const fraction : {0.2, 0.6, 1.0}) + { + double gap{0.0}; + distance_on(B, point_on(A, reach * fraction).point, gap); + worst = std::max(worst, std::abs(gap - 4.5)); + } + return reach < 100.0 ? std::numeric_limits::max() : worst; + }; + // the pairs running side by side the longest first: a siding along a main track for a while is no line + std::vector> pairs; + for (std::size_t i = 0; i < result.size(); ++i) + for (std::size_t j = i + 1; j < result.size(); ++j) + if (start(result[i]) == start(result[j])) + if (auto const gap{gap_between(result[i], result[j])}; gap <= 2.0) + pairs.emplace_back(-std::min(result[i].length(), result[j].length()), gap, i, j); + std::sort(pairs.begin(), pairs.end()); + std::vector paired(result.size(), 0); + for (auto const &[reach_order, gap, i, partner] : pairs) + { + if (paired[i] != 0 || paired[partner] != 0) + continue; + paired[i] = paired[partner] = 1; + result[i].partner = static_cast(partner); + result[partner].partner = static_cast(i); + // the trains towards the station on the track on their right; along the chainage the right one is track 1 + auto const &travel{Signals[result[i].entry].travel}; + glm::dvec2 const right{-travel.z, travel.x}; + auto const reach{std::min({result[i].length(), result[partner].length(), 200.0})}; + auto const here{point_on(result[i], reach).point}; + auto const there{point_on(result[partner], reach).point}; + auto const partner_inbound{glm::dot(glm::dvec2{there.x - here.x, there.z - here.z}, right) > 0.0}; + result[i].inbound = false == partner_inbound; + result[partner].inbound = partner_inbound; + for (auto *found : {&result[i], &result[partner]}) + found->number = found->inbound == start(*found) ? 1 : 2; + } + return result; +} + +on_line point_on(line const &Line, double const Distance) +{ + on_line result; + if (Line.points.empty()) + return result; + auto const after{std::lower_bound(Line.distances.begin(), Line.distances.end(), Distance)}; + auto const i{std::clamp(static_cast(after - Line.distances.begin()), 1, std::max(1, Line.points.size() - 1))}; + if (Line.points.size() < 2) + { + result.track = Line.tracks[Line.owners.front()]; + result.outward = Line.outward[Line.owners.front()]; + result.point = Line.points.front(); + return result; + } + auto const step{Line.distances[i] - Line.distances[i - 1]}; + auto const t{step > 1e-9 ? std::clamp((Distance - Line.distances[i - 1]) / step, 0.0, 1.0) : 0.0}; + result.track = Line.tracks[Line.owners[i]]; + result.outward = Line.outward[Line.owners[i]]; + result.point = glm::mix(Line.points[i - 1], Line.points[i], t); + auto const way{Line.points[i] - Line.points[i - 1]}; + if (glm::length(way) > 1e-9) + result.way = glm::normalize(way); + return result; +} + +double distance_on(line const &Line, glm::dvec3 const &Point, double &Gap) +{ + Gap = std::numeric_limits::max(); + auto result{0.0}; + glm::dvec2 const point{Point.x, Point.z}; + for (std::size_t i = 1; i < Line.points.size(); ++i) + { + glm::dvec2 const a{Line.points[i - 1].x, Line.points[i - 1].z}; + glm::dvec2 const b{Line.points[i].x, Line.points[i].z}; + auto const along{b - a}; + auto const length{glm::length(along)}; + if (length < 1e-9) + continue; + auto const t{std::clamp(glm::dot(point - a, along) / (length * length), 0.0, 1.0)}; + if (auto const gap{glm::distance(point, a + along * t)}; gap < Gap) + { + Gap = gap; + result = Line.distances[i - 1] + length * t; + } + } + return result; +} + +std::string block_number(double const Km, int const Track) +{ + auto const hectometres{std::max(Km, 0.0) * 10.0}; + auto number{static_cast(std::llround(hectometres))}; + if ((number % 2 == 1) != (Track % 2 == 1)) + number += hectometres > static_cast(number) ? 1 : -1; + if (number < 1) + number = Track % 2 == 1 ? 1 : 2; + return std::to_string(number); +} + } // namespace station diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index f8fcceeb..c61690df 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -122,7 +122,9 @@ struct signal { entry, exit, - shunting + shunting, + block, + warning }; role kind{role::entry}; std::string name; // without the code of the station: A, F1, Tm3 @@ -145,4 +147,38 @@ struct signal // station std::vector signals(layout const &Layout, std::vector const &Tracks, std::function const &Inside); +// a track of a line out beyond an entry signal of the station, to the next station or the end of the tracks +struct line +{ + std::size_t entry{0}; // the entry signal standing on it, of the signals given + int number{0}; // of the track of a double-track line: 1 the one on the right along the chainage, 2 the other; 0 on a single-track line + bool inbound{true}; // its trains run towards the station: on a double-track line the track on their right + bool station{false}; // ends at a switch, of the next station, rather than at the end of the tracks + int partner{-1}; // the other track of a double-track line, of the lines + std::vector tracks; // from the entry signal out + std::vector outward; // of each track: the line runs out towards the end of its path + std::vector points; // along the tracks from the entry signal on, a metre or so apart + std::vector distances; // of the points, from the entry signal + std::vector owners; // of the points, the track each lies on + double length() const { return distances.empty() ? 0.0 : distances.back(); } +}; +// where the line runs at the distance out: on what track, the point on its axis, the way out along the line +struct on_line +{ + TTrack *track{nullptr}; + bool outward{true}; // the way out runs towards the end of the path of the track + glm::dvec3 point{0.0}; + glm::dvec3 way{0.0, 0.0, 1.0}; +}; + +// the lines beyond the entry signals among the signals given, no longer than the limit; two lines out of the same end side by side +// are the tracks of a double-track line +std::vector lines_out(layout const &Layout, std::vector const &Signals, double const Limit); +on_line point_on(line const &Line, double const Distance); +// how far out along the line the point lies, Gap: how far from its axis in the plan +double distance_on(line const &Line, glm::dvec3 const &Point, double &Gap); +// the number of a block signal at the km, as PKP PLK gives them: the km and the hectometre, the last digit odd on track 1, even on +// track 2: 241, 242 +std::string block_number(double const Km, int const Track); + } // namespace station diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 79ca1635..229924d2 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10488,3 +10488,51 @@ msgstr[0] "%zu przemianowany" msgstr[1] "%zu przemianowane" msgstr[2] "%zu przemianowanych" +msgid "Lines" +msgstr "Szlaki" + +msgid "Set the line signals" +msgstr "Ustaw sygnalizatory szlakowe" + +msgid "Places the signals of the lines out of the station, from its entry signals on. A double-track line gets the automatic block: block signals evenly to the entry signals of the next station, about 1.4 km apart, numbered after the km, 241 on track 1, 242 on track 2, and the block sections between them named as on the lines of PKP PLK, 2411-2571@LK2.1. A single-track line gets a warning disc ToA 1000 m before the entry signal, 1300 m on a line faster than 120 km/h. Needs the line and the km of the station. Run it at the next station too, it keeps what stands. One step of undo" +msgstr "Stawia sygnalizatory szlaków wychodzących ze stacji, od jej semaforów wjazdowych. Szlak dwutorowy dostaje samoczynną blokadę liniową: semafory SBL rozstawione równo do semaforów wjazdowych następnej stacji, co około 1,4 km, numerowane według kilometra, 241 na torze 1, 242 na torze 2, a odstępy między nimi nazwane jak na liniach PKP PLK, 2411-2571@LK2.1. Szlak jednotorowy dostaje tarczę ostrzegawczą ToA 1000 m przed semaforem wjazdowym, 1300 m na linii szybszej niż 120 km/h. Potrzebna linia i kilometr stacji. Uruchom to też na następnej stacji, zostawia to, co już stoi. Jeden krok cofania" + +msgid "Km" +msgstr "Kilometr" + +msgid "The km of the middle of the station along the line, rising the way the switches are numbered: the block signals of the lines are numbered after the km" +msgstr "Kilometr środka stacji na linii, rosnący w stronę numeracji rozjazdów: według kilometra numerowane są semafory SBL na szlakach" + +msgid "Give the station its line and its km first: the block signals and the block sections are named after them" +msgstr "Najpierw podaj linię i kilometr stacji: według nich nazywają się semafory SBL i odstępy" + +msgid "Set the signals first: the lines begin at the entry signals" +msgstr "Najpierw ustaw semafory: szlaki zaczynają się od semaforów wjazdowych" + +msgid "No line leads out of the station beyond its entry signals" +msgstr "Za semaforami wjazdowymi nie wychodzi ze stacji żaden szlak" + +msgid "The lines are set already" +msgstr "Szlaki są już ustawione" + +msgid "%s isn't in the scenery folder: the block signals won't change their lights" +msgstr "Brak %s w katalogu scenery: semafory SBL nie będą zmieniać świateł" + +msgid "%zu block signal placed" +msgid_plural "%zu block signals placed" +msgstr[0] "Postawiono %zu semafor SBL" +msgstr[1] "Postawiono %zu semafory SBL" +msgstr[2] "Postawiono %zu semaforów SBL" + +msgid "%zu warning disc" +msgid_plural "%zu warning discs" +msgstr[0] "%zu tarczę ostrzegawczą" +msgstr[1] "%zu tarcze ostrzegawcze" +msgstr[2] "%zu tarcz ostrzegawczych" + +msgid "%zu block section" +msgid_plural "%zu block sections" +msgstr[0] "%zu odstęp" +msgstr[1] "%zu odstępy" +msgstr[2] "%zu odstępów" + From 2b40a3617309205b3985ab71385d177e3aff9896 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 12:13:22 +0200 Subject: [PATCH 050/145] feat(editor): the switch numbering of the signalling mode renames the switch in the objects of the scenery naming it, as the lanterns of the switches --- application/editormodesrk.cpp | 31 +++++++++++++++++++++++++++++-- lang/editor_pl.po | 10 ++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 90ad37a1..52b05daa 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -542,7 +542,7 @@ void srk_tool::render_options() if (ImGui::Button(STR_C("Number the switches"), ImVec2(-1.0f, 0.0f))) number_switches(); item_tooltip("Numbers the switches 1, 2, 3... along the station and names them after its code, as on the lines of PKP PLK: bpd_1, " - "the parts of a slip bpd_15a to bpd_15d. The drives follow their switches, events written by hand keep the old names. One step of undo"); + "the parts of a slip bpd_15a to bpd_15d. The drives and the objects naming a switch follow it, events written by hand keep the old names. One step of undo"); step_heading(2, STR_C("Track circuits")); if (main_button(STR_C("Set the track circuits"), ImVec2(-1.0f, tall_button_height()))) set_circuits(); @@ -678,6 +678,24 @@ void srk_tool::number_switches() if (false == units[index].record.empty()) bases[units[index].record] = station::switch_name(code, static_cast(index) + 1, 0, 1); switch_store::rename(parts, bases); + // the objects of the scenery naming a switch, as the lanterns of rainsted/lwzm.inc, follow it + std::map renamed; + for (auto const &[track, name] : names) + if (track->name() != name) + renamed[ToLower(track->name())] = name; + std::vector>> following; + for (auto &standing : m_templates.standing_templates([](std::string const &) { return true; })) + { + auto changed{false}; + for (auto &value : standing.values) + if (auto const found{renamed.find(ToLower(value))}; found != renamed.end()) + { + value = found->second; + changed = true; + } + if (changed) + following.emplace_back(standing.instance, std::move(standing.values)); + } // the names go round among the switches: a switch whose name another one gets gives it up first for (auto const &entry : names) if (entry.first->name() != entry.second && std::any_of(names.begin(), names.end(), [&](auto const &Other) { return Other.first != entry.first && Other.second == entry.first->name(); })) @@ -698,8 +716,17 @@ void srk_tool::number_switches() } if (before.empty()) before.emplace_back(units.front().parts.front(), editor_track::capture(*units.front().parts.front())); - m_history.push_track_snapshot(std::move(before)); + auto const followed{following.size()}; + if (following.empty()) + m_history.push_track_snapshot(std::move(before)); + else + { + std::string error; + m_signals.signal_batch({{}, {}, {}, std::move(following)}, std::move(before), {}, error); + } m_status = format(STR_P("%zu switch numbered", "%zu switches numbered", units.size()), units.size()); + if (followed > 0) + m_status += ", " + format(STR_P("%zu object of the scenery follows them", "%zu objects of the scenery follow them", followed), followed); } void srk_tool::set_circuits() diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 229924d2..b1e39a85 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10389,8 +10389,8 @@ msgstr "Od wschodniego końca stacji" msgid "Number the switches" msgstr "Ponumeruj rozjazdy" -msgid "Numbers the switches 1, 2, 3... along the station and names them after its code, as on the lines of PKP PLK: bpd_1, the parts of a slip bpd_15a to bpd_15d. The drives follow their switches, events written by hand keep the old names. One step of undo" -msgstr "Numeruje rozjazdy 1, 2, 3... wzdłuż stacji i nazywa je od jej skrótu, jak na liniach PKP PLK: bpd_1, części rozjazdu krzyżowego bpd_15a do bpd_15d. Napędy idą za swoimi rozjazdami, eventy pisane ręcznie zostają przy starych nazwach. Jeden krok cofania" +msgid "Numbers the switches 1, 2, 3... along the station and names them after its code, as on the lines of PKP PLK: bpd_1, the parts of a slip bpd_15a to bpd_15d. The drives and the objects naming a switch follow it, events written by hand keep the old names. One step of undo" +msgstr "Numeruje rozjazdy 1, 2, 3... wzdłuż stacji i nazywa je od jej skrótu, jak na liniach PKP PLK: bpd_1, części rozjazdu krzyżowego bpd_15a do bpd_15d. Napędy i obiekty z nazwą rozjazdu, jak latarnie zwrotnicowe, idą za nim, eventy pisane ręcznie zostają przy starych nazwach. Jeden krok cofania" msgid "Give the station its code first: the switches are named after it" msgstr "Najpierw wpisz skrót stacji: od niego biorą się nazwy rozjazdów" @@ -10536,3 +10536,9 @@ msgstr[0] "%zu odstęp" msgstr[1] "%zu odstępy" msgstr[2] "%zu odstępów" +msgid "%zu object of the scenery follows them" +msgid_plural "%zu objects of the scenery follow them" +msgstr[0] "%zu obiekt scenerii idzie za nimi" +msgstr[1] "%zu obiekty scenerii idą za nimi" +msgstr[2] "%zu obiektów scenerii idzie za nimi" + From f287ecf8405461a0a60b8a02f84c7c00c23bf3cc Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 12:34:53 +0200 Subject: [PATCH 051/145] fix(editor): the signalling mode places the joints and the signals as Ie-4 gives them, a branch joint 4.2 m beyond the fouling point, the joint a main signal drops at 15 m beyond it, the block sections no shorter than the braking distance of the line and the warning discs that far out The block signals are numbered after the nearest hectometre of their parity ahead of them, and two signals no longer take one spot between the tracks. --- application/editormodesrk.cpp | 130 ++++++++++++++++------- editor/editorStation.cpp | 194 ++++++++++++++++++++++++++-------- editor/editorStation.hpp | 13 ++- lang/editor_pl.po | 12 +-- 4 files changed, 259 insertions(+), 90 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 52b05daa..a1f5651a 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -48,11 +48,16 @@ double constexpr kLabelRange{700.0}; // m, of the names shown there, farther one double constexpr kRailHead{0.18}; // m, above the axis of a path double constexpr kLineLimit{30000.0}; // m, of a line followed out to the next station -double constexpr kBlock{1400.0}; // m, of a block section of the automatic block +double constexpr kBlockShare{1.1}; // of the braking distance, the block sections towards the end of the tracks double constexpr kPredicted{305.0}; // m, out from the next station to its entry signal, while it has none -double constexpr kWarning{1000.0}; // m, from a warning disc to its entry signal, the braking distance -double constexpr kWarningFast{1300.0}; // m, the same on lines faster than 120 km/h -char const *const kControl{"sbl_odstep.inc"}; // the events of a block section, switching the block signal guarding it +char const *const kControl{"sbl_odstep.inc"}; + +// m, of a train at the speed, as Ie-4 § 8 takes it: the least block section of the automatic block, a warning disc that far before +// its signal +double braking_distance(double const Speed) +{ + return Speed <= 60.0 ? 400.0 : Speed <= 100.0 ? 700.0 : Speed <= 140.0 ? 1000.0 : 1300.0; +} // the events of a block section, switching the block signal guarding it ImU32 constexpr kPlain{IM_COL32(170, 176, 188, 255)}; ImU32 constexpr kSwitch{IM_COL32(240, 200, 90, 255)}; @@ -546,27 +551,29 @@ void srk_tool::render_options() step_heading(2, STR_C("Track circuits")); if (main_button(STR_C("Set the track circuits"), ImVec2(-1.0f, tall_button_height()))) set_circuits(); - item_tooltip("Cuts the tracks of the station at the joints of the track circuits and names the circuits, as on the lines of PKP PLK: " - "a joint 1.5 m before the start of a switch, beyond the fouling point of each branch where the axes are 4 m apart, in the middle " - "of a link too short for both. Each switch gets bpd_iz, each station track bpd_it: 1 and 2 the main tracks, " - "track 1 on the right along the km, the odd ones beyond it, the even ones on the other side. Run again after changes, it keeps " - "the numbers the tracks have. One step of undo"); + item_tooltip("Cuts the tracks of the station at the joints of the track circuits and names the circuits, as on the lines of PKP PLK: a " + "joint 1.5 m before the start of a switch, 4.2 m beyond the fouling point of each branch, where the axes are 3.5 m apart, as " + "Ie-4 gives it, in the middle of a link too short for both. Each switch gets bpd_iz, each station track " + "bpd_it: 1 and 2 the main tracks, track 1 on the right along the km, the odd ones beyond it, the even ones on the " + "other side. Run again after changes, it keeps the numbers the tracks have. One step of undo"); step_heading(3, STR_C("Signals")); if (ImGui::Button(STR_C("Set the signals"), ImVec2(-1.0f, 0.0f))) set_signals(); item_tooltip("Places the signals of the station, as on the lines of PKP PLK, and names them after its code: an entry signal 300 m out on " - "each line, A, B... from the start of the km, Z, Y... from the end, the circuit from it to the station bpd_ita; an exit " - "signal at the end of each station track a train is sent out of, F1, G2 towards the start, R1, P2 towards the end; a " - "shunting signal Tm1, Tm2... at the end of a shorter track. A signal standing by the place of one gets its name, none is " - "doubled. The kinds of signals chosen in the Signals tool. One step of undo"); + "each line, A, B... from the start of the km, Z, Y... from the end, the circuit from 15 m beyond it to the station bpd_ita; " + "an exit signal 15 m before the joint at the end of each station track a train is sent out of, F1, G2 towards the start, R1, " + "P2 towards the end; a shunting signal Tm1, Tm2... at the end of a shorter track. A signal standing by the place of one gets " + "its name, none is doubled. The kinds of signals chosen in the Signals tool. One step of undo"); step_heading(4, STR_C("Lines")); if (ImGui::Button(STR_C("Set the line signals"), ImVec2(-1.0f, 0.0f))) set_lines(); - item_tooltip("Places the signals of the lines out of the station, from its entry signals on. A double-track line gets the automatic " - "block: block signals evenly to the entry signals of the next station, about 1.4 km apart, numbered after the km, 241 on " - "track 1, 242 on track 2, and the block sections between them named as on the lines of PKP PLK, 2411-2571@LK2.1. A " - "single-track line gets a warning disc ToA 1000 m before the entry signal, 1300 m on a line faster than 120 km/h. Needs " - "the line and the km of the station. Run it at the next station too, it keeps what stands. One step of undo"); + item_tooltip("Places the signals of the lines out of the station, from its entry signals on. A double-track line gets the automatic block: " + "block signals evenly to the entry signals of the next station, a section no shorter than the braking distance, numbered " + "after the hectometre ahead of them, odd on track 1, even on track 2, and the block sections from 15 m beyond them named as " + "on the lines of PKP PLK, 2411-2571@LK2.1. A single-track line gets a warning disc ToA the braking distance before the entry " + "signal. The braking distance after the speed of the line, as Ie-4 gives it: 400 m up to 60 km/h, 700 m up to 100, 1000 m up " + "to 140, 1300 m above. Needs the line and the km of the station. Run it at the next station too, it keeps what stands. One " + "step of undo"); if (auto const *selected{m_selection.selected_track()}; selected != nullptr && false == selected->Isolated.empty()) if (auto const number{track_of(selected->Isolated.front()->asName, stem_of_open())}; false == number.empty() && begin_fields("##srktrack")) { @@ -1007,6 +1014,7 @@ void srk_tool::set_signals() glm::dvec3 point{0.0}; bool outward{false}; std::vector named; // between the station and the cut + std::vector beyond; // out from the cut to the signal and on }; std::vector cuts; for (std::size_t i = 0; i < plan.size(); ++i) @@ -1014,7 +1022,11 @@ void srk_tool::set_signals() auto const &planned{plan[i]}; if (planned.kind != station::signal::role::entry || planned.run.empty()) continue; - auto const at{match[i] >= 0 ? standing[match[i]].position : planned.point}; + // the joint the trains pass the signal to, in from it towards the station + auto const run{station::run_line(planned)}; + double gap{0.0}; + auto const signal_at{station::distance_on(run, match[i] >= 0 ? standing[match[i]].position : planned.point, gap)}; + auto const at{station::point_on(run, std::max(0.0, signal_at - station::signal_drop)).point}; cut entry; entry.signal = i; auto best{std::numeric_limits::max()}; @@ -1033,6 +1045,8 @@ void srk_tool::set_signals() } for (std::size_t k = 0; k < nearest; ++k) entry.named.push_back(planned.run[k].first); + for (std::size_t k = nearest + 1; k < planned.run.size(); ++k) + entry.beyond.push_back(planned.run[k].first); entry.track = planned.run[nearest].first; entry.outward = planned.run[nearest].second; auto const &ends{entry.track->m_paths.front().points}; @@ -1042,8 +1056,7 @@ void srk_tool::set_signals() if (std::min(to_start, to_end) <= 0.3) { // at an end already - if ((to_end < to_start) == entry.outward) - entry.named.push_back(entry.track); + ((to_end < to_start) == entry.outward ? entry.named : entry.beyond).push_back(entry.track); entry.track = nullptr; } else if (false == editor_track::can_edit_geometry(*entry.track, reason)) @@ -1074,8 +1087,9 @@ void srk_tool::set_signals() created.push_back(piece); captured.insert(piece); entry.named.push_back(entry.outward ? entry.track : piece); - planned.track = entry.outward ? piece : entry.track; - planned.point = entry.point; + entry.beyond.push_back(entry.outward ? piece : entry.track); + if (planned.track == entry.track) + planned.track = entry.outward ? piece : entry.track; } } auto const name{stem + "it" + ToLower(planned.name)}; @@ -1087,6 +1101,15 @@ void srk_tool::set_signals() before.emplace_back(part, editor_track::capture(*part)); editor_track::isolated(*part, {name}); } + // out of the circuit beyond its joint, where an earlier one ended at the signal + for (auto *part : entry.beyond) + { + if (part->Isolated.size() != 1 || ToLower(part->Isolated.front()->asName) != name) + continue; + if (captured.insert(part).second) + before.emplace_back(part, editor_track::capture(*part)); + editor_track::isolated(*part, {}); + } } std::vector placed; std::vector renamed; @@ -1165,12 +1188,21 @@ void srk_tool::set_lines() m_status = STR_C("Set the signals first: the lines begin at the entry signals"); return; } - auto const lines{station::lines_out(layout, entries, kLineLimit)}; + auto lines{station::lines_out(layout, entries, kLineLimit)}; if (lines.empty()) { m_status = STR_C("No line leads out of the station beyond its entry signals"); return; } + // the distances from the entry signals as they stand + for (auto &line : lines) + { + double gap{0.0}; + auto const at{station::distance_on(line, standing[entry_standing[line.entry]].position, gap)}; + if (gap < 4.5) + for (auto &distance : line.distances) + distance -= at; + } auto const &described{m_records[m_record]}; double known{0.0}; if (std::any_of(lines.begin(), lines.end(), [](station::line const &Line) { return Line.number > 0; }) && (false == km_of_open(0.0, known) || described.line.empty())) @@ -1201,11 +1233,22 @@ void srk_tool::set_lines() return glm::dot(travel_of(Signal.yaw), glm::dvec2{way.x, way.z}); }; auto const km_on = [&](std::size_t const Index, double const Distance) { - auto const chainage{station::project(layout, lines[Index].points.front()).x}; + auto const chainage{station::project(layout, station::point_on(lines[Index], 0.0).point).x}; double km{0.0}; km_of_open(chainage < middle ? chainage - Distance : chainage + Distance, km); return km; }; + // the trains of the line run towards the higher km + auto const rising = [&](std::size_t const Index) { return (km_on(Index, 1000.0) > km_on(Index, 0.0)) != lines[Index].inbound; }; + // the braking distance at the highest speed out along the line, at 160 km/h while its tracks have none + auto const braking = [&](std::size_t const Index) { + auto speed{0.0}; + auto const &line{lines[Index]}; + for (std::size_t k = 0; k < line.points.size(); ++k) + if (line.distances[k] >= 0.0) + speed = std::max(speed, line.tracks[line.owners[k]]->VelocityGet()); + return braking_distance(speed > 0.0 ? speed : 160.0); + }; for (std::size_t index = 0; index < lines.size(); ++index) { auto const &line{lines[index]}; @@ -1233,26 +1276,28 @@ void srk_tool::set_lines() auto const &line{lines[index]}; auto const &end{ends[index]}; auto const &entry{entries[line.entry]}; + auto const least{braking(index)}; if (line.number > 0) { - // the block signals: the line to the next station parted evenly, towards the end of the tracks every 1.4 km + // the block signals, Ie-4 § 51: the sections no shorter than the braking distance and no longer than twice it, the line to the + // next station parted evenly, towards the end of the tracks a little over the braking distance each std::vector places; if (line.station || end.signal >= 0) { // as many on both tracks of the line auto const reach{line.partner >= 0 ? (end.distance + ends[line.partner].distance) * 0.5 : end.distance}; - auto const sections{std::max(1L, std::lround(reach / kBlock))}; - for (long k = 1; k < sections; ++k) - places.push_back(end.distance * static_cast(k) / static_cast(sections)); + auto const sections{std::max(1.0, std::floor(reach / (line.partner >= 0 ? std::max(least, braking(line.partner)) : least)))}; + for (auto k{1.0}; k < sections; k += 1.0) + places.push_back(end.distance * k / sections); } else - for (auto distance{kBlock}; distance <= end.distance - 100.0; distance += kBlock) + for (auto distance{least * kBlockShare}; distance <= end.distance - 100.0; distance += least * kBlockShare) places.push_back(distance); for (auto const distance : places) { planned_signal signal; signal.kind = station::signal::role::block; - signal.name = station::block_number(km_on(index, distance), line.number); + signal.name = station::block_number(km_on(index, distance), line.number, rising(index)); signal.plate = signal.name; signal.line = index; signal.distance = distance; @@ -1262,8 +1307,7 @@ void srk_tool::set_lines() else { // the warning disc of the entry signal, the braking distance before it - auto const fast{entry.track != nullptr && entry.track->VelocityGet() > 120.0}; - auto const distance{std::min(fast ? kWarningFast : kWarning, end.distance - 100.0)}; + auto const distance{std::min(least, end.distance - 100.0)}; if (distance >= 400.0) { planned_signal signal; @@ -1366,6 +1410,12 @@ void srk_tool::set_lines() std::sort(order.begin(), order.end(), [](stop const &A, stop const &B) { return A.distance < B.distance; }); if (line.inbound) std::reverse(order.begin(), order.end()); + // the joints the trains pass the signals to: on beyond a block signal, in towards the station from an entry one + auto const joint_of = [&](stop const &Stop) { + if (Stop.signal >= 0) + return Stop.distance + (line.inbound ? -station::signal_drop : station::signal_drop); + return Stop.distance + (Stop.distance > 0.0 ? station::signal_drop : -station::signal_drop); + }; for (std::size_t i = 0; i < order.size(); ++i) { auto const &here{order[i]}; @@ -1385,8 +1435,8 @@ void srk_tool::set_lines() section found; found.name = ToLower(section_tag(low.name, line.number) + "-" + section_tag(high.name, line.number) + "@lk" + described.line + "." + std::to_string(line.number)); found.line = index; - found.from = std::min(before.distance, here.distance); - found.to = std::max(before.distance, here.distance); + found.from = std::min(joint_of(before), joint_of(here)); + found.to = std::max(joint_of(before), joint_of(here)); found.guard = before.signal >= 0 ? before.name : std::string{}; found.start = station::point_on(line, before.distance).point; sections.push_back(std::move(found)); @@ -2047,7 +2097,13 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri refresh(); std::string list; for (auto const &mark : m_signal_marks) - list += format(" %s@%.0f/%.1f%s", mark.text.c_str(), mark.at.x, mark.at.y, mark.ahead ? ">" : "<"); + { + // and the joint nearest to it, in the plan + auto nearest{std::numeric_limits::max()}; + for (auto const &joint : m_joints) + nearest = std::min(nearest, glm::distance(glm::dvec2{joint.at.x, joint.at.z}, glm::dvec2{mark.head.x, mark.head.z})); + list += format(" %s@%.0f/%.1f%s~%.1f", mark.text.c_str(), mark.at.x, mark.at.y, mark.ahead ? ">" : "<", nearest); + } WriteLog(format("SELFTEST srksignals %zu:", m_signal_marks.size()) + list); return editor_selftest_result::handled; } @@ -2080,7 +2136,7 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri along.emplace_back(distance, format("%s(%s,%s)", signal.name.c_str(), signal.kind.c_str(), signal.linked.c_str())); } std::string last; - for (std::size_t i = 1; i < line.points.size(); i += 20) + for (std::size_t i = 1; i < line.points.size(); ++i) { auto const *track{line.tracks[line.owners[i]]}; auto const name{track->Isolated.empty() ? std::string{"-"} : track->Isolated.front()->asName}; diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index ce6b598b..f609af15 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -232,16 +232,19 @@ glm::dvec2 map_of(glm::dvec3 const &Point) } double constexpr kStart{1.5}; // m, a joint before the start of a switch -double constexpr kClear{4.0}; // m, between the axes at a joint beyond a fouling point +double constexpr kFouling{3.5}; // m, between the axes at the fouling point of two tracks +double constexpr kBeyond{4.2}; // m, from a fouling point on to the joint beyond it, Ie-4 § 33 +double constexpr kBranches{10.0}; // m, the ends of a switch closer together are the ends of its paths at the frog double constexpr kReach{120.0}; // m, along a branch looked for its fouling point double constexpr kSection{30.0}; // m, the shortest stretch between two joints made a track circuit of its own double constexpr kSnap{0.3}; // m, a joint this close to the end of a track lies at the end double constexpr kThrough{400.0}; // m, of the switches a station track is followed through for its circuits beyond double constexpr kEntry{300.0}; // m, from the joint of the station out to its entry signal -double constexpr kJoint{3.5}; // m, from a signal to the joint of the circuit it guards +double constexpr kJoint{3.5}; // m, from a shunting signal to the joint of the circuit it guards double constexpr kStopping{200.0}; // m, the shortest circuit of a station track a train is sent out of by an exit signal double constexpr kSignalSide{2.25}; // m, from the axis to a signal double constexpr kSignalClear{2.5}; // m, the least from a signal to the axis of another track +double constexpr kSignalApart{3.0}; // m, the least between two signals // the points of the way out of an end of the track, as a train runs: along the main path of a switch run into from its start, // back along the path of one run into from the end of it @@ -326,12 +329,12 @@ double plan_gap(glm::dvec3 const &Point, std::vector const &Line) return best; } -// how far from the end of the switch its joint lies: beyond the fouling point at a branch, before the start elsewhere +// how far from the end of the switch its joint lies: 4.2 m beyond the fouling point at a branch, before the start elsewhere double clearance(TTrack const &Switch, int const Path, bool const Atend) { auto const point{end_of(Switch, Path, Atend)}; std::optional> other; - auto nearest{kClear}; + auto nearest{kBranches}; for (auto const &[path, atend] : ends_of(Switch)) if (auto const distance{plan_distance(end_of(Switch, path, atend), point)}; distance > same_point && distance < nearest) { @@ -340,16 +343,29 @@ double clearance(TTrack const &Switch, int const Path, bool const Atend) } if (false == other.has_value()) return kStart; - auto const mine{ahead(Switch, Path, Atend, kReach)}; auto const theirs{ahead(Switch, other->first, other->second, kReach + 50.0)}; + if (plan_gap(point, theirs) >= kFouling) + { + // the fouling point within the switch: back along its paths to it + double length{0.0}; + auto mine{dense_points(Switch, Path, length)}; + auto const own{dense_points(Switch, other->first, length)}; + if (Atend) + std::reverse(mine.begin(), mine.end()); + double back{0.0}; + for (std::size_t i = 1; i < mine.size() && plan_gap(mine[i], own) >= kFouling; ++i) + back += plan_distance(mine[i - 1], mine[i]); + return std::max(0.0, kBeyond - back); + } + auto const mine{ahead(Switch, Path, Atend, kReach)}; double along{0.0}; auto gap{plan_gap(mine.front(), theirs)}; for (std::size_t i = 1; i < mine.size(); ++i) { auto const step{plan_distance(mine[i - 1], mine[i])}; auto const next{plan_gap(mine[i], theirs)}; - if (next >= kClear) - return along + (next > gap ? step * std::clamp((kClear - gap) / (next - gap), 0.0, 1.0) : step); + if (next >= kFouling) + return along + (next > gap ? step * std::clamp((kFouling - gap) / (next - gap), 0.0, 1.0) : step) + kBeyond; along += step; gap = next; } @@ -1007,12 +1023,46 @@ std::pair in_from(TTrack const &Track, bool const Atend, return {points.front(), glm::dvec3{0.0, 0.0, 1.0}}; } -// how far a signal on the side of the track at the point keeps from the other tracks; Way: of the path there -double side_gap(layout const &Layout, glm::dvec3 const &Point, glm::dvec3 const &Way, int const Side) +// the point the distance in from the end of the track, on into the tracks before it while they're shorter, short of a switch: the +// track, the end of it the trains running out reach, the point and the way to that end +std::tuple back_from(TTrack *Track, bool Atend, double Distance) +{ + std::unordered_set seen{Track}; + while (true) + { + auto const length{plan_length(*Track, 0)}; + if (Distance < length - 1.0) + break; + auto const inner{end_of(*Track, 0, false == Atend)}; + auto *before{neighbour(*Track, 0, false == Atend)}; + int path{0}; + bool atend{false}; + if (before == nullptr || node(*before) || false == rail(*before) || false == seen.insert(before).second || false == end_at(*before, inner, path, atend)) + { + Distance = std::min(Distance, length * 0.5); + break; + } + Distance -= length; + Track = before; + Atend = atend; + } + auto const [at, way] = in_from(*Track, Atend, Distance); + return {Track, Atend, at, way}; +} + +// where a signal on the side of the track at the point stands, in the plan; Way: of the path there +glm::dvec2 side_spot(glm::dvec3 const &Point, glm::dvec3 const &Way, int const Side) { glm::dvec2 const way{glm::normalize(glm::dvec2{Way.x, Way.z} + glm::dvec2{1e-12, 0.0})}; - glm::dvec2 const right{-way.y, way.x}; - auto const spot{glm::dvec2{Point.x, Point.z} + right * (Side * kSignalSide)}; + return glm::dvec2{Point.x, Point.z} + glm::dvec2{-way.y, way.x} * (Side * kSignalSide); +} + +// how far a signal on the side of the track at the point keeps from the other tracks, none where another signal stands already +double side_gap(layout const &Layout, glm::dvec3 const &Point, glm::dvec3 const &Way, int const Side, std::vector const &Taken) +{ + auto const spot{side_spot(Point, Way, Side)}; + if (std::any_of(Taken.begin(), Taken.end(), [&](glm::dvec2 const &Other) { return glm::distance(Other, spot) < kSignalApart; })) + return 0.0; glm::dvec3 const at{spot.x, Point.y, spot.y}; auto gap{std::numeric_limits::max()}; for (auto const &path : Layout.paths) @@ -1021,14 +1071,20 @@ double side_gap(layout const &Layout, glm::dvec3 const &Point, glm::dvec3 const return gap; } -// on the right of the trains, unless another track runs too close by there and less so on the left -int side_for(layout const &Layout, glm::dvec3 const &Point, glm::dvec3 const &Way, int const Facing) +// on the right of the trains, unless another track runs too close by there or another signal stands there, and less so on the left; +// the spot taken +int side_for(layout const &Layout, glm::dvec3 const &Point, glm::dvec3 const &Way, int const Facing, std::vector &Taken) { - auto const right{side_gap(Layout, Point, Way, Facing)}; - if (right >= kSignalClear) - return Facing; - auto const left{side_gap(Layout, Point, Way, -Facing)}; - return left >= kSignalClear || left > right ? -Facing : Facing; + auto const right{side_gap(Layout, Point, Way, Facing, Taken)}; + auto side{Facing}; + if (right < kSignalClear) + { + auto const left{side_gap(Layout, Point, Way, -Facing, Taken)}; + if (left >= kSignalClear || left > right) + side = -Facing; + } + Taken.push_back(side_spot(Point, Way, side)); + return side; } // the ends of the tracks out of the set, Free where no track goes on @@ -1073,6 +1129,7 @@ std::vector signals(layout const &Layout, std::vector const &T { std::vector result; auto const middle{(Layout.from + Layout.to) * 0.5}; + std::vector taken; // the spots of the signals placed // the entry signals: out along each line from where it leaves the circuits of the station struct entry { @@ -1126,7 +1183,7 @@ std::vector signals(layout const &Layout, std::vector const &T found.placed.track = next; found.placed.point = at; found.placed.facing = outward ? -1 : 1; - found.placed.side = side_for(Layout, at, outward ? -way : way, found.placed.facing); + found.placed.side = side_for(Layout, at, outward ? -way : way, found.placed.facing, taken); found.placed.travel = way; break; } @@ -1143,7 +1200,7 @@ std::vector signals(layout const &Layout, std::vector const &T found.placed.track = last; found.placed.point = at; found.placed.facing = outward ? -1 : 1; - found.placed.side = side_for(Layout, at, outward ? way : -way, found.placed.facing); + found.placed.side = side_for(Layout, at, outward ? way : -way, found.placed.facing, taken); found.placed.travel = -way; } if (found.placed.track != nullptr) @@ -1226,13 +1283,14 @@ std::vector signals(layout const &Layout, std::vector const &T if (end.track == nullptr || end.free) continue; signal placed; - auto const [at, way] = in_from(*end.track, end.atend, std::min(kJoint, plan_length(*end.track, 0) * 0.5)); - placed.track = end.track; + auto const exit{stopping && (start ? starts : ends)}; + auto const [track, atend, at, way] = back_from(end.track, end.atend, exit ? signal_drop : kJoint); + placed.track = track; placed.point = at; - placed.facing = end.atend ? 1 : -1; - placed.side = side_for(Layout, at, end.atend ? way : -way, placed.facing); + placed.facing = atend ? 1 : -1; + placed.side = side_for(Layout, at, atend ? way : -way, placed.facing, taken); placed.travel = way; - if (stopping && (start ? starts : ends)) + if (exit) { placed.kind = signal::role::exit; placed.name = std::string(1, start ? (odd ? odd_start : even_start) : (odd ? odd_end : even_end)) + upper; @@ -1264,36 +1322,61 @@ std::vector lines_out(layout const &Layout, std::vector const &Sig continue; line found; found.entry = index; + std::unordered_set seen; + auto const add = [&](TTrack *Track, bool const Outward, std::vector Points) { + if (false == found.points.empty()) + Points.erase(Points.begin()); + seen.insert(Track); + found.tracks.push_back(Track); + found.outward.push_back(Outward); + for (auto const &point : Points) + { + found.distances.push_back(found.points.empty() ? 0.0 : found.distances.back() + plan_distance(found.points.back(), point)); + found.points.push_back(point); + found.owners.push_back(found.tracks.size() - 1); + } + }; + // in from the signal to the joint of the station first: its trains run on there beyond it + for (auto const &[part, out] : entry.run) + { + if (part == entry.track) + break; + double length{0.0}; + auto points{dense_points(*part, 0, length)}; + if (false == out) + std::reverse(points.begin(), points.end()); + add(part, out, std::move(points)); + } // out where the trains it faces come from auto *track{entry.track}; auto outward{entry.facing < 0}; auto first{true}; - std::unordered_set seen; + auto origin{0.0}; while (true) { double length{0.0}; auto points{dense_points(*track, 0, length)}; if (false == outward) std::reverse(points.begin(), points.end()); + std::size_t at{0}; if (first) { auto const nearest{std::min_element(points.begin(), points.end(), [&](glm::dvec3 const &A, glm::dvec3 const &B) { return plan_distance(A, entry.point) < plan_distance(B, entry.point); })}; - points.erase(points.begin(), nearest); - points.front() = entry.point; - } - else - points.erase(points.begin()); - seen.insert(track); - found.tracks.push_back(track); - found.outward.push_back(outward); - for (auto const &point : points) - { - found.distances.push_back(found.points.empty() ? 0.0 : found.distances.back() + plan_distance(found.points.back(), point)); - found.points.push_back(point); - found.owners.push_back(found.tracks.size() - 1); + *nearest = entry.point; + at = static_cast(nearest - points.begin()); + if (found.points.empty()) + { + points.erase(points.begin(), nearest); + at = 0; + } + else + at += found.points.size() - 1; } + add(track, outward, std::move(points)); + if (first) + origin = found.distances[at]; first = false; - if (found.length() > Limit) + if (found.length() - origin > Limit) break; auto const end{end_of(*track, 0, outward)}; auto *next{neighbour(*track, 0, outward)}; @@ -1311,6 +1394,8 @@ std::vector lines_out(layout const &Layout, std::vector const &Sig track = next; outward = false == atend; } + for (auto &distance : found.distances) + distance -= origin; if (found.length() > 1.0) result.push_back(std::move(found)); } @@ -1359,6 +1444,29 @@ std::vector lines_out(layout const &Layout, std::vector const &Sig return result; } +line run_line(signal const &Entry) +{ + line result; + for (auto const &[track, outward] : Entry.run) + { + double length{0.0}; + auto points{dense_points(*track, 0, length)}; + if (false == outward) + std::reverse(points.begin(), points.end()); + if (false == result.points.empty()) + points.erase(points.begin()); + result.tracks.push_back(track); + result.outward.push_back(outward); + for (auto const &point : points) + { + result.distances.push_back(result.points.empty() ? 0.0 : result.distances.back() + plan_distance(result.points.back(), point)); + result.points.push_back(point); + result.owners.push_back(result.tracks.size() - 1); + } + } + return result; +} + on_line point_on(line const &Line, double const Distance) { on_line result; @@ -1407,12 +1515,12 @@ double distance_on(line const &Line, glm::dvec3 const &Point, double &Gap) return result; } -std::string block_number(double const Km, int const Track) +std::string block_number(double const Km, int const Track, bool const Rising) { auto const hectometres{std::max(Km, 0.0) * 10.0}; - auto number{static_cast(std::llround(hectometres))}; + auto number{static_cast(Rising ? std::ceil(hectometres - 1e-6) : std::floor(hectometres + 1e-6))}; if ((number % 2 == 1) != (Track % 2 == 1)) - number += hectometres > static_cast(number) ? 1 : -1; + number += Rising ? 1 : -1; if (number < 1) number = Track % 2 == 1 ? 1 : 2; return std::to_string(number); diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index c61690df..76bb2047 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -116,6 +116,9 @@ struct numbered }; // a signal of the station, as PKP PLK places and names them +// m, from a main signal on to the joint its trains turn it to stop at: Ie-4 § 9, at least 15 m, at most 30 for an entry or a block signal +double constexpr signal_drop{15.0}; + struct signal { enum class role @@ -158,7 +161,7 @@ struct line std::vector tracks; // from the entry signal out std::vector outward; // of each track: the line runs out towards the end of its path std::vector points; // along the tracks from the entry signal on, a metre or so apart - std::vector distances; // of the points, from the entry signal + std::vector distances; // of the points, from the entry signal: below 0 in from it to the joint of the station std::vector owners; // of the points, the track each lies on double length() const { return distances.empty() ? 0.0 : distances.back(); } }; @@ -174,11 +177,13 @@ struct on_line // the lines beyond the entry signals among the signals given, no longer than the limit; two lines out of the same end side by side // are the tracks of a double-track line std::vector lines_out(layout const &Layout, std::vector const &Signals, double const Limit); +// the run of an entry signal as a line, from the joint of the station out, the distances from the joint +line run_line(signal const &Entry); on_line point_on(line const &Line, double const Distance); // how far out along the line the point lies, Gap: how far from its axis in the plan double distance_on(line const &Line, glm::dvec3 const &Point, double &Gap); -// the number of a block signal at the km, as PKP PLK gives them: the km and the hectometre, the last digit odd on track 1, even on -// track 2: 241, 242 -std::string block_number(double const Km, int const Track); +// the number of a block signal at the km, as PKP PLK gives them (Ie-4 § 10): the km and the hectometre, the nearest one ahead of the +// signal odd on track 1, even on track 2: 241, 242; Rising: its trains run towards the higher km +std::string block_number(double const Km, int const Track, bool const Rising); } // namespace station diff --git a/lang/editor_pl.po b/lang/editor_pl.po index b1e39a85..b0193fbd 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10413,8 +10413,8 @@ msgstr[2] "Ponumerowano %zu rozjazdów" msgid "Set the track circuits" msgstr "Ustaw izolacje" -msgid "Cuts the tracks of the station at the joints of the track circuits and names the circuits, as on the lines of PKP PLK: a joint 1.5 m before the start of a switch, beyond the fouling point of each branch where the axes are 4 m apart, in the middle of a link too short for both. Each switch gets bpd_iz, each station track bpd_it: 1 and 2 the main tracks, track 1 on the right along the km, the odd ones beyond it, the even ones on the other side. Run again after changes, it keeps the numbers the tracks have. One step of undo" -msgstr "Tnie tory stacji na stykach izolowanych i nazywa odcinki jak na liniach PKP PLK: styk 1,5 m przed początkiem rozjazdu, za ukresem każdej gałęzi tam, gdzie osie są 4 m od siebie, w połowie łącznika za krótkiego na oba. Każdy rozjazd dostaje bpd_iz, każdy tor stacyjny bpd_it: 1 i 2 to tory główne, tor 1 po prawej w stronę rosnącego km, nieparzyste za nim, parzyste po drugiej stronie. Kliknięte ponownie po zmianach zostawia numery, które tory już mają. Jeden krok cofania" +msgid "Cuts the tracks of the station at the joints of the track circuits and names the circuits, as on the lines of PKP PLK: a joint 1.5 m before the start of a switch, 4.2 m beyond the fouling point of each branch, where the axes are 3.5 m apart, as Ie-4 gives it, in the middle of a link too short for both. Each switch gets bpd_iz, each station track bpd_it: 1 and 2 the main tracks, track 1 on the right along the km, the odd ones beyond it, the even ones on the other side. Run again after changes, it keeps the numbers the tracks have. One step of undo" +msgstr "Tnie tory stacji na stykach izolowanych i nazywa odcinki jak na liniach PKP PLK: styk 1,5 m przed początkiem rozjazdu, 4,2 m za ukresem każdej gałęzi, tam gdzie osie są 3,5 m od siebie, jak w Ie-4, w połowie łącznika za krótkiego na oba. Każdy rozjazd dostaje bpd_iz, każdy tor stacyjny bpd_it: 1 i 2 to tory główne, tor 1 po prawej w stronę rosnącego km, nieparzyste za nim, parzyste po drugiej stronie. Kliknięte ponownie po zmianach zostawia numery, które tory już mają. Jeden krok cofania" msgid "Give the station its code first: the track circuits are named after it" msgstr "Najpierw wpisz skrót stacji: od niego biorą się nazwy odcinków izolowanych" @@ -10458,8 +10458,8 @@ msgstr "Tor %s ma teraz numer %s" msgid "Set the signals" msgstr "Ustaw semafory" -msgid "Places the signals of the station, as on the lines of PKP PLK, and names them after its code: an entry signal 300 m out on each line, A, B... from the start of the km, Z, Y... from the end, the circuit from it to the station bpd_ita; an exit signal at the end of each station track a train is sent out of, F1, G2 towards the start, R1, P2 towards the end; a shunting signal Tm1, Tm2... at the end of a shorter track. A signal standing by the place of one gets its name, none is doubled. The kinds of signals chosen in the Signals tool. One step of undo" -msgstr "Stawia semafory stacji jak na liniach PKP PLK i nazywa je od jej skrótu: wjazdowy 300 m przed stacją na każdej linii, A, B... od początku km, Z, Y... od końca, a odcinek od niego do stacji to bpd_ita; wyjazdowy na końcu każdego toru, z którego wyprawia się pociągi, F1, G2 w stronę początku, R1, P2 w stronę końca; tarcza manewrowa Tm1, Tm2... na końcu krótszego toru. Semafor, który już stoi w miejscu któregoś z nich, dostaje tylko jego nazwę, nic się nie dubluje. Modele semaforów te wybrane w narzędziu Sygnalizatory. Jeden krok cofania" +msgid "Places the signals of the station, as on the lines of PKP PLK, and names them after its code: an entry signal 300 m out on each line, A, B... from the start of the km, Z, Y... from the end, the circuit from 15 m beyond it to the station bpd_ita; an exit signal 15 m before the joint at the end of each station track a train is sent out of, F1, G2 towards the start, R1, P2 towards the end; a shunting signal Tm1, Tm2... at the end of a shorter track. A signal standing by the place of one gets its name, none is doubled. The kinds of signals chosen in the Signals tool. One step of undo" +msgstr "Stawia semafory stacji jak na liniach PKP PLK i nazywa je od jej skrótu: wjazdowy 300 m przed stacją na każdej linii, A, B... od początku km, Z, Y... od końca, a odcinek od 15 m za nim do stacji to bpd_ita; wyjazdowy 15 m przed stykiem na końcu każdego toru, z którego wyprawia się pociągi, F1, G2 w stronę początku, R1, P2 w stronę końca; tarcza manewrowa Tm1, Tm2... na końcu krótszego toru. Semafor, który już stoi w miejscu któregoś z nich, dostaje tylko jego nazwę, nic się nie dubluje. Modele semaforów te wybrane w narzędziu Sygnalizatory. Jeden krok cofania" msgid "Give the station its code first: the signals are named after it" msgstr "Najpierw wpisz skrót stacji: od niego biorą się nazwy semaforów" @@ -10494,8 +10494,8 @@ msgstr "Szlaki" msgid "Set the line signals" msgstr "Ustaw sygnalizatory szlakowe" -msgid "Places the signals of the lines out of the station, from its entry signals on. A double-track line gets the automatic block: block signals evenly to the entry signals of the next station, about 1.4 km apart, numbered after the km, 241 on track 1, 242 on track 2, and the block sections between them named as on the lines of PKP PLK, 2411-2571@LK2.1. A single-track line gets a warning disc ToA 1000 m before the entry signal, 1300 m on a line faster than 120 km/h. Needs the line and the km of the station. Run it at the next station too, it keeps what stands. One step of undo" -msgstr "Stawia sygnalizatory szlaków wychodzących ze stacji, od jej semaforów wjazdowych. Szlak dwutorowy dostaje samoczynną blokadę liniową: semafory SBL rozstawione równo do semaforów wjazdowych następnej stacji, co około 1,4 km, numerowane według kilometra, 241 na torze 1, 242 na torze 2, a odstępy między nimi nazwane jak na liniach PKP PLK, 2411-2571@LK2.1. Szlak jednotorowy dostaje tarczę ostrzegawczą ToA 1000 m przed semaforem wjazdowym, 1300 m na linii szybszej niż 120 km/h. Potrzebna linia i kilometr stacji. Uruchom to też na następnej stacji, zostawia to, co już stoi. Jeden krok cofania" +msgid "Places the signals of the lines out of the station, from its entry signals on. A double-track line gets the automatic block: block signals evenly to the entry signals of the next station, a section no shorter than the braking distance, numbered after the hectometre ahead of them, odd on track 1, even on track 2, and the block sections from 15 m beyond them named as on the lines of PKP PLK, 2411-2571@LK2.1. A single-track line gets a warning disc ToA the braking distance before the entry signal. The braking distance after the speed of the line, as Ie-4 gives it: 400 m up to 60 km/h, 700 m up to 100, 1000 m up to 140, 1300 m above. Needs the line and the km of the station. Run it at the next station too, it keeps what stands. One step of undo" +msgstr "Stawia sygnalizatory szlaków wychodzących ze stacji, od jej semaforów wjazdowych. Szlak dwutorowy dostaje samoczynną blokadę liniową: semafory SBL rozstawione równo do semaforów wjazdowych następnej stacji, odstęp nie krótszy od drogi hamowania, numerowane według najbliższego hektometra przed nimi, nieparzyste na torze 1, parzyste na torze 2, a odstępy od 15 m za nimi nazwane jak na liniach PKP PLK, 2411-2571@LK2.1. Szlak jednotorowy dostaje tarczę ostrzegawczą ToA w odległości drogi hamowania przed semaforem wjazdowym. Droga hamowania według prędkości szlaku, jak w Ie-4: 400 m do 60 km/h, 700 m do 100, 1000 m do 140, 1300 m powyżej. Potrzebna linia i kilometr stacji. Uruchom to też na następnej stacji, zostawia to, co już stoi. Jeden krok cofania" msgid "Km" msgstr "Kilometr" From 8fde4dc8437ba9c5ce0fa1808fab9f106edfbe26 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 13:27:29 +0200 Subject: [PATCH 052/145] fix(editor): the readout of a straight dragged beside another one writes the arrow as ->, the font of the interface has no arrows and showed a question mark --- application/editormodestraights.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/editormodestraights.cpp b/application/editormodestraights.cpp index 265a0e83..239ced1e 100644 --- a/application/editormodestraights.cpp +++ b/application/editormodestraights.cpp @@ -189,7 +189,7 @@ void editor_mode::draw_straights_overlay() const if (projection.project((foot + target) * 0.5, screen)) { auto const angle{glm::degrees(std::abs(std::asin(std::clamp(line.direction.x * other.direction.y - line.direction.y * other.direction.x, -1.0, 1.0))))}; - auto const text{highlighted ? (offer.snaps ? format(STR_C("parallel %.2f m"), offer.spacing) : format("%.2f → %.2f m %.1f°", std::abs(lateral), offer.spacing, offer.angle)) : + auto const text{highlighted ? (offer.snaps ? format(STR_C("parallel %.2f m"), offer.spacing) : format("%.2f -> %.2f m %.1f°", std::abs(lateral), offer.spacing, offer.angle)) : (angle > 0.05 ? format("%.2f m %.1f°", std::abs(lateral), angle) : format("%.2f m", std::abs(lateral)))}; auto const *label{text.c_str()}; auto const size{ImGui::CalcTextSize(label)}; From 5ecb6ebe00dc98e8c27be2dab4c4dbfcc7c55d57 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 13:27:37 +0200 Subject: [PATCH 053/145] feat(editor): the kilometrage tool sets the km of the railway lines, points where the km is known and jumps of the km, the rest along the axis of the line, between the tracks of a double-track one and along the longer track where they run apart, kept in the scenery as //$k lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The track mode Kilometrage (X, under More) puts a point of the km where a track is clicked: the line, the km and, at a jump, the km on from it. The axis of a line follows the main paths of the switches through the tracks of its points; on a double-track line it runs between the tracks, and where they part on formations of their own and come together again it runs along the longer one, the shorter one going by its own length with its last hectometre before they meet taking up the difference (PKP GK-1 6.1.12). The km between the points are stretched to the length of the scenery evenly; the panel lists the points with the stretch, the 3D view shows the axis, a tick at each hectometre, the points with their km and the km of the lines by the mouse. Read the km from the posts turns the hectometre posts along the line into the points of its km, a jump where two runs of numbers meet, and warns of a jump on a curve or at a station (Id-1 § 19). Each change is a step of undo. --- CMakeLists.txt | 2 + application/editormode.cpp | 21 +- application/editormode.h | 15 +- application/editormodekm.cpp | 904 +++++++++++++++++++++++++++++ application/editormodekm.h | 90 +++ application/editormodelineside.cpp | 2 + application/editormodetrack.cpp | 34 +- application/editormodetrackux.cpp | 14 + application/editormodeviewport.cpp | 5 +- application/editortool.h | 4 +- editor/editorIcons.hpp | 1 + editor/editorKilometrage.cpp | 779 +++++++++++++++++++++++++ editor/editorKilometrage.hpp | 111 ++++ editor/icons/editor_icons.png | Bin 47042 -> 47923 bytes editor/icons/editor_icons.py | 12 +- lang/editor_pl.po | 182 +++++- scene/scenelayers.cpp | 5 +- 17 files changed, 2166 insertions(+), 15 deletions(-) create mode 100644 application/editormodekm.cpp create mode 100644 application/editormodekm.h create mode 100644 editor/editorKilometrage.cpp create mode 100644 editor/editorKilometrage.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e3430f3..b920162f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,6 +191,7 @@ set(SOURCES "editor/editorGroundFollow.cpp" "editor/editorAlignmentStore.cpp" "editor/editorSwitchStore.cpp" +"editor/editorKilometrage.cpp" "editor/editorStation.cpp" "editor/editorSwitchDesign.cpp" "editor/editorConnections.cpp" @@ -232,6 +233,7 @@ set(SOURCES "application/editormodeinfra.cpp" "application/editormodespeed.cpp" "application/editormodesrk.cpp" +"application/editormodekm.cpp" "application/editormodejoints.cpp" "application/editormodeparallel.cpp" "application/editormodelineside.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 3de5f4a5..171d726e 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -510,6 +510,7 @@ void editor_mode::editormode_input::poll() editor_mode::editor_mode() { alignment_store::reset(); switch_store::reset(); + kilometrage::reset(); m_userinterface = std::make_shared(); // the area fill settings live in the node bank window, in the tab of the fill mode ui()->set_fill_options([this]() { render_area_fill(); }); @@ -1014,6 +1015,13 @@ void editor_mode::undo_one() return; } + if (snap.action == EditorSnapshot::Action::Kilometrage) + { + restore_kilometrage(snap); + g_redo.push_back(std::move(snap)); + return; + } + if (snap.action == EditorSnapshot::Action::Bend) { restore_bend(snap, true); @@ -1183,6 +1191,13 @@ void editor_mode::redo_one() return; } + if (snap.action == EditorSnapshot::Action::Kilometrage) + { + restore_kilometrage(snap); + m_history.push_back(std::move(snap)); + return; + } + if (snap.action == EditorSnapshot::Action::Bend) { restore_bend(snap, false); @@ -1506,6 +1521,8 @@ bool editor_mode::update() m_speed.update(); m_joints.update(); m_srk.update(); + kilometrage::follow(m_track_edits); + m_km.update(); draw_track_hints(); draw_empty_scenery_hint(); draw_profile_overlay(); @@ -1513,6 +1530,7 @@ bool editor_mode::update() m_speed.draw_overlay(); m_joints.draw_overlay(); m_srk.draw_overlay(); + m_km.draw_overlay(); } else { @@ -6863,7 +6881,8 @@ void editor_mode::render_change_history(){ s.action == EditorSnapshot::Action::TrackEdit ? "TRK" : s.action == EditorSnapshot::Action::RoadEdit ? "ROAD" : s.action == EditorSnapshot::Action::Array ? "ARR" : - s.action == EditorSnapshot::Action::Terrain ? "TER" : "OTH", + s.action == EditorSnapshot::Action::Terrain ? "TER" : + s.action == EditorSnapshot::Action::Kilometrage ? "KM" : "OTH", s.node_name.empty() ? STR_C("(no name)") : s.node_name.c_str(), s.position.x, s.position.y, s.position.z); diff --git a/application/editormode.h b/application/editormode.h index 4b58cad9..4c31165f 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormodeparallel.h" #include "application/editormodespeed.h" #include "application/editormodesrk.h" +#include "application/editormodekm.h" #include "application/editormodevehicle.h" #include "application/editortool.h" #include "scene/sceneeditor.h" @@ -34,6 +35,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorAlignment.hpp" #include "editor/editorAlignmentStore.hpp" #include "editor/editorSwitchStore.hpp" +#include "editor/editorKilometrage.hpp" #include "editor/editorSpeed.hpp" #include "editor/editorRoad.hpp" #include "editor/editorProfile.hpp" @@ -123,7 +125,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri struct stored_profile; struct EditorSnapshot { - enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend, Terrain }; + enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend, Terrain, Kilometrage }; // copy of a model an array made struct array_copy { @@ -161,6 +163,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::vector profile_library; std::optional> alignments; // the kept horizontal alignments changed too, these were the ones before std::optional> switch_records; // the kept switches changed too, these were the ones before + std::optional> kilometrage; // the marks of the kilometrage before the change (Action::Kilometrage) std::vector copies; // models an array added to the one it was made of bool joined{false}; // undone and redone together with the change before it std::shared_ptr terrain; // the heightmap terrain from before the change (Action::Terrain) @@ -185,6 +188,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri void terrain_history_step(); // puts the terrain back as the step recorded it, the step left with the state it replaced void restore_terrain(EditorSnapshot &Snapshot); + // swaps the marks of the kilometrage with the ones the snapshot keeps + void restore_kilometrage(EditorSnapshot &Snapshot); bool m_history_busy{false}; // undo or redo under way void handle_brush_mouse_hold(int Action, int Button); void apply_rotation_for_new_node(scene::basic_node *node, int rotation_mode, float fixed_rotation_value); @@ -790,6 +795,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void commit_track_drag(bool const Force); void cancel_track_tools(); void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}) override; + void push_kilometrage_snapshot() override; void set_current_node(scene::basic_node *Node) override; alignment::limits const &route_norms() const override; void clear_track_point() override; @@ -1545,7 +1551,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri editor_icons::icon picture; int family; // 0 builds the track, 1 equips it, 2 checks it }; - static std::array const &track_modes(); + static std::array const &track_modes(); bool track_plan_tab() const { return m_track_tab == track_tab::straights || m_track_tab == track_tab::route; } track_tab plan_tab_for(TTrack const *Track) const; // the objects along the track: a tab for each tool @@ -1632,7 +1638,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void draw_track_hover() const; struct track_intent { - enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling, measure, route_trace }; + enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling, measure, route_trace, kilometrage }; kind what{kind::none}; TTrack *track{nullptr}; int path{0}; @@ -2384,7 +2390,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri fouling_tool m_fouling{*this, *this, *this, *this, *this}; parallel_tool m_parallel{*this, *this, *this, *this}; srk_tool m_srk{*this, *this, *this, *this, *this}; - std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle, &m_hekto, &m_fouling, &m_parallel, &m_srk}; + km_tool m_km{*this, *this, *this, *this}; + std::vector m_tools{&m_gauge, &m_speed, &m_joints, &m_vehicle, &m_hekto, &m_fouling, &m_parallel, &m_srk, &m_km}; void infra_rebase(); void render_infra_search(); diff --git a/application/editormodekm.cpp b/application/editormodekm.cpp new file mode 100644 index 00000000..f28c2bf3 --- /dev/null +++ b/application/editormodekm.cpp @@ -0,0 +1,904 @@ +/* +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/editormodekm.h" +#include "application/editorgestures.h" +#include "application/editormodeshared.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorTrack.hpp" + +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::plan_distance; +using geometry::plan_of; + +double constexpr kOverlayRange{3000.0}; +double constexpr kTickRange{1500.0}; // m, of the hectometres shown in the scenery +double constexpr kLabelRange{700.0}; // m, of the km written there, farther ones crowd the horizon +double constexpr kDigitRange{250.0}; // m, of the hectometres written there +double constexpr kRailHead{0.18}; // m, above the axis of a path +double constexpr kPickReach{15.0}; // m from a point of the km a click on a track takes it +double constexpr kReadoutReach{30.0}; // m from the axis of a line its km is shown by the mouse +double constexpr kPostReach{60000.0}; // m along the line each way, of the posts read +double constexpr kPostCorridor{15.0}; // m from the track, posts farther away belong to another line +double constexpr kPostTolerance{10.0}; // m, of the distance between two posts against their numbers +double constexpr kCurve{10000.0}; // m, of the radius a jump of the km is on a curve under +double constexpr kStation{300.0}; // m from a switch a jump of the km is at a station + +ImU32 constexpr kAxis{IM_COL32(110, 200, 255, 220)}; +ImU32 constexpr kGuessed{IM_COL32(110, 200, 255, 90)}; +ImU32 constexpr kTick{IM_COL32(235, 240, 250, 230)}; +ImU32 constexpr kKm{IM_COL32(255, 215, 90, 255)}; +ImU32 constexpr kJump{IM_COL32(255, 95, 215, 255)}; +ImU32 constexpr kMark{IM_COL32(95, 220, 140, 255)}; +ImU32 constexpr kAstray{IM_COL32(240, 90, 80, 255)}; + +// the digits after the rotation make the number of a hectometre 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; +} + +// chainage of the point of the samples nearest to the point, and how far from them 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 ab{plan_of(Samples[i].position) - 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}; + if (auto const distance{glm::length(point - (a + ab * t))}; distance < Distance) + { + Distance = distance; + chainage = Samples[i - 1].chainage + (Samples[i].chainage - Samples[i - 1].chainage) * t; + } + } + return chainage; +} + +std::size_t sample_at(std::vector const &Samples, double const Chainage) +{ + auto const found{std::lower_bound(Samples.begin(), Samples.end(), Chainage, [](editor_track::route_sample const &Sample, double const Value) { return Sample.chainage < Value; })}; + return std::min(static_cast(found - Samples.begin()), Samples.size() - 1); +} + +// the number of a line: blanks taken out +std::string line_of(std::string const &Text) +{ + std::string result; + for (auto const c : Text) + if (false == std::isspace(static_cast(c))) + result += c; + return result; +} + +// a km typed as PKP PLK write it, 100,234, or with a point +bool km_of(char const *Text, double &Km) +{ + std::string text{Text}; + std::replace(text.begin(), text.end(), ',', '.'); + char *end{nullptr}; + auto const value{std::strtod(text.c_str(), &end)}; + if (end == text.c_str() || false == std::isfinite(value)) + return false; + Km = value; + return true; +} + +std::string mark_text(kilometrage::mark const &Mark) +{ + return Mark.beyond.has_value() ? kilometrage::text(Mark.km) + " = " + kilometrage::text(*Mark.beyond) : kilometrage::text(Mark.km); +} + +void draw_label(ImDrawList *List, ImVec2 const At, ImU32 const Ground, ImU32 const Colour, std::string const &Text) +{ + auto const size{ImGui::CalcTextSize(Text.c_str())}; + List->AddRectFilled(ImVec2(At.x - size.x * 0.5f - 4.0f, At.y - size.y - 4.0f), ImVec2(At.x + size.x * 0.5f + 4.0f, At.y), Ground, 3.0f); + List->AddText(ImVec2(At.x - size.x * 0.5f, At.y - size.y - 2.0f), Colour, Text.c_str()); +} + +void draw_arrow(screen_projection const &Projection, ImDrawList *List, glm::dvec3 const &From, glm::dvec2 const &Way, double const Length, ImU32 const Colour) +{ + glm::dvec3 const along{Way.x, 0.0, Way.y}; + glm::dvec3 const across{-Way.y, 0.0, Way.x}; + auto const tip{From + along * Length}; + Projection.line(List, From, tip, Colour, 3.0f); + Projection.line(List, tip, tip - along * (Length * 0.3) + across * (Length * 0.15), Colour, 3.0f); + Projection.line(List, tip, tip - along * (Length * 0.3) - across * (Length * 0.15), Colour, 3.0f); +} + +} // namespace + +void km_tool::refresh() +{ + m_drawn.clear(); + for (auto const &line : kilometrage::lines()) + { + auto const *axis{kilometrage::axis_of(line)}; + if (axis == nullptr || axis->points.size() < 2) + continue; + drawn entry; + entry.line = line; + entry.points = axis->points; + auto const low{axis->anchors.front().chainage - 1.0}; + auto const high{axis->anchors.back().chainage + 1.0}; + for (auto const chainage : axis->chainage) + entry.guessed.push_back(chainage < low || chainage > high ? 1 : 0); + std::vector jumps; + for (auto const &anchor : axis->anchors) + { + if (std::abs(anchor.after - anchor.before) < 1e-6) + continue; + jumps.push_back(anchor.chainage); + auto const way{kilometrage::direction_on(*axis, anchor.chainage)}; + entry.ticks.push_back({kilometrage::point_on(*axis, anchor.chainage), {-way.y, 0.0, way.x}, 0, true}); + } + // a tick at each hectometre the km pass along the axis, none at a jump + auto const hectometres_along = [&](std::vector const &Points, std::vector const &Km, std::vector const &Chainage, std::vector &Ticks) { + for (std::size_t i = 1; i < Points.size(); ++i) + { + auto const from{std::min(Chainage[i - 1], Chainage[i])}; + auto const to{std::max(Chainage[i - 1], Chainage[i])}; + if (std::any_of(jumps.begin(), jumps.end(), [&](double const Jump) { return Jump >= from - 1e-6 && Jump <= to + 1e-6; })) + continue; + auto const k0{Km[i - 1]}; + auto const k1{Km[i]}; + if (std::abs(k1 - k0) < 1e-9) + continue; + auto way{plan_of(Points[i] - Points[i - 1])}; + way = glm::length(way) > 1e-9 ? glm::normalize(way) : glm::dvec2{0.0, 1.0}; + auto const first{static_cast(std::floor(std::min(k0, k1) * 10.0 + 1e-9)) + 1}; + auto const last{static_cast(std::floor(std::max(k0, k1) * 10.0 + 1e-9))}; + for (auto hectometres = first; hectometres <= last && hectometres < first + 10; ++hectometres) + Ticks.push_back({glm::mix(Points[i - 1], Points[i], (hectometres / 10.0 - k0) / (k1 - k0)), {-way.y, 0.0, way.x}, hectometres, false}); + } + }; + std::vector km; + for (auto const chainage : axis->chainage) + km.push_back(kilometrage::km_on(*axis, chainage)); + hectometres_along(axis->points, km, axis->chainage, entry.ticks); + m_drawn.push_back(std::move(entry)); + // the shorter track where the tracks run apart, with its irregular hectometre + for (auto const &side : axis->sides) + { + drawn apart; + apart.line = line; + apart.points = side.points; + apart.guessed.assign(side.points.size(), 0); + km.clear(); + for (auto const along : side.along) + km.push_back(kilometrage::km_on(*axis, side, along)); + hectometres_along(side.points, km, side.chainage, apart.ticks); + m_drawn.push_back(std::move(apart)); + } + } + m_revision = kilometrage::revision(); + m_drawn_once = true; +} + +void km_tool::update() +{ + if (false == m_open) + return; + auto const &marks{kilometrage::marks()}; + // after an undo the point edited may be gone + if (m_edited >= 0 && (m_edited >= static_cast(marks.size()) || false == m_pick.has_value() || plan_distance(marks[m_edited].point, m_pick->point) > 0.5)) + m_edited = -1; + if (false == m_drawn_once || kilometrage::revision() != m_revision) + refresh(); +} + +void km_tool::forget(scene::basic_node const *Node) +{ + if (m_pick.has_value() && m_pick->track == Node) + m_pick->track = nullptr; +} + +glm::dvec2 km_tool::rising() const +{ + if (false == m_pick.has_value()) + return {0.0, 1.0}; + return m_pick->along * (m_against ? -1.0 : 1.0); +} + +std::string km_tool::jump_warning(glm::dvec3 const &Point) const +{ + auto *track{kilometrage::track_near(Point, 6.0)}; + if (track == nullptr) + return {}; + auto route{editor_track::run_route(*track, kStation + 50.0)}; + auto const samples{editor_track::sample_route(route, 2.0)}; + if (samples.size() < 2) + return {}; + double gap{0.0}; + auto const at{project(samples, Point, gap)}; + bool curve{false}; + bool station{false}; + for (auto const &sample : samples) + { + auto const apart{std::abs(sample.chainage - at)}; + if (apart <= 30.0 && std::abs(sample.curvature) > 1.0 / kCurve) + curve = true; + if (apart <= kStation && sample.span < route.spans.size() && route.spans[sample.span].track->eType != tt_Normal) + station = true; + } + if (curve && station) + return STR("Id-1 § 19: the km shouldn't jump on a curve nor at a station, move the jump to a straight on the open line"); + if (curve) + return STR("Id-1 § 19: the km shouldn't jump on a curve, move the jump to a straight"); + if (station) + return STR("Id-1 § 19: the km shouldn't jump at a station, move the jump out to the open line"); + return {}; +} + +void km_tool::pick(TTrack &Track, glm::dvec3 const &Point) +{ + auto const t{editor_track::nearest_parameter(Track, Point)}; + picked entry; + entry.track = &Track; + entry.point = editor_track::point_at(Track, t); + auto const ahead{plan_of(editor_track::point_at(Track, std::min(1.0, t + 0.01)) - editor_track::point_at(Track, std::max(0.0, t - 0.01)))}; + entry.along = glm::length(ahead) > 1e-9 ? glm::normalize(ahead) : glm::dvec2{0.0, 1.0}; + m_status.clear(); + m_edited = -1; + auto const &marks{kilometrage::marks()}; + auto nearest{kPickReach}; + for (std::size_t index = 0; index < marks.size(); ++index) + if (auto const distance{plan_distance(marks[index].point, entry.point)}; distance < nearest) + { + nearest = distance; + m_edited = static_cast(index); + } + if (m_edited >= 0) + { + auto const &mark{marks[m_edited]}; + entry.point = mark.point; + m_line = mark.line; + m_km = mark.km; + m_jump = mark.beyond.has_value(); + m_beyond = mark.beyond.value_or(mark.km); + m_against = glm::dot(mark.way, entry.along) < 0.0; + } + else + { + // the line the point lies on, and its km there + auto best{kReadoutReach}; + for (auto const &line : kilometrage::lines()) + if (auto const *axis{kilometrage::axis_of(line)}; axis != nullptr) + { + double gap{0.0}; + kilometrage::chainage_at(*axis, entry.point, gap); + if (gap < best) + { + best = gap; + m_line = line; + } + } + double km{0.0}; + if (false == m_line.empty() && kilometrage::km_at(m_line, entry.point, km, kReadoutReach)) + { + m_km = std::round(km * 1000.0) / 1000.0; + auto const *axis{kilometrage::axis_of(m_line)}; + double gap{0.0}; + auto const along{kilometrage::chainage_at(*axis, entry.point, gap)}; + m_against = glm::dot(kilometrage::direction_on(*axis, along) * static_cast(axis->growth), entry.along) < 0.0; + } + m_jump = false; + m_beyond = m_km; + } + m_pick = entry; +} + +std::string km_tool::readout(glm::dvec3 const &Point) const +{ + std::string result; + for (auto const &line : kilometrage::lines()) + { + double km{0.0}; + if (false == kilometrage::km_at(line, Point, km, kReadoutReach)) + continue; + if (false == result.empty()) + result += '\n'; + result += format(STR_C("line %s: km %s"), line.c_str(), kilometrage::text(km).c_str()); + } + return result; +} + +void km_tool::put() +{ + m_status.clear(); + if (false == m_pick.has_value()) + return; + auto const line{line_of(m_line)}; + if (line.empty()) + { + m_status = STR("Give the number of the line first"); + return; + } + if (m_jump && std::abs(m_beyond - m_km) < 0.0005) + { + m_status = STR("A jump needs another km on from the point"); + return; + } + kilometrage::mark entry; + entry.line = line; + entry.point = m_pick->point; + entry.way = rising(); + entry.km = m_km; + if (m_jump) + entry.beyond = m_beyond; + if (m_edited >= 0) + kilometrage::change(static_cast(m_edited), entry); + else + { + kilometrage::put(entry); + m_edited = static_cast(kilometrage::marks().size()) - 1; + } + m_line = line; + m_history.push_kilometrage_snapshot(); + m_status = format(STR_C("Line %s: km %s at the point"), line.c_str(), mark_text(entry).c_str()); +} + +void km_tool::remove() +{ + m_status.clear(); + if (m_edited < 0) + return; + kilometrage::remove(static_cast(m_edited)); + m_edited = -1; + m_history.push_kilometrage_snapshot(); + m_status = STR("The point of the km removed"); +} + +void km_tool::show(std::size_t const Index) +{ + auto const &marks{kilometrage::marks()}; + if (Index >= marks.size()) + return; + auto const point{marks[Index].point}; + if (auto *track{kilometrage::track_near(point, 6.0)}; track != nullptr) + pick(*track, point); + m_view.fly_to(point, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 40.0); +} + +void km_tool::read_posts() +{ + m_status.clear(); + auto const line{line_of(m_line)}; + if (false == m_pick.has_value() || m_pick->track == nullptr) + { + m_status = STR("Click a track of the line first"); + return; + } + if (line.empty()) + { + m_status = STR("Give the number of the line first"); + return; + } + auto route{editor_track::run_route(*m_pick->track, kPostReach)}; + auto const samples{editor_track::sample_route(route, 2.0)}; + if (samples.size() < 2) + { + m_status = STR("No line through this track"); + return; + } + auto const length{samples.back().chainage}; + struct post + { + double chainage{0.0}; + int hectometres{0}; + }; + std::vector posts; + for (auto const &standing : m_templates.standing_templates([](std::string const &File) { return File.find("hekto") != std::string::npos; })) + { + post entry; + if (false == hectometres_of(standing.values, entry.hectometres)) + continue; + double distance{0.0}; + entry.chainage = project(samples, standing.location, distance); + if (distance > kPostCorridor || entry.chainage <= 0.0 || entry.chainage >= length) + continue; + posts.push_back(entry); + } + if (posts.size() < 2) + { + m_status = STR("Fewer than two hectometre posts stand along the line, nothing to read"); + return; + } + std::sort(posts.begin(), posts.end(), [](post const &A, post const &B) { return A.chainage < B.chainage; }); + // runs of posts numbered by the distance between them, a post of the same number by the other track taken once + auto const fits = [](post const &From, post const &To, int const Sign) { + auto const counted{(To.hectometres - From.hectometres) * 100.0}; + auto const apart{To.chainage - From.chainage}; + return counted != 0.0 && (Sign == 0 || (counted > 0.0 ? 1 : -1) == Sign) && std::abs(std::abs(counted) - apart) <= std::max(kPostTolerance, apart * 0.02); + }; + auto const sign_of = [](std::vector const &Run) { return Run.size() < 2 ? 0 : Run.back().hectometres > Run.front().hectometres ? 1 : -1; }; + std::vector> runs; + std::size_t twins{0}; + for (auto const &entry : posts) + { + if (false == runs.empty()) + { + auto &run{runs.back()}; + if (entry.hectometres == run.back().hectometres && entry.chainage - run.back().chainage < kPostTolerance * 3.0) + { + ++twins; + continue; + } + if (fits(run.back(), entry, sign_of(run))) + { + run.push_back(entry); + continue; + } + } + runs.push_back({entry}); + } + // a post alone between the runs is one out of place + std::size_t stray{0}; + std::vector> kept; + for (auto &run : runs) + { + if (run.size() < 2) + { + ++stray; + continue; + } + if (false == kept.empty() && fits(kept.back().back(), run.front(), sign_of(kept.back())) && sign_of(kept.back()) == sign_of(run)) + kept.back().insert(kept.back().end(), run.begin(), run.end()); + else + kept.push_back(std::move(run)); + } + std::size_t rising{0}; + std::size_t falling{0}; + for (auto const &run : kept) + (sign_of(run) > 0 ? rising : falling) += run.size(); + auto const growth{rising >= falling ? 1 : -1}; + kept.erase(std::remove_if(kept.begin(), kept.end(), [&](std::vector const &Run) { + if (sign_of(Run) == growth) + return false; + stray += Run.size(); + return true; + }), kept.end()); + if (kept.empty()) + { + m_status = STR("The numbers of the posts don't go by 100 m anywhere along the line, nothing read"); + return; + } + auto const mark_at = [&](double const Chainage, double const Km, std::optional const Beyond) { + kilometrage::mark entry; + entry.line = line; + entry.point = editor_track::sampled_position(samples, Chainage); + entry.way = samples[sample_at(samples, Chainage)].direction * static_cast(growth); + entry.km = Km; + entry.beyond = Beyond; + return entry; + }; + std::vector made; + std::size_t jumps{0}; + std::size_t misplaced{0}; + for (std::size_t r = 0; r < kept.size(); ++r) + { + auto const &run{kept[r]}; + auto const &first{run.front()}; + auto const &last{run.back()}; + // the jump to the next run at the post it starts the irregular hectometre at, along the rising km + auto const jump_before{r > 0 && growth > 0}; + auto const jump_after{r + 1 < kept.size() && growth < 0}; + if (jump_before) + { + auto const &previous{kept[r - 1].back()}; + made.push_back(mark_at(first.chainage, previous.hectometres / 10.0 + (first.chainage - previous.chainage) / 1000.0, first.hectometres / 10.0)); + } + else + made.push_back(mark_at(first.chainage, first.hectometres / 10.0, std::nullopt)); + if (jump_after) + { + auto const &next{kept[r + 1].front()}; + made.push_back(mark_at(last.chainage, next.hectometres / 10.0 + (next.chainage - last.chainage) / 1000.0, last.hectometres / 10.0)); + } + else + made.push_back(mark_at(last.chainage, last.hectometres / 10.0, std::nullopt)); + if (jump_before || jump_after) + { + ++jumps; + if (false == jump_warning((jump_before ? made[made.size() - 2] : made.back()).point).empty()) + ++misplaced; + } + } + // the points of the line along the stretch read give way to the ones read + auto const &standing{kilometrage::marks()}; + for (auto index{static_cast(standing.size()) - 1}; index >= 0; --index) + { + if (standing[index].line != line) + continue; + double gap{0.0}; + project(samples, standing[index].point, gap); + if (gap <= kPostCorridor) + kilometrage::remove(static_cast(index)); + } + for (auto &entry : made) + kilometrage::put(std::move(entry)); + m_line = line; + m_edited = -1; + m_history.push_kilometrage_snapshot(); + m_status = format(STR_C("%zu posts read: %zu points of the km, %zu jumps"), posts.size() - twins, made.size(), jumps); + if (stray > 0) + m_status += "\n" + format(STR_P("%zu post doesn't fit its neighbours and was left out", "%zu posts don't fit their neighbours and were left out", stray), stray); + if (misplaced > 0) + m_status += "\n" + format(STR_P("%zu jump lies on a curve or at a station, Id-1 § 19 wants none there", "%zu jumps lie on a curve or at a station, Id-1 § 19 wants none there", misplaced), misplaced); +} + +void km_tool::render_options() +{ + ImGui::TextDisabled("%s", STR_C("The km of a railway line: the points where the km is known, from a post or a document, and where it jumps. The km of the rest go from them along the axis of the line: between the tracks of a double-track one, along the longer track where the tracks run on formations of their own, the shorter one getting an irregular hectometre")); + ImGui::Spacing(); + gesture_rows({{"LMB on a track", "a point of the km there, or the one standing by it"}}); + auto const &marks{kilometrage::marks()}; + auto const lines{kilometrage::lines()}; + std::size_t jumps{0}; + for (auto const &mark : marks) + if (mark.beyond.has_value()) + ++jumps; + metric_tiles({{format("%zu", lines.size()), STR_C("lines"), kAxis}, {format("%zu", marks.size()), STR_C("points"), kMark}, {format("%zu", jumps), STR_C("jumps"), kJump}}); + + step_heading(1, STR_C("A point of the km")); + if (false == m_pick.has_value()) + ImGui::TextDisabled("%s", STR_C("Click a track where the km is known: by a post, a station or a bridge")); + else + { + if (begin_fields("##kmpoint")) + { + field_row("Line"); + std::array buffer{}; + m_line.copy(buffer.data(), buffer.size() - 1); + if (ImGui::InputTextWithHint("##kmline", "91", buffer.data(), buffer.size())) + m_line = line_of(buffer.data()); + item_tooltip("The number of the railway line, as 91: a scenery can hold several lines, each with its own km"); + auto const km_field = [&](char const *Label, char const *Id, double &Value) { + field_row(Label); + std::array text{}; + kilometrage::text(Value).copy(text.data(), text.size() - 1); + if (ImGui::InputText(Id, text.data(), text.size())) + km_of(text.data(), Value); + }; + km_field("Km", "##kmkm", m_km); + item_tooltip("The km of the point, as PKP PLK write it: 100,234 is km 100 and 234 m. At a jump: the km the point is reached at, coming along the rising km"); + field_row("Jump"); + ImGui::Checkbox("##kmjump", &m_jump); + item_tooltip("The km jumps at the point: a line built anew, shortened or lengthened keeps the km of the rest. The irregular hectometre is " + "marked by a black stripe under the digit of the post (Id-1 § 19)"); + if (m_jump) + { + km_field("Km on", "##kmbeyond", m_beyond); + item_tooltip("The km on from the point, along the rising km"); + } + ImGui::EndTable(); + } + auto const *axis{m_line.empty() ? nullptr : kilometrage::axis_of(m_line)}; + auto const fixed{axis != nullptr && axis->anchors.size() > 1 && std::abs(axis->anchors.back().before - axis->anchors.front().before) > 1e-6}; + if (false == fixed) + { + int way{m_against ? 1 : 0}; + if (segmented("##kmway", way, {{0, STR_C("Rise this way"), "The km rise the way the arrow at the point shows"}, {1, STR_C("Rise the other way"), "The km rise against the arrow"}})) + m_against = way == 1; + } + if (m_jump) + if (auto const warning{jump_warning(m_pick->point)}; false == warning.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", warning.c_str()); + if (main_button(m_edited >= 0 ? STR_C("Change the point") : STR_C("Put the point of the km"), ImVec2(-1.0f, tall_button_height()))) + put(); + item_tooltip("The point goes to the file of the scenery with the track, the simulation skips it. One step of undo"); + if (m_edited >= 0) + { + if (ImGui::Button(STR_C("Remove the point"), ImVec2(-1.0f, 0.0f))) + remove(); + item_tooltip("One step of undo"); + } + } + + step_heading(2, STR_C("From the posts")); + if (ImGui::Button(STR_C("Read the km from the posts"), ImVec2(-1.0f, 0.0f))) + read_posts(); + item_tooltip("Reads the hectometre posts standing along the line through the track clicked: a point of the km at the first and the last " + "post of each run numbered by 100 m, a jump where two runs meet. The points of the line along it before give way. One step of undo"); + if (false == m_status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); + + step_heading(3, STR_C("The points of the lines")); + if (lines.empty()) + { + ImGui::TextDisabled("%s", STR_C("None yet")); + return; + } + for (auto const &line : lines) + { + auto const *axis{kilometrage::axis_of(line)}; + auto const count{static_cast(std::count_if(marks.begin(), marks.end(), [&](kilometrage::mark const &Mark) { return Mark.line == line; }))}; + if (false == ImGui::CollapsingHeader(format("%s %s (%zu)###kmline%s", STR_C("Line"), line.c_str(), count, line.c_str()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + continue; + if (axis == nullptr) + { + ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f), "%s", STR_C("No track by the points of the line")); + continue; + } + ImGui::TextDisabled("%s", format(STR_C("%.1f km of the axis, %s"), axis->length() / 1000.0, axis->between ? STR_C("between the tracks") : STR_C("along the track")).c_str()); + if (false == axis->sides.empty()) + { + ImGui::TextDisabled("%s", format(STR_P("%zu stretch with the tracks on formations of their own", "%zu stretches with the tracks on formations of their own", axis->sides.size()), axis->sides.size()).c_str()); + item_tooltip("Where the tracks part more than 15 m and come together again, the km run along the longer one, the shorter one goes by " + "its own length and its last hectometre before they meet takes up the difference, as PKP GK-1 gives it (6.1.12)"); + } + auto const rows{axis->anchors.size() + axis->astray.size()}; + if (false == ImGui::BeginTable(("##kmmarks" + line).c_str(), 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(260.0f, ImGui::GetTextLineHeightWithSpacing() * (rows + 1.5f))))) + continue; + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn(STR_C("Km"), ImGuiTableColumnFlags_WidthStretch, 0.4f); + ImGui::TableSetupColumn(STR_C("In the scenery"), ImGuiTableColumnFlags_WidthStretch, 0.22f); + ImGui::TableSetupColumn(STR_C("By the km"), ImGuiTableColumnFlags_WidthStretch, 0.22f); + ImGui::TableSetupColumn(STR_C("Off by"), ImGuiTableColumnFlags_WidthStretch, 0.16f); + ImGui::TableHeadersRow(); + auto const growth{axis->growth}; + for (std::size_t i = 0; i < axis->anchors.size(); ++i) + { + auto const &anchor{axis->anchors[i]}; + auto const &mark{marks[anchor.mark]}; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable((mark_text(mark) + "##kmmark" + std::to_string(anchor.mark)).c_str(), static_cast(anchor.mark) == m_edited, ImGuiSelectableFlags_SpanAllColumns)) + show(anchor.mark); + if (i == 0) + continue; + auto const &previous{axis->anchors[i - 1]}; + auto const scenery{anchor.chainage - previous.chainage}; + auto const counted{std::abs((growth > 0 ? anchor.before - previous.after : previous.before - anchor.after) * 1000.0)}; + ImGui::TableNextColumn(); + ImGui::Text("%.0f m", scenery); + ImGui::TableNextColumn(); + ImGui::Text("%.0f m", counted); + ImGui::TableNextColumn(); + auto off{scenery > 1.0 ? (scenery - counted) / scenery * 100.0 : 0.0}; + if (std::abs(off) < 0.05) + off = 0.0; + if (std::abs(off) > 2.0) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%+.1f%%", off); + else + ImGui::Text("%+.1f%%", off); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("How much longer the stretch is in the scenery than by the km: the km between the points are stretched to it evenly")); + } + for (auto const index : axis->astray) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.4f, 1.0f)); + if (ImGui::Selectable((mark_text(marks[index]) + "##kmmark" + std::to_string(index)).c_str(), static_cast(index) == m_edited, ImGuiSelectableFlags_SpanAllColumns)) + show(index); + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The point is away from the axis of the line the other points make: the main paths of the switches don't lead to it")); + } + ImGui::EndTable(); + } +} + +void km_tool::draw_overlay() const +{ + if (false == m_open) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + glm::dvec3 const camera{Global.pCamera.Pos}; + auto const near_enough = [&](glm::dvec3 const &Point, double const Range) { return Global.EditorOrtho || glm::distance(Point, camera) < Range; }; + glm::dvec3 const lift{0.0, kRailHead, 0.0}; + for (auto const &entry : m_drawn) + { + // the axis, faint where the km run on past the last point + for (std::size_t i = 1; i < entry.points.size(); i += 5) + { + auto const next{std::min(i + 4, entry.points.size() - 1)}; + if (near_enough(entry.points[next], kOverlayRange)) + projection.line(drawlist, entry.points[i - 1] + lift, entry.points[next] + lift, entry.guessed[next] != 0 ? kGuessed : kAxis, Global.EditorOrtho ? 4.0f : 2.5f); + } + for (auto const &tick : entry.ticks) + { + if (false == near_enough(tick.at, kTickRange)) + continue; + auto const centre{tick.at + lift}; + auto const whole{tick.hectometres % 10 == 0}; + auto const reach{tick.jump ? 3.5 : whole ? 2.6 : 1.4}; + projection.line(drawlist, centre - tick.across * reach, centre + tick.across * reach, tick.jump ? kJump : whole ? kKm : kTick, tick.jump || whole ? 3.5f : 2.0f); + if (tick.jump) + continue; + ImVec2 screen; + if (whole && near_enough(tick.at, kLabelRange) && projection.project(centre + tick.across * reach + glm::dvec3{0.0, 0.6, 0.0}, screen)) + draw_label(drawlist, screen, IM_COL32(0, 0, 0, 170), kKm, format("km %d", tick.hectometres / 10)); + else if (false == whole && near_enough(tick.at, kDigitRange) && projection.project(centre + tick.across * reach + glm::dvec3{0.0, 0.4, 0.0}, screen)) + draw_label(drawlist, screen, IM_COL32(0, 0, 0, 140), kTick, std::to_string(std::abs(tick.hectometres) % 10)); + } + } + // the points of the km: a pole with the km over it, the arrow the way the km rise + auto const &marks{kilometrage::marks()}; + for (std::size_t index = 0; index < marks.size(); ++index) + { + auto const &mark{marks[index]}; + if (false == near_enough(mark.point, kLabelRange * 2.0)) + continue; + auto astray{true}; + for (auto const &entry : m_drawn) + if (entry.line == mark.line) + if (auto const *axis{kilometrage::axis_of(mark.line)}; axis != nullptr) + astray = std::find(axis->astray.begin(), axis->astray.end(), index) != axis->astray.end(); + auto const colour{astray ? kAstray : mark.beyond.has_value() ? kJump : kMark}; + auto const foot{mark.point + lift}; + auto const top{foot + glm::dvec3{0.0, 3.0, 0.0}}; + projection.line(drawlist, foot, top, colour, static_cast(index) == m_edited ? 4.0f : 2.5f); + draw_arrow(projection, drawlist, foot, mark.way, 6.0, colour); + ImVec2 screen; + if (projection.project(top, screen)) + draw_label(drawlist, screen, colour, IM_COL32(16, 18, 22, 255), mark.line + ": " + mark_text(mark)); + } + if (m_pick.has_value() && m_edited < 0) + { + auto const foot{m_pick->point + lift}; + ImVec2 screen; + if (projection.project(foot, screen)) + drawlist->AddCircle(screen, 9.0f, kKm, 20, 2.5f); + draw_arrow(projection, drawlist, foot, rising(), 8.0, kKm); + } +} + +editor_selftest_result km_tool::selftest(std::string const &Command, std::istringstream &Words) +{ + if (Command == "kmpick") + { + // kmpick track= [at=<0..1>]: a click on the track + auto const values{selftest_values(Words)}; + auto const found{values.find("track")}; + auto *track{found != values.end() ? simulation::Paths.find(found->second) : nullptr}; + if (track == nullptr) + { + WriteLog("SELFTEST kmpick: no such track"); + return editor_selftest_result::handled; + } + auto const at{values.count("at") ? std::atof(values.at("at").c_str()) : 0.5}; + pick(*track, editor_track::point_at(*track, at)); + WriteLog(format("SELFTEST kmpick %s: line %s km %s jump %d against %d edited %d at %.1f %.1f", track->name().c_str(), m_line.c_str(), kilometrage::text(m_km).c_str(), m_jump ? 1 : 0, m_against ? 1 : 0, m_edited, m_pick->point.x, m_pick->point.z)); + return editor_selftest_result::handled; + } + if (Command == "kmset") + { + // kmset line= km= [beyond=] [against=1]: the fields set and the point put + auto const values{selftest_values(Words)}; + if (auto const line{values.find("line")}; line != values.end()) + m_line = line->second; + if (auto const km{values.find("km")}; km != values.end()) + km_of(km->second.c_str(), m_km); + m_jump = values.count("beyond") > 0; + if (m_jump) + km_of(values.at("beyond").c_str(), m_beyond); + if (auto const against{values.find("against")}; against != values.end()) + m_against = against->second == "1"; + put(); + WriteLog("SELFTEST kmset: " + m_status); + return editor_selftest_result::handled; + } + if (Command == "kmremove") + { + remove(); + WriteLog("SELFTEST kmremove: " + m_status); + return editor_selftest_result::handled; + } + if (Command == "kmposts") + { + // kmposts line=: the km read from the posts along the line through the track picked + auto const values{selftest_values(Words)}; + if (auto const line{values.find("line")}; line != values.end()) + m_line = line->second; + read_posts(); + WriteLog("SELFTEST kmposts: " + m_status); + return editor_selftest_result::handled; + } + if (Command == "kmstate") + { + WriteLog(format("SELFTEST kmstate: picked %d line %s km %s jump %d edited %d", m_pick.has_value() ? 1 : 0, m_line.c_str(), kilometrage::text(m_km).c_str(), m_jump ? 1 : 0, m_edited)); + return editor_selftest_result::handled; + } + if (Command == "kmshow") + { + // kmshow mark=: the point of the km picked, the camera over it + auto const values{selftest_values(Words)}; + auto const index{values.count("mark") ? std::atoi(values.at("mark").c_str()) : 0}; + show(static_cast(std::max(index, 0))); + WriteLog(format("SELFTEST kmshow %d: line %s km %s edited %d", index, m_line.c_str(), kilometrage::text(m_km).c_str(), m_edited)); + return editor_selftest_result::handled; + } + if (Command == "kmlist") + { + refresh(); + for (auto const &entry : m_drawn) + { + std::string list; + for (std::size_t i = 0; i < entry.ticks.size() && entry.points.size() < 2000; ++i) + list += format(" %d@%.0f,%.0f", entry.ticks[i].hectometres, entry.ticks[i].at.x, entry.ticks[i].at.z); + WriteLog(format("SELFTEST kmlist ticks %s: %zu points, %zu ticks", entry.line.c_str(), entry.points.size(), entry.ticks.size()) + list); + } + auto const &marks{kilometrage::marks()}; + WriteLog(format("SELFTEST kmlist: %zu points", marks.size())); + for (auto const &line : kilometrage::lines()) + { + auto const *axis{kilometrage::axis_of(line)}; + if (axis == nullptr) + { + WriteLog("SELFTEST kmlist line " + line + ": no axis"); + continue; + } + std::string list; + for (auto const &anchor : axis->anchors) + list += format(" %s@%.0f/%.1f", mark_text(marks[anchor.mark]).c_str(), anchor.chainage, anchor.gap); + for (auto const &side : axis->sides) + WriteLog(format("SELFTEST kmlist apart: %.1f m of track by %.1f m of axis (%.1f..%.1f), km %s..%s", side.length, side.to - side.from, side.from, side.to, kilometrage::text(kilometrage::km_on(*axis, side, 0.0)).c_str(), + kilometrage::text(kilometrage::km_on(*axis, side, side.length)).c_str())); + WriteLog(format("SELFTEST kmlist line %s: axis %.0f m %s growth %d, %zu astray, km %s..%s:", line.c_str(), axis->length(), axis->between ? "between" : "along", axis->growth, axis->astray.size(), kilometrage::text(kilometrage::km_on(*axis, 0.0)).c_str(), + kilometrage::text(kilometrage::km_on(*axis, axis->length())).c_str()) + + list); + } + return editor_selftest_result::handled; + } + if (Command == "kmat") + { + // kmat track= [at=<0..1>]: the km of the lines there + auto const values{selftest_values(Words)}; + auto const found{values.find("track")}; + auto *track{found != values.end() ? simulation::Paths.find(found->second) : nullptr}; + if (track == nullptr) + { + WriteLog("SELFTEST kmat: no such track"); + return editor_selftest_result::handled; + } + auto const at{values.count("at") ? std::atof(values.at("at").c_str()) : 0.5}; + auto text{readout(editor_track::point_at(*track, at))}; + std::replace(text.begin(), text.end(), '\n', ';'); + WriteLog(format("SELFTEST kmat %s %.2f: ", track->name().c_str(), at) + (text.empty() ? std::string{"-"} : text)); + return editor_selftest_result::handled; + } + return editor_selftest_result::unhandled; +} diff --git a/application/editormodekm.h b/application/editormodekm.h new file mode 100644 index 00000000..8b3c0b38 --- /dev/null +++ b/application/editormodekm.h @@ -0,0 +1,90 @@ +/* +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/editortool.h" +#include "editor/editorKilometrage.hpp" + +#include +#include +#include +#include + +// the kilometrage of the lines: the points of a line where its km is known and where it jumps, the km of the rest from them +class km_tool final : public editor_tool +{ + public: + km_tool(editor_selection &Selection, editor_view &View, editor_history &History, editor_templates &Templates) + : m_selection{Selection}, m_view{View}, m_history{History}, m_templates{Templates} + { + } + void update() override; + void draw_overlay() const override; + void render_options() override; + void forget(scene::basic_node const *Node) override; + editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; + void set_open(bool const Open) { m_open = Open; } + // the point of the track clicked: a new point of the km there, or the one standing by it + void pick(TTrack &Track, glm::dvec3 const &Point); + // the km of the lines at the point, a line of text each, empty where none reaches + std::string readout(glm::dvec3 const &Point) const; + + private: + editor_selection &m_selection; + editor_view &m_view; + editor_history &m_history; + editor_templates &m_templates; + bool m_open{false}; + struct picked + { + TTrack *track{nullptr}; + glm::dvec3 point{0.0}; // on the axis of the track + glm::dvec2 along{0.0, 1.0}; // of the track there, in the plan + }; + std::optional m_pick; + int m_edited{-1}; // of the marks, the one standing by the point picked, -1 for a new one + std::string m_line; + double m_km{0.0}; + bool m_jump{false}; + double m_beyond{0.0}; + bool m_against{false}; // the km rise against the track picked + std::string m_status; + // what the overlay draws, made again when the axes change + struct tick + { + glm::dvec3 at{0.0}; + glm::dvec3 across{0.0}; // in the plan, a unit + int hectometres{0}; + bool jump{false}; + }; + struct drawn + { + std::string line; + std::vector points; + std::vector guessed; // of the points, beyond the last point of the km: the km run on from it + std::vector ticks; + }; + std::vector m_drawn; + std::size_t m_revision{0}; + bool m_drawn_once{false}; + + void refresh(); + void put(); + void remove(); + // the point of the km picked and the camera over it + void show(std::size_t const Index); + // the hectometre posts standing along the line through the track picked read into points of the km: the first and the last + // post of each run of them numbered by 100 m, a jump where the runs meet; one step of the history + void read_posts(); + // the way the km rise at the point picked, in the plan + glm::dvec2 rising() const; + // warnings of a jump at the point: Id-1 § 19 wants none on a curve nor at a station + std::string jump_warning(glm::dvec3 const &Point) const; +}; diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp index 61073625..906d419a 100644 --- a/application/editormodelineside.cpp +++ b/application/editormodelineside.cpp @@ -1054,6 +1054,8 @@ editor_selftest_result hekto_tool::selftest(std::string const &Command, std::ist } start(*track, track->m_paths.front().points[segment_data::point::start]); auto &tool{m_hekto}; + if (values.count("reach")) + tool.reach = std::stod(values["reach"]); tool.dirty = true; hekto_update(); if (values.count("km")) diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 919a1b93..e8b4dec0 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -297,6 +297,28 @@ void editor_mode::push_track_snapshot(std::vector const &Tracks, std::string &Reason) { for (auto const *track : Tracks) @@ -422,9 +444,9 @@ 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", editor_icons::icon::select, 0}, {"Lay track", "L", track_tab::lay, "Lays new track through the clicked points, on the ground or from a free end of a track", editor_icons::icon::lay_track, 0}, {"Plan", "G", track_tab::straights, "The plan of the line laid: LMB on a straight drags its ends or the middle, on a curve its vertex and radius. C works too", editor_icons::icon::curve, 0}, @@ -433,6 +455,7 @@ std::array const &editor_mode::track_modes() {"Turntable", "U", track_tab::turntable, "Places a turntable, leads tracks out of it and fits the tracks around to it", editor_icons::icon::turntable, 1}, {"Signals", "H", track_tab::signals, "Signals by the track, from the templates of the scenery, with the event the train reads them by", editor_icons::icon::signal, 1}, {"Signalling", "I", track_tab::srk, "The signalling of a station: LMB on a switch lays its station out as a schematic", editor_icons::icon::station, 1}, + {"Kilometrage", "X", track_tab::kilometrage, "The km of the railway lines: LMB on a track puts a point of the km there, a jump of the km too", editor_icons::icon::kilometrage, 1}, {"Objects", "B", track_tab::lineside, "Along the track: hectometre posts, fouling point markers, trainsets", editor_icons::icon::objects, 1}, {"Profile", "P", track_tab::profile, "Vertical profile (grade line) along the line", editor_icons::icon::profile, 2}, {"Speed", "V", track_tab::speed, "Speed limits of the paths against the speed their geometry allows", editor_icons::icon::speed, 2}, @@ -578,6 +601,7 @@ std::string editor_mode::track_mode_name() const case track_tab::parallel: return STR_C("Parallel track"); case track_tab::signals: return STR_C("Signal"); case track_tab::srk: return STR_C("Signalling"); + case track_tab::kilometrage: return STR_C("Kilometrage"); case track_tab::turntable: return STR_C("Turntable"); default: return STR_C("Select"); } @@ -591,6 +615,7 @@ void editor_mode::render_track_inspector() m_infra.open = m_track_window_open && m_track_tab == track_tab::infra; m_joints.set_open(m_track_window_open && m_track_tab == track_tab::joints); m_srk.set_open(m_track_window_open && m_track_tab == track_tab::srk); + m_km.set_open(m_track_window_open && m_track_tab == track_tab::kilometrage); if (false == m_track_window_open || m_track_tab != track_tab::lineside) { m_sweep.open = false; @@ -675,6 +700,7 @@ void editor_mode::render_track_tool_options() case track_tab::lineside: render_lineside_ui(); break; case track_tab::signals: render_signal_ui(); break; case track_tab::srk: m_srk.render_options(); break; + case track_tab::kilometrage: m_km.render_options(); break; case track_tab::turntable: render_turntable_ui(); break; default: render_track_set_ui(); @@ -1694,6 +1720,10 @@ std::vector editor_mode::track_key_hints(bool const All) hints = {{"LMB around it", "leads a track out"}, {"LMB on a free end", "fits that track to the turntable"}}; else if (m_track_window_open && m_track_tab == track_tab::turntable) hints = {{"LMB", "places a new turntable"}}; + else if (m_track_window_open && m_track_tab == track_tab::srk) + hints = {{"LMB on a switch", "opens its station"}}; + else if (m_track_window_open && m_track_tab == track_tab::kilometrage) + hints = {{"LMB on a track", "a point of the km there"}, {"Mouse over a track", "its km by the cursor"}}; 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) diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 1c562ed1..3850906e 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -465,6 +465,16 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) intent.position = m_hover.point; return make(kind::select, m_hover.track->eType != tt_Normal ? "Click: the station of this switch" : "Click: the station of the switch nearest along this track"); } + if (m_track_tab == track_tab::kilometrage && m_hover.track != nullptr) + { + intent.track = m_hover.track; + intent.path = m_hover.path; + intent.position = m_hover.point; + make(kind::kilometrage, "Click: a point of the km here"); + if (auto const km{m_km.readout(m_hover.point)}; false == km.empty()) + intent.action = km + "\n" + intent.action; + return intent; + } if (m_track_style.picking && (m_track_tab == track_tab::path || m_track_tab == track_tab::turnout)) { intent.track = m_hover.track; @@ -796,6 +806,10 @@ void editor_mode::track_press(track_intent const &Intent) handle_drag_start(Intent.position); return; case kind::switch_at: start_switch_placement(); return; + case kind::kilometrage: + if (Intent.track != nullptr) + m_km.pick(*Intent.track, Intent.position); + return; case kind::fouling: if (Intent.path == 1) m_fouling.fix(Intent.index); diff --git a/application/editormodeviewport.cpp b/application/editormodeviewport.cpp index 3f9887f7..3f20b92c 100644 --- a/application/editormodeviewport.cpp +++ b/application/editormodeviewport.cpp @@ -177,10 +177,10 @@ void editor_mode::work_area_tools(std::vector &Buttons) add(track_tab::straights, false); add(track_tab::turnout, false); add(track_tab::parallel, false); - auto const advanced{m_track_tab == track_tab::turntable || m_track_tab == track_tab::signals || m_track_tab == track_tab::srk || m_track_tab == track_tab::lineside || m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || + auto const advanced{m_track_tab == track_tab::turntable || m_track_tab == track_tab::signals || m_track_tab == track_tab::srk || m_track_tab == track_tab::kilometrage || m_track_tab == track_tab::lineside || m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || m_track_tab == track_tab::joints || m_gauge.is_open()}; auto const more{EditorSettings.track_tools_more()}; - Buttons.push_back({icon::more, more ? STR_C("Less") : STR_C("More"), nullptr, STR_C("Turntable, signals, signalling, objects, trainset, profile, speed, joints, gauge"), more, [more]() { + Buttons.push_back({icon::more, more ? STR_C("Less") : STR_C("More"), nullptr, STR_C("Turntable, signals, signalling, kilometrage, objects, trainset, profile, speed, joints, gauge"), more, [more]() { EditorSettings.track_tools_more(false == more); EditorSettings.save(); }, true}); @@ -189,6 +189,7 @@ void editor_mode::work_area_tools(std::vector &Buttons) add(track_tab::turntable, true); add(track_tab::signals, false); add(track_tab::srk, false); + add(track_tab::kilometrage, false); add(track_tab::lineside, false); Buttons.push_back({icon::vehicle, STR_C("Trainset"), nullptr, STR_C("A trainset on the selected track: written to the scenery on save, and to drive at once"), vehicle, [this, vehicle]() { show_track_tab(vehicle ? track_tab::path : track_tab::lineside); diff --git a/application/editortool.h b/application/editortool.h index 340899c8..70f1c41e 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -36,7 +36,7 @@ enum class editor_selftest_result enum class editor_first_step { tracks, lay, laid, wheel, join, turnout, undo, save, drive, look, top, select, shape, profile, profile_point, profile_apply, terrain, nmt, plan, plan_band, plan_apply, signal, posts, count }; -enum class editor_track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals, parallel, srk }; +enum class editor_track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals, parallel, srk, kilometrage }; struct track_hover { @@ -167,6 +167,8 @@ class editor_history public: virtual std::size_t history_size() const = 0; virtual void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}) = 0; + // the changes of the kilometrage made since the last step: one step of the history + virtual void push_kilometrage_snapshot() = 0; protected: ~editor_history() = default; diff --git a/editor/editorIcons.hpp b/editor/editorIcons.hpp index 8b583b40..2065d320 100644 --- a/editor/editorIcons.hpp +++ b/editor/editorIcons.hpp @@ -52,6 +52,7 @@ enum class icon turntable, parallel, station, + kilometrage, more, // the fields of work work_surroundings, diff --git a/editor/editorKilometrage.cpp b/editor/editorKilometrage.cpp new file mode 100644 index 00000000..b6f354c7 --- /dev/null +++ b/editor/editorKilometrage.cpp @@ -0,0 +1,779 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorKilometrage.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorTrack.hpp" + +#include "world/Track.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace kilometrage +{ + +namespace +{ + +using geometry::plan_distance; +using geometry::plan_of; + +std::string const kMark{"//$k"}; +double const kReach{60000.0}; // m, of the tracks followed each way from a mark +double const kStep{2.0}; // m between the points of an axis +double const kOnAxis{12.0}; // m, of a mark from the axis of its line +double const kNear{6.0}; // m, of a mark from the track it stands on +double const kBetweenLow{3.0}; // m between the axes of the tracks of a double-track line +double const kBetweenHigh{6.5}; +double const kCell{10.0}; // m, of the squares the track beside is sorted into +double const kApart{15.0}; // m between the tracks, of a double-track line on two formations +double const kBlend{100.0}; // m, the axis takes to go over from between the tracks to one of them +double const kLonger{1.0}; // m, of the track beside longer than the track of the line, for the axis to follow it + +std::vector g_marks; +std::vector g_base; // as they were before the changes not yet in the undo history +bool g_changed{false}; +bool g_loaded{false}; +std::size_t g_edits{0}; +std::map> g_axes; +std::size_t g_revision{0}; + +void drop_axes() +{ + g_axes.clear(); + ++g_revision; +} + +using span = editor_track::route_span; + +std::string word(std::string const &Text) +{ + return Text.empty() ? std::string{"-"} : Text; +} + +// format of the lines, coordinates in the space of the file: +// mark way km [beyond ] +void store(scene::layer_handle const Layer) +{ + if (false == scene::Layers.valid(Layer)) + return; + auto const &context{scene::Layers.layer(Layer).context_insert()}; + std::vector lines; + for (auto const &entry : g_marks) + { + if (entry.layer != Layer) + continue; + if (lines.empty()) + lines.emplace_back("# kilometrage of the lines set in the track editor, the simulation skips these lines"); + auto const point{context.to_local(entry.point)}; + auto const ahead{context.to_local(entry.point + glm::dvec3{entry.way.x, 0.0, entry.way.y})}; + auto way{plan_of(ahead - point)}; + way = glm::length(way) > 1e-12 ? glm::normalize(way) : entry.way; + auto text{format("mark %s %.4f %.4f %.4f way %.9f %.9f km %.3f", word(entry.line).c_str(), point.x, point.y, point.z, way.x, way.y, entry.km)}; + if (entry.beyond.has_value()) + text += format(" beyond %.3f", *entry.beyond); + lines.push_back(text); + } + scene::Layers.mark(Layer, kMark, std::move(lines)); +} + +void changed(std::set const &Layers) +{ + g_changed = true; + drop_axes(); + for (auto const layer : Layers) + store(layer); +} + +std::vector flipped(std::vector Spans) +{ + std::reverse(Spans.begin(), Spans.end()); + for (auto &entry : Spans) + entry.forward = false == entry.forward; + return Spans; +} + +std::size_t index_of(std::vector const &Spans, TTrack const *Track) +{ + auto const found{std::find_if(Spans.begin(), Spans.end(), [&](span const &Entry) { return Entry.track == Track; })}; + return found == Spans.end() ? Spans.size() : static_cast(found - Spans.begin()); +} + +// the run on from the end of the span the way it goes, along the main paths of the switches +std::vector onward(span const &Last) +{ + auto const run{editor_track::run_route(*Last.track, kReach)}; + auto const at{index_of(run.spans, Last.track)}; + if (at == run.spans.size()) + return {}; + if (run.spans[at].forward == Last.forward) + return {run.spans.begin() + at + 1, run.spans.end()}; + return flipped({run.spans.begin(), run.spans.begin() + at}); +} + +// the spans with each track once, the first time it comes +std::vector once(std::vector const &Spans) +{ + std::vector result; + std::unordered_set seen; + for (auto const &entry : Spans) + if (seen.insert(entry.track).second) + result.push_back(entry); + return result; +} + +double plan_gap(std::vector const &Samples, glm::dvec3 const &Point) +{ + auto best{std::numeric_limits::max()}; + for (auto const &sample : Samples) + best = std::min(best, plan_distance(sample.position, Point)); + return best; +} + +// the track beside the line: of each sample of the line the sample of it beside, where they run side by side +struct pairing +{ + std::vector partner; // along the track beside, empty for none + std::vector match; // of each sample, -1 where the track beside isn't there + std::vector across; // to the sample beside, along the normal of the sample +}; + +pairing pair_with(std::vector const &Samples, TTrack &Beside) +{ + pairing result; + result.match.assign(Samples.size(), -1); + result.across.assign(Samples.size(), 0.0); + auto other{editor_track::run_route(Beside, kReach)}; + result.partner = editor_track::sample_route(other, kStep); + auto const &partner{result.partner}; + auto const cell = [](glm::dvec3 const &Point) { return std::make_pair(static_cast(std::floor(Point.x / kCell)), static_cast(std::floor(Point.z / kCell))); }; + std::map, std::vector> grid; + for (std::size_t j = 0; j < partner.size(); ++j) + grid[cell(partner[j].position)].push_back(j); + for (std::size_t i = 0; i < Samples.size(); ++i) + { + auto const &sample{Samples[i]}; + glm::dvec2 const normal{-sample.direction.y, sample.direction.x}; + auto const [cx, cz]{cell(sample.position)}; + auto best{std::numeric_limits::max()}; + for (auto x = cx - 1; x <= cx + 1; ++x) + for (auto z = cz - 1; z <= cz + 1; ++z) + { + auto const found{grid.find({x, z})}; + if (found == grid.end()) + continue; + for (auto const j : found->second) + { + auto const offset{plan_of(partner[j].position) - plan_of(sample.position)}; + auto const across{glm::dot(offset, normal)}; + auto const along{glm::dot(offset, sample.direction)}; + if (std::abs(across) < kBetweenLow || std::abs(across) > kBetweenHigh || std::abs(along) >= kStep * 1.5 || std::abs(glm::dot(sample.direction, partner[j].direction)) <= 0.97 || std::abs(along) >= best) + continue; + best = std::abs(along); + result.match[i] = static_cast(j); + result.across[i] = across; + } + } + } + return result; +} + +// the track beside the line, looked for by the first mark and then every 500 m along the line: a track the line doesn't run +// through, the spacing of a double-track line away, beside it for 100 m at least +pairing pair_up(std::vector const &Samples, editor_track::route const &Route, std::size_t const Origin) +{ + std::unordered_set own; + for (auto const &entry : Route.spans) + own.insert(entry.track); + std::unordered_set tried; + auto const step{static_cast(500.0 / kStep)}; + for (std::size_t k = 0; k * step < Samples.size() && tried.size() < 4; ++k) + for (auto const way : {1, -1}) + { + if (k == 0 && way < 0) + continue; + auto const index{static_cast(Origin) + way * static_cast(k * step)}; + if (index < 0 || index >= static_cast(Samples.size())) + continue; + auto const &at{Samples[static_cast(index)]}; + glm::dvec2 const left{-at.direction.y, at.direction.x}; + for (auto const side : {1.0, -1.0}) + { + auto const probe{plan_of(at.position) + left * (side * (kBetweenLow + kBetweenHigh) * 0.5)}; + auto *track{track_near({probe.x, at.position.y, probe.y}, 1.6)}; + if (track == nullptr || own.count(track) > 0 || false == tried.insert(track).second) + continue; + auto result{pair_with(Samples, *track)}; + if (std::count_if(result.match.begin(), result.match.end(), [](int const Match) { return Match >= 0; }) * kStep >= 100.0) + return result; + } + } + pairing none; + none.match.assign(Samples.size(), -1); + none.across.assign(Samples.size(), 0.0); + return none; +} + +// the axis: between the tracks where they run side by side, along the longer one where they part and come together again +// (PKP GK-1 6.1.12), the other one getting the km by the share of its length, on the track of the line where the other one ends +void compose(axis &Result, std::vector const &Samples, pairing const &Pairs) +{ + using sample = editor_track::route_sample; + auto const emit = [&](glm::dvec3 const &Point, glm::dvec2 const &Direction) { + Result.chainage.push_back(Result.points.empty() ? 0.0 : Result.chainage.back() + plan_distance(Result.points.back(), Point)); + Result.points.push_back(Point); + Result.directions.push_back(Direction); + }; + auto const shifted = [](glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Offset) { + auto const planar{plan_of(Point) + glm::dvec2{-Direction.y, Direction.x} * Offset}; + return glm::dvec3{planar.x, Point.y, planar.y}; + }; + std::vector matched; + for (std::size_t i = 0; i < Samples.size(); ++i) + if (Pairs.match[i] >= 0) + matched.push_back(i); + if (matched.empty()) + { + for (auto const &entry : Samples) + emit(entry.position, entry.direction); + return; + } + Result.between = true; + auto const half = [&](std::size_t const Index) { return Pairs.across[Index] * 0.5; }; + // from between the tracks over to one of them, within kBlend of the ends of the stretch apart + auto const eased = [&](double const From, double const To, double const Low, double const High) { + return From < To ? Low * (1.0 - std::clamp(From / kBlend, 0.0, 1.0)) : High * (1.0 - std::clamp(To / kBlend, 0.0, 1.0)); + }; + auto const first{matched.front()}; + for (std::size_t i = 0; i < first; ++i) + emit(shifted(Samples[i].position, Samples[i].direction, half(first) * std::clamp(1.0 - (Samples[first].chainage - Samples[i].chainage) / kBlend, 0.0, 1.0)), Samples[i].direction); + std::optional pending; + std::vector shares; + for (std::size_t k = 0; k < matched.size(); ++k) + { + auto const a{matched[k]}; + emit(shifted(Samples[a].position, Samples[a].direction, half(a)), Samples[a].direction); + if (pending) + { + pending->to = Result.chainage.back(); + for (auto const share : shares) + pending->chainage.push_back(pending->from + share * (pending->to - pending->from)); + Result.sides.push_back(std::move(*pending)); + pending.reset(); + } + if (k + 1 == matched.size()) + break; + auto const b{matched[k + 1]}; + if (b == a + 1) + continue; + // the tracks apart between a and b: the track beside from the sample by a to the one by b, turned along the line + auto const ja{Pairs.match[a]}; + auto const jb{Pairs.match[b]}; + std::vector beside; + for (auto j = ja; j != jb; j += ja < jb ? 1 : -1) + { + auto entry{Pairs.partner[static_cast(j)]}; + if (ja > jb) + entry.direction = -entry.direction; + beside.push_back(entry); + } + beside.push_back(Pairs.partner[static_cast(jb)]); + if (ja > jb) + beside.back().direction = -beside.back().direction; + std::vector along{0.0}; + for (std::size_t q = 1; q < beside.size(); ++q) + along.push_back(along.back() + plan_distance(beside[q - 1].position, beside[q].position)); + auto const own_length{Samples[b].chainage - Samples[a].chainage}; + auto const beside_length{along.back()}; + auto const at_share = [&](double const Share) { + auto const target{Share * beside_length}; + auto const next{std::min(beside.size() - 1, static_cast(std::lower_bound(along.begin(), along.end(), target) - along.begin()))}; + if (next == 0) + return beside.front().position; + auto const span{along[next] - along[next - 1]}; + return glm::mix(beside[next - 1].position, beside[next].position, span > 1e-9 ? (target - along[next - 1]) / span : 0.0); + }; + auto widest{0.0}; + for (auto i = a + 1; i < b; ++i) + widest = std::max(widest, plan_distance(Samples[i].position, at_share((Samples[i].chainage - Samples[a].chainage) / own_length))); + if (widest <= kApart || own_length < 1e-6 || beside_length < 1e-6) + { + // spread a little, as through a station: between them still + for (auto i = a + 1; i < b; ++i) + { + auto const t{(Samples[i].chainage - Samples[a].chainage) / own_length}; + emit(shifted(Samples[i].position, Samples[i].direction, half(a) + (half(b) - half(a)) * t), Samples[i].direction); + } + continue; + } + pending = axis::side{}; + shares.clear(); + pending->from = Result.chainage.back(); + if (beside_length > own_length + kLonger) + { + for (std::size_t q = 1; q + 1 < beside.size(); ++q) + emit(shifted(beside[q].position, beside[q].direction, -eased(along[q], beside_length - along[q], half(a), half(b))), beside[q].direction); + pending->length = own_length; + for (auto i = a + 1; i < b; ++i) + { + pending->points.push_back(Samples[i].position); + pending->along.push_back(Samples[i].chainage - Samples[a].chainage); + shares.push_back(pending->along.back() / own_length); + } + } + else + { + for (auto i = a + 1; i < b; ++i) + emit(shifted(Samples[i].position, Samples[i].direction, eased(Samples[i].chainage - Samples[a].chainage, Samples[b].chainage - Samples[i].chainage, half(a), half(b))), Samples[i].direction); + pending->length = beside_length; + for (std::size_t q = 1; q + 1 < beside.size(); ++q) + { + pending->points.push_back(beside[q].position); + pending->along.push_back(along[q]); + shares.push_back(along[q] / beside_length); + } + } + } + auto const last{matched.back()}; + for (auto i = last + 1; i < Samples.size(); ++i) + emit(shifted(Samples[i].position, Samples[i].direction, half(last) * std::clamp(1.0 - (Samples[i].chainage - Samples[last].chainage) / kBlend, 0.0, 1.0)), Samples[i].direction); +} + +std::optional build(std::string const &Line) +{ + std::vector own; + for (std::size_t index = 0; index < g_marks.size(); ++index) + if (g_marks[index].line == Line) + own.push_back(index); + if (own.empty()) + return std::nullopt; + std::vector tracks; + for (auto const index : own) + tracks.push_back(track_near(g_marks[index].point, kNear)); + auto const start{std::find_if(tracks.begin(), tracks.end(), [](TTrack const *Track) { return Track != nullptr; })}; + if (start == tracks.end()) + return std::nullopt; + auto *first{*start}; + auto const first_mark{own[static_cast(start - tracks.begin())]}; + auto route{editor_track::run_route(*first, kReach)}; + if (route.spans.empty()) + return std::nullopt; + // the marks the main paths of the switches lead away from: the line goes their way + std::set tried; + std::vector samples; + pairing pairs; + auto const origin_of = [&](std::vector const &Samples) { + std::size_t origin{0}; + auto nearest{std::numeric_limits::max()}; + for (std::size_t i = 0; i < Samples.size(); ++i) + if (auto const distance{plan_distance(Samples[i].position, g_marks[first_mark].point)}; distance < nearest) + { + nearest = distance; + origin = i; + } + return origin; + }; + for (std::size_t round = 0; round <= own.size(); ++round) + { + samples = editor_track::sample_route(route, kStep); + pairs = pair_up(samples, route, origin_of(samples)); + std::size_t away{own.size()}; + for (std::size_t k = 0; k < own.size() && away == own.size(); ++k) + if (tracks[k] != nullptr && tried.count(k) == 0 && plan_gap(samples, g_marks[own[k]].point) > kOnAxis && plan_gap(pairs.partner, g_marks[own[k]].point) > kOnAxis) + away = k; + if (away == own.size()) + break; + tried.insert(away); + editor_track::route found; + std::string error; + if (false == editor_track::find_route(first, tracks[away], found, error) || found.spans.size() < 2) + continue; + auto const at{index_of(route.spans, first)}; + if (at == route.spans.size()) + break; + std::vector spans; + if (found.spans.front().forward == route.spans[at].forward) + { + spans.assign(route.spans.begin(), route.spans.begin() + at); + spans.insert(spans.end(), found.spans.begin(), found.spans.end()); + auto const more{onward(found.spans.back())}; + spans.insert(spans.end(), more.begin(), more.end()); + } + else + { + spans = flipped(onward(found.spans.back())); + auto const back{flipped(found.spans)}; + spans.insert(spans.end(), back.begin(), back.end()); + spans.insert(spans.end(), route.spans.begin() + at + 1, route.spans.end()); + } + route.spans = once(spans); + } + if (samples.size() < 2) + return std::nullopt; + axis result; + result.line = Line; + compose(result, samples, pairs); + for (auto const index : own) + { + axis::anchor anchor; + anchor.mark = index; + anchor.chainage = chainage_at(result, g_marks[index].point, anchor.gap); + if (anchor.gap > kOnAxis) + { + result.astray.push_back(index); + continue; + } + anchor.before = g_marks[index].km; + anchor.after = g_marks[index].beyond.value_or(g_marks[index].km); + result.anchors.push_back(anchor); + } + if (result.anchors.empty()) + return std::nullopt; + std::sort(result.anchors.begin(), result.anchors.end(), [](axis::anchor const &A, axis::anchor const &B) { return A.chainage < B.chainage; }); + // the km rise the way the marks grow, the way the only one says + auto const &low{result.anchors.front()}; + auto const &high{result.anchors.back()}; + if (result.anchors.size() > 1 && std::abs(high.before - low.before) > 1e-6) + result.growth = high.before > low.before ? 1 : -1; + else + result.growth = glm::dot(g_marks[low.mark].way, direction_on(result, low.chainage)) >= 0.0 ? 1 : -1; + return result; +} + +} // namespace + +void reset() +{ + g_marks.clear(); + g_base.clear(); + drop_axes(); + g_changed = false; + g_loaded = false; +} + +void load() +{ + if (g_loaded || scene::Layers.empty()) + return; + g_loaded = true; + g_marks.clear(); + drop_axes(); + for (auto const layer : scene::Layers.marked_layers(kMark)) + { + auto const &context{scene::Layers.layer(layer).context_insert()}; + for (auto const &text : scene::Layers.marked(layer, kMark)) + { + std::istringstream line{text}; + std::string keyword; + mark entry; + entry.layer = layer; + glm::dvec3 point{0.0}; + glm::dvec2 way{0.0, 1.0}; + line >> keyword; + if (keyword != "mark") + continue; + line >> entry.line >> point.x >> point.y >> point.z >> keyword >> way.x >> way.y >> keyword >> entry.km; + if (line.fail() || entry.line.empty() || false == std::isfinite(entry.km)) + continue; + while (line >> keyword) + if (keyword == "beyond") + { + double beyond{0.0}; + if (line >> beyond) + entry.beyond = beyond; + } + entry.point = context.to_world(point); + entry.way = plan_of(context.to_world(point + glm::dvec3{way.x, 0.0, way.y}) - entry.point); + entry.way = glm::length(entry.way) > 1e-12 ? glm::normalize(entry.way) : glm::dvec2{0.0, 1.0}; + g_marks.push_back(std::move(entry)); + } + } + g_base = g_marks; + g_changed = false; + if (false == g_marks.empty()) + WriteLog("Editor: " + std::to_string(g_marks.size()) + " kilometrage mark(s) read from the scenery", logtype::generic); +} + +std::vector const &marks() +{ + load(); + return g_marks; +} + +void put(mark Entry) +{ + load(); + auto *track{track_near(Entry.point, kNear)}; + Entry.layer = track != nullptr ? alignment_store::layer_of(*track) : scene::Layers.active(); + if (false == scene::Layers.valid(Entry.layer)) + Entry.layer = 1; + auto const layer{Entry.layer}; + g_marks.push_back(std::move(Entry)); + changed({layer}); +} + +void change(std::size_t const Index, mark Entry) +{ + load(); + if (Index >= g_marks.size()) + return; + Entry.layer = g_marks[Index].layer; + g_marks[Index] = std::move(Entry); + changed({g_marks[Index].layer}); +} + +void remove(std::size_t const Index) +{ + load(); + if (Index >= g_marks.size()) + return; + auto const layer{g_marks[Index].layer}; + g_marks.erase(g_marks.begin() + static_cast(Index)); + changed({layer}); +} + +std::optional> take_changes() +{ + if (false == g_changed) + return std::nullopt; + auto before{std::move(g_base)}; + g_base = g_marks; + g_changed = false; + return before; +} + +void settle() +{ + g_base = g_marks; + g_changed = false; +} + +void restore(std::vector Marks) +{ + std::set layers; + for (auto const *list : {&g_marks, &Marks}) + for (auto const &entry : *list) + layers.insert(entry.layer); + g_marks = std::move(Marks); + changed(layers); + settle(); +} + +void follow(std::size_t const Edits) +{ + if (Edits == g_edits) + return; + g_edits = Edits; + drop_axes(); +} + +std::size_t revision() +{ + return g_revision; +} + +std::vector lines() +{ + load(); + std::vector result; + for (auto const &entry : g_marks) + if (std::find(result.begin(), result.end(), entry.line) == result.end()) + result.push_back(entry.line); + std::sort(result.begin(), result.end()); + return result; +} + +axis const *axis_of(std::string const &Line) +{ + load(); + auto found{g_axes.find(Line)}; + if (found == g_axes.end()) + found = g_axes.emplace(Line, build(Line)).first; + return found->second.has_value() ? &*found->second : nullptr; +} + +double km_on(axis const &Axis, double const Chainage) +{ + auto const &anchors{Axis.anchors}; + if (anchors.empty()) + return 0.0; + auto const growth{static_cast(Axis.growth)}; + // the km of an anchor on the side of the lower chainage, and of the higher one + auto const lower = [&](axis::anchor const &Anchor) { return Axis.growth > 0 ? Anchor.before : Anchor.after; }; + auto const higher = [&](axis::anchor const &Anchor) { return Axis.growth > 0 ? Anchor.after : Anchor.before; }; + if (Chainage <= anchors.front().chainage) + return lower(anchors.front()) - growth * (anchors.front().chainage - Chainage) / 1000.0; + if (Chainage >= anchors.back().chainage) + return higher(anchors.back()) + growth * (Chainage - anchors.back().chainage) / 1000.0; + auto const next{std::upper_bound(anchors.begin(), anchors.end(), Chainage, [](double const Value, axis::anchor const &Anchor) { return Value < Anchor.chainage; })}; + auto const &to{*next}; + auto const &from{*std::prev(next)}; + auto const span{to.chainage - from.chainage}; + if (span < 1e-6) + return higher(from); + return higher(from) + (lower(to) - higher(from)) * (Chainage - from.chainage) / span; +} + +double km_on(axis const &Axis, axis::side const &Side, double const Along) +{ + auto const low{km_on(Axis, Side.from + 1e-6)}; + auto const high{km_on(Axis, Side.to - 1e-6)}; + auto const length{Side.length}; + if (length < 1e-6 || Side.to - Side.from < 1e-6) + return low; + // along the rising km: from the km the stretch starts at, the irregular hectometre before the one it ends at + auto const rising{Axis.growth > 0}; + auto const start{rising ? low : high}; + auto const end{rising ? high : low}; + auto const s{std::clamp(rising ? Along : length - Along, 0.0, length)}; + auto const scale{std::abs(end - start) / ((Side.to - Side.from) / 1000.0)}; // km of the axis to a km of the scenery + auto const regular{start + scale * length / 1000.0}; + auto const last{(std::ceil(std::min(regular, end) * 10.0 - 1e-9) - 1.0) / 10.0}; + if (end <= start || last <= start || scale <= 1e-9) + return start + (end - start) * s / length; + auto const at{(last - start) / scale * 1000.0}; + if (s <= at || at >= length) + return start + scale * s / 1000.0; + return last + (end - last) * (s - at) / (length - at); +} + +double chainage_of(axis const &Axis, glm::dvec3 const &Point, double &Gap) +{ + Gap = std::numeric_limits::max(); + auto result{0.0}; + auto const point{plan_of(Point)}; + for (std::size_t i = 1; i < Axis.points.size(); ++i) + { + auto const a{plan_of(Axis.points[i - 1])}; + auto const along{plan_of(Axis.points[i]) - a}; + auto const length2{glm::dot(along, along)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, along) / length2, 0.0, 1.0) : 0.0}; + if (auto const gap{glm::distance(point, a + along * t)}; gap < Gap) + { + Gap = gap; + result = Axis.chainage[i - 1] + (Axis.chainage[i] - Axis.chainage[i - 1]) * t; + } + } + return result; +} + +double chainage_at(axis const &Axis, glm::dvec3 const &Point, double &Gap) +{ + auto result{chainage_of(Axis, Point, Gap)}; + auto const point{plan_of(Point)}; + for (auto const &entry : Axis.sides) + for (std::size_t i = 1; i < entry.points.size(); ++i) + { + auto const a{plan_of(entry.points[i - 1])}; + auto const along{plan_of(entry.points[i]) - a}; + auto const length2{glm::dot(along, along)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, along) / length2, 0.0, 1.0) : 0.0}; + if (auto const gap{glm::distance(point, a + along * t)}; gap < Gap) + { + Gap = gap; + result = entry.chainage[i - 1] + (entry.chainage[i] - entry.chainage[i - 1]) * t; + } + } + return result; +} + +glm::dvec3 point_on(axis const &Axis, double const Chainage) +{ + if (Axis.points.empty()) + return glm::dvec3{0.0}; + auto const after{std::lower_bound(Axis.chainage.begin(), Axis.chainage.end(), Chainage)}; + if (after == Axis.chainage.begin()) + return Axis.points.front(); + if (after == Axis.chainage.end()) + return Axis.points.back(); + auto const i{static_cast(after - Axis.chainage.begin())}; + auto const step{Axis.chainage[i] - Axis.chainage[i - 1]}; + return glm::mix(Axis.points[i - 1], Axis.points[i], step > 1e-9 ? (Chainage - Axis.chainage[i - 1]) / step : 0.0); +} + +glm::dvec2 direction_on(axis const &Axis, double const Chainage) +{ + if (Axis.directions.empty()) + return {0.0, 1.0}; + auto const after{std::lower_bound(Axis.chainage.begin(), Axis.chainage.end(), Chainage)}; + auto const i{std::min(Axis.directions.size() - 1, static_cast(after - Axis.chainage.begin()))}; + return Axis.directions[i]; +} + +bool km_at(std::string const &Line, glm::dvec3 const &Point, double &Km, double const Reach) +{ + auto const *found{axis_of(Line)}; + if (found == nullptr) + return false; + double gap{0.0}; + auto const chainage{chainage_of(*found, Point, gap)}; + auto const point{plan_of(Point)}; + axis::side const *apart{nullptr}; + auto along{0.0}; + for (auto const &entry : found->sides) + for (std::size_t i = 1; i < entry.points.size(); ++i) + { + auto const a{plan_of(entry.points[i - 1])}; + auto const ab{plan_of(entry.points[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}; + if (auto const distance{glm::distance(point, a + ab * t)}; distance < gap) + { + gap = distance; + apart = &entry; + along = entry.along[i - 1] + (entry.along[i] - entry.along[i - 1]) * t; + } + } + if (gap > Reach) + return false; + Km = apart != nullptr ? km_on(*found, *apart, along) : km_on(*found, chainage); + return true; +} + +TTrack *track_near(glm::dvec3 const &Point, double const Reach) +{ + TTrack *best{nullptr}; + auto nearest{Reach}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 1) == 0 || false == editor_track::is_supported(*track) || track->m_paths.empty()) + continue; + auto const &ends{track->m_paths.front().points}; + auto const chord{plan_distance(ends[segment_data::point::start], ends[segment_data::point::end])}; + if (std::min(plan_distance(ends[segment_data::point::start], Point), plan_distance(ends[segment_data::point::end], Point)) > chord * 0.8 + nearest + 1.0) + continue; + auto const on{editor_track::point_at(*track, editor_track::nearest_parameter(*track, Point))}; + if (auto const distance{plan_distance(on, Point)}; distance < nearest) + { + nearest = distance; + best = track; + } + } + return best; +} + +std::string text(double const Km) +{ + auto result{format("%.3f", Km)}; + std::replace(result.begin(), result.end(), '.', ','); + return result; +} + +} // namespace kilometrage diff --git a/editor/editorKilometrage.hpp b/editor/editorKilometrage.hpp new file mode 100644 index 00000000..50f61b1b --- /dev/null +++ b/editor/editorKilometrage.hpp @@ -0,0 +1,111 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "scene/scenelayers.h" + +class TTrack; + +// the kilometrage of the railway lines, set in the track editor and kept in the scenery files as "//$k" comment lines: the +// points of a line where its km is known, from a post or a document, and the points where it jumps, with the km on both +// sides. the km run along the axis of a single-track line and between the tracks of a double-track one, along the longer track +// where the tracks part on formations of their own (PKP GK-1 6.1.12), the other tracks get the km straight across from them +namespace kilometrage +{ + +struct mark +{ + scene::layer_handle layer{null_handle}; + std::string line; // the number of the line, as 91 + glm::dvec3 point{0.0}; // on the axis of a track of the line + glm::dvec2 way{0.0, 1.0}; // the km rise this way along the line there, in the plan + double km{0.0}; // the km the point is reached at, coming along the rising km + std::optional beyond; // of a jump: the km on from the point +}; + +void reset(); +void load(); +std::vector const &marks(); +// a new mark, kept in the file of the layer of the track nearest to it +void put(mark Entry); +void change(std::size_t const Index, mark Entry); +void remove(std::size_t const Index); +// the marks as they were before the changes made since the last call, if there were any; the current ones become the base +std::optional> take_changes(); +// the current marks become the base, the changes made so far aren't a part of the undo history +void settle(); +// puts the marks back as they were, for undo and redo +void restore(std::vector Marks); +// the axes found are dropped when the tracks change: Edits counts the changes of the tracks made in the editor +void follow(std::size_t const Edits); +// counts the times the axes were dropped: what is made from them is made again when it changes +std::size_t revision(); + +// the axis a line is measured along +struct axis +{ + std::string line; + bool between{false}; // between the tracks of a double-track line, else along the track of the line + std::vector points; // a couple of metres apart + std::vector directions; // of the points, in the plan + std::vector chainage; // of the points, along them + int growth{1}; // the km rise along the chainage, -1: against it + struct anchor + { + std::size_t mark{0}; // of the marks + double chainage{0.0}; + double gap{0.0}; // m from the mark to the axis, in the plan + double before{0.0}; // the km the point is reached at, coming along the rising km + double after{0.0}; // the km on from it, other than before at a jump + }; + // the shorter track of a double-track line where the tracks run on formations of their own, the axis along the longer one: its + // km go by its own length from the end of the stretch the km rise from, the irregular hectometre at the other end takes up the + // difference (PKP GK-1 6.1.12) + struct side + { + std::vector points; + std::vector along; // m from the start of the stretch, along the track + std::vector chainage; // of the axis, by the share of the length: where a mark on the track anchors + double length{0.0}; // of the track between the ends of the stretch + double from{0.0}; // the chainage of the ends of the stretch + double to{0.0}; + }; + std::vector sides; + std::vector anchors; // by the chainage + std::vector astray; // the marks of the line away from its axis + double length() const { return chainage.empty() ? 0.0 : chainage.back(); } +}; +// the numbers of the lines with marks +std::vector lines(); +// the axis of the line through the tracks of its marks, nullptr while it has none; kept until the marks or the tracks change +axis const *axis_of(std::string const &Line); +double km_on(axis const &Axis, double const Chainage); +// where the point lies along the axis, Gap: how far from it in the plan +double chainage_of(axis const &Axis, glm::dvec3 const &Point, double &Gap); +// the same through the tracks running apart from the axis too, where they are nearer: the chainage the point takes the km of +double chainage_at(axis const &Axis, glm::dvec3 const &Point, double &Gap); +glm::dvec3 point_on(axis const &Axis, double const Chainage); +glm::dvec2 direction_on(axis const &Axis, double const Chainage); +// the km of a track running apart from the axis, Along: m from the start of the stretch +double km_on(axis const &Axis, axis::side const &Side, double const Along); +// the km of the line at the point, when it lies within the reach of the axis +bool km_at(std::string const &Line, glm::dvec3 const &Point, double &Km, double const Reach = 30.0); +// the track nearest to the point in the plan within the reach, nullptr for none +TTrack *track_near(glm::dvec3 const &Point, double const Reach); +// "100,234", as PKP PLK write the km +std::string text(double const Km); + +} // namespace kilometrage diff --git a/editor/icons/editor_icons.png b/editor/icons/editor_icons.png index c720314949e1c43ffdcc2d39690640ffc635a039..583b85fcfd0963b4cfaed11e754e5eeb30bd4dc8 100644 GIT binary patch literal 47923 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

+94g;`Ri|;SNhUC#~ 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 JYSgU5{tsfq-rxWL diff --git a/editor/icons/editor_icons.py b/editor/icons/editor_icons.py index 3015a1a0..29fc5884 100644 --- a/editor/icons/editor_icons.py +++ b/editor/icons/editor_icons.py @@ -340,6 +340,16 @@ def station(d): line(d, [(46, 36), (46, 56)], W - 10) +def kilometrage(d): + # a hectometre post by the track with its digits, and the way the km rise along the track + rect(d, 16, 4, 36, 40, outline=True, width=W - 6, radius=3) + line(d, [(22, 15), (30, 15)], W - 8) + line(d, [(22, 28), (30, 28)], W - 8) + line(d, [(26, 40), (26, 48)], W - 6) + line(d, [(4, 56), (48, 56)], W - 6) + arrowhead(d, 54, 56, 0, 12) + + def more(d): for x in (12, 32, 52): circle(d, x, 32, 6) @@ -390,7 +400,7 @@ def work_terrain(d): ICONS = [select, insert, brush, area_fill, copy_to_bank, translate, rotate, scale, local_space, ortho, perspective, sculpt, smooth, chunks, orthophoto, road, place, lanes, lay_track, switch, straight, curve, signal, objects, - vehicle, profile, speed, joints, infra, gauge, turntable, parallel, station, more, work_surroundings, + vehicle, profile, speed, joints, infra, gauge, turntable, parallel, station, kilometrage, more, work_surroundings, work_tracks, work_roads, work_terrain] diff --git a/lang/editor_pl.po b/lang/editor_pl.po index b0193fbd..842aa4b7 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5981,8 +5981,8 @@ msgstr "Rozjazd krzyżowy pojedynczy (Rkp) albo podwójny (Rkpd) na skrzyżowani msgid "Crossover, double, scissors, ladder" msgstr "Przejście rozjazdowe, podwójne przejście rozjazdowe, nożyce, ulica zwrotnicowa" -msgid "Turntable, signals, signalling, objects, trainset, profile, speed, joints, gauge" -msgstr "Obrotnica, sygnalizatory, SRK, obiekty, skład, profil, prędkość, styki, skrajnia" +msgid "Turntable, signals, signalling, kilometrage, objects, trainset, profile, speed, joints, gauge" +msgstr "Obrotnica, sygnalizatory, SRK, kilometraż, obiekty, skład, profil, prędkość, styki, skrajnia" msgid "No switch drive models" msgstr "Brak modeli napędów" @@ -10542,3 +10542,181 @@ msgstr[0] "%zu obiekt scenerii idzie za nimi" msgstr[1] "%zu obiekty scenerii idą za nimi" msgstr[2] "%zu obiektów scenerii idzie za nimi" +# kilometrage tool +msgid "Kilometrage" +msgstr "Kilometraż" + +msgid "The km of the railway lines: LMB on a track puts a point of the km there, a jump of the km too" +msgstr "Kilometraż linii kolejowych: LPM na torze wstawia tam punkt kilometra, także uskok kilometrażu" + +msgid "Click: a point of the km here" +msgstr "Klik: punkt kilometra tutaj" + +msgid "Id-1 § 19: the km shouldn't jump on a curve nor at a station, move the jump to a straight on the open line" +msgstr "Id-1 § 19: uskoku kilometrażu nie daje się na łuku ani na stacji, przesuń go na prostą na szlaku" + +msgid "Id-1 § 19: the km shouldn't jump on a curve, move the jump to a straight" +msgstr "Id-1 § 19: uskoku kilometrażu nie daje się na łuku, przesuń go na prostą" + +msgid "Id-1 § 19: the km shouldn't jump at a station, move the jump out to the open line" +msgstr "Id-1 § 19: uskoku kilometrażu nie daje się na stacji, przesuń go na szlak" + +msgid "line %s: km %s" +msgstr "linia %s: km %s" + +msgid "Give the number of the line first" +msgstr "Najpierw wpisz numer linii" + +msgid "A jump needs another km on from the point" +msgstr "Uskok wymaga innego kilometra za punktem" + +msgid "Line %s: km %s at the point" +msgstr "Linia %s: km %s w punkcie" + +msgid "The point of the km removed" +msgstr "Punkt kilometra usunięty" + +msgid "Click a track of the line first" +msgstr "Najpierw kliknij tor linii" + +msgid "No line through this track" +msgstr "Przez ten tor nie biegnie żadna linia" + +msgid "Fewer than two hectometre posts stand along the line, nothing to read" +msgstr "Wzdłuż linii stoją mniej niż dwa słupki hektometrowe, nie ma czego odczytać" + +msgid "The numbers of the posts don't go by 100 m anywhere along the line, nothing read" +msgstr "Numery słupków nigdzie wzdłuż linii nie idą co 100 m, nic nie odczytano" + +msgid "%zu posts read: %zu points of the km, %zu jumps" +msgstr "Odczytane słupki: %zu, punkty kilometra: %zu, uskoki: %zu" + +msgid "The km of a railway line: the points where the km is known, from a post or a document, and where it jumps. The km of the rest go from them along the axis of the line: between the tracks of a double-track one, along the longer track where the tracks run on formations of their own, the shorter one getting an irregular hectometre" +msgstr "Kilometraż linii kolejowej: punkty, w których kilometr jest znany ze słupka albo z dokumentu, i miejsca uskoków. Kilometraż reszty liczy się od nich wzdłuż osi linii: na dwutorowej po osi międzytorza, a gdzie tory biegną osobnymi torowiskami, po torze dłuższym, krótszy dostaje hektometr o nieprawidłowej długości" + +msgid "a point of the km there, or the one standing by it" +msgstr "punkt kilometra w tym miejscu albo ten, który już tam jest" + +msgid "jumps" +msgstr "uskoki" + +msgid "A point of the km" +msgstr "Punkt kilometra" + +msgid "Click a track where the km is known: by a post, a station or a bridge" +msgstr "Kliknij tor tam, gdzie znasz kilometr: przy słupku, stacji albo moście" + +msgid "The number of the railway line, as 91: a scenery can hold several lines, each with its own km" +msgstr "Numer linii kolejowej, np. 91: sceneria może mieć kilka linii, każda z własnym kilometrażem" + +msgid "The km of the point, as PKP PLK write it: 100,234 is km 100 and 234 m. At a jump: the km the point is reached at, coming along the rising km" +msgstr "Kilometr punktu, zapisany jak w PKP PLK: 100,234 to km 100 i 234 m. Przy uskoku: kilometr, z jakim dojeżdża się do punktu, jadąc w stronę rosnącego kilometrażu" + +msgid "Jump" +msgstr "Uskok" + +msgid "The km jumps at the point: a line built anew, shortened or lengthened keeps the km of the rest. The irregular hectometre is marked by a black stripe under the digit of the post (Id-1 § 19)" +msgstr "W tym punkcie kilometraż skacze: przebudowana, skrócona albo wydłużona linia zachowuje kilometraż reszty. Nieregularny hektometr oznacza się czarnym paskiem pod cyfrą słupka (Id-1 § 19)" + +msgid "Km on" +msgstr "Km dalej" + +msgid "The km on from the point, along the rising km" +msgstr "Kilometr za punktem, w stronę rosnącego kilometrażu" + +msgid "Rise this way" +msgstr "Rośnie w tę stronę" + +msgid "The km rise the way the arrow at the point shows" +msgstr "Kilometraż rośnie tam, gdzie pokazuje strzałka w punkcie" + +msgid "Rise the other way" +msgstr "Rośnie w drugą stronę" + +msgid "The km rise against the arrow" +msgstr "Kilometraż rośnie przeciwnie do strzałki" + +msgid "Change the point" +msgstr "Zmień punkt" + +msgid "Put the point of the km" +msgstr "Wstaw punkt kilometra" + +msgid "The point goes to the file of the scenery with the track, the simulation skips it. One step of undo" +msgstr "Punkt trafia do pliku scenerii z tym torem, symulacja go pomija. Jeden krok cofania" + +msgid "Remove the point" +msgstr "Usuń punkt" + +msgid "One step of undo" +msgstr "Jeden krok cofania" + +msgid "From the posts" +msgstr "Ze słupków" + +msgid "Read the km from the posts" +msgstr "Odczytaj kilometraż ze słupków" + +msgid "Reads the hectometre posts standing along the line through the track clicked: a point of the km at the first and the last post of each run numbered by 100 m, a jump where two runs meet. The points of the line along it before give way. One step of undo" +msgstr "Odczytuje słupki hektometrowe stojące wzdłuż linii przez kliknięty tor: punkt kilometra na pierwszym i ostatnim słupku każdego ciągu numerowanego co 100 m, uskok tam, gdzie dwa ciągi się stykają. Wcześniejsze punkty tej linii na tym odcinku ustępują nowym. Jeden krok cofania" + +msgid "The points of the lines" +msgstr "Punkty linii" + +msgid "None yet" +msgstr "Jeszcze żadnych" + +msgid "No track by the points of the line" +msgstr "Przy punktach linii nie ma toru" + +msgid "%.1f km of the axis, %s" +msgstr "%.1f km osi, %s" + +msgid "between the tracks" +msgstr "w osi międzytorza" + +msgid "In the scenery" +msgstr "W scenerii" + +msgid "By the km" +msgstr "Wg kilometrażu" + +msgid "Off by" +msgstr "Różnica" + +msgid "How much longer the stretch is in the scenery than by the km: the km between the points are stretched to it evenly" +msgstr "O ile odcinek w scenerii jest dłuższy niż wg kilometrażu: kilometry między punktami rozciąga się na nim równo" + +msgid "The point is away from the axis of the line the other points make: the main paths of the switches don't lead to it" +msgstr "Punkt leży z dala od osi linii wyznaczonej przez pozostałe punkty: zasadnicze tory rozjazdów do niego nie prowadzą" + +msgid "%zu post doesn't fit its neighbours and was left out" +msgid_plural "%zu posts don't fit their neighbours and were left out" +msgstr[0] "%zu słupek nie pasuje do sąsiednich i został pominięty" +msgstr[1] "%zu słupki nie pasują do sąsiednich i zostały pominięte" +msgstr[2] "%zu słupków nie pasuje do sąsiednich i zostało pominiętych" + +msgid "%zu jump lies on a curve or at a station, Id-1 § 19 wants none there" +msgid_plural "%zu jumps lie on a curve or at a station, Id-1 § 19 wants none there" +msgstr[0] "%zu uskok leży na łuku albo na stacji, Id-1 § 19 tego nie dopuszcza" +msgstr[1] "%zu uskoki leżą na łuku albo na stacji, Id-1 § 19 tego nie dopuszcza" +msgstr[2] "%zu uskoków leży na łuku albo na stacji, Id-1 § 19 tego nie dopuszcza" + +msgid "a point of the km there" +msgstr "punkt kilometra w tym miejscu" + +msgid "Mouse over a track" +msgstr "Mysz nad torem" + +msgid "its km by the cursor" +msgstr "jego kilometr przy kursorze" + +msgid "%zu stretch with the tracks on formations of their own" +msgid_plural "%zu stretches with the tracks on formations of their own" +msgstr[0] "%zu odcinek z torami na osobnych torowiskach" +msgstr[1] "%zu odcinki z torami na osobnych torowiskach" +msgstr[2] "%zu odcinków z torami na osobnych torowiskach" + +msgid "Where the tracks part more than 15 m and come together again, the km run along the longer one, the shorter one goes by its own length and its last hectometre before they meet takes up the difference, as PKP GK-1 gives it (6.1.12)" +msgstr "Gdzie tory rozchodzą się na więcej niż 15 m i znów schodzą, kilometraż biegnie po dłuższym, krótszy liczy się po własnej długości, a różnicę bierze jego ostatni hektometr przed zejściem, jak podaje PKP GK-1 (6.1.12)" + diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index c7163477..5731f748 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -843,17 +843,18 @@ bool node_layers::pending() const namespace { // marks of the lines the editor keeps track of -std::array const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c", "//$o", "//$s"}; +std::array const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c", "//$o", "//$s", "//$k"}; // other tools put lines of their own after some of these marks: Rainsted its includes after //$r, the launcher the descriptions // of the trainsets after //$o. The editor's lines of them are told by their first word bool editor_line(char const *Mark, std::string const &Text) { - static std::array>, 4> const words{{ + static std::array>, 5> const words{{ {"//$h", {"#", "alignment", "source", "ends", "norms", "pieces", "vertex", "element", "end"}}, {"//$r", {"#", "switch", "slip", "crossing", "double", "connection"}}, {"//$o", {"on"}}, {"//$s", {"#", "station"}}, + {"//$k", {"#", "mark"}}, }}; auto const lookup{std::find_if(words.begin(), words.end(), [&](auto const &Entry) { return Entry.first == Mark; })}; if (lookup == words.end()) From 7cc8e5bb9aa7cfc1a7799cecab1b85457a8ec790 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 14:16:42 +0200 Subject: [PATCH 054/145] feat(editor): the kilometrage tool leads a line through a station along the tracks chosen, the straight paths of the switches by default, from the track chosen on along them until the line meets its tracks again or on and away at a junction, kept as //$k via lines; a track beside that comes back onto the line between the stretches it runs beside it or both ways is a track of a station, not the other track of a double-track line --- application/editormode.h | 4 +- application/editormodekm.cpp | 170 +++++++++++++++++++- application/editormodekm.h | 12 ++ application/editormodetrack.cpp | 2 +- editor/editorKilometrage.cpp | 272 +++++++++++++++++++++++++++++--- editor/editorKilometrage.hpp | 40 ++++- editor/editorTrack.cpp | 4 +- editor/editorTrack.hpp | 5 +- lang/editor_pl.po | 60 +++++++ scene/scenelayers.cpp | 2 +- 10 files changed, 533 insertions(+), 38 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 4c31165f..ccb9ea1d 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -163,7 +163,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::vector profile_library; std::optional> alignments; // the kept horizontal alignments changed too, these were the ones before std::optional> switch_records; // the kept switches changed too, these were the ones before - std::optional> kilometrage; // the marks of the kilometrage before the change (Action::Kilometrage) + std::optional kilometrage; // the kilometrage before the change (Action::Kilometrage) std::vector copies; // models an array added to the one it was made of bool joined{false}; // undone and redone together with the change before it std::shared_ptr terrain; // the heightmap terrain from before the change (Action::Terrain) @@ -188,7 +188,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void terrain_history_step(); // puts the terrain back as the step recorded it, the step left with the state it replaced void restore_terrain(EditorSnapshot &Snapshot); - // swaps the marks of the kilometrage with the ones the snapshot keeps + // swaps the kilometrage with the one the snapshot keeps void restore_kilometrage(EditorSnapshot &Snapshot); bool m_history_busy{false}; // undo or redo under way void handle_brush_mouse_hold(int Action, int Button); diff --git a/application/editormodekm.cpp b/application/editormodekm.cpp index f28c2bf3..c851fb0d 100644 --- a/application/editormodekm.cpp +++ b/application/editormodekm.cpp @@ -278,7 +278,9 @@ void km_tool::pick(TTrack &Track, glm::dvec3 const &Point) entry.point = editor_track::point_at(Track, t); auto const ahead{plan_of(editor_track::point_at(Track, std::min(1.0, t + 0.01)) - editor_track::point_at(Track, std::max(0.0, t - 0.01)))}; entry.along = glm::length(ahead) > 1e-9 ? glm::normalize(ahead) : glm::dvec2{0.0, 1.0}; + entry.path = kilometrage::path_near(Track, Point, entry.on); m_status.clear(); + m_route_status.clear(); m_edited = -1; auto const &marks{kilometrage::marks()}; auto nearest{kPickReach}; @@ -400,6 +402,84 @@ void km_tool::show(std::size_t const Index) m_view.fly_to(point, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 40.0); } +void km_tool::show_via(std::size_t const Index) +{ + auto const &vias{kilometrage::vias()}; + if (Index >= vias.size()) + return; + auto const point{vias[Index].point}; + auto const line{vias[Index].line}; + int path{0}; + if (auto *track{kilometrage::track_near(point, 6.0, path)}; track != nullptr) + { + pick(*track, point); + m_line = line; + } + m_view.fly_to(point, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 40.0); +} + +int km_tool::via_here() const +{ + if (false == m_pick.has_value() || m_pick->track == nullptr) + return -1; + auto const &vias{kilometrage::vias()}; + for (std::size_t index = 0; index < vias.size(); ++index) + { + if (vias[index].line != m_line) + continue; + int path{0}; + if (kilometrage::track_near(vias[index].point, 6.0, path) == m_pick->track && path == m_pick->path) + return static_cast(index); + } + return -1; +} + +void km_tool::lead_along() +{ + m_route_status.clear(); + if (false == m_pick.has_value() || m_pick->track == nullptr) + return; + auto const line{line_of(m_line)}; + if (line.empty()) + { + m_route_status = STR("Give the number of the line first"); + return; + } + auto const astray_of = [&]() { + auto const *axis{kilometrage::axis_of(line)}; + return axis != nullptr ? axis->astray.size() : kilometrage::marks().size(); + }; + auto const before{kilometrage::current()}; + auto const astray{astray_of()}; + kilometrage::via entry; + entry.line = line; + entry.point = m_pick->on; + kilometrage::put(entry); + auto const *axis{kilometrage::axis_of(line)}; + if (axis == nullptr || false == axis->runs_along(m_pick->track, m_pick->path)) + { + kilometrage::restore(before); + m_route_status = STR("The line can't run along this track: no way leads from it onto the line"); + return; + } + m_line = line; + m_history.push_kilometrage_snapshot(); + m_route_status = format(STR_C("Line %s runs along this track now"), line.c_str()); + if (auto const lost{astray_of()}; lost > astray) + m_route_status += "\n" + format(STR_P("%zu point of the km is off the line now: the track leads it away from it", "%zu points of the km are off the line now: the track leads it away from them", lost - astray), lost - astray); +} + +void km_tool::drop_via() +{ + m_route_status.clear(); + auto const index{via_here()}; + if (index < 0) + return; + kilometrage::remove_via(static_cast(index)); + m_history.push_kilometrage_snapshot(); + m_route_status = format(STR_C("Line %s runs along the straight paths of the switches here again"), m_line.c_str()); +} + void km_tool::read_posts() { m_status.clear(); @@ -635,7 +715,36 @@ void km_tool::render_options() } } - step_heading(2, STR_C("From the posts")); + step_heading(2, STR_C("The way through a station")); + auto const *line_axis{m_line.empty() ? nullptr : kilometrage::axis_of(m_line)}; + if (false == m_pick.has_value() || m_pick->track == nullptr) + ImGui::TextDisabled("%s", STR_C("Through a station the km run along the straight paths of the switches. Click a track of the station to lead them along it instead")); + else if (line_axis == nullptr) + ImGui::TextDisabled("%s", STR_C("Put a point of the km of the line first")); + else if (auto const via{via_here()}; via >= 0) + { + if (std::find(line_axis->blocked.begin(), line_axis->blocked.end(), static_cast(via)) != line_axis->blocked.end()) + ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f), "%s", STR_C("The line doesn't run along this track: another track chosen leads it elsewhere")); + else + ImGui::TextUnformatted(format(STR_C("Line %s runs along this track, as chosen"), m_line.c_str()).c_str()); + if (ImGui::Button(STR_C("Back onto the straight paths"), ImVec2(-1.0f, 0.0f))) + drop_via(); + item_tooltip("The track chosen is forgotten, the line runs along the straight paths of the switches here again. One step of undo"); + } + else if (line_axis->runs_along(m_pick->track, m_pick->path)) + ImGui::TextDisabled("%s", format(STR_C("Line %s runs along this track"), m_line.c_str()).c_str()); + else + { + ImGui::TextDisabled("%s", format(STR_C("Line %s doesn't run along this track"), m_line.c_str()).c_str()); + if (ImGui::Button(STR_C("Lead the km along this track"), ImVec2(-1.0f, 0.0f))) + lead_along(); + item_tooltip("The line runs along the track clicked, along the path of the switch clicked, and on along the straight paths of the switches " + "until it meets its tracks again; where it doesn't, as at a junction, it goes on that way. One step of undo"); + } + if (false == m_route_status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_route_status.c_str()); + + step_heading(3, STR_C("From the posts")); if (ImGui::Button(STR_C("Read the km from the posts"), ImVec2(-1.0f, 0.0f))) read_posts(); item_tooltip("Reads the hectometre posts standing along the line through the track clicked: a point of the km at the first and the last " @@ -643,7 +752,7 @@ void km_tool::render_options() if (false == m_status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); - step_heading(3, STR_C("The points of the lines")); + step_heading(4, STR_C("The points of the lines")); if (lines.empty()) { ImGui::TextDisabled("%s", STR_C("None yet")); @@ -667,6 +776,22 @@ void km_tool::render_options() item_tooltip("Where the tracks part more than 15 m and come together again, the km run along the longer one, the shorter one goes by " "its own length and its last hectometre before they meet takes up the difference, as PKP GK-1 gives it (6.1.12)"); } + auto const &vias{kilometrage::vias()}; + for (std::size_t index = 0; index < vias.size(); ++index) + { + if (vias[index].line != line) + continue; + auto const blocked{std::find(axis->blocked.begin(), axis->blocked.end(), index) != axis->blocked.end()}; + double km{0.0}; + auto const label{kilometrage::km_at(line, vias[index].point, km, kReadoutReach) ? format(STR_C("Along the track chosen by km %s"), kilometrage::text(km).c_str()) : std::string{STR_C("Along the track chosen")}}; + if (blocked) + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.4f, 1.0f)); + if (ImGui::Selectable((label + "##kmvia" + std::to_string(index)).c_str(), false)) + show_via(index); + if (blocked) + ImGui::PopStyleColor(); + item_tooltip(blocked ? "The line doesn't run along this track: another track chosen leads it elsewhere" : "Through the station the line runs along this track, not along the straight paths of the switches"); + } auto const rows{axis->anchors.size() + axis->astray.size()}; if (false == ImGui::BeginTable(("##kmmarks" + line).c_str(), 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(260.0f, ImGui::GetTextLineHeightWithSpacing() * (rows + 1.5f))))) continue; @@ -776,6 +901,23 @@ void km_tool::draw_overlay() const if (projection.project(top, screen)) draw_label(drawlist, screen, colour, IM_COL32(16, 18, 22, 255), mark.line + ": " + mark_text(mark)); } + // the tracks chosen for the way through a station: a rhombus on the track + auto const &vias{kilometrage::vias()}; + for (std::size_t index = 0; index < vias.size(); ++index) + { + auto const &via{vias[index]}; + ImVec2 screen; + if (false == near_enough(via.point, kLabelRange * 2.0) || false == projection.project(via.point + lift, screen)) + continue; + auto blocked{false}; + if (auto const *axis{kilometrage::axis_of(via.line)}; axis != nullptr) + blocked = std::find(axis->blocked.begin(), axis->blocked.end(), index) != axis->blocked.end(); + auto const colour{blocked ? kAstray : kAxis}; + auto const r{8.0f}; + drawlist->AddQuadFilled(ImVec2(screen.x, screen.y - r), ImVec2(screen.x + r, screen.y), ImVec2(screen.x, screen.y + r), ImVec2(screen.x - r, screen.y), colour); + drawlist->AddQuad(ImVec2(screen.x, screen.y - r), ImVec2(screen.x + r, screen.y), ImVec2(screen.x, screen.y + r), ImVec2(screen.x - r, screen.y), IM_COL32(16, 18, 22, 255), 1.5f); + draw_label(drawlist, ImVec2(screen.x, screen.y - r - 2.0f), colour, IM_COL32(16, 18, 22, 255), format(STR_C("%s: this way"), via.line.c_str())); + } if (m_pick.has_value() && m_edited < 0) { auto const foot{m_pick->point + lift}; @@ -800,8 +942,11 @@ editor_selftest_result km_tool::selftest(std::string const &Command, std::istrin return editor_selftest_result::handled; } auto const at{values.count("at") ? std::atof(values.at("at").c_str()) : 0.5}; - pick(*track, editor_track::point_at(*track, at)); - WriteLog(format("SELFTEST kmpick %s: line %s km %s jump %d against %d edited %d at %.1f %.1f", track->name().c_str(), m_line.c_str(), kilometrage::text(m_km).c_str(), m_jump ? 1 : 0, m_against ? 1 : 0, m_edited, m_pick->point.x, m_pick->point.z)); + auto const path{values.count("path") ? std::atoi(values.at("path").c_str()) : 0}; + pick(*track, path > 0 && path < static_cast(track->m_paths.size()) ? geometry::bezier{track->m_paths[path]}.point(at) : editor_track::point_at(*track, at)); + auto const *axis{m_line.empty() ? nullptr : kilometrage::axis_of(m_line)}; + WriteLog(format("SELFTEST kmpick %s: line %s km %s jump %d against %d edited %d at %.1f %.1f path %d along %d via %d", track->name().c_str(), m_line.c_str(), kilometrage::text(m_km).c_str(), m_jump ? 1 : 0, m_against ? 1 : 0, m_edited, m_pick->point.x, m_pick->point.z, + m_pick->path, axis != nullptr && axis->runs_along(m_pick->track, m_pick->path) ? 1 : 0, via_here())); return editor_selftest_result::handled; } if (Command == "kmset") @@ -821,6 +966,21 @@ editor_selftest_result km_tool::selftest(std::string const &Command, std::istrin WriteLog("SELFTEST kmset: " + m_status); return editor_selftest_result::handled; } + if (Command == "kmvia") + { + // kmvia [line=] [remove=1]: the line led along the track picked, or back onto the straight paths there + auto const values{selftest_values(Words)}; + if (auto const line{values.find("line")}; line != values.end()) + m_line = line->second; + if (values.count("remove") > 0) + drop_via(); + else + lead_along(); + auto text{m_route_status}; + std::replace(text.begin(), text.end(), '\n', ';'); + WriteLog("SELFTEST kmvia: " + text); + return editor_selftest_result::handled; + } if (Command == "kmremove") { remove(); @@ -877,7 +1037,7 @@ editor_selftest_result km_tool::selftest(std::string const &Command, std::istrin for (auto const &side : axis->sides) WriteLog(format("SELFTEST kmlist apart: %.1f m of track by %.1f m of axis (%.1f..%.1f), km %s..%s", side.length, side.to - side.from, side.from, side.to, kilometrage::text(kilometrage::km_on(*axis, side, 0.0)).c_str(), kilometrage::text(kilometrage::km_on(*axis, side, side.length)).c_str())); - WriteLog(format("SELFTEST kmlist line %s: axis %.0f m %s growth %d, %zu astray, km %s..%s:", line.c_str(), axis->length(), axis->between ? "between" : "along", axis->growth, axis->astray.size(), kilometrage::text(kilometrage::km_on(*axis, 0.0)).c_str(), + WriteLog(format("SELFTEST kmlist line %s: axis %.0f m %s growth %d, %zu astray, %zu blocked, km %s..%s:", line.c_str(), axis->length(), axis->between ? "between" : "along", axis->growth, axis->astray.size(), axis->blocked.size(), kilometrage::text(kilometrage::km_on(*axis, 0.0)).c_str(), kilometrage::text(kilometrage::km_on(*axis, axis->length())).c_str()) + list); } diff --git a/application/editormodekm.h b/application/editormodekm.h index 8b3c0b38..2e22bafb 100644 --- a/application/editormodekm.h +++ b/application/editormodekm.h @@ -47,6 +47,8 @@ class km_tool final : public editor_tool TTrack *track{nullptr}; glm::dvec3 point{0.0}; // on the axis of the track glm::dvec2 along{0.0, 1.0}; // of the track there, in the plan + int path{0}; // of the switch, the one nearest to the click + glm::dvec3 on{0.0}; // the point of it nearest to the click }; std::optional m_pick; int m_edited{-1}; // of the marks, the one standing by the point picked, -1 for a new one @@ -56,6 +58,7 @@ class km_tool final : public editor_tool double m_beyond{0.0}; bool m_against{false}; // the km rise against the track picked std::string m_status; + std::string m_route_status; // of the way through a station // what the overlay draws, made again when the axes change struct tick { @@ -80,6 +83,15 @@ class km_tool final : public editor_tool void remove(); // the point of the km picked and the camera over it void show(std::size_t const Index); + // the line of the field led along the track picked, along the path of the switch picked, instead of the straight paths of the + // switches; one step of the history + void lead_along(); + // the line back on the straight paths of the switches by the track picked; one step of the history + void drop_via(); + // of the vias of the line of the field, the one on the track picked, -1 for none + int via_here() const; + // the track of the via picked and the camera over it + void show_via(std::size_t const Index); // the hectometre posts standing along the line through the track picked read into points of the km: the first and the last // post of each run of them numbered by 100 m, a jump where the runs meet; one step of the history void read_posts(); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index e8b4dec0..1470d8e5 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -314,7 +314,7 @@ void editor_mode::restore_kilometrage(EditorSnapshot &Snapshot) { if (false == Snapshot.kilometrage.has_value()) return; - auto current{kilometrage::marks()}; + auto current{kilometrage::current()}; kilometrage::restore(std::move(*Snapshot.kilometrage)); Snapshot.kilometrage = std::move(current); } diff --git a/editor/editorKilometrage.cpp b/editor/editorKilometrage.cpp index b6f354c7..860612ff 100644 --- a/editor/editorKilometrage.cpp +++ b/editor/editorKilometrage.cpp @@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include #include #include @@ -46,9 +47,11 @@ double const kCell{10.0}; // m, of the squares the track beside is sorted into double const kApart{15.0}; // m between the tracks, of a double-track line on two formations double const kBlend{100.0}; // m, the axis takes to go over from between the tracks to one of them double const kLonger{1.0}; // m, of the track beside longer than the track of the line, for the axis to follow it +double const kDetour{20000.0}; // m, of the tracks followed each way from a via back onto the line std::vector g_marks; -std::vector g_base; // as they were before the changes not yet in the undo history +std::vector g_vias; +state g_base; // as it was before the changes not yet in the undo history bool g_changed{false}; bool g_loaded{false}; std::size_t g_edits{0}; @@ -70,18 +73,22 @@ std::string word(std::string const &Text) // format of the lines, coordinates in the space of the file: // mark way km [beyond ] +// via void store(scene::layer_handle const Layer) { if (false == scene::Layers.valid(Layer)) return; auto const &context{scene::Layers.layer(Layer).context_insert()}; std::vector lines; + auto const heading = [&]() { + if (lines.empty()) + lines.emplace_back("# kilometrage of the lines set in the track editor, the simulation skips these lines"); + }; for (auto const &entry : g_marks) { if (entry.layer != Layer) continue; - if (lines.empty()) - lines.emplace_back("# kilometrage of the lines set in the track editor, the simulation skips these lines"); + heading(); auto const point{context.to_local(entry.point)}; auto const ahead{context.to_local(entry.point + glm::dvec3{entry.way.x, 0.0, entry.way.y})}; auto way{plan_of(ahead - point)}; @@ -91,6 +98,14 @@ void store(scene::layer_handle const Layer) text += format(" beyond %.3f", *entry.beyond); lines.push_back(text); } + for (auto const &entry : g_vias) + { + if (entry.layer != Layer) + continue; + heading(); + auto const point{context.to_local(entry.point)}; + lines.push_back(format("via %s %.4f %.4f %.4f", word(entry.line).c_str(), point.x, point.y, point.z)); + } scene::Layers.mark(Layer, kMark, std::move(lines)); } @@ -139,6 +154,79 @@ std::vector once(std::vector const &Spans) return result; } +// the route runs along the track of the via, along the path of a switch it stands on +bool runs_by(editor_track::route const &Route, std::size_t const Via) +{ + int path{0}; + auto const *track{track_near(g_vias[Via].point, kNear, path)}; + return track != nullptr && std::any_of(Route.spans.begin(), Route.spans.end(), [&](span const &Entry) { return Entry.track == track && Entry.path == path; }); +} + +// the route led along the tracks of the vias: from the track of each the run along the straight paths of the switches both ways +// back onto the route takes the place of the stretch of the route between where it comes back; where it comes back one way only, +// as at a junction, the route goes on along it from there +void through(editor_track::route &Route, std::vector const &Vias) +{ + for (auto const index : Vias) + { + int path{0}; + auto *track{track_near(g_vias[index].point, kNear, path)}; + if (track == nullptr || runs_by(Route, index)) + continue; + auto const run{editor_track::run_route(*track, kDetour, path)}; + auto const at{index_of(run.spans, track)}; + if (at == run.spans.size()) + continue; + auto const none{Route.spans.size()}; + auto const on = [&](std::size_t const Index) { return index_of(Route.spans, run.spans[Index].track); }; + std::optional low; + for (auto i = at; i-- > 0;) + if (on(i) != none) + { + low = i; + break; + } + std::optional high; + for (auto i = at + 1; i < run.spans.size(); ++i) + if (on(i) != none) + { + high = i; + break; + } + auto const part = [&](std::size_t const From, std::size_t const To) { return std::vector{run.spans.begin() + From, run.spans.begin() + To}; }; + auto const head = [&](std::size_t const To) { return std::vector{Route.spans.begin(), Route.spans.begin() + To}; }; + auto const tail = [&](std::size_t const From) { return std::vector{Route.spans.begin() + From, Route.spans.end()}; }; + auto const joined = [](std::vector> const &Parts) { + std::vector result; + for (auto const &entry : Parts) + result.insert(result.end(), entry.begin(), entry.end()); + return result; + }; + std::vector spans; + if (low && high) + { + auto const from{on(*low)}; + auto const to{on(*high)}; + if (from == to) + continue; + spans = from < to ? joined({head(from), part(*low, *high + 1), tail(to + 1)}) : joined({head(to), flipped(part(*low, *high + 1)), tail(from + 1)}); + } + else if (low) + { + auto const from{on(*low)}; + spans = run.spans[*low].forward == Route.spans[from].forward ? joined({head(from), part(*low, run.spans.size())}) : joined({flipped(part(*low, run.spans.size())), tail(from + 1)}); + } + else if (high) + { + auto const to{on(*high)}; + spans = run.spans[*high].forward == Route.spans[to].forward ? joined({part(0, *high + 1), tail(to + 1)}) : joined({head(to), flipped(part(0, *high + 1))}); + } + else + continue; + Route.spans = once(spans); + } +} + double plan_gap(std::vector const &Samples, glm::dvec3 const &Point) { auto best{std::numeric_limits::max()}; @@ -150,18 +238,20 @@ double plan_gap(std::vector const &Samples, glm::dve // the track beside the line: of each sample of the line the sample of it beside, where they run side by side struct pairing { + editor_track::route route; // of the track beside std::vector partner; // along the track beside, empty for none std::vector match; // of each sample, -1 where the track beside isn't there std::vector across; // to the sample beside, along the normal of the sample }; -pairing pair_with(std::vector const &Samples, TTrack &Beside) +pairing pair_with(std::vector const &Samples, TTrack &Beside, std::vector const &Vias) { pairing result; result.match.assign(Samples.size(), -1); result.across.assign(Samples.size(), 0.0); - auto other{editor_track::run_route(Beside, kReach)}; - result.partner = editor_track::sample_route(other, kStep); + result.route = editor_track::run_route(Beside, kReach); + through(result.route, Vias); + result.partner = editor_track::sample_route(result.route, kStep); auto const &partner{result.partner}; auto const cell = [](glm::dvec3 const &Point) { return std::make_pair(static_cast(std::floor(Point.x / kCell)), static_cast(std::floor(Point.z / kCell))); }; std::map, std::vector> grid; @@ -195,9 +285,29 @@ pairing pair_with(std::vector const &Samples, TTrack return result; } +// the track beside comes back onto the line between the stretches it runs beside it, or both ways, as a track of a station or a +// passing loop does: not the other track +bool looped(pairing const &Pairs, std::unordered_set const &Own) +{ + auto low{Pairs.partner.size()}; + std::size_t high{0}; + for (auto const match : Pairs.match) + if (match >= 0) + { + low = std::min(low, static_cast(match)); + high = std::max(high, static_cast(match)); + } + auto const on_line = [&](editor_track::route_sample const &Sample) { return Sample.span < Pairs.route.spans.size() && Own.count(Pairs.route.spans[Sample.span].track) > 0; }; + if (low >= Pairs.partner.size()) + return false; + auto const first{Pairs.partner.begin() + static_cast(low)}; + auto const last{Pairs.partner.begin() + static_cast(high) + 1}; + return std::any_of(first, last, on_line) || (std::any_of(Pairs.partner.begin(), first, on_line) && std::any_of(last, Pairs.partner.end(), on_line)); +} + // the track beside the line, looked for by the first mark and then every 500 m along the line: a track the line doesn't run // through, the spacing of a double-track line away, beside it for 100 m at least -pairing pair_up(std::vector const &Samples, editor_track::route const &Route, std::size_t const Origin) +pairing pair_up(std::vector const &Samples, editor_track::route const &Route, std::size_t const Origin, std::vector const &Vias) { std::unordered_set own; for (auto const &entry : Route.spans) @@ -220,8 +330,8 @@ pairing pair_up(std::vector const &Samples, editor_t auto *track{track_near({probe.x, at.position.y, probe.y}, 1.6)}; if (track == nullptr || own.count(track) > 0 || false == tried.insert(track).second) continue; - auto result{pair_with(Samples, *track)}; - if (std::count_if(result.match.begin(), result.match.end(), [](int const Match) { return Match >= 0; }) * kStep >= 100.0) + auto result{pair_with(Samples, *track, Vias)}; + if (std::count_if(result.match.begin(), result.match.end(), [](int const Match) { return Match >= 0; }) * kStep >= 100.0 && false == looped(result, own)) return result; } } @@ -372,7 +482,12 @@ std::optional build(std::string const &Line) return std::nullopt; auto *first{*start}; auto const first_mark{own[static_cast(start - tracks.begin())]}; + std::vector passes; + for (std::size_t index = 0; index < g_vias.size(); ++index) + if (g_vias[index].line == Line) + passes.push_back(index); auto route{editor_track::run_route(*first, kReach)}; + through(route, passes); if (route.spans.empty()) return std::nullopt; // the marks the main paths of the switches lead away from: the line goes their way @@ -393,7 +508,10 @@ std::optional build(std::string const &Line) for (std::size_t round = 0; round <= own.size(); ++round) { samples = editor_track::sample_route(route, kStep); - pairs = pair_up(samples, route, origin_of(samples)); + // the track beside goes along the vias the line doesn't + std::vector beside; + std::copy_if(passes.begin(), passes.end(), std::back_inserter(beside), [&](std::size_t const Via) { return false == runs_by(route, Via); }); + pairs = pair_up(samples, route, origin_of(samples), beside); std::size_t away{own.size()}; for (std::size_t k = 0; k < own.size() && away == own.size(); ++k) if (tracks[k] != nullptr && tried.count(k) == 0 && plan_gap(samples, g_marks[own[k]].point) > kOnAxis && plan_gap(pairs.partner, g_marks[own[k]].point) > kOnAxis) @@ -424,12 +542,22 @@ std::optional build(std::string const &Line) spans.insert(spans.end(), route.spans.begin() + at + 1, route.spans.end()); } route.spans = once(spans); + through(route, passes); } if (samples.size() < 2) return std::nullopt; axis result; result.line = Line; compose(result, samples, pairs); + for (auto const &entry : route.spans) + result.paths.emplace_back(entry.track, entry.path); + auto const paired{std::any_of(pairs.match.begin(), pairs.match.end(), [](int const Match) { return Match >= 0; })}; + if (paired) + for (auto const &entry : pairs.route.spans) + result.paths.emplace_back(entry.track, entry.path); + for (auto const index : passes) + if (false == runs_by(route, index) && (false == paired || false == runs_by(pairs.route, index))) + result.blocked.push_back(index); for (auto const index : own) { axis::anchor anchor; @@ -462,7 +590,8 @@ std::optional build(std::string const &Line) void reset() { g_marks.clear(); - g_base.clear(); + g_vias.clear(); + g_base = {}; drop_axes(); g_changed = false; g_loaded = false; @@ -474,6 +603,7 @@ void load() return; g_loaded = true; g_marks.clear(); + g_vias.clear(); drop_axes(); for (auto const layer : scene::Layers.marked_layers(kMark)) { @@ -487,6 +617,18 @@ void load() glm::dvec3 point{0.0}; glm::dvec2 way{0.0, 1.0}; line >> keyword; + if (keyword == "via") + { + via entry; + entry.layer = layer; + glm::dvec3 point{0.0}; + line >> entry.line >> point.x >> point.y >> point.z; + if (line.fail() || entry.line.empty()) + continue; + entry.point = context.to_world(point); + g_vias.push_back(std::move(entry)); + continue; + } if (keyword != "mark") continue; line >> entry.line >> point.x >> point.y >> point.z >> keyword >> way.x >> way.y >> keyword >> entry.km; @@ -505,10 +647,10 @@ void load() g_marks.push_back(std::move(entry)); } } - g_base = g_marks; + g_base = current(); g_changed = false; if (false == g_marks.empty()) - WriteLog("Editor: " + std::to_string(g_marks.size()) + " kilometrage mark(s) read from the scenery", logtype::generic); + WriteLog("Editor: " + std::to_string(g_marks.size()) + " kilometrage mark(s) and " + std::to_string(g_vias.size()) + " via(s) read from the scenery", logtype::generic); } std::vector const &marks() @@ -517,6 +659,17 @@ std::vector const &marks() return g_marks; } +std::vector const &vias() +{ + load(); + return g_vias; +} + +state current() +{ + return {g_marks, g_vias}; +} + void put(mark Entry) { load(); @@ -549,29 +702,56 @@ void remove(std::size_t const Index) changed({layer}); } -std::optional> take_changes() +void put(via Entry) +{ + load(); + int path{0}; + auto *track{track_near(Entry.point, kNear, path)}; + Entry.layer = track != nullptr ? alignment_store::layer_of(*track) : scene::Layers.active(); + if (false == scene::Layers.valid(Entry.layer)) + Entry.layer = 1; + auto const layer{Entry.layer}; + g_vias.push_back(std::move(Entry)); + changed({layer}); +} + +void remove_via(std::size_t const Index) +{ + load(); + if (Index >= g_vias.size()) + return; + auto const layer{g_vias[Index].layer}; + g_vias.erase(g_vias.begin() + static_cast(Index)); + changed({layer}); +} + +std::optional take_changes() { if (false == g_changed) return std::nullopt; auto before{std::move(g_base)}; - g_base = g_marks; + g_base = current(); g_changed = false; return before; } void settle() { - g_base = g_marks; + g_base = current(); g_changed = false; } -void restore(std::vector Marks) +void restore(state State) { std::set layers; - for (auto const *list : {&g_marks, &Marks}) + for (auto const *list : {&g_marks, &State.marks}) for (auto const &entry : *list) layers.insert(entry.layer); - g_marks = std::move(Marks); + for (auto const *list : {&g_vias, &State.vias}) + for (auto const &entry : *list) + layers.insert(entry.layer); + g_marks = std::move(State.marks); + g_vias = std::move(State.vias); changed(layers); settle(); } @@ -769,6 +949,60 @@ TTrack *track_near(glm::dvec3 const &Point, double const Reach) return best; } +int path_near(TTrack const &Track, glm::dvec3 const &Point, glm::dvec3 &On) +{ + int result{0}; + auto nearest{std::numeric_limits::max()}; + On = Point; + for (int path = 0; path < static_cast(Track.m_paths.size()); ++path) + { + geometry::bezier const curve{Track.m_paths[path]}; + auto previous{curve.point(0.0)}; + for (int i = 1; i <= 64; ++i) + { + auto const next{curve.point(i / 64.0)}; + auto const ab{plan_of(next - previous)}; + auto const length2{glm::dot(ab, ab)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(plan_of(Point - previous), ab) / length2, 0.0, 1.0) : 0.0}; + auto const at{glm::mix(previous, next, t)}; + // where the paths of a switch run together, the first one + if (auto const distance{plan_distance(at, Point)}; distance < nearest - 1e-3) + { + nearest = distance; + result = path; + On = at; + } + previous = next; + } + } + return result; +} + +TTrack *track_near(glm::dvec3 const &Point, double const Reach, int &Path) +{ + TTrack *best{nullptr}; + auto nearest{Reach}; + Path = 0; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 1) == 0 || false == editor_track::is_supported(*track) || track->m_paths.empty()) + continue; + auto const &ends{track->m_paths.front().points}; + auto const chord{plan_distance(ends[segment_data::point::start], ends[segment_data::point::end])}; + if (std::min(plan_distance(ends[segment_data::point::start], Point), plan_distance(ends[segment_data::point::end], Point)) > chord * 0.8 + nearest + 1.0) + continue; + glm::dvec3 on{0.0}; + auto const path{path_near(*track, Point, on)}; + if (auto const distance{plan_distance(on, Point)}; distance < nearest) + { + nearest = distance; + best = track; + Path = path; + } + } + return best; +} + std::string text(double const Km) { auto result{format("%.3f", Km)}; diff --git a/editor/editorKilometrage.hpp b/editor/editorKilometrage.hpp index 50f61b1b..d266f6ee 100644 --- a/editor/editorKilometrage.hpp +++ b/editor/editorKilometrage.hpp @@ -9,9 +9,11 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include #include #include #include +#include #include #include @@ -22,7 +24,8 @@ class TTrack; // the kilometrage of the railway lines, set in the track editor and kept in the scenery files as "//$k" comment lines: the // points of a line where its km is known, from a post or a document, and the points where it jumps, with the km on both // sides. the km run along the axis of a single-track line and between the tracks of a double-track one, along the longer track -// where the tracks part on formations of their own (PKP GK-1 6.1.12), the other tracks get the km straight across from them +// where the tracks part on formations of their own (PKP GK-1 6.1.12), the other tracks get the km straight across from them. +// through a station the line runs along the straight paths of the switches, unless the tracks it runs along there are set namespace kilometrage { @@ -35,20 +38,38 @@ struct mark double km{0.0}; // the km the point is reached at, coming along the rising km std::optional beyond; // of a jump: the km on from the point }; +// a track a line runs along through a station, other than the one the straight paths of the switches lead it along: from it +// the line goes on along the straight paths until it meets the rest of it again, or on and away where it doesn't +struct via +{ + scene::layer_handle layer{null_handle}; + std::string line; + glm::dvec3 point{0.0}; // on the axis of the track, of the path of a switch the line takes +}; +// all of the kilometrage set, for the undo history +struct state +{ + std::vector marks; + std::vector vias; +}; void reset(); void load(); std::vector const &marks(); +std::vector const &vias(); +state current(); // a new mark, kept in the file of the layer of the track nearest to it void put(mark Entry); void change(std::size_t const Index, mark Entry); void remove(std::size_t const Index); -// the marks as they were before the changes made since the last call, if there were any; the current ones become the base -std::optional> take_changes(); -// the current marks become the base, the changes made so far aren't a part of the undo history +void put(via Entry); +void remove_via(std::size_t const Index); +// the kilometrage as it was before the changes made since the last call, if there were any; the current one becomes the base +std::optional take_changes(); +// the current kilometrage becomes the base, the changes made so far aren't a part of the undo history void settle(); -// puts the marks back as they were, for undo and redo -void restore(std::vector Marks); +// puts the kilometrage back as it was, for undo and redo +void restore(state State); // the axes found are dropped when the tracks change: Edits counts the changes of the tracks made in the editor void follow(std::size_t const Edits); // counts the times the axes were dropped: what is made from them is made again when it changes @@ -86,6 +107,9 @@ struct axis std::vector sides; std::vector anchors; // by the chainage std::vector astray; // the marks of the line away from its axis + std::vector blocked; // the vias of the line it doesn't run along: no track of theirs, or another via led it elsewhere + std::vector> paths; // the paths of the tracks the line runs along, of the track beside too + bool runs_along(TTrack const *Track, int const Path) const { return std::find(paths.begin(), paths.end(), std::make_pair(Track, Path)) != paths.end(); } double length() const { return chainage.empty() ? 0.0 : chainage.back(); } }; // the numbers of the lines with marks @@ -105,6 +129,10 @@ double km_on(axis const &Axis, axis::side const &Side, double const Along); bool km_at(std::string const &Line, glm::dvec3 const &Point, double &Km, double const Reach = 30.0); // the track nearest to the point in the plan within the reach, nullptr for none TTrack *track_near(glm::dvec3 const &Point, double const Reach); +// the path of the track nearest to the point in the plan, On: the point of it nearest +int path_near(TTrack const &Track, glm::dvec3 const &Point, glm::dvec3 &On); +// the track with a path nearest to the point within the reach, nullptr for none, Path: the path +TTrack *track_near(glm::dvec3 const &Point, double const Reach, int &Path); // "100,234", as PKP PLK write the km std::string text(double const Km); diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index db98f6b2..853ed3c7 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -2188,7 +2188,7 @@ bool editor_track::find_route(TTrack *From, TTrack *To, route &Route, std::strin return true; } -editor_track::route editor_track::run_route(TTrack &Track, double const Maximum) +editor_track::route editor_track::run_route(TTrack &Track, double const Maximum, int const Path) { route result; if (false == is_supported(Track)) @@ -2228,7 +2228,7 @@ editor_track::route editor_track::run_route(TTrack &Track, double const Maximum) } return nodes; }; - auto const own{main_of(Track)}; + auto const own{Path >= 0 && Path < static_cast(Track.m_paths.size()) ? Path : main_of(Track)}; auto const ahead{walk({&Track, own, true})}; auto const behind{walk({&Track, own, false})}; for (auto node = behind.rbegin(); node != behind.rend(); ++node) diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 7e69cc1c..acaf502a 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -375,8 +375,9 @@ class editor_track double length{0.0}; }; static bool find_route(TTrack *From, TTrack *To, route &Route, std::string &Error); - // continues both ways from the path, through switches along their path 0 - static route run_route(TTrack &Track, double const Maximum); + // continues both ways from the path, through switches along their path 0; Path: the path of the track to start along, -1 for its + // main one + static route run_route(TTrack &Track, double const Maximum, int const Path = -1); static route route_of(chain const &Chain); // the speed most of the sampled route is laid for, by the length: a few tracks set faster or slower don't set the curves of // all of it. 100 where none is set diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 842aa4b7..3a2baf9a 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10720,3 +10720,63 @@ msgstr[2] "%zu odcinków z torami na osobnych torowiskach" msgid "Where the tracks part more than 15 m and come together again, the km run along the longer one, the shorter one goes by its own length and its last hectometre before they meet takes up the difference, as PKP GK-1 gives it (6.1.12)" msgstr "Gdzie tory rozchodzą się na więcej niż 15 m i znów schodzą, kilometraż biegnie po dłuższym, krótszy liczy się po własnej długości, a różnicę bierze jego ostatni hektometr przed zejściem, jak podaje PKP GK-1 (6.1.12)" +msgid "The way through a station" +msgstr "Przebieg przez stację" + +msgid "Through a station the km run along the straight paths of the switches. Click a track of the station to lead them along it instead" +msgstr "Przez stację kilometraż biegnie po prostych torach rozjazdów. Kliknij tor stacji, żeby poprowadzić go tamtędy" + +msgid "Put a point of the km of the line first" +msgstr "Najpierw wstaw punkt kilometra linii" + +msgid "The line doesn't run along this track: another track chosen leads it elsewhere" +msgstr "Linia nie biegnie tym torem: inny wybrany tor prowadzi ją gdzie indziej" + +msgid "Line %s runs along this track, as chosen" +msgstr "Linia %s biegnie tym torem, tak jak wybrano" + +msgid "Back onto the straight paths" +msgstr "Wróć na proste tory rozjazdów" + +msgid "The track chosen is forgotten, the line runs along the straight paths of the switches here again. One step of undo" +msgstr "Wybór toru znika, linia znów biegnie tu po prostych torach rozjazdów. Jeden krok cofania" + +msgid "Line %s runs along this track" +msgstr "Linia %s biegnie tym torem" + +msgid "Line %s doesn't run along this track" +msgstr "Linia %s nie biegnie tym torem" + +msgid "Lead the km along this track" +msgstr "Prowadź kilometraż tym torem" + +msgid "The line runs along the track clicked, along the path of the switch clicked, and on along the straight paths of the switches until it meets its tracks again; where it doesn't, as at a junction, it goes on that way. One step of undo" +msgstr "Linia pobiegnie klikniętym torem, przez rozjazd tą jego drogą, którą kliknięto, i dalej po prostych torach rozjazdów, aż wróci na swoje tory; gdzie nie wraca, jak na odgałęzieniu, pobiegnie dalej tamtędy. Jeden krok cofania" + +msgid "The line can't run along this track: no way leads from it onto the line" +msgstr "Linia nie może biec tym torem: żadna droga nie prowadzi z niego na linię" + +msgid "Line %s runs along this track now" +msgstr "Linia %s biegnie teraz tym torem" + +msgid "%zu point of the km is off the line now: the track leads it away from it" +msgid_plural "%zu points of the km are off the line now: the track leads it away from them" +msgstr[0] "%zu punkt kilometra jest teraz poza linią: ten tor wyprowadza ją od niego" +msgstr[1] "%zu punkty kilometra są teraz poza linią: ten tor wyprowadza ją od nich" +msgstr[2] "%zu punktów kilometra jest teraz poza linią: ten tor wyprowadza ją od nich" + +msgid "Line %s runs along the straight paths of the switches here again" +msgstr "Linia %s znów biegnie tu po prostych torach rozjazdów" + +msgid "Along the track chosen by km %s" +msgstr "Wybrany tor przy km %s" + +msgid "Along the track chosen" +msgstr "Wybrany tor" + +msgid "Through the station the line runs along this track, not along the straight paths of the switches" +msgstr "Przez stację linia biegnie tym torem, a nie po prostych torach rozjazdów" + +msgid "%s: this way" +msgstr "%s: tędy" + diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index 5731f748..18274767 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -854,7 +854,7 @@ bool editor_line(char const *Mark, std::string const &Text) {"//$r", {"#", "switch", "slip", "crossing", "double", "connection"}}, {"//$o", {"on"}}, {"//$s", {"#", "station"}}, - {"//$k", {"#", "mark"}}, + {"//$k", {"#", "mark", "via"}}, }}; auto const lookup{std::find_if(words.begin(), words.end(), [&](auto const &Entry) { return Entry.first == Mark; })}; if (lookup == words.end()) From 435b80eb558933dbbb95409c3694265e59d3daad Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 14:33:18 +0200 Subject: [PATCH 055/145] feat(editor): the hectometre posts go by the kilometrage of the line where the Kilometrage tool sets it, its jumps too, a post at a jump of the km on from it or of the km it is reached at, the km given at the point still there as a choice; the posts standing go with the place of their number nearest to them --- application/editormodekm.cpp | 55 ++------- application/editormodelineside.cpp | 175 ++++++++++++++++++++--------- application/editormodelineside.h | 5 + editor/editorKilometrage.cpp | 80 +++++++++++++ editor/editorKilometrage.hpp | 14 +++ lang/editor_pl.po | 12 ++ 6 files changed, 240 insertions(+), 101 deletions(-) diff --git a/application/editormodekm.cpp b/application/editormodekm.cpp index c851fb0d..d1fe8362 100644 --- a/application/editormodekm.cpp +++ b/application/editormodekm.cpp @@ -163,51 +163,22 @@ void km_tool::refresh() auto const high{axis->anchors.back().chainage + 1.0}; for (auto const chainage : axis->chainage) entry.guessed.push_back(chainage < low || chainage > high ? 1 : 0); - std::vector jumps; for (auto const &anchor : axis->anchors) - { - if (std::abs(anchor.after - anchor.before) < 1e-6) - continue; - jumps.push_back(anchor.chainage); - auto const way{kilometrage::direction_on(*axis, anchor.chainage)}; - entry.ticks.push_back({kilometrage::point_on(*axis, anchor.chainage), {-way.y, 0.0, way.x}, 0, true}); - } - // a tick at each hectometre the km pass along the axis, none at a jump - auto const hectometres_along = [&](std::vector const &Points, std::vector const &Km, std::vector const &Chainage, std::vector &Ticks) { - for (std::size_t i = 1; i < Points.size(); ++i) + if (std::abs(anchor.after - anchor.before) >= 1e-6) { - auto const from{std::min(Chainage[i - 1], Chainage[i])}; - auto const to{std::max(Chainage[i - 1], Chainage[i])}; - if (std::any_of(jumps.begin(), jumps.end(), [&](double const Jump) { return Jump >= from - 1e-6 && Jump <= to + 1e-6; })) - continue; - auto const k0{Km[i - 1]}; - auto const k1{Km[i]}; - if (std::abs(k1 - k0) < 1e-9) - continue; - auto way{plan_of(Points[i] - Points[i - 1])}; - way = glm::length(way) > 1e-9 ? glm::normalize(way) : glm::dvec2{0.0, 1.0}; - auto const first{static_cast(std::floor(std::min(k0, k1) * 10.0 + 1e-9)) + 1}; - auto const last{static_cast(std::floor(std::max(k0, k1) * 10.0 + 1e-9))}; - for (auto hectometres = first; hectometres <= last && hectometres < first + 10; ++hectometres) - Ticks.push_back({glm::mix(Points[i - 1], Points[i], (hectometres / 10.0 - k0) / (k1 - k0)), {-way.y, 0.0, way.x}, hectometres, false}); + auto const way{kilometrage::direction_on(*axis, anchor.chainage)}; + entry.ticks.push_back({kilometrage::point_on(*axis, anchor.chainage), {-way.y, 0.0, way.x}, 0, true}); } - }; - std::vector km; - for (auto const chainage : axis->chainage) - km.push_back(kilometrage::km_on(*axis, chainage)); - hectometres_along(axis->points, km, axis->chainage, entry.ticks); + for (auto const &hectometre : kilometrage::hectometres(*axis)) + entry.ticks.push_back({hectometre.at, {-hectometre.rising.y, 0.0, hectometre.rising.x}, hectometre.number, false}); m_drawn.push_back(std::move(entry)); - // the shorter track where the tracks run apart, with its irregular hectometre + // the shorter track where the tracks run apart for (auto const &side : axis->sides) { drawn apart; apart.line = line; apart.points = side.points; apart.guessed.assign(side.points.size(), 0); - km.clear(); - for (auto const along : side.along) - km.push_back(kilometrage::km_on(*axis, side, along)); - hectometres_along(side.points, km, side.chainage, apart.ticks); m_drawn.push_back(std::move(apart)); } } @@ -303,18 +274,8 @@ void km_tool::pick(TTrack &Track, glm::dvec3 const &Point) else { // the line the point lies on, and its km there - auto best{kReadoutReach}; - for (auto const &line : kilometrage::lines()) - if (auto const *axis{kilometrage::axis_of(line)}; axis != nullptr) - { - double gap{0.0}; - kilometrage::chainage_at(*axis, entry.point, gap); - if (gap < best) - { - best = gap; - m_line = line; - } - } + if (auto const line{kilometrage::line_at(entry.point, kReadoutReach)}; false == line.empty()) + m_line = line; double km{0.0}; if (false == m_line.empty() && kilometrage::km_at(m_line, entry.point, km, kReadoutReach)) { diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp index 906d419a..5a5ab50f 100644 --- a/application/editormodelineside.cpp +++ b/application/editormodelineside.cpp @@ -16,6 +16,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIncludeInfo.hpp" +#include "editor/editorKilometrage.hpp" #include "scene/scene.h" #include "scene/scenelayers.h" @@ -442,61 +443,97 @@ void hekto_tool::hekto_update() continue; tool.found.push_back(post); } - if (false == tool.km_read) + tool.line_set = kilometrage::line_at(tool.point); + tool.line = tool.own_km ? std::string{} : tool.line_set; + if (tool.line.empty() && 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)}; + auto const place = [&](hekto_post &Post) { + auto const axis{editor_track::sampled_position(tool.samples, Post.chainage)}; + auto const growing{sampled_direction(tool.samples, Post.chainage) * static_cast(Post.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 side{tool.side != 0 ? tool.side : (Post.hectometres % 2 == 0 ? 1 : -1)}; auto const planar{plan_of(axis) + left * (side * tool.offset)}; - post.position = m_ground.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) + Post.position = m_ground.placement_on_ground({planar.x, axis.y, planar.y}); + Post.yaw = std::fmod(heading_of(growing) + tool.turn, 360.0); + tool.posts.push_back(Post); + }; + auto const *axis{tool.line.empty() ? nullptr : kilometrage::axis_of(tool.line)}; + if (axis != nullptr) + { + // the hectometres of the kilometrage of the line, the jumps in it too + double km{0.0}; + if (kilometrage::km_at(tool.line, tool.point, km)) + tool.km = std::round(km * 1000.0) / 1000.0; + tool.growth = kilometrage::growth_at(tool.line, tool.point, sampled_direction(tool.samples, tool.origin)) < 0 ? -1 : 1; + for (auto const &hectometre : kilometrage::hectometres(*axis)) { - auto &found{tool.found[i]}; - if (found.hectometres != hectometres) + hekto_post post; + post.hectometres = hectometre.number; + post.chainage = project(tool.samples, hectometre.at, distance); + if (distance > kLineCorridor || post.chainage <= 0.0 || post.chainage >= length || hectometre.number < 0 || tool.posts.size() >= 5000) continue; - found.outside = false; - found.distance = plan_distance(found.post.location, post.position); - if (found.distance < best) + post.growth = glm::dot(hectometre.rising, sampled_direction(tool.samples, post.chainage)) < 0.0 ? -1 : 1; + place(post); + } + std::sort(tool.posts.begin(), tool.posts.end(), [](hekto_post const &A, hekto_post const &B) { return A.chainage < B.chainage; }); + } + else + { + 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 = tool.origin + tool.growth * (hectometres / 10.0 - tool.km) * 1000.0; + post.growth = tool.growth; + if (post.chainage < 0.0 || post.chainage > length) + continue; + place(post); + } + } + // each post standing goes with the place of its number nearest to it; of the posts going with a place the one nearest to it + // 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 + for (auto &found : tool.found) + { + found.outside = true; + found.place = -1; + for (int i = 0; i < static_cast(tool.posts.size()); ++i) + { + auto const &post{tool.posts[i]}; + if (post.hectometres != found.hectometres) + continue; + auto const distance{plan_distance(found.post.location, post.position)}; + if (found.place < 0 || distance < found.distance) { - best = found.distance; - post.standing = i; + found.outside = false; + found.place = i; + found.distance = distance; + found.expected = post.chainage; + found.error = found.chainage - post.chainage; } } - 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); + } + for (int i = 0; i < static_cast(tool.found.size()); ++i) + { + auto const &found{tool.found[i]}; + if (found.place < 0) + continue; + auto &post{tool.posts[found.place]}; + if (post.standing < 0 || found.distance < tool.found[post.standing].distance) + post.standing = i; + } + for (int i = 0; i < static_cast(tool.found.size()); ++i) + { + auto &found{tool.found[i]}; + found.duplicate = found.place >= 0 && tool.posts[found.place].standing != i; + found.wrong = found.place >= 0 && false == found.duplicate && found.distance > tool.tolerance; } } @@ -576,21 +613,40 @@ void hekto_tool::render_options() if (Changed) tool.dirty = true; }; - if (begin_fields("##hektokm", 0.45f)) + if (false == tool.line.empty()) { - field_row("Kilometre at the point (km)"); - changed(ImGui::InputDouble("##hektokilometre", &tool.km, 0.0, 0.0, "%.3f")); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(1); - if (ImGui::Button(STR_C("Read from the posts"), ImVec2(-FLT_MIN, 0.0f))) + ImGui::TextWrapped("%s", format(STR_C("The km of line %s, as the Kilometrage tool sets them, its jumps too: km %s at the point"), tool.line.c_str(), kilometrage::text(tool.km).c_str()).c_str()); + if (ImGui::SmallButton(STR_C("Give the km here instead"))) { + tool.own_km = true; tool.km_read = false; tool.dirty = true; } - ImGui::EndTable(); + item_tooltip("The posts go by the km given at the point and the way it grows, without the jumps of the line"); + } + else + { + if (false == tool.line_set.empty() && ImGui::SmallButton(format(STR_C("Follow the km of line %s"), tool.line_set.c_str()).c_str())) + { + tool.own_km = false; + tool.dirty = true; + } + if (begin_fields("##hektokm", 0.45f)) + { + field_row("Kilometre at the point (km)"); + changed(ImGui::InputDouble("##hektokilometre", &tool.km, 0.0, 0.0, "%.3f")); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + if (ImGui::Button(STR_C("Read from the posts"), ImVec2(-FLT_MIN, 0.0f))) + { + tool.km_read = false; + tool.dirty = true; + } + ImGui::EndTable(); + } + ImGui::TextDisabled("%s", STR_C("The kilometrage grows")); + changed(segmented("growth", tool.growth, {{1, STR_C("along the line")}, {-1, STR_C("against it")}})); } - ImGui::TextDisabled("%s", STR_C("The kilometrage grows")); - changed(segmented("growth", tool.growth, {{1, STR_C("along the line")}, {-1, STR_C("against it")}})); ImGui::TextDisabled("%s", STR_C("Side, looking where it grows")); changed(segmented("side", tool.side, {{1, STR_C("on the right")}, {-1, STR_C("on the left")}, {0, STR_C("alternately"), "The even hectometres on the right, the odd ones on the left"}})); if (ImGui::TreeNode(STR_C("More settings"))) @@ -1056,6 +1112,7 @@ editor_selftest_result hekto_tool::selftest(std::string const &Command, std::ist auto &tool{m_hekto}; if (values.count("reach")) tool.reach = std::stod(values["reach"]); + tool.own_km = values["own"] == "1"; tool.dirty = true; hekto_update(); if (values.count("km")) @@ -1067,7 +1124,10 @@ editor_selftest_result hekto_tool::selftest(std::string const &Command, std::ist 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())); + 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; })}; + WriteLog(format("SELFTEST hekto: line %s km %.3f growth %d, %zu posts along %.0f m, %d to place, %zu standing, %d wrong, %d duplicate. %s", tool.line.empty() ? "-" : tool.line.c_str(), tool.km, tool.growth, tool.posts.size(), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, static_cast(missing), tool.found.size(), + static_cast(wrong), static_cast(duplicates), tool.status.c_str())); for (std::size_t i = 0; i < tool.posts.size() && i < 4; ++i) { auto const &post{tool.posts[i]}; @@ -1076,10 +1136,17 @@ editor_selftest_result hekto_tool::selftest(std::string const &Command, std::ist 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}; + auto const right{(direction.x * offset.y - direction.z * offset.x) * post.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["list"] == "1") + { + std::string list; + for (auto const &post : tool.posts) + list += format(" %d@%.0f", post.hectometres, post.chainage); + WriteLog("SELFTEST hekto posts:" + list); + } if (values["place"] == "1") { hekto_place(); diff --git a/application/editormodelineside.h b/application/editormodelineside.h index d7b23606..1ace4a1c 100644 --- a/application/editormodelineside.h +++ b/application/editormodelineside.h @@ -41,6 +41,7 @@ class hekto_tool final : public editor_tool double chainage{0.0}; glm::dvec3 position{0.0}; double yaw{0.0}; + int growth{1}; // 1: the kilometrage grows along the line there, -1: against int standing{-1}; // post already there with this number }; struct hekto_found @@ -54,6 +55,7 @@ class hekto_tool final : public editor_tool 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 + int place{-1}; // of the posts, the one with its number nearest to it }; struct hekto_state { @@ -62,6 +64,9 @@ class hekto_tool final : public editor_tool 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 + std::string line; // of the kilometrage set for the line in the Kilometrage tool the posts follow, empty for the one given here + std::string line_set; // the line the Kilometrage tool sets the kilometrage of at the point, empty for none + bool own_km{false}; // the kilometrage given here, even where the line has one set 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 diff --git a/editor/editorKilometrage.cpp b/editor/editorKilometrage.cpp index 860612ff..9f527dd2 100644 --- a/editor/editorKilometrage.cpp +++ b/editor/editorKilometrage.cpp @@ -927,6 +927,86 @@ bool km_at(std::string const &Line, glm::dvec3 const &Point, double &Km, double return true; } +std::string line_at(glm::dvec3 const &Point, double const Reach) +{ + std::string result; + auto nearest{Reach}; + for (auto const &line : lines()) + if (auto const *found{axis_of(line)}; found != nullptr) + { + double gap{0.0}; + chainage_at(*found, Point, gap); + if (gap < nearest) + { + nearest = gap; + result = line; + } + } + return result; +} + +int growth_at(std::string const &Line, glm::dvec3 const &Point, glm::dvec2 const &Way, double const Reach) +{ + auto const *found{axis_of(Line)}; + if (found == nullptr) + return 0; + double gap{0.0}; + auto const chainage{chainage_at(*found, Point, gap)}; + if (gap > Reach) + return 0; + return glm::dot(direction_on(*found, chainage) * static_cast(found->growth), Way) >= 0.0 ? 1 : -1; +} + +std::vector hectometres(axis const &Axis) +{ + std::vector result; + std::vector jumps; + for (auto const &anchor : Axis.anchors) + if (std::abs(anchor.after - anchor.before) >= 1e-6) + jumps.push_back(anchor.chainage); + auto const along = [&](std::vector const &Points, std::vector const &Km, std::vector const &Chainage) { + for (std::size_t i = 1; i < Points.size(); ++i) + { + auto const from{std::min(Chainage[i - 1], Chainage[i])}; + auto const to{std::max(Chainage[i - 1], Chainage[i])}; + if (std::any_of(jumps.begin(), jumps.end(), [&](double const Jump) { return Jump >= from - 1e-6 && Jump <= to + 1e-6; })) + continue; + auto const k0{Km[i - 1]}; + auto const k1{Km[i]}; + if (std::abs(k1 - k0) < 1e-9) + continue; + auto way{plan_of(Points[i] - Points[i - 1])}; + way = glm::length(way) > 1e-9 ? glm::normalize(way) : glm::dvec2{0.0, 1.0}; + auto const first{static_cast(std::floor(std::min(k0, k1) * 10.0 + 1e-9)) + 1}; + auto const last{static_cast(std::floor(std::max(k0, k1) * 10.0 + 1e-9))}; + for (auto number = first; number <= last && number < first + 10; ++number) + result.push_back({glm::mix(Points[i - 1], Points[i], (number / 10.0 - k0) / (k1 - k0)), k1 > k0 ? way : -way, number}); + } + }; + // at a jump the post of the km on from it, as the posts of a line built anew begin there, else of the km the jump is reached at + for (auto const &anchor : Axis.anchors) + { + if (std::abs(anchor.after - anchor.before) < 1e-6) + continue; + auto const round = [](double const Km) { return std::abs(Km * 10.0 - std::round(Km * 10.0)) < 1e-4; }; + auto const km{round(anchor.after) ? anchor.after : anchor.before}; + if (round(km)) + result.push_back({point_on(Axis, anchor.chainage), direction_on(Axis, anchor.chainage) * static_cast(Axis.growth), static_cast(std::lround(km * 10.0))}); + } + std::vector km; + for (auto const chainage : Axis.chainage) + km.push_back(km_on(Axis, chainage)); + along(Axis.points, km, Axis.chainage); + for (auto const &side : Axis.sides) + { + km.clear(); + for (auto const distance : side.along) + km.push_back(km_on(Axis, side, distance)); + along(side.points, km, side.chainage); + } + return result; +} + TTrack *track_near(glm::dvec3 const &Point, double const Reach) { TTrack *best{nullptr}; diff --git a/editor/editorKilometrage.hpp b/editor/editorKilometrage.hpp index d266f6ee..2005a7a6 100644 --- a/editor/editorKilometrage.hpp +++ b/editor/editorKilometrage.hpp @@ -127,6 +127,20 @@ glm::dvec2 direction_on(axis const &Axis, double const Chainage); double km_on(axis const &Axis, axis::side const &Side, double const Along); // the km of the line at the point, when it lies within the reach of the axis bool km_at(std::string const &Line, glm::dvec3 const &Point, double &Km, double const Reach = 30.0); +// the line whose axis, or a track running apart from it, passes nearest to the point within the reach, empty for none +std::string line_at(glm::dvec3 const &Point, double const Reach = 30.0); +// the km of the line rise along the way at the point: 1, against it: -1, 0 where the axis doesn't reach +int growth_at(std::string const &Line, glm::dvec3 const &Point, glm::dvec2 const &Way, double const Reach = 30.0); +// a hectometre the km of a line pass +struct hectometre +{ + glm::dvec3 at{0.0}; + glm::dvec2 rising{0.0, 1.0}; // the way the km rise there, in the plan + int number{0}; // of the hectometres from km 0, 1043 for km 104,3 +}; +// the hectometres along the axis and along the tracks running apart from it; at a jump the one of the km on from it, else of +// the km it is reached at, whichever is a whole hectometre +std::vector hectometres(axis const &Axis); // the track nearest to the point in the plan within the reach, nullptr for none TTrack *track_near(glm::dvec3 const &Point, double const Reach); // the path of the track nearest to the point in the plan, On: the point of it nearest diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 3a2baf9a..0acac6d8 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10780,3 +10780,15 @@ msgstr "Przez stację linia biegnie tym torem, a nie po prostych torach rozjazd msgid "%s: this way" msgstr "%s: tędy" +msgid "The km of line %s, as the Kilometrage tool sets them, its jumps too: km %s at the point" +msgstr "Kilometraż linii %s z narzędzia Kilometraż, razem z uskokami: km %s w punkcie" + +msgid "Give the km here instead" +msgstr "Podaj kilometr tutaj" + +msgid "The posts go by the km given at the point and the way it grows, without the jumps of the line" +msgstr "Słupki pójdą według kilometra podanego w punkcie i kierunku, w którym rośnie, bez uskoków linii" + +msgid "Follow the km of line %s" +msgstr "Według kilometrażu linii %s" + From 192dfd20350e87121cdbdd52db66241186d571cc Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 14:33:18 +0200 Subject: [PATCH 056/145] feat(editor): the signalling mode takes the km of the station and of the block signals from the kilometrage of the line where the Kilometrage tool sets it, jumps included, the way the km rise from its axis, the line of each line out at a junction too, the station of a line set there without typing its number --- application/editormodesrk.cpp | 97 +++++++++++++++++++++++++++++------ application/editormodesrk.h | 7 ++- editor/editorStation.cpp | 9 +++- lang/editor_pl.po | 6 +++ 4 files changed, 101 insertions(+), 18 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index a1f5651a..a06c9c31 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -16,6 +16,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIncludeInfo.hpp" +#include "editor/editorKilometrage.hpp" #include "editor/editorSwitchStore.hpp" #include "editor/editorTrack.hpp" @@ -174,6 +175,18 @@ template std::string anchor_of(Record_ const &Entry) return Entry.track != nullptr && false == Entry.track->m_editorremoved ? Entry.track->name() : Entry.anchor; } +glm::dvec3 axis_point(station::layout const &Layout, double const Chainage) +{ + auto const after{std::lower_bound(Layout.chainage.begin(), Layout.chainage.end(), Chainage)}; + if (after == Layout.chainage.begin()) + return Layout.axis.front(); + if (after == Layout.chainage.end()) + return Layout.axis.back(); + auto const i{static_cast(after - Layout.chainage.begin())}; + auto const step{Layout.chainage[i] - Layout.chainage[i - 1]}; + return glm::mix(Layout.axis[i - 1], Layout.axis[i], step > 1e-9 ? (Chainage - Layout.chainage[i - 1]) / step : 0.0); +} + } // namespace void srk_tool::load() @@ -522,16 +535,26 @@ void srk_tool::render_options() text("Name", "##srkname", entry != nullptr ? entry->name : std::string{}, "Biała Podlaska", [&](std::string const &Value) { record_of_open().name = Value; }); text("Code", "##srkcode", entry != nullptr ? entry->code : std::string{}, "BPd", [&](std::string const &Value) { record_of_open().code = word_of(Value) == "-" ? std::string{} : word_of(Value); }); item_tooltip("The abbreviation of the station: the names of its switches, track circuits and signals begin with it"); - text("Line", "##srkline", entry != nullptr ? entry->line : std::string{}, "2", [&](std::string const &Value) { record_of_open().line = word_of(Value) == "-" ? std::string{} : word_of(Value); }); + auto const line_hint{line_of_open()}; + text("Line", "##srkline", entry != nullptr ? entry->line : std::string{}, line_hint.empty() ? "2" : line_hint.c_str(), [&](std::string const &Value) { record_of_open().line = word_of(Value) == "-" ? std::string{} : word_of(Value); }); item_tooltip("The number of the railway line the station lies on, as the block sections between the stations are named after it"); double known{0.0}; - auto const hint{layout.kilometrage && km_of_open((layout.from + layout.to) * 0.5, known) ? format("%.3f", known) : std::string{"189.563"}}; - text("Km", "##srkkm", entry != nullptr ? entry->km : std::string{}, hint.c_str(), [&](std::string const &Value) { - auto km{word_of(Value)}; - std::replace(km.begin(), km.end(), ',', '.'); - record_of_open().km = km == "-" ? std::string{} : km; - }); - item_tooltip("The km of the middle of the station along the line, rising the way the switches are numbered: the block signals of the lines are numbered after the km"); + if (km_of_line((layout.from + layout.to) * 0.5, known)) + { + field_row("Km"); + ImGui::TextDisabled("%s", format(STR_C("%s, of the kilometrage of line %s"), kilometrage::text(known).c_str(), line_of_open().c_str()).c_str()); + item_tooltip("The km of the middle of the station, as the Kilometrage tool sets them for the line, the jumps too: the block signals of the lines are numbered after them"); + } + else + { + auto const hint{layout.kilometrage && km_of_open((layout.from + layout.to) * 0.5, known) ? format("%.3f", known) : std::string{"189.563"}}; + text("Km", "##srkkm", entry != nullptr ? entry->km : std::string{}, hint.c_str(), [&](std::string const &Value) { + auto km{word_of(Value)}; + std::replace(km.begin(), km.end(), ',', '.'); + record_of_open().km = km == "-" ? std::string{} : km; + }); + item_tooltip("The km of the middle of the station along the line, rising the way the switches are numbered: the block signals of the lines are numbered after the km"); + } ImGui::EndTable(); } step_heading(1, STR_C("Switch numbers")); @@ -860,10 +883,37 @@ std::string srk_tool::stem_of_open() const return m_record >= 0 && false == m_records[m_record].code.empty() ? station::prefix(m_records[m_record].code) : std::string{}; } +std::string srk_tool::line_of_open() const +{ + if (m_record >= 0 && false == m_records[m_record].line.empty()) + return m_records[m_record].line; + if (false == m_layout.has_value()) + return {}; + return kilometrage::line_at(axis_point(*m_layout, (m_layout->from + m_layout->to) * 0.5)); +} + +bool srk_tool::km_of_line(double const Chainage, double &Km) const +{ + auto const line{line_of_open()}; + if (false == m_layout.has_value() || line.empty()) + return false; + // along the axis of the station, on beyond its switches the way the km go through it + auto const &layout{*m_layout}; + auto const within{std::clamp(Chainage, layout.from, layout.to)}; + double low{0.0}; + double high{0.0}; + if (false == kilometrage::km_at(line, axis_point(layout, within), Km) || false == kilometrage::km_at(line, axis_point(layout, layout.from), low) || false == kilometrage::km_at(line, axis_point(layout, layout.to), high)) + return false; + Km += (Chainage - within) / 1000.0 * (high >= low ? 1.0 : -1.0); + return true; +} + bool srk_tool::km_of_open(double const Chainage, double &Km) const { if (false == m_layout.has_value()) return false; + if (km_of_line(Chainage, Km)) + return true; if (m_record >= 0 && false == m_records[m_record].km.empty()) { char *end{nullptr}; @@ -1203,9 +1253,9 @@ void srk_tool::set_lines() for (auto &distance : line.distances) distance -= at; } - auto const &described{m_records[m_record]}; + auto const described{line_of_open()}; double known{0.0}; - if (std::any_of(lines.begin(), lines.end(), [](station::line const &Line) { return Line.number > 0; }) && (false == km_of_open(0.0, known) || described.line.empty())) + if (std::any_of(lines.begin(), lines.end(), [](station::line const &Line) { return Line.number > 0; }) && (false == km_of_open(0.0, known) || described.empty())) { m_status = STR_C("Give the station its line and its km first: the block signals and the block sections are named after them"); return; @@ -1232,14 +1282,31 @@ void srk_tool::set_lines() auto const way{station::point_on(Line, Distance).way}; return glm::dot(travel_of(Signal.yaw), glm::dvec2{way.x, way.z}); }; - auto const km_on = [&](std::size_t const Index, double const Distance) { - auto const chainage{station::project(layout, station::point_on(lines[Index], 0.0).point).x}; + // the lines of the kilometrage the lines out run along, at a junction another one than the line of the station + std::vector numbers(lines.size(), described); + for (std::size_t index = 0; index < lines.size(); ++index) + { + auto const out{station::point_on(lines[index], std::min(2000.0, lines[index].length() * 0.5)).point}; double km{0.0}; + if (false == kilometrage::km_at(described, out, km)) + if (auto const found{kilometrage::line_at(out)}; false == found.empty()) + numbers[index] = found; + } + auto const km_on = [&](std::size_t const Index, double const Distance) { + double km{0.0}; + if (kilometrage::km_at(numbers[Index], station::point_on(lines[Index], Distance).point, km)) + return km; + auto const chainage{station::project(layout, station::point_on(lines[Index], 0.0).point).x}; km_of_open(chainage < middle ? chainage - Distance : chainage + Distance, km); return km; }; // the trains of the line run towards the higher km - auto const rising = [&](std::size_t const Index) { return (km_on(Index, 1000.0) > km_on(Index, 0.0)) != lines[Index].inbound; }; + auto const rising = [&](std::size_t const Index) { + auto const at{station::point_on(lines[Index], 100.0)}; + if (auto const growth{kilometrage::growth_at(numbers[Index], at.point, {at.way.x, at.way.z})}; growth != 0) + return (growth > 0) != lines[Index].inbound; + return (km_on(Index, 1000.0) > km_on(Index, 0.0)) != lines[Index].inbound; + }; // the braking distance at the highest speed out along the line, at 160 km/h while its tracks have none auto const braking = [&](std::size_t const Index) { auto speed{0.0}; @@ -1429,11 +1496,11 @@ void srk_tool::set_lines() continue; auto const &before{order[i - 1]}; // the lower km first, as on the lines of PKP PLK: 2411-2571@LK2.1 - auto const ascending{km_on(index, before.distance) <= km_on(index, here.distance)}; + auto const ascending{rising(index)}; auto const &low{ascending ? before : here}; auto const &high{ascending ? here : before}; section found; - found.name = ToLower(section_tag(low.name, line.number) + "-" + section_tag(high.name, line.number) + "@lk" + described.line + "." + std::to_string(line.number)); + found.name = ToLower(section_tag(low.name, line.number) + "-" + section_tag(high.name, line.number) + "@lk" + numbers[index] + "." + std::to_string(line.number)); found.line = index; found.from = std::min(joint_of(before), joint_of(here)); found.to = std::max(joint_of(before), joint_of(here)); diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 8d0f2be2..a5f965af 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -134,8 +134,13 @@ class srk_tool final : public editor_tool void set_lines(); // the names of the switches and the circuits of the open station begin with it, empty while it has no code std::string stem_of_open() const; - // the km at the chainage of the open station: of its record, else of its switches; false while unknown + // the km at the chainage of the open station: of the kilometrage of its line, else of its record, else of its switches; false + // while unknown bool km_of_open(double const Chainage, double &Km) const; + // the km at the chainage of the open station, as the Kilometrage tool sets them for its line; false where it doesn't + bool km_of_line(double const Chainage, double &Km) const; + // the line of the open station: of its record, else the one the Kilometrage tool sets the km of through it; empty for none + std::string line_of_open() const; // the station tracks by their numbers with their circuits as the tracks have them, Inside: the tracks of the circuits; false // while it has none bool station_tracks(std::vector &Tracks, std::unordered_set &Inside) const; diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index f609af15..b44ed5e4 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorStation.hpp" #include "editor/editorAlignmentStore.hpp" #include "editor/editorGeometry.hpp" +#include "editor/editorKilometrage.hpp" #include "editor/editorSwitchStore.hpp" #include "editor/editorTrack.hpp" #include "simulation/simulation.h" @@ -427,14 +428,18 @@ std::optional find(TTrack &From, options const &Options) result.chainage.push_back(result.chainage.back() + plan_distance(result.axis[i - 1], result.axis[i])); }; measure(); - // the chainage runs with the km of the switches, when they have one and most of them agree on it + // the chainage runs with the km of the switches, when they have one and most of them agree on it: of the kilometrage of the + // line, of their records, of the alignment double constexpr kAgree{50.0}; // m std::vector> marks; // the chainage and the km, in m for (auto *track : found.switches) { auto const start{end_of(*track, 0, false)}; std::optional km; - if (auto const record{switch_store::named(*track)}; record && record->km) + double line_km{0.0}; + if (auto const line{kilometrage::line_at(start)}; false == line.empty() && kilometrage::km_at(line, start, line_km)) + km = line_km * 1000.0; + else if (auto const record{switch_store::named(*track)}; record && record->km) km = record->km; else km = alignment_store::chainage_at(start); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 0acac6d8..9666d70b 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10780,6 +10780,12 @@ msgstr "Przez stację linia biegnie tym torem, a nie po prostych torach rozjazd msgid "%s: this way" msgstr "%s: tędy" +msgid "%s, of the kilometrage of line %s" +msgstr "%s, z kilometrażu linii %s" + +msgid "The km of the middle of the station, as the Kilometrage tool sets them for the line, the jumps too: the block signals of the lines are numbered after them" +msgstr "Kilometr środka stacji z kilometrażu linii ustalonego w narzędziu Kilometraż, z uskokami: według niego numeruje się semafory SBL" + msgid "The km of line %s, as the Kilometrage tool sets them, its jumps too: km %s at the point" msgstr "Kilometraż linii %s z narzędzia Kilometraż, razem z uskokami: km %s w punkcie" From a217c9ba75c6a849d3c02082900aad0cd73de665 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 14:42:42 +0200 Subject: [PATCH 057/145] fix(editor): the hectometre posts write the km at the point on a line of its own under the kilometrage of the line, the wrapped text broke the number at its comma --- application/editormodelineside.cpp | 3 ++- lang/editor_pl.po | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp index 5a5ab50f..58985fb0 100644 --- a/application/editormodelineside.cpp +++ b/application/editormodelineside.cpp @@ -615,7 +615,8 @@ void hekto_tool::render_options() }; if (false == tool.line.empty()) { - ImGui::TextWrapped("%s", format(STR_C("The km of line %s, as the Kilometrage tool sets them, its jumps too: km %s at the point"), tool.line.c_str(), kilometrage::text(tool.km).c_str()).c_str()); + ImGui::TextWrapped("%s", format(STR_C("The km of line %s, as the Kilometrage tool sets them, its jumps too"), tool.line.c_str()).c_str()); + ImGui::TextDisabled("%s", format(STR_C("km %s at the point"), kilometrage::text(tool.km).c_str()).c_str()); if (ImGui::SmallButton(STR_C("Give the km here instead"))) { tool.own_km = true; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 9666d70b..851fb7dd 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10786,8 +10786,11 @@ msgstr "%s, z kilometrażu linii %s" msgid "The km of the middle of the station, as the Kilometrage tool sets them for the line, the jumps too: the block signals of the lines are numbered after them" msgstr "Kilometr środka stacji z kilometrażu linii ustalonego w narzędziu Kilometraż, z uskokami: według niego numeruje się semafory SBL" -msgid "The km of line %s, as the Kilometrage tool sets them, its jumps too: km %s at the point" -msgstr "Kilometraż linii %s z narzędzia Kilometraż, razem z uskokami: km %s w punkcie" +msgid "The km of line %s, as the Kilometrage tool sets them, its jumps too" +msgstr "Kilometraż linii %s z narzędzia Kilometraż, razem z uskokami" + +msgid "km %s at the point" +msgstr "km %s w punkcie" msgid "Give the km here instead" msgstr "Podaj kilometr tutaj" From 1f8a477ec4f1a0f61039968febf6638b323d5dc0 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 15:24:02 +0200 Subject: [PATCH 058/145] =?UTF-8?q?fix(editor):=20the=20exit=20signals=20o?= =?UTF-8?q?f=20a=20station=20track=20stand=20at=20the=20ends=20of=20its=20?= =?UTF-8?q?longest=20circuit,=20for=20the=20longest=20useful=20length=20bo?= =?UTF-8?q?th=20ways=20as=20Ie-4=20=C2=A7=2013=20ust.=208=20wants,=20not?= =?UTF-8?q?=20at=20the=20outer=20ends=20of=20the=20outermost=20long=20circ?= =?UTF-8?q?uits,=20which=20left=20the=20trains=20of=20one=20way=20a=20shor?= =?UTF-8?q?t=20stretch=20beyond=20a=20switch=20within=20the=20track?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- editor/editorStation.cpp | 12 ++++++++++-- editor/editorStation.hpp | 10 +++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index b44ed5e4..1ae54d43 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -1245,7 +1245,8 @@ std::vector signals(layout const &Layout, std::vector const &T auto const odd{digit == track.number.rend() || (*digit - '0') % 2 == 1}; auto upper{track.number}; std::transform(upper.begin(), upper.end(), upper.begin(), [](char const Letter) { return static_cast(std::toupper(static_cast(Letter))); }); - // the outer ends of the outermost circuits long enough, else of the outermost ones + // the ends of the longest circuit long enough, for the longest useful length of the track both ways (Ie-4 § 13 ust. 8), else + // the outer ends of the outermost ones struct reach { track_end low; @@ -1255,15 +1256,22 @@ std::vector signals(layout const &Layout, std::vector const &T }; reach longest; reach any; + double best{0.0}; for (auto const &circuit : track.circuits) { double length{0.0}; for (auto const *part : circuit) length += node(*part) ? 0.0 : plan_length(*part, 0); + auto const chosen{length >= kStopping && length > best}; + if (chosen) + { + best = length; + longest = reach{}; + } for (auto const &end : ends_out(circuit)) { auto const x{project(Layout, end.point).x}; - for (auto *found : {&any, length >= kStopping ? &longest : static_cast(nullptr)}) + for (auto *found : {&any, chosen ? &longest : static_cast(nullptr)}) { if (found == nullptr) continue; diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index 76bb2047..5be6b707 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -143,11 +143,11 @@ struct signal }; // the signals of the station: the entry ones 300 m out along the lines beyond the circuits of the station, A, B... from the start -// of the chainage, Z, Y... from its end, the leftmost first; the exit ones facing out at the outer ends of the outermost circuits -// at least 200 m long of the station tracks, a letter for the odd tracks and one for the even ones at each end, with the number: -// F1, G2 towards the start, R1, P2 towards the end; the shunting ones at the ends of the shorter tracks, Tm1, Tm2... along the -// chainage. Each on the right of the trains, unless another track runs too close by there. Inside: a track of the circuits of the -// station +// of the chainage, Z, Y... from its end, the leftmost first; the exit ones facing out at the ends of the longest circuit at least +// 200 m long of each station track, for its longest useful length, a letter for the odd tracks and one for the even ones at each +// end, with the number: F1, G2 towards the start, R1, P2 towards the end; the shunting ones at the ends of the shorter tracks, Tm1, +// Tm2... along the chainage. Each on the right of the trains, unless another track runs too close by there. Inside: a track of the +// circuits of the station std::vector signals(layout const &Layout, std::vector const &Tracks, std::function const &Inside); // a track of a line out beyond an entry signal of the station, to the next station or the end of the tracks From 43346b61e7fa54002cc7ec63f69d2eff45a30e70 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 15:24:02 +0200 Subject: [PATCH 059/145] =?UTF-8?q?feat(editor):=20the=20SRK=20tool=20meas?= =?UTF-8?q?ures=20the=20station=20tracks:=20the=20construction=20length=20?= =?UTF-8?q?and=20the=20useful=20one=20each=20way,=20as=20the=20rozporz?= =?UTF-8?q?=C4=85dzenie=20on=20the=20railway=20structures=20(Dz.U.=202026?= =?UTF-8?q?=20poz.=201225)=20=C2=A7=202=20defines=20it,=20from=20the=20sig?= =?UTF-8?q?nal=20the=20trains=20stop=20by=20back=20to=20the=20joint,=20the?= =?UTF-8?q?=20fouling=20point=20or=20the=20level=20crossing=20behind=20the?= =?UTF-8?q?m,=20between=20those=20where=20no=20signal=20stands;=20the=20sw?= =?UTF-8?q?itches=20within=20a=20track=20part=20it=20into=20stretches.=20A?= =?UTF-8?q?=20list=20in=20the=20panel=20with=20the=20longest=20train,=20th?= =?UTF-8?q?e=20two=20ways=20marked=20where=20they=20differ,=20the=20track?= =?UTF-8?q?=20under=20the=20mouse=20or=20selected=20drawn=20in=20the=20sch?= =?UTF-8?q?ematic=20and=20the=20scenery,=20the=20list=20copied=20as=20a=20?= =?UTF-8?q?table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/editormodesrk.cpp | 331 +++++++++++++++++++++- application/editormodesrk.h | 14 + editor/editorStation.cpp | 516 +++++++++++++++++++++++++++++++++- editor/editorStation.hpp | 40 +++ lang/editor_pl.po | 84 ++++++ 5 files changed, 978 insertions(+), 7 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index a06c9c31..f55c9e0a 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -66,6 +66,9 @@ ImU32 constexpr kApproach{IM_COL32(95, 100, 112, 255)}; ImU32 constexpr kHovered{IM_COL32(255, 255, 255, 255)}; ImU32 constexpr kSignal{IM_COL32(240, 95, 85, 255)}; ImU32 constexpr kJoint{IM_COL32(255, 255, 255, 235)}; +ImU32 constexpr kUseful{IM_COL32(120, 230, 255, 255)}; +double constexpr kTrainMargin{25.0}; // m, of a useful length the train leaves: 10 to stop, 10 to see the signal, 5 to the joint, ST-T1-A6 +double constexpr kUnequal{20.0}; // m, the useful lengths of the two ways differing more are shown: ST-T1-A6 wants them the same bool is_switch(TTrack const &Track) { @@ -175,6 +178,52 @@ template std::string anchor_of(Record_ const &Entry) return Entry.track != nullptr && false == Entry.track->m_editorremoved ? Entry.track->name() : Entry.anchor; } +// the name without the code of the station: bpd_4 as 4 +std::string without_stem(std::string const &Name, std::string const &Stem) +{ + return false == Stem.empty() && Name.size() > Stem.size() && Name.compare(0, Stem.size(), Stem) == 0 ? Name.substr(Stem.size()) : Name; +} + +// a limit of a useful length in words, as "from" and "to" take it +std::string limit_text(station::limit const &Limit, std::string const &Stem) +{ + auto const name{without_stem(Limit.name, Stem)}; + switch (Limit.what) + { + case station::limit::kind::signal: + return format(STR_C("signal %s"), name.c_str()); + case station::limit::kind::fouling: + return format(STR_C("the fouling point of switch %s"), name.c_str()); + case station::limit::kind::start: + return format(STR_C("the start of switch %s"), name.c_str()); + case station::limit::kind::buffer: + return STR_C("the buffer stop"); + case station::limit::kind::crossing: + return STR_C("the level crossing"); + default: + return name.empty() ? std::string{STR_C("the joint")} : format(STR_C("the joint by switch %s"), name.c_str()); + } +} + +// the points of the track from one distance along it to the other +std::vector stretch_of(station::lengths const &Track, double const From, double const To) +{ + auto const at = [&](double const Along) { + auto const after{std::min(std::upper_bound(Track.distances.begin(), Track.distances.end(), Along) - Track.distances.begin(), Track.distances.size() - 1)}; + auto const before{after > 0 ? after - 1 : 0}; + auto const span{Track.distances[after] - Track.distances[before]}; + return glm::mix(Track.points[before], Track.points[after], span > 1e-9 ? std::clamp((Along - Track.distances[before]) / span, 0.0, 1.0) : 0.0); + }; + std::vector result{at(std::min(From, To))}; + for (std::size_t i = 0; i < Track.distances.size(); ++i) + if (Track.distances[i] > std::min(From, To) + 0.05 && Track.distances[i] < std::max(From, To) - 0.05) + result.push_back(Track.points[i]); + result.push_back(at(std::max(From, To))); + if (From > To) + std::reverse(result.begin(), result.end()); + return result; +} + glm::dvec3 axis_point(station::layout const &Layout, double const Chainage) { auto const after{std::lower_bound(Layout.chainage.begin(), Layout.chainage.end(), Chainage)}; @@ -360,6 +409,9 @@ void srk_tool::update() refresh(); else if (m_layout.has_value() && false == m_signals_known) survey_signals(); + m_length_shown = m_length_hover >= 0 ? m_length_hover : m_length_canvas >= 0 ? m_length_canvas : length_of(m_selection.selected_track()); + if (m_length_shown >= static_cast(m_lengths.size())) + m_length_shown = -1; // a track of another station selected opens it auto *track{m_selection.selected_track()}; if (track == nullptr || track == m_picked) @@ -454,6 +506,37 @@ void srk_tool::draw_overlay() const drawlist->AddRectFilled(ImVec2(screen.x - 3.0f, screen.y - size.y - 4.0f), ImVec2(screen.x + size.x + 3.0f, screen.y), IM_COL32(0, 0, 0, 160), 3.0f); drawlist->AddText(ImVec2(screen.x, screen.y - size.y - 2.0f), kSwitch, text.c_str()); } + // the useful lengths of the station track shown, beside it on the right of the trains, a post where they stop + if (m_length_shown >= 0 && m_length_shown < static_cast(m_lengths.size())) + { + auto const &track{m_lengths[m_length_shown]}; + for (auto const &part : track.ways) + { + if (part.length <= 0.0) + continue; + auto const points{stretch_of(track, part.rear.along, part.head.along)}; + std::vector beside; + for (std::size_t i = 0; i < points.size(); ++i) + { + auto const way{points[std::min(i + 1, points.size() - 1)] - points[i > 0 && i + 1 == points.size() ? i - 1 : i]}; + glm::dvec2 const along{glm::normalize(glm::dvec2{way.x, way.z} + glm::dvec2{1e-12, 0.0})}; + beside.push_back(points[i] + lift + glm::dvec3{-along.y, 0.1, along.x} * 0.9); + } + for (std::size_t i = 1; i < beside.size(); ++i) + if (near_enough(beside[i], kOverlayRange)) + projection.line(drawlist, beside[i - 1], beside[i], kUseful, 3.0f); + auto const top{beside.back() + glm::dvec3{0.0, 2.5, 0.0}}; + projection.line(drawlist, beside.front(), beside.front() + glm::dvec3{0.0, 1.0, 0.0}, kUseful, 2.0f); + projection.line(drawlist, beside.back(), top, kUseful, 2.0f); + ImVec2 screen; + if (false == near_enough(top, kLabelRange) || false == projection.project(top, screen)) + continue; + auto const text{format(STR_C("track %s: %.0f m"), track.number.c_str(), part.length)}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + drawlist->AddRectFilled(ImVec2(screen.x - size.x * 0.5f - 4.0f, screen.y - size.y - 4.0f), ImVec2(screen.x + size.x * 0.5f + 4.0f, screen.y), IM_COL32(0, 0, 0, 180), 3.0f); + drawlist->AddText(ImVec2(screen.x - size.x * 0.5f, screen.y - size.y - 2.0f), kUseful, text.c_str()); + } + } for (auto const &mark : m_signal_marks) { ImVec2 screen; @@ -611,6 +694,10 @@ void srk_tool::render_options() if (false == m_status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); ImGui::Spacing(); + m_length_hover = -1; + if (ImGui::CollapsingHeader(format("%s (%zu)###srklengths", STR_C("Track lengths"), m_lengths.size()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + render_lengths(); + ImGui::Spacing(); if (ImGui::CollapsingHeader(format("%s (%zu)###srkswitches", STR_C("Switches"), layout.switches.size()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::BeginTable("##srkswitchlist", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(320.0f, ImGui::GetTextLineHeightWithSpacing() * (layout.switches.size() + 1.5f))))) @@ -657,6 +744,138 @@ void srk_tool::render_options() item_tooltip("Leaves this station; LMB on a switch opens another one"); } +void srk_tool::render_lengths() +{ + auto const dimmed = [](char const *Text) { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextWrapped("%s", Text); + ImGui::PopStyleColor(); + }; + if (m_lengths.empty()) + { + if (stem_of_open().empty()) + dimmed(STR_C("Give the station its code first: its tracks are numbered and measured after it")); + else if (m_numbered) + dimmed(STR_C("The station tracks have no ends to be measured between")); + else + { + dimmed(STR_C("Set the track circuits first: the station tracks are measured by their numbers, from their joints, the fouling points and the signals")); + if (ImGui::Button(STR_C("Set the track circuits"), ImVec2(-1.0f, 0.0f))) + set_circuits(); + } + return; + } + auto const stem{stem_of_open()}; + char const *const ways[] = {STR_C("To the right"), STR_C("To the left")}; + if (ImGui::BeginTable("##srklengthlist", 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(320.0f, ImGui::GetTextLineHeightWithSpacing() * (m_lengths.size() + 1.5f))))) + { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn(STR_C("Track"), ImGuiTableColumnFlags_WidthStretch, 0.16f); + ImGui::TableSetupColumn(ways[0], ImGuiTableColumnFlags_WidthStretch, 0.28f); + ImGui::TableSetupColumn(ways[1], ImGuiTableColumnFlags_WidthStretch, 0.28f); + ImGui::TableSetupColumn(STR_C("Train up to"), ImGuiTableColumnFlags_WidthStretch, 0.28f); + ImGui::TableHeadersRow(); + for (std::size_t index = 0; index < m_lengths.size(); ++index) + { + auto const &track{m_lengths[index]}; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable((track.number + "##srklength" + std::to_string(index)).c_str(), static_cast(index) == m_length_shown, ImGuiSelectableFlags_SpanAllColumns)) + { + // the track at the middle of its useful length selected, the camera over it + auto const &way{track.ways[0]}; + auto const middle{stretch_of(track, way.rear.along, way.head.along)}; + auto const at{middle[middle.size() / 2]}; + station::path const *nearest{nullptr}; + auto best{std::numeric_limits::max()}; + for (auto const &path : m_layout->paths) + for (auto const &point : path.points) + if (auto const distance{glm::distance(glm::dvec2{point.x, point.z}, glm::dvec2{at.x, at.z})}; false == path.approach && distance < best) + { + best = distance; + nearest = &path; + } + if (nearest != nullptr) + { + m_selection.set_current_node(nearest->track); + m_picked = nearest->track; + m_view.focus_track(*nearest->track, nearest->index, at); + } + } + if (ImGui::IsItemHovered()) + { + m_length_hover = static_cast(index); + ImGui::BeginTooltip(); + ImGui::TextUnformatted(format(STR_C("Track %s"), track.number.c_str()).c_str()); + for (int way = 0; way < 2; ++way) + { + auto const &part{track.ways[way]}; + ImGui::TextUnformatted(format(STR_C("%s: %.0f m, from %s to %s"), ways[way], part.length, limit_text(part.rear, stem).c_str(), limit_text(part.head, stem).c_str()).c_str()); + } + ImGui::TextDisabled(STR_C("Construction length %.0f m: between the switches at its ends, the switches within it left out"), track.construction); + if (std::abs(track.ways[0].length - track.ways[1].length) > kUnequal) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", STR_C("The two ways differ: their trains stop on other stretches of the track, between other switches or signals")); + ImGui::TextDisabled("%s", STR_C("LMB: show it")); + ImGui::EndTooltip(); + } + auto const unequal{std::abs(track.ways[0].length - track.ways[1].length) > kUnequal}; + for (int way = 0; way < 2; ++way) + { + ImGui::TableNextColumn(); + auto const &part{track.ways[way]}; + if (unequal) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, part.head.what == station::limit::kind::signal ? 1.0f : 0.6f), "%.0f m", part.length); + else if (part.head.what == station::limit::kind::signal) + ImGui::Text("%.0f m", part.length); + else + ImGui::TextDisabled("%.0f m", part.length); + } + ImGui::TableNextColumn(); + ImGui::Text("%.0f m", std::max(0.0, std::min(track.ways[0].length, track.ways[1].length) - kTrainMargin)); + } + ImGui::EndTable(); + } + if (std::none_of(m_lengths.begin(), m_lengths.end(), [](station::lengths const &Track) { return Track.ways[0].head.what == station::limit::kind::signal || Track.ways[1].head.what == station::limit::kind::signal; })) + { + dimmed(STR_C("No signal stands by the station tracks yet, the lengths run between the fouling points: set the signals for the useful ones")); + if (ImGui::Button(STR_C("Set the signals"), ImVec2(-1.0f, 0.0f))) + set_signals(); + } + if (ImGui::Button(STR_C("Copy the list of tracks"), ImVec2(-1.0f, 0.0f))) + { + ImGui::SetClipboardText(lengths_table().c_str()); + m_status = format(STR_P("The lengths of %zu track copied", "The lengths of %zu tracks copied", m_lengths.size()), m_lengths.size()); + } + item_tooltip("The lengths of the station tracks as a table to paste into a spreadsheet: the construction length and the useful one each way with its limits, as the list of the tracks of a station gives them"); + dimmed(STR_C("The useful length: where a train stands, from the signal it stops by back to the joint, the fouling point or the level crossing behind it, between those where no signal stands (dimmed). The longest train leaves 25 m of it: 10 m to stop, 10 m to see the signal, 5 m to the joint")); +} + +std::string srk_tool::lengths_table() const +{ + auto const stem{stem_of_open()}; + std::string result{STR_C("Track\tConstruction length [m]\tUseful to the right [m]\tfrom\tto\tUseful to the left [m]\tfrom\tto")}; + result += '\n'; + for (auto const &track : m_lengths) + { + result += format("%s\t%.0f", track.number.c_str(), track.construction); + for (auto const &part : track.ways) + result += format("\t%.0f\t%s\t%s", part.length, limit_text(part.rear, stem).c_str(), limit_text(part.head, stem).c_str()); + result += '\n'; + } + return result; +} + +int srk_tool::length_of(TTrack const *Track) const +{ + if (Track == nullptr || Track->Isolated.empty()) + return -1; + auto const number{track_of(Track->Isolated.front()->asName, stem_of_open())}; + for (std::size_t index = 0; index < m_lengths.size() && false == number.empty(); ++index) + if (m_lengths[index].number == number) + return static_cast(index); + return -1; +} + void srk_tool::number_switches() { m_status.clear(); @@ -941,7 +1160,7 @@ bool srk_tool::station_tracks(std::vector &Tracks, std::unord std::map>> numbers; for (auto const &path : m_layout->paths) { - if (path.track->Isolated.empty() || Inside.count(path.track) > 0) + if (path.track->m_editorremoved || path.track->Isolated.empty() || Inside.count(path.track) > 0) continue; auto const &name{path.track->Isolated.front()->asName}; if (name.size() <= stem.size() || name.compare(0, stem.size(), stem) != 0 || entry_circuit(name, stem)) @@ -1697,8 +1916,13 @@ void srk_tool::survey_signals() std::vector standing; m_signals_known = m_layout.has_value() && m_signals.standing_signals(standing, false); if (false == m_signals_known) + { + if (false == m_measured) + measure({}); return; + } auto const stem{stem_of_open()}; + std::vector stops; for (auto const &signal : standing) { if (gap_to(*m_layout, signal.position) > 8.0) @@ -1707,7 +1931,30 @@ void srk_tool::survey_signals() auto const at{station::project(*m_layout, signal.position)}; auto const ahead{station::project(*m_layout, signal.position + glm::dvec3{travel.x, 0.0, travel.y} * 5.0)}; m_signal_marks.push_back({at, ahead.x > at.x, signal_shown(signal.name, stem), signal.position + glm::dvec3{0.0, signal.kind == "shunt" ? 3.0 : 7.0, 0.0}}); + if (signal.kind != "main" && signal.kind != "shunt") + continue; + station::signal stop; + stop.name = signal_shown(signal.name, stem); + stop.point = signal.position; + stop.travel = glm::dvec3{travel.x, 0.0, travel.y}; + stops.push_back(std::move(stop)); } + measure(stops); +} + +void srk_tool::measure(std::vector const &Signals) +{ + m_measured = true; + m_lengths.clear(); + std::vector tracks; + std::unordered_set inside; + m_numbered = m_layout.has_value() && station_tracks(tracks, inside); + if (false == m_numbered) + return; + m_lengths = station::track_lengths(*m_layout, tracks, Signals); + std::sort(m_lengths.begin(), m_lengths.end(), [](station::lengths const &A, station::lengths const &B) { + return std::make_pair(std::atoi(A.number.c_str()), A.number) < std::make_pair(std::atoi(B.number.c_str()), B.number); + }); } void srk_tool::survey() @@ -1715,6 +1962,9 @@ void srk_tool::survey() m_circuit_of.assign(m_layout.has_value() ? m_layout->paths.size() : 0, -1); m_circuits.clear(); m_ticks.clear(); + m_lengths.clear(); + m_measured = false; + m_numbered = false; if (false == m_layout.has_value()) return; auto const &layout{*m_layout}; @@ -1889,6 +2139,7 @@ void srk_tool::render_canvas(float const Height) } else if (false == hovered && false == ImGui::IsItemActive()) m_hover = -1; + m_length_canvas = m_hover >= 0 && false == m_dragging ? length_of(layout.paths[m_hover].track) : -1; if (ImGui::IsItemDeactivated()) { if (false == m_dragging && m_hover >= 0) @@ -1942,6 +2193,35 @@ void srk_tool::render_canvas(float const Height) draw_path(path, overlay_color::selected, 4.0f); if (m_hover >= 0) draw_path(layout.paths[m_hover], kHovered, 4.0f); + // the useful lengths of the station track shown: of the trains to the right over it, to the left under it, an arrowhead where + // they stop + std::vector> useful; + if (m_length_shown >= 0 && m_length_shown < static_cast(m_lengths.size())) + { + auto const &track{m_lengths[m_length_shown]}; + for (int way = 0; way < 2; ++way) + { + auto const &part{track.ways[way]}; + if (part.length <= 0.0) + continue; + auto const shift{way == 0 ? -7.0f : 7.0f}; + std::vector line; + for (auto const &point : stretch_of(track, part.rear.along, part.head.along)) + { + auto at{screen_of(station::project(layout, point))}; + at.y += shift; + line.push_back(at); + } + draw->AddPolyline(line.data(), static_cast(line.size()), kUseful, ImDrawFlags_None, 2.0f); + auto const &rear{line.front()}; + auto const &head{line.back()}; + draw->AddLine(ImVec2(rear.x, rear.y - 4.0f), ImVec2(rear.x, rear.y + 4.0f), kUseful, 2.0f); + auto const way_x{head.x >= line[line.size() > 1 ? line.size() - 2 : 0].x ? 1.0f : -1.0f}; + draw->AddTriangleFilled(ImVec2(head.x - 7.0f * way_x, head.y - 4.5f), ImVec2(head.x - 7.0f * way_x, head.y + 4.5f), ImVec2(head.x + 1.0f * way_x, head.y), kUseful); + auto const &middle{line[line.size() / 2]}; + useful.emplace_back(format("%.0f m", part.length), ImVec2(middle.x, way == 0 ? middle.y - ImGui::GetTextLineHeight() - 3.0f : middle.y + 3.0f)); + } + } // the labels, none over another: the switches first, a dot at the start and the name over or under it in turn, the number // alone once named after the station; then the station tracks under or over them, the longest first std::vector placed; @@ -1958,6 +2238,8 @@ void srk_tool::render_canvas(float const Height) return; } }; + for (auto const &[text, at] : useful) + label(text, kUseful, {at}); auto const stem{stem_of_open()}; auto const height{ImGui::GetTextLineHeight()}; for (std::size_t index = 0; index < layout.switches.size(); ++index) @@ -2006,6 +2288,11 @@ void srk_tool::render_canvas(float const Height) ImGui::TextDisabled("km %.3f", station::km_of(layout, along)); if (false == path.track->Isolated.empty()) ImGui::TextDisabled(STR_C("track circuit %s"), path.track->Isolated.front()->asName.c_str()); + if (m_length_canvas >= 0 && m_length_canvas < static_cast(m_lengths.size())) + { + auto const &track{m_lengths[m_length_canvas]}; + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(kUseful), STR_C("Track %s, useful length %.0f m to the right, %.0f m to the left"), track.number.c_str(), track.ways[0].length, track.ways[1].length); + } ImGui::TextDisabled("%s", STR_C("LMB: select it")); ImGui::EndTooltip(); } @@ -2013,6 +2300,7 @@ void srk_tool::render_canvas(float const Height) void srk_tool::render_windows() { + m_length_canvas = -1; if (false == m_open || false == m_strip || false == m_layout.has_value()) return; auto const &io{ImGui::GetIO()}; @@ -2219,6 +2507,47 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri } return editor_selftest_result::handled; } + if (Command == "srklengths") + { + // srklengths [show=]: the lengths of the station tracks of the open station, the one shown drawn + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srklengths: no station open"); + return editor_selftest_result::handled; + } + std::vector standing; + m_signals.standing_signals(standing, true); + refresh(); + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srklengths: the station is gone"); + return editor_selftest_result::handled; + } + auto const values{selftest_values(Words)}; + auto const show{values.find("show")}; + auto const code = [](station::limit const &Limit) { + char const *const kinds[] = {"S", "J", "F", "St", "B", "X"}; + return std::string{kinds[static_cast(Limit.what)]} + ":" + Limit.name + format("@%.1f", Limit.along); + }; + for (std::size_t index = 0; index < m_lengths.size(); ++index) + { + auto const &track{m_lengths[index]}; + WriteLog(format("SELFTEST srklengths %s: right %.1f (%s .. %s), left %.1f (%s .. %s), construction %.1f", track.number.c_str(), track.ways[0].length, code(track.ways[0].rear).c_str(), code(track.ways[0].head).c_str(), track.ways[1].length, + code(track.ways[1].rear).c_str(), code(track.ways[1].head).c_str(), track.construction)); + } + WriteLog(format("SELFTEST srklengths: %zu tracks", m_lengths.size())); + if (auto const copy{values.find("copy")}; copy != values.end() && copy->second == "1") + WriteLog("SELFTEST srklengths table:\n" + lengths_table()); + // the track shown selected + for (auto const &path : m_layout->paths) + if (show != values.end() && false == path.track->Isolated.empty() && track_of(path.track->Isolated.front()->asName, stem_of_open()) == show->second) + { + m_selection.set_current_node(path.track); + m_picked = path.track; + break; + } + return editor_selftest_result::handled; + } if (Command == "srkstrip") { // srkstrip shown=0: the schematic hidden, the 3d view whole for a screenshot diff --git a/application/editormodesrk.h b/application/editormodesrk.h index a5f965af..605676c3 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -107,6 +107,12 @@ class srk_tool final : public editor_tool }; std::vector m_signal_marks; bool m_signals_known{false}; // the signals standing surveyed, once their templates are found + std::vector m_lengths; // of the station tracks, by their numbers + bool m_measured{false}; + bool m_numbered{false}; // the station has tracks numbered by their circuits + int m_length_hover{-1}; // of the lengths, the row under the mouse + int m_length_canvas{-1}; // of the lengths, of the track under the mouse in the schematic + int m_length_shown{-1}; // of the lengths, the one drawn: of the row or the track under the mouse, else of the track selected std::string m_status; void load(); @@ -129,6 +135,14 @@ class srk_tool final : public editor_tool void set_signals(); // the signals standing by the station, for the schematic void survey_signals(); + // the lengths of the station tracks, the signals given by them + void measure(std::vector const &Signals); + // the lengths of the station tracks as the list of the tracks gives them, the useful ones each way + void render_lengths(); + // the lengths as a table to paste into a spreadsheet, a line a track, the columns parted by tabs + std::string lengths_table() const; + // of the lengths, the one of the station track the track is a part of, -1 for none + int length_of(TTrack const *Track) const; // the signals of the lines out of the station: on a double-track line the block signals to the next station and the block // sections between them, on a single-track one a warning disc before the entry signal; one step of the history void set_lines(); diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index 1ae54d43..042fd4e8 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -330,8 +330,9 @@ double plan_gap(glm::dvec3 const &Point, std::vector const &Line) return best; } -// how far from the end of the switch its joint lies: 4.2 m beyond the fouling point at a branch, before the start elsewhere -double clearance(TTrack const &Switch, int const Path, bool const Atend) +// how far out from the end of the switch along the track leading on its fouling point lies, below 0 within the switch: where the +// axes of the path there and of the other one are 3.5 m apart; none at the start of a switch +std::optional fouling(TTrack const &Switch, int const Path, bool const Atend) { auto const point{end_of(Switch, Path, Atend)}; std::optional> other; @@ -343,7 +344,7 @@ double clearance(TTrack const &Switch, int const Path, bool const Atend) other = std::make_pair(path, atend); } if (false == other.has_value()) - return kStart; + return std::nullopt; auto const theirs{ahead(Switch, other->first, other->second, kReach + 50.0)}; if (plan_gap(point, theirs) >= kFouling) { @@ -356,7 +357,7 @@ double clearance(TTrack const &Switch, int const Path, bool const Atend) double back{0.0}; for (std::size_t i = 1; i < mine.size() && plan_gap(mine[i], own) >= kFouling; ++i) back += plan_distance(mine[i - 1], mine[i]); - return std::max(0.0, kBeyond - back); + return -back; } auto const mine{ahead(Switch, Path, Atend, kReach)}; double along{0.0}; @@ -366,11 +367,18 @@ double clearance(TTrack const &Switch, int const Path, bool const Atend) auto const step{plan_distance(mine[i - 1], mine[i])}; auto const next{plan_gap(mine[i], theirs)}; if (next >= kFouling) - return along + (next > gap ? step * std::clamp((kFouling - gap) / (next - gap), 0.0, 1.0) : step) + kBeyond; + return along + (next > gap ? step * std::clamp((kFouling - gap) / (next - gap), 0.0, 1.0) : step); along += step; gap = next; } - return along; + return along - kBeyond; +} + +// how far from the end of the switch its joint lies: 4.2 m beyond the fouling point at a branch, before the start elsewhere +double clearance(TTrack const &Switch, int const Path, bool const Atend) +{ + auto const point{fouling(Switch, Path, Atend)}; + return point.has_value() ? std::max(0.0, *point + kBeyond) : kStart; } } // namespace @@ -1539,4 +1547,500 @@ std::string block_number(double const Km, int const Track, bool const Rising) return std::to_string(number); } +namespace +{ + +double constexpr kBeside{4.5}; // m, from the axis of a station track to a signal of it, at most +double constexpr kFacing{0.5}; // of the cosine between the way of a signal and the track, at least +double constexpr kLevel{1.5}; // m, the most a road crossing a track lies above or below it at a level crossing +double constexpr kSkew{0.2}; // of the sine between a road and a track, the least a level crossing is taken at +int constexpr kSearch{4096}; // of the steps of the way through the switches within a station track, the most tried + +// the points of the path from its end, Length: in the plan +std::vector from_end(TTrack const &Track, int const Path, bool const Atend, double &Length) +{ + auto points{dense_points(Track, Path, Length)}; + if (Atend) + std::reverse(points.begin(), points.end()); + return points; +} + +// a path of a track along a station track, run along from its end +struct step +{ + TTrack *track{nullptr}; + int path{0}; + bool atend{false}; +}; + +// the shortest way out of the end of the track through the tracks of others, the switches within a station track, on to one of +// its own not taken yet, no longer than the limit; its last step on that one, empty for none +std::vector way_on(TTrack const &Track, bool const Atend, std::unordered_set const &Own, std::unordered_set const &Taken, double const Limit) +{ + std::vector best; + auto shortest{Limit}; + std::vector current; + auto budget{kSearch}; + std::function search = [&](TTrack const &From, int const Path, bool const End, double const Length) { + auto const point{end_of(From, Path, End)}; + auto *next{neighbour(From, Path, End)}; + if (--budget < 0 || next == nullptr || false == rail(*next) || Length >= shortest || std::any_of(current.begin(), current.end(), [&](step const &Step) { return Step.track == next; })) + return; + if (Own.count(next) > 0) + { + int path{0}; + bool atend{false}; + if (Taken.count(next) == 0 && end_at(*next, point, path, atend)) + { + best = current; + best.push_back({next, 0, atend}); + shortest = Length; + } + return; + } + for (int path = 0; path < static_cast(next->m_paths.size()); ++path) + for (auto const atend : {false, true}) + if (glm::distance(end_of(*next, path, atend), point) < same_point) + { + current.push_back({next, path, atend}); + search(*next, path, false == atend, Length + plan_length(*next, path)); + current.pop_back(); + } + }; + search(Track, 0, Atend, 0.0); + return best; +} + +// out of an end of a station track to the switch beyond it or to the end of the tracks +struct beyond +{ + TTrack const *node{nullptr}; // the switch, none at the end of the tracks + bool open{false}; // neither within reach: the circuit of the track ends by another of plain track + double out{0.0}; // from the end of the track to the end of the switch, or to the end of the tracks + std::optional fouling; // from the end of the track out to the fouling point of the switch + std::vector points; // out from the end of the track, into the switch + std::vector distances; +}; + +beyond out_of(TTrack const &Track, bool const Atend) +{ + beyond result; + auto point{end_of(Track, 0, Atend)}; + result.points.push_back(point); + result.distances.push_back(0.0); + auto *next{neighbour(Track, 0, Atend)}; + std::unordered_set seen{&Track}; + while (next != nullptr && rail(*next) && seen.insert(next).second) + { + int path{0}; + bool atend{false}; + if (false == end_at(*next, point, path, atend) || result.out > kReach) + break; + double length{0.0}; + auto const points{from_end(*next, path, atend, length)}; + for (std::size_t i = 1; i < points.size(); ++i) + { + result.distances.push_back(result.distances.back() + plan_distance(result.points.back(), points[i])); + result.points.push_back(points[i]); + } + if (node(*next)) + { + result.node = next; + if (auto const at{fouling(*next, path, atend)}) + result.fouling = result.out - *at; + return result; + } + result.out = result.distances.back(); + point = end_of(*next, 0, false == atend); + next = neighbour(*next, 0, false == atend); + } + result.open = next != nullptr && rail(*next); + return result; +} + +glm::dvec3 point_along(std::vector const &Points, std::vector const &Distances, double const Along) +{ + auto const after{std::min(std::upper_bound(Distances.begin(), Distances.end(), Along) - Distances.begin(), Distances.size() - 1)}; + auto const before{after > 0 ? after - 1 : 0}; + auto const span{Distances[after] - Distances[before]}; + return glm::mix(Points[before], Points[after], span > 1e-9 ? std::clamp((Along - Distances[before]) / span, 0.0, 1.0) : 0.0); +} + +// an end of a stretch of a station track between the switches: at a switch within the track or at an end of it, the distances along +// the track +struct bound +{ + TTrack const *node{nullptr}; // the switch, none at the end of the tracks + bool open{false}; // no switch: the circuit ends by another of plain track + double joint{0.0}; // the end of the circuit of the stretch, the edge of a level crossing + std::optional fouling; // of the switch + double start{0.0}; // of the switch, where the stretch meets it at its start; else the end of the tracks + bool crossing{false}; // a level crossing +}; + +// the limit of a stretch at its end, Inward: the way into it along the track. Behind the trains stopped by a signal the joint, or the +// fouling point where it lies within; else the fouling point, the start of the switch or the end of the tracks +limit limit_at(bound const &Bound, double const Inward, bool const Signalled) +{ + limit found; + found.along = Bound.joint; + if (Bound.crossing) + found.what = limit::kind::crossing; + if (Bound.open || Bound.crossing) + return found; + if (Bound.node == nullptr) + { + found.what = limit::kind::buffer; + found.along = Bound.start; + return found; + } + found.name = Bound.node->name(); + if (Bound.fouling.has_value()) + { + if (false == Signalled || (*Bound.fouling - Bound.joint) * Inward > 0.0) + { + found.what = limit::kind::fouling; + found.along = *Bound.fouling; + } + } + else if (false == Signalled) + { + found.what = limit::kind::start; + found.along = Bound.start; + } + return found; +} + +// the roads by the station, each as points along its axis with its width +struct road +{ + std::vector points; + double width{0.0}; + glm::dvec2 low{0.0}; // the corners of the plan around it + glm::dvec2 high{0.0}; +}; + +std::vector roads_by(layout const &Layout) +{ + std::vector result; + glm::dvec2 low{std::numeric_limits::max()}; + glm::dvec2 high{std::numeric_limits::lowest()}; + for (auto const &path : Layout.paths) + for (auto const &point : path.points) + { + low = glm::min(low, glm::dvec2{point.x, point.z}); + high = glm::max(high, glm::dvec2{point.x, point.z}); + } + low -= glm::dvec2{50.0}; + high += glm::dvec2{50.0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || (track->iCategoryFlag & 2) == 0 || track->m_editorremoved) + continue; + for (auto const &segment : track->m_paths) + { + bezier const curve{segment}; + auto const length{curve.plan_length(16)}; + if (false == std::isfinite(length)) + continue; + auto const count{std::clamp(static_cast(length / 2.0), 2, 64)}; + road entry; + entry.width = std::max(track->fTrackWidth, track->fTrackWidth2); + entry.low = glm::dvec2{std::numeric_limits::max()}; + entry.high = glm::dvec2{std::numeric_limits::lowest()}; + for (int i = 0; i <= count; ++i) + { + entry.points.push_back(curve.point(static_cast(i) / count)); + entry.low = glm::min(entry.low, glm::dvec2{entry.points.back().x, entry.points.back().z}); + entry.high = glm::max(entry.high, glm::dvec2{entry.points.back().x, entry.points.back().z}); + } + if (entry.low.x <= high.x && entry.high.x >= low.x && entry.low.y <= high.y && entry.high.y >= low.y) + result.push_back(std::move(entry)); + } + } + return result; +} + +double cross(glm::dvec2 const &A, glm::dvec2 const &B) +{ + return A.x * B.y - A.y * B.x; +} + +// the stretches along the track the roads crossing it on its level take, from the edge to the edge of each, in order +std::vector> crossings_of(std::vector const &Points, std::vector const &Distances, std::vector const &Roads) +{ + std::vector> result; + glm::dvec2 low{std::numeric_limits::max()}; + glm::dvec2 high{std::numeric_limits::lowest()}; + for (auto const &point : Points) + { + low = glm::min(low, glm::dvec2{point.x, point.z}); + high = glm::max(high, glm::dvec2{point.x, point.z}); + } + for (auto const &road : Roads) + { + if (road.low.x > high.x || road.high.x < low.x || road.low.y > high.y || road.high.y < low.y) + continue; + for (std::size_t k = 1; k < road.points.size(); ++k) + { + glm::dvec2 const r0{road.points[k - 1].x, road.points[k - 1].z}; + glm::dvec2 const across{glm::dvec2{road.points[k].x, road.points[k].z} - r0}; + if (glm::dot(across, across) < 1e-9) + continue; + for (std::size_t i = 1; i < Points.size(); ++i) + { + glm::dvec2 const p0{Points[i - 1].x, Points[i - 1].z}; + glm::dvec2 const along{glm::dvec2{Points[i].x, Points[i].z} - p0}; + auto const determinant{cross(along, across)}; + if (std::abs(determinant) < 1e-9) + continue; + auto const u{cross(r0 - p0, across) / determinant}; + auto const t{cross(r0 - p0, along) / determinant}; + if (u < 0.0 || u > 1.0 || t < 0.0 || t > 1.0) + continue; + if (std::abs(glm::mix(road.points[k - 1].y, road.points[k].y, t) - glm::mix(Points[i - 1].y, Points[i].y, u)) > kLevel) + continue; + auto const sine{std::abs(determinant) / (glm::length(along) * glm::length(across))}; + auto const half{road.width * 0.5 / std::max(sine, kSkew)}; + auto const at{Distances[i - 1] + (Distances[i] - Distances[i - 1]) * u}; + result.emplace_back(at - half, at + half); + } + } + } + std::sort(result.begin(), result.end()); + std::vector> merged; + for (auto const &part : result) + if (false == merged.empty() && part.first <= merged.back().second) + merged.back().second = std::max(merged.back().second, part.second); + else + merged.push_back(part); + return merged; +} + +} // namespace + +std::vector track_lengths(layout const &Layout, std::vector const &Tracks, std::vector const &Signals) +{ + std::vector result; + auto const roads{roads_by(Layout)}; + for (auto const &track : Tracks) + { + std::vector own; + std::unordered_map circuit_of; + for (std::size_t circuit = 0; circuit < track.circuits.size(); ++circuit) + for (auto *part : track.circuits[circuit]) + if (false == node(*part) && circuit_of.emplace(part, circuit).second) + own.push_back(part); + auto const ends{ends_out(own)}; + if (ends.empty()) + continue; + // from the end at the lower chainage along its tracks, through the switches between them: the stretches between the switches + // and the joints, each between the end of one bound and the start of the next + auto const first{std::min_element(ends.begin(), ends.end(), [&](track_end const &A, track_end const &B) { return project(Layout, A.point).x < project(Layout, B.point).x; })}; + std::unordered_set const members(own.begin(), own.end()); + std::unordered_set taken; + std::vector points; + std::vector distances; + std::vector> paths; + std::vector> bounds; // of the stretch before, of the one after + double within{0.0}; + auto const add = [&](TTrack const &Track, int const Path, bool const Atend) { + double length{0.0}; + auto const along{from_end(Track, Path, Atend, length)}; + for (std::size_t i = points.empty() ? 0 : 1; i < along.size(); ++i) + { + distances.push_back(points.empty() ? 0.0 : distances.back() + plan_distance(points.back(), along[i])); + points.push_back(along[i]); + } + paths.emplace_back(&Track, Path); + }; + auto const low{out_of(*first->track, first->atend)}; + { + bound after; + after.node = low.node; + after.open = low.open; + if (low.fouling.has_value()) + after.fouling = -*low.fouling; + after.start = -low.out; + bounds.emplace_back(bound{}, after); + } + auto *at{first->track}; + auto atend{first->atend}; + taken.insert(at); + add(*at, 0, atend); + while (true) + { + auto const way{way_on(*at, false == atend, members, taken, kReach)}; + if (way.empty()) + break; + if (way.size() == 1 && circuit_of[way.front().track] == circuit_of[at]) + { + at = way.front().track; + atend = way.front().atend; + taken.insert(at); + add(*at, 0, atend); + continue; + } + bound before; + bound after; + before.joint = distances.back(); + for (std::size_t i = 0; i < way.size(); ++i) + { + auto const &step{way[i]}; + auto const from{distances.back()}; + if (i + 1 == way.size()) + after.joint = from; + add(*step.track, step.path, step.atend); + if (false == node(*step.track)) + continue; + within += distances.back() - from; + if (before.node == nullptr) + { + before.node = step.track; + before.start = from; + if (auto const out{fouling(*step.track, step.path, step.atend)}) + before.fouling = from - *out; + } + after.node = step.track; + after.start = distances.back(); + after.fouling.reset(); + if (auto const out{fouling(*step.track, step.path, false == step.atend)}) + after.fouling = distances.back() + *out; + } + before.open = after.open = before.node == nullptr; + bounds.emplace_back(before, after); + at = way.back().track; + atend = way.back().atend; + taken.insert(at); + } + auto const length{distances.back()}; + auto const high{out_of(*at, false == atend)}; + { + bound before; + before.node = high.node; + before.open = high.open; + before.joint = length; + if (high.fouling.has_value()) + before.fouling = length + *high.fouling; + before.start = length + high.out; + bounds.emplace_back(before, bound{}); + } + // the level crossings within the stretches part them + for (auto const &[from, to] : crossings_of(points, distances, roads)) + { + auto const middle{(from + to) * 0.5}; + for (std::size_t index = 0; index + 1 < bounds.size(); ++index) + if (middle > bounds[index].second.joint && middle < bounds[index + 1].first.joint) + { + bound before; + before.crossing = true; + before.joint = from; + bound after; + after.crossing = true; + after.joint = to; + bounds.insert(bounds.begin() + index + 1, {before, after}); + break; + } + } + lengths entry; + entry.number = track.number; + entry.construction = length + (low.open ? 0.0 : low.out) + (high.open ? 0.0 : high.out) - within; + for (std::size_t i = low.points.size(); i-- > 1;) + { + entry.points.push_back(low.points[i]); + entry.distances.push_back(-low.distances[i]); + } + entry.points.insert(entry.points.end(), points.begin(), points.end()); + entry.distances.insert(entry.distances.end(), distances.begin(), distances.end()); + for (std::size_t i = 1; i < high.points.size(); ++i) + { + entry.points.push_back(high.points[i]); + entry.distances.push_back(length + high.distances[i]); + } + // the signals by it: nearer to it than to any other track, facing along it + struct stop + { + double along{0.0}; + int facing{1}; + std::string name; + }; + std::vector stops; + for (auto const &signal : Signals) + { + glm::dvec2 const point{signal.point.x, signal.point.z}; + auto gap{std::numeric_limits::max()}; + double along{0.0}; + glm::dvec2 tangent{0.0, 1.0}; + for (std::size_t i = 1; i < points.size(); ++i) + { + glm::dvec2 const a{points[i - 1].x, points[i - 1].z}; + glm::dvec2 const segment{glm::dvec2{points[i].x, points[i].z} - a}; + auto const length2{glm::dot(segment, segment)}; + if (length2 < 1e-12) + continue; + auto const t{std::clamp(glm::dot(point - a, segment) / length2, 0.0, 1.0)}; + if (auto const distance{glm::distance(point, a + segment * t)}; distance < gap) + { + gap = distance; + along = distances[i - 1] + (distances[i] - distances[i - 1]) * t; + tangent = segment / std::sqrt(length2); + } + } + if (gap > kBeside || std::any_of(Layout.paths.begin(), Layout.paths.end(), [&](path const &Path) { + return std::none_of(paths.begin(), paths.end(), [&](auto const &Own) { return Own.first == Path.track && Own.second == Path.index; }) && plan_gap(signal.point, Path.points) < gap - 0.01; + })) + continue; + auto const cosine{glm::dot(glm::normalize(glm::dvec2{signal.travel.x, signal.travel.z} + glm::dvec2{1e-12, 0.0}), tangent)}; + if (std::abs(cosine) >= kFacing) + stops.push_back({along, cosine > 0.0 ? 1 : -1, signal.name}); + } + // each way the longest: from a signal facing the trains back to the end of its stretch behind them, else of the stretches + // between the fouling points + for (int way = 0; way < 2; ++way) + { + auto const forward{way == 0}; + auto const inward{forward ? 1.0 : -1.0}; + auto &part{entry.ways[way]}; + part.length = -1.0; + for (std::size_t index = 0; index + 1 < bounds.size(); ++index) + { + auto const &before{bounds[index].second}; + auto const &after{bounds[index + 1].first}; + auto const rear{forward ? limit_at(before, inward, true) : limit_at(after, inward, true)}; + for (auto const &stop : stops) + { + if (stop.facing != (forward ? 1 : -1) || stop.along < before.joint || stop.along > after.joint) + continue; + auto const length{(stop.along - rear.along) * inward}; + if (length > 0.0 && length > part.length) + { + part.rear = rear; + part.head.what = limit::kind::signal; + part.head.name = stop.name; + part.head.along = stop.along; + part.length = length; + } + } + } + if (part.length < 0.0) + for (std::size_t index = 0; index + 1 < bounds.size(); ++index) + { + auto const lower{limit_at(bounds[index].second, 1.0, false)}; + auto const upper{limit_at(bounds[index + 1].first, -1.0, false)}; + if (auto const length{upper.along - lower.along}; length > part.length) + { + part.rear = forward ? lower : upper; + part.head = forward ? upper : lower; + part.length = length; + } + } + part.length = std::max(0.0, part.length); + part.head.point = point_along(entry.points, entry.distances, part.head.along); + part.rear.point = point_along(entry.points, entry.distances, part.rear.along); + } + result.push_back(std::move(entry)); + } + return result; +} + } // namespace station diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index 5be6b707..ff676c15 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include #include #include #include @@ -186,4 +187,43 @@ double distance_on(line const &Line, glm::dvec3 const &Point, double &Gap); // signal odd on track 1, even on track 2: 241, 242; Rising: its trains run towards the higher km std::string block_number(double const Km, int const Track, bool const Rising); +// a limit of the part of a station track a train stands on +struct limit +{ + enum class kind + { + signal, + joint, // of the track circuits + fouling, // the fouling point of a switch + start, // of a switch + buffer, // the end of the tracks, where the buffer stop stands + crossing // the edge of a road crossing the track on its level + }; + kind what{kind::joint}; + std::string name; // of the signal, of the switch for the others + double along{0.0}; // along the track, from its end at the lower chainage + glm::dvec3 point{0.0}; +}; +// the part of a station track the trains running one way stand on +struct stand +{ + limit head; // the trains stop by it + limit rear; // behind them + double length{0.0}; +}; +// the lengths of a station track, as the list of the tracks of a station gives them: the rozporządzenie of the minister of +// infrastructure on the railway structures (Dz.U. 2026 poz. 1225) § 2 +struct lengths +{ + std::string number; + double construction{0.0}; // between the ends of the switches at its ends, or to the buffer stop, the switches within it left out + // the useful one, of the trains running along the chainage, then of those running against it: from the signal they stop by back + // to the joint, the fouling point or the level crossing behind them, between those where no signal stands + std::array ways; + std::vector points; // along the track from its end at the lower chainage, through the switches within it + std::vector distances; // of the points, from that end +}; +// the lengths of the station tracks, the signals standing by them among the signals given +std::vector track_lengths(layout const &Layout, std::vector const &Tracks, std::vector const &Signals); + } // namespace station diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 851fb7dd..7178f8c3 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10801,3 +10801,87 @@ msgstr "Słupki pójdą według kilometra podanego w punkcie i kierunku, w któr msgid "Follow the km of line %s" msgstr "Według kilometrażu linii %s" +msgid "signal %s" +msgstr "sygnalizatora %s" + +msgid "the fouling point of switch %s" +msgstr "ukresu rozjazdu %s" + +msgid "the start of switch %s" +msgstr "początku rozjazdu %s" + +msgid "the buffer stop" +msgstr "kozła oporowego" + +msgid "the joint" +msgstr "złącza izolowanego" + +msgid "the joint by switch %s" +msgstr "złącza izolowanego przy rozjeździe %s" + +msgid "Track lengths" +msgstr "Długości torów" + +msgid "Set the track circuits first: the station tracks are measured by their numbers, from their joints, the fouling points and the signals" +msgstr "Najpierw ustaw izolacje: tory stacyjne mierzy się po ich numerach, od złączy izolowanych, ukresów i sygnalizatorów" + +msgid "To the right" +msgstr "W prawo" + +msgid "To the left" +msgstr "W lewo" + +msgid "Train up to" +msgstr "Pociąg do" + +msgid "Track %s" +msgstr "Tor %s" + +msgid "%s: %.0f m, from %s to %s" +msgstr "%s: %.0f m, od %s do %s" + +msgid "Construction length %.0f m: between the switches at its ends, the switches within it left out" +msgstr "Długość budowlana %.0f m: między rozjazdami na jego końcach, bez rozjazdów pośrednich" + +msgid "LMB: show it" +msgstr "LPM: pokaż go" + +msgid "The useful length: where a train stands, from the signal it stops by back to the joint, the fouling point or the level crossing behind it, between those where no signal stands (dimmed). The longest train leaves 25 m of it: 10 m to stop, 10 m to see the signal, 5 m to the joint" +msgstr "Długość użyteczna: miejsce postoju pociągu, od sygnalizatora, przy którym staje, do złącza izolowanego, ukresu albo przejazdu za nim, a gdzie sygnalizatora nie ma, między nimi (wyszarzone). Najdłuższy pociąg zostawia z niej 25 m: 10 m na zatrzymanie, 10 m na widoczność sygnału, 5 m do złącza" + +msgid "track %s: %.0f m" +msgstr "tor %s: %.0f m" + +msgid "the level crossing" +msgstr "przejazdu kolejowo-drogowego" + +msgid "Give the station its code first: its tracks are numbered and measured after it" +msgstr "Najpierw nadaj stacji skrót: po nim numeruje się i mierzy jej tory" + +msgid "The station tracks have no ends to be measured between" +msgstr "Tory stacyjne nie mają końców, między którymi dałoby się je zmierzyć" + +msgid "The two ways differ: their trains stop on other stretches of the track, between other switches or signals" +msgstr "Kierunki się różnią: pociągi każdego z nich stają na innym odcinku toru, między innymi rozjazdami albo sygnalizatorami" + +msgid "No signal stands by the station tracks yet, the lengths run between the fouling points: set the signals for the useful ones" +msgstr "Przy torach stacyjnych nie stoi jeszcze żaden sygnalizator, długości liczą się między ukresami: ustaw semafory, żeby wyszły użyteczne" + +msgid "Copy the list of tracks" +msgstr "Kopiuj wykaz torów" + +msgid "The lengths of %zu track copied" +msgid_plural "The lengths of %zu tracks copied" +msgstr[0] "Skopiowano długości %zu toru" +msgstr[1] "Skopiowano długości %zu torów" +msgstr[2] "Skopiowano długości %zu torów" + +msgid "The lengths of the station tracks as a table to paste into a spreadsheet: the construction length and the useful one each way with its limits, as the list of the tracks of a station gives them" +msgstr "Długości torów stacyjnych jako tabela do wklejenia w arkusz: długość budowlana i użyteczna w każdą stronę z jej granicami, tak jak podaje je wykaz torów stacji" + +msgid "Track\tConstruction length [m]\tUseful to the right [m]\tfrom\tto\tUseful to the left [m]\tfrom\tto" +msgstr "Tor\tDługość budowlana [m]\tUżyteczna w prawo [m]\tod\tdo\tUżyteczna w lewo [m]\tod\tdo" + +msgid "Track %s, useful length %.0f m to the right, %.0f m to the left" +msgstr "Tor %s, długość użyteczna %.0f m w prawo, %.0f m w lewo" + From 2452c1cd0da9efb97caa1571277320451eaac984 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 16:06:25 +0200 Subject: [PATCH 060/145] fix(editor): the tools see each change of the history by a revision of it, not by its size, which stood still once the oldest steps were dropped at the limit of 200 and came back to the same after an undo and another change: the SRK schematic, the lineside lists, the gauge, the joints, the speeds and the parallel track missed them --- application/editormode.h | 8 +++++++- application/editormodegauge.cpp | 8 ++++---- application/editormodejoints.cpp | 6 +++--- application/editormodelineside.cpp | 8 ++++---- application/editormodeparallel.cpp | 4 ++-- application/editormodespeed.cpp | 6 +++--- application/editormodesrk.cpp | 8 ++++---- application/editormodesrk.h | 2 +- application/editormodetrack.cpp | 5 +++-- application/editortool.h | 3 ++- 10 files changed, 33 insertions(+), 25 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index ccb9ea1d..9fd03efa 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -167,6 +167,12 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::vector copies; // models an array added to the one it was made of bool joined{false}; // undone and redone together with the change before it std::shared_ptr terrain; // the heightmap terrain from before the change (Action::Terrain) + std::uint64_t serial{next_serial()}; // of each step made, for the revision of the history + static std::uint64_t next_serial() + { + static std::uint64_t counter{0}; + return ++counter; + } }; void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); @@ -782,7 +788,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri TTrack *selected_track() const override; bool track_editing() const override; - std::size_t history_size() const override; + std::size_t history_revision() const override; glm::dvec3 camera_position() const override; track_hover const &hovered_track() const override; std::vector const &track_set() const override; diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 4260033f..42ecf58c 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -604,10 +604,10 @@ void gauge_tool::update() m_gauge.published = false; } } - else if (track != m_gauge.track || m_history.history_size() != m_gauge.history || m_gauge.pending) + else if (track != m_gauge.track || m_history.history_revision() != m_gauge.history || m_gauge.pending) { m_gauge.track = track; - m_gauge.history = m_history.history_size(); + m_gauge.history = m_history.history_revision(); m_gauge.pending = false; scan_gauge(*track); m_gauge.published = false; @@ -645,7 +645,7 @@ void gauge_tool::scan_gauge_map() scan->space.emplace(scan->lines); m_gauge.map = {}; m_gauge.map.scanned = true; - m_gauge.map.history = m_history.history_size(); + m_gauge.map.history = m_history.history_revision(); auto const name{[](gauge::profile const *Profile) { return Profile != nullptr ? Profile->name : std::string{"-"}; }}; m_gauge.map.profile = name(chosen.railway) + ", " + name(chosen.road); m_gauge.current = -1; @@ -799,7 +799,7 @@ void gauge_tool::render_windows() m_gauge.current = -1; m_gauge.published = false; } - else if (m_gauge.map.history != m_history.history_size()) + else if (m_gauge.map.history != m_history.history_revision()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("The scenery changed since the scan (%s)"), m_gauge.map.profile.c_str()); } render_gauge_hits(); diff --git a/application/editormodejoints.cpp b/application/editormodejoints.cpp index dff1cf4b..1d61aa66 100644 --- a/application/editormodejoints.cpp +++ b/application/editormodejoints.cpp @@ -164,7 +164,7 @@ void joints_tool::joints_start() continue; state.queue.push_back(track); } - state.history = m_history.history_size(); + state.history = m_history.history_revision(); state.places.clear(); state.runs.clear(); state.run_next = 0; @@ -568,7 +568,7 @@ void joints_tool::joints_fix(std::vector const &Indices) editor_track::commit(changed); for (auto *track : changed) track->mark_dirty(); - state.history = m_history.history_size(); + state.history = m_history.history_revision(); state.status = format(STR_C("%zu joints fixed, Ctrl+Z takes them back"), count); WriteLog("Editor: joints check - " + state.status, logtype::generic); } @@ -646,7 +646,7 @@ void joints_tool::render_options() ImGui::TextWrapped("%s", state.status.c_str()); if (state.issues.empty()) return; - if (state.history != m_history.history_size()) + if (state.history != m_history.history_revision()) 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); diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp index 58985fb0..20c5294e 100644 --- a/application/editormodelineside.cpp +++ b/application/editormodelineside.cpp @@ -404,10 +404,10 @@ void hekto_tool::hekto_read_kilometrage() void hekto_tool::hekto_update() { auto &tool{m_hekto}; - if (false == tool.dirty && tool.history == m_history.history_size()) + if (false == tool.dirty && tool.history == m_history.history_revision()) return; tool.dirty = false; - tool.history = m_history.history_size(); + tool.history = m_history.history_revision(); tool.posts.clear(); tool.found.clear(); tool.error.clear(); @@ -774,10 +774,10 @@ void fouling_tool::fouling_update() auto *selected{m_selection.selected_track()}; if (tool.scope == 0 && selected != tool.selected) tool.dirty = true; - if (false == tool.dirty && tool.history == m_history.history_size()) + if (false == tool.dirty && tool.history == m_history.history_revision()) return; tool.dirty = false; - tool.history = m_history.history_size(); + tool.history = m_history.history_revision(); tool.selected = selected; tool.points.clear(); tool.found.clear(); diff --git a/application/editormodeparallel.cpp b/application/editormodeparallel.cpp index 9253d51f..4a35bdd2 100644 --- a/application/editormodeparallel.cpp +++ b/application/editormodeparallel.cpp @@ -240,13 +240,13 @@ void parallel_tool::parallel_update() else if (track != nullptr && target != nullptr) tool.measured = std::abs(lateral_of(*track, *target)); - if (track == tool.built_for && tool.spacing == tool.built_spacing && tool.side == tool.built_side && tool.scope == tool.built_scope && m_history.history_size() == tool.built_history) + if (track == tool.built_for && tool.spacing == tool.built_spacing && tool.side == tool.built_side && tool.scope == tool.built_scope && m_history.history_revision() == 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.history_size(); + tool.built_history = m_history.history_revision(); tool.pieces.clear(); tool.styles.clear(); tool.length = 0.0; diff --git a/application/editormodespeed.cpp b/application/editormodespeed.cpp index 8927886b..f3278dce 100644 --- a/application/editormodespeed.cpp +++ b/application/editormodespeed.cpp @@ -87,7 +87,7 @@ void speed_tool::speed_start() if (track != nullptr && rail(*track)) add(track); } - state.history = m_history.history_size(); + state.history = m_history.history_revision(); } void speed_tool::update() @@ -198,7 +198,7 @@ void speed_tool::speed_apply(std::vector const &Indices, bool const Branche } for (auto &verdict : state.results) verdict.set = speed_check::path_velocity(*verdict.track, verdict.path); - state.history = m_history.history_size(); + state.history = m_history.history_revision(); state.status = format(STR_C("Speed limits of %zu paths lowered to the safe ones, Ctrl+Z brings them back"), changes.size()) + skipped; WriteLog("Editor: speed check - " + state.status, logtype::generic); } @@ -262,7 +262,7 @@ void speed_tool::render_options() ImGui::TextWrapped("%s", state.status.c_str()); if (state.results.empty()) return; - if (state.history != m_history.history_size()) + if (state.history != m_history.history_revision()) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("The scenery changed since the check")); auto const *const within{STR_C("Within their limits too")}; auto const checkbox{ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x + ImGui::CalcTextSize(within).x}; diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index f55c9e0a..d447dafc 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -318,7 +318,7 @@ void srk_tool::open(TTrack &Track) m_layout = std::move(found); m_hover = -1; m_view_state.exaggeration = 0.0f; - m_history_size = m_history.history_size(); + m_history_revision = m_history.history_revision(); // the record of the station, by any of its switches m_record = -1; for (int index = 0; index < static_cast(m_records.size()) && m_record < 0; ++index) @@ -336,7 +336,7 @@ void srk_tool::open(TTrack &Track) void srk_tool::refresh() { - m_history_size = m_history.history_size(); + m_history_revision = m_history.history_revision(); if (m_anchor == nullptr || m_anchor->m_editorremoved) { m_layout.reset(); @@ -405,7 +405,7 @@ void srk_tool::update() entry.anchor = name; m_changed = true; } - if ((m_layout.has_value() || m_anchor != nullptr) && m_history.history_size() != m_history_size) + if ((m_layout.has_value() || m_anchor != nullptr) && m_history.history_revision() != m_history_revision) refresh(); else if (m_layout.has_value() && false == m_signals_known) survey_signals(); @@ -445,7 +445,7 @@ void srk_tool::forget(scene::basic_node const *Node) // laid out again from its anchor at the next update m_layout.reset(); m_hover = -1; - m_history_size = 0; + m_history_revision = static_cast(-1); } } diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 605676c3..d5c79e34 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -74,7 +74,7 @@ class srk_tool final : public editor_tool int m_record{-1}; // of the open station, -1 while it has none TTrack *m_anchor{nullptr}; // the switch the open station was found from bool m_reversed{false}; // the switches numbered from the right end of the schematic - std::size_t m_history_size{0}; + std::size_t m_history_revision{0}; TTrack const *m_picked{nullptr}; // the track selected when the station was looked for view m_view_state; int m_hover{-1}; // path of the layout under the mouse in the schematic diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 1470d8e5..7deed462 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -59,9 +59,10 @@ bool editor_mode::track_editing() const return m_track_gizmo_using || m_dragging; } -std::size_t editor_mode::history_size() const +std::size_t editor_mode::history_revision() const { - return m_history.size(); + auto const top{m_history.empty() ? std::uint64_t{0} : m_history.back().serial}; + return static_cast(top * 1000003u) ^ (m_history.size() << 20) ^ g_redo.size(); } glm::dvec3 editor_mode::camera_position() const diff --git a/application/editortool.h b/application/editortool.h index 70f1c41e..267350ed 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -165,7 +165,8 @@ class editor_view class editor_history { public: - virtual std::size_t history_size() const = 0; + // changes with each step of the history taken, undone or redone, also once the oldest steps are dropped + virtual std::size_t history_revision() const = 0; virtual void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}) = 0; // the changes of the kilometrage made since the last step: one step of the history virtual void push_kilometrage_snapshot() = 0; From ad0a7e7d62f20bb0c8eb165344e79f94aace8c80 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 16:06:37 +0200 Subject: [PATCH 061/145] fix(editor): the records of the stations, the switches, the alignments and the kilometrage stay out of a _ter.scm too, as out of a .txtf: with the .sbt of the scenery the file loads as terrain, isn't written back and the changes kept there were lost --- application/editormodesrk.cpp | 7 ++----- editor/editorAlignmentStore.cpp | 7 ++++++- editor/editorAlignmentStore.hpp | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index d447dafc..45385c72 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormodeshared.h" #include "application/editorprojection.h" #include "application/editoruilayer.h" +#include "editor/editorAlignmentStore.hpp" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIncludeInfo.hpp" @@ -360,11 +361,7 @@ srk_tool::record &srk_tool::record_of_open() auto *first{m_layout->switches.front()}; entry.anchor = first->name(); entry.track = first; - entry.layer = scene::Layers.resolve(first->layer()); - if (false == scene::Layers.valid(entry.layer) || false == scene::Layers.accepts(entry.layer)) - entry.layer = scene::Layers.active(); - if (false == scene::Layers.valid(entry.layer)) - entry.layer = 1; + entry.layer = alignment_store::layer_of(*first); m_records.push_back(std::move(entry)); m_record = static_cast(m_records.size()) - 1; m_anchor = first; diff --git a/editor/editorAlignmentStore.cpp b/editor/editorAlignmentStore.cpp index e57258d2..37b70795 100644 --- a/editor/editorAlignmentStore.cpp +++ b/editor/editorAlignmentStore.cpp @@ -863,7 +863,12 @@ void restore(std::vector Records) scene::layer_handle layer_of(TTrack const &Track) { - auto const usable = [](scene::layer_handle const Layer) { return scene::Layers.valid(Layer) && scene::Layers.accepts(Layer) && false == scene::Layers.layer(Layer).name.ends_with(".txtf"); }; + auto const usable = [](scene::layer_handle const Layer) { + if (false == scene::Layers.valid(Layer) || false == scene::Layers.accepts(Layer)) + return false; + auto const &name{scene::Layers.layer(Layer).name}; + return false == name.ends_with(".txtf") && false == name.ends_with("_ter.scm"); + }; auto layer{scene::Layers.resolve(Track.layer())}; if (false == usable(layer)) layer = scene::Layers.active(); diff --git a/editor/editorAlignmentStore.hpp b/editor/editorAlignmentStore.hpp index 61dff97d..aea28a14 100644 --- a/editor/editorAlignmentStore.hpp +++ b/editor/editorAlignmentStore.hpp @@ -104,8 +104,8 @@ std::optional> take_changes(); void settle(); // puts the records back as they were, for undo and redo void restore(std::vector Records); -// layer the editor keeps the records of the track in: its own, unless it's a terrain file (.txtf), whose binary version -// (.btf) the editor doesn't read the records from +// layer the editor keeps the records of the track in: its own, unless it's a terrain file: a .txtf, whose binary version (.btf) +// the editor doesn't read the records from, or a _ter.scm, which isn't written back while the scenery has its .sbt scene::layer_handle layer_of(TTrack const &Track); } // namespace alignment_store From 92552b49cd94e7c34d4cf5130b14e628d0a3f5d5 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 16:07:04 +0200 Subject: [PATCH 062/145] =?UTF-8?q?feat(editor):=20the=20SRK=20tool=20chec?= =?UTF-8?q?ks=20the=20lights=20of=20the=20signals=20against=20the=20routes?= =?UTF-8?q?=20of=20their=20trains:=20the=20speed=20of=20each=20route=20thr?= =?UTF-8?q?ough=20the=20switches=20by=20the=20radius=20of=20their=20curves?= =?UTF-8?q?=20and=20the=20speed=20set=20on=20them,=2040,=2060=20or=20100?= =?UTF-8?q?=20km/h,=20a=20lower=20orange=20light=20for=20these=20with=20an?= =?UTF-8?q?=20orange=20strip=20for=2060=20and=20a=20green=20one=20for=2010?= =?UTF-8?q?0=20as=20Ie-1=20gives=20them,=20an=20upper=20orange=20one=20whe?= =?UTF-8?q?re=20the=20next=20signal=20can=20show=20stop,=20the=20white=20o?= =?UTF-8?q?ne=20of=20the=20substitute=20signal;=20a=20signal=20telling=20o?= =?UTF-8?q?f=20the=20next=20one=20nearer=20than=20the=20braking=20distance?= =?UTF-8?q?=20slows=20the=20trains=20with=20it=20at=20stop,=20Ie-4=20?= =?UTF-8?q?=C2=A7=208=20ust.=205,=20a=20W21=20for=20140.=20The=20templates?= =?UTF-8?q?=20read=20for=20the=20speeds=20their=20events=20show;=20the=20s?= =?UTF-8?q?ignals=20set=20get=20the=20template=20of=20the=20kind=20with=20?= =?UTF-8?q?these=20lights=20and=20the=20least=20beyond=20them,=20the=20sta?= =?UTF-8?q?nding=20ones=20fitted=20so=20in=20one=20step=20of=20undo,=20the?= =?UTF-8?q?=20template=20swapped=20with=20the=20name,=20the=20plate=20and?= =?UTF-8?q?=20the=20read=20event=20kept,=20a=20variant=20leaning=20the=20o?= =?UTF-8?q?ther=20way=20only=20with=20the=20same=20speeds.=20The=20braking?= =?UTF-8?q?=20distance=20shown=20with=20the=20speed=20of=20the=20main=20tr?= =?UTF-8?q?acks=20it=20follows,=20set=20by=20hand=20in=20the=20record=20of?= =?UTF-8?q?=20the=20station=20and=20used=20for=20the=20block=20sections=20?= =?UTF-8?q?too;=20the=20warning=20discs=20and=20the=20block=20sections=20n?= =?UTF-8?q?earer=20than=20it=20marked,=20the=20signals=20that=20don't=20fi?= =?UTF-8?q?t=20marked=20in=20the=20schematic=20and=20the=20scenery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/editormode.h | 3 + application/editormodesignal.cpp | 70 ++- application/editormodesrk.cpp | 749 ++++++++++++++++++++++++++++++- application/editormodesrk.h | 40 ++ application/editortool.h | 19 + editor/editorIncludeInfo.cpp | 43 ++ editor/editorIncludeInfo.hpp | 3 + editor/editorStation.cpp | 186 ++++++++ editor/editorStation.hpp | 23 + lang/editor_pl.po | 177 ++++++++ 10 files changed, 1300 insertions(+), 13 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 9fd03efa..8cb25919 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1904,6 +1904,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::size_t remove_templates(std::vector const &Instances) override; bool standing_signals(std::vector &Signals, bool const Wait) override; std::size_t signal_batch(signal_changes const &Changes, std::vector> States, std::vector Created, std::string &Error) override; + bool signal_heads(std::vector &Heads, bool const Wait) override; struct signal_template { @@ -1918,6 +1919,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri int parameters{0}; bool plate{false}; bool linked{false}; + std::vector speeds; + bool informs{false}; }; struct signal_spot { diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 596f8fea..7bccb392 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -264,6 +264,7 @@ bool editor_mode::signal_scan(bool const Wait) if (parameter.id == 7) item.linked = true; } + editor_includes::signal_speeds(entry.file, item.speeds, item.informs); tool.templates.push_back(std::move(item)); } std::sort(tool.templates.begin(), tool.templates.end(), [](signal_template const &A, signal_template const &B) { @@ -698,7 +699,7 @@ std::string editor_mode::signal_variant(signal_template const &Template, signal_ auto best{std::numeric_limits::max()}; for (auto const &other : m_signal.templates) { - if (other.kind != Template.kind || group_of(other.mount, other.lamps) != group || lean_of(other.lean) != wanted) + if (other.kind != Template.kind || group_of(other.mount, other.lamps) != group || lean_of(other.lean) != wanted || other.speeds != Template.speeds) continue; auto const distance{edit_distance(other.name, Template.name)}; if (distance < best) @@ -891,6 +892,27 @@ bool editor_mode::standing_signals(std::vector &Signals, bool c auto const *item{signal_template_of(standing.include.file)}; auto const linked{item != nullptr && item->linked && editor_includes::parse_directive(scene::Layers.directive(standing.include.instance), file, values) && values.size() > 6}; Signals.push_back({standing.include.instance, standing.name, kKinds[standing.kind], standing.include.location, standing.include.yaw, linked ? values[6] : std::string{}}); + auto &signal{Signals.back()}; + signal.file = standing.include.file; + signal.track = standing.track; + signal.facing = standing.track == nullptr ? 0 : standing.list % 3 == 2 ? 1 : standing.list % 3 == 1 ? -1 : 0; + } + return true; +} + +bool editor_mode::signal_heads(std::vector &Heads, bool const Wait) +{ + Heads.clear(); + signal_scan(Wait); + if (m_signal.templates.empty()) + return false; + for (int index = 0; index < static_cast(m_signal.templates.size()); ++index) + { + auto const &item{m_signal.templates[index]}; + if (item.lamps.empty()) + continue; + auto const kind{kind_index(item.kind)}; + Heads.push_back({item.file, item.name, item.kind, item.mount, item.lamps, item.speeds, item.informs, kind >= 0 && m_signal.chosen[kind] == index}); } return true; } @@ -906,6 +928,8 @@ std::size_t editor_mode::signal_batch(signal_changes const &Changes, std::vector { auto const kind{kind_index(order.kind)}; auto const *item{kind >= 0 && tool.chosen[kind] >= 0 && tool.chosen[kind] < static_cast(tool.templates.size()) ? &tool.templates[tool.chosen[kind]] : nullptr}; + if (auto const *wanted{order.file.empty() ? nullptr : signal_template_of(order.file)}; wanted != nullptr && wanted->kind == order.kind) + item = wanted; if (item == nullptr) { Error = format(STR_C("no template of the kind %s"), STR(kKindLabels[std::max(0, kind)]).c_str()); @@ -980,6 +1004,50 @@ std::size_t editor_mode::signal_batch(signal_changes const &Changes, std::vector set_include_directive(instance, directive); } } + for (auto const &[instance, wanted] : Changes.files) + { + auto const *standing{signal_standing_of(instance)}; + auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr}; + auto const *chosen{signal_template_of(wanted)}; + auto const before{scene::Layers.directive(instance)}; + std::string file; + std::vector values; + if (item == nullptr || chosen == nullptr || chosen->kind != item->kind || false == editor_includes::parse_directive(before, file, values) || values.empty()) + continue; + signal_spot spot; + auto const *variant{signal_standing_spot(*standing, spot) ? signal_template_of(signal_variant(*chosen, spot)) : chosen}; + if (variant == nullptr) + variant = chosen; + if (variant->file == item->file) + continue; + include_info had; + include_info info; + std::string missing; + editor_includes::load(item->file, had, missing); + editor_includes::load(variant->file, info, missing); + auto const role_of = [](include_info &Info, int const Id) { return Info.parameter(Id).role; }; + std::vector after(std::max(variant->parameters, 1)); + for (int id = 1; id <= static_cast(after.size()); ++id) + { + auto &value{after[id - 1]}; + auto const role{role_of(info, id)}; + for (int old = 1; old <= static_cast(values.size()) && value.empty(); ++old) + if (role != "free" && role != "text" && role_of(had, old) == role) + value = values[old - 1]; + if (id == 7 && variant->linked) + value = item->linked && values.size() > 6 ? values[6] : std::string{"none"}; + if (value.empty() && id == 6 && variant->plate) + value = plate_for(std::string{}, values[0].substr(values[0].rfind('_') + 1)); + if (value.empty()) + value = info.parameter(id).value.empty() ? std::string{"none"} : info.parameter(id).value; + } + auto const read_before{item->read.empty() ? std::string{} : editor_includes::substitute(item->read, values)}; + auto const read_after{variant->read.empty() ? std::string{} : editor_includes::substitute(variant->read, after)}; + record(instance, before); + set_include_directive(instance, editor_includes::compose_directive(variant->file, after)); + if (false == read_before.empty() && false == read_after.empty() && ToLower(read_before) != ToLower(read_after)) + signal_renamed(read_before, read_after, step); + } std::vector instances; if (Error.empty() && false == items.empty()) { diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 45385c72..5ba0c5f4 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -52,6 +52,7 @@ double constexpr kRailHead{0.18}; // m, above the axis of a path double constexpr kLineLimit{30000.0}; // m, of a line followed out to the next station double constexpr kBlockShare{1.1}; // of the braking distance, the block sections towards the end of the tracks double constexpr kPredicted{305.0}; // m, out from the next station to its entry signal, while it has none +// the events of a block section, switching the block signal guarding it char const *const kControl{"sbl_odstep.inc"}; // m, of a train at the speed, as Ie-4 § 8 takes it: the least block section of the automatic block, a warning disc that far before @@ -59,7 +60,7 @@ char const *const kControl{"sbl_odstep.inc"}; double braking_distance(double const Speed) { return Speed <= 60.0 ? 400.0 : Speed <= 100.0 ? 700.0 : Speed <= 140.0 ? 1000.0 : 1300.0; -} // the events of a block section, switching the block signal guarding it +} ImU32 constexpr kPlain{IM_COL32(170, 176, 188, 255)}; ImU32 constexpr kSwitch{IM_COL32(240, 200, 90, 255)}; @@ -70,6 +71,9 @@ ImU32 constexpr kJoint{IM_COL32(255, 255, 255, 235)}; ImU32 constexpr kUseful{IM_COL32(120, 230, 255, 255)}; double constexpr kTrainMargin{25.0}; // m, of a useful length the train leaves: 10 to stop, 10 to see the signal, 5 to the joint, ST-T1-A6 double constexpr kUnequal{20.0}; // m, the useful lengths of the two ways differing more are shown: ST-T1-A6 wants them the same +ImU32 constexpr kUnfit{IM_COL32(255, 190, 40, 255)}; +double constexpr kFastest{250.0}; // km/h, a track set faster is taken for one without a limit +ImVec4 constexpr kWrong{1.0f, 0.42f, 0.36f, 1.0f}; bool is_switch(TTrack const &Track) { @@ -237,6 +241,224 @@ glm::dvec3 axis_point(station::layout const &Layout, double const Chainage) return glm::mix(Layout.axis[i - 1], Layout.axis[i], step > 1e-9 ? (Chainage - Layout.chainage[i - 1]) / step : 0.0); } +// km/h, a signal shows for the route at the speed: 40, 60 or 100 with a light strip, Ie-1; 0 for the highest the line allows +int shown_speed(double const Speed, double const Line) +{ + if (Speed <= 0.0 || Speed >= Line - 1.0) + return 0; + auto const shown{Speed >= 98.0 ? 100 : Speed >= 58.0 ? 60 : 40}; + return shown >= Line ? 0 : shown; +} + +// km/h, a signal slows the trains to with the next one at stop standing that far on, Ie-4 § 8 ust. 5; 140 with a W21 indicator +int shortened_speed(double const Length) +{ + return Length < 400.0 ? 40 : Length < 700.0 ? 60 : Length < 1000.0 ? 100 : 140; +} + +// what the lights of a signal must show for the routes of its trains +struct head_need +{ + std::vector speeds; // km/h, the lower ones + bool informs{false}; + bool white{false}; + int shortened{0}; + bool indicator{false}; +}; + +head_need need_of(std::vector const &Routes, bool const Main, double const Line, double const Braking) +{ + head_need need; + need.white = Main; + auto const add = [&](int const Speed) { + if (Speed > 0 && std::find(need.speeds.begin(), need.speeds.end(), Speed) == need.speeds.end()) + need.speeds.push_back(Speed); + }; + for (auto const &route : Routes) + { + auto const shown{Main ? shown_speed(route.speed, Line) : 0}; + add(shown); + auto const told{route.next >= 0 && route.warning < 0}; + need.informs = need.informs || told; + if (false == Main || false == told || route.length >= Braking) + continue; + auto const slower{shortened_speed(route.length)}; + if (slower >= (shown > 0 ? shown : Line)) + continue; + if (slower > 100) + need.indicator = true; + else + { + add(slower); + need.shortened = need.shortened == 0 ? slower : std::min(need.shortened, slower); + } + } + std::sort(need.speeds.begin(), need.speeds.end()); + return need; +} + +bool has_speed(std::vector const &Speeds, int const Speed) +{ + return std::find(Speeds.begin(), Speeds.end(), Speed) != Speeds.end(); +} + +// the template has the lights the need takes: a lower orange one for a lower speed, a strip for 60 and 100, Ie-1 § 7, two orange +// ones to show a lower speed with the next signal at stop +bool shows(signal_head const &Head, head_need const &Need) +{ + auto const &lamps{Head.lamps}; + auto const count = [&](char const Lamp) { return std::count(lamps.begin(), lamps.end(), Lamp); }; + if (count('z') == 0 || count('c') == 0 || (Need.white && count('b') == 0)) + return false; + if (Need.informs && (false == Head.informs || count('p') == 0)) + return false; + if (Need.speeds.empty()) + return true; + if (lamps.find('p', lamps.find('c')) == std::string::npos || (Need.informs && count('p') < 2)) + return false; + return std::all_of(Need.speeds.begin(), Need.speeds.end(), [&](int const Speed) { return has_speed(Head.speeds, Speed); }); +} + +// the more lights and strips beyond the need, the more +int surplus(signal_head const &Head, head_need const &Need) +{ + auto result{static_cast(Head.lamps.size()) * 4}; + for (auto const speed : {60, 100}) + if (has_speed(Head.speeds, speed) && false == has_speed(Need.speeds, speed)) + ++result; + return result; +} + +// of the templates of the kind and the mount that fit the need, the one with the least beyond it, the most like the one named; +// -1 for none +int best_head(std::vector const &Heads, std::string const &Kind, std::string const &Mount, std::string const &Like, head_need const &Need) +{ + auto best{-1}; + std::tuple score; + for (int index = 0; index < static_cast(Heads.size()); ++index) + { + auto const &head{Heads[index]}; + if (head.kind != Kind || head.mount != Mount || false == shows(head, Need)) + continue; + std::size_t common{0}; + while (common < head.name.size() && common < Like.size() && head.name[common] == Like[common]) + ++common; + std::tuple const candidate{surplus(head, Need), -static_cast(common), head.name.size()}; + if (best < 0 || candidate < score) + { + best = index; + score = candidate; + } + } + return best; +} + +// the lights of the need in words: green, two orange, red, white; 40, 60 km/h with an orange strip +std::string need_text(head_need const &Need) +{ + std::string text{STR("green")}; + if (Need.informs && false == Need.speeds.empty()) + text += ", " + STR("two orange"); + else if (Need.informs || false == Need.speeds.empty()) + text += ", " + STR("orange"); + text += ", " + STR("red"); + if (Need.white) + text += ", " + STR("white"); + if (Need.speeds.empty()) + return text; + std::string speeds; + for (auto const speed : Need.speeds) + speeds += (speeds.empty() ? "" : ", ") + std::to_string(speed); + text += "; " + format(STR_C("%s km/h"), speeds.c_str()); + if (has_speed(Need.speeds, 60)) + text += ", " + STR("an orange strip"); + if (has_speed(Need.speeds, 100)) + text += ", " + STR("a green strip"); + return text; +} + +ImU32 lamp_colour(char const Lamp) +{ + switch (Lamp) + { + case 'z': return IM_COL32(40, 210, 80, 255); + case 'c': return IM_COL32(230, 40, 40, 255); + case 'p': return IM_COL32(255, 160, 30, 255); + case 'b': return IM_COL32(235, 235, 235, 255); + case 'n': return IM_COL32(60, 110, 255, 255); + default: return IM_COL32(70, 70, 70, 255); + } +} + +// the lights of a template in a row, from its top on the left, its strips after them +void lamp_row(std::string const &Lamps, std::vector const &Speeds) +{ + auto const height{ImGui::GetTextLineHeight()}; + auto const radius{height * 0.3f}; + auto const origin{ImGui::GetCursorScreenPos()}; + auto *draw{ImGui::GetWindowDrawList()}; + auto x{origin.x + radius}; + for (auto const lamp : Lamps) + { + draw->AddCircleFilled(ImVec2(x, origin.y + height * 0.5f), radius, lamp_colour(lamp)); + x += radius * 2.6f; + } + for (auto const &[speed, colour] : {std::make_pair(60, lamp_colour('p')), std::make_pair(100, lamp_colour('z'))}) + if (has_speed(Speeds, speed)) + { + draw->AddRectFilled(ImVec2(x, origin.y + height * 0.38f), ImVec2(x + radius * 3.0f, origin.y + height * 0.62f), colour, 1.0f); + x += radius * 3.6f; + } + ImGui::Dummy(ImVec2(std::max(1.0f, x - origin.x), height)); +} + +// m, the routes of the trains are followed from a signal at most, beyond it the next one needs no telling of +double route_reach(double const Braking) +{ + return std::max(3000.0, 3.0 * Braking); +} + +// a standing signal by the track its trains read it on, no track for a signal of another kind or by none +station::post post_of(standing_signal const &Signal) +{ + using role = station::signal::role; + station::post post; + if (Signal.kind == "main") + post.kind = role::entry; + else if (Signal.kind == "block") + post.kind = role::block; + else if (Signal.kind == "warning") + post.kind = role::warning; + else if (Signal.kind == "shunt") + post.kind = role::shunting; + else + return post; + post.point = Signal.position; + if (Signal.track != nullptr && Signal.facing != 0 && false == Signal.track->m_editorremoved) + { + post.track = Signal.track; + post.facing = Signal.facing; + return post; + } + int path{0}; + post.track = kilometrage::track_near(Signal.position, 4.5, path); + if (post.track == nullptr) + return post; + geometry::bezier const curve{post.track->m_paths[path]}; + auto parameter{0.0}; + auto best{std::numeric_limits::max()}; + for (int k = 0; k <= 64; ++k) + if (auto const distance{geometry::plan_distance(curve.point(k / 64.0), Signal.position)}; distance < best) + { + best = distance; + parameter = k / 64.0; + } + auto const tangent{curve.first(parameter)}; + auto const travel{travel_of(Signal.yaw)}; + post.facing = travel.x * tangent.x + travel.y * tangent.z >= 0.0 ? 1 : -1; + return post; +} + } // namespace void srk_tool::load() @@ -265,6 +487,8 @@ void srk_tool::load() words >> entry.km; else if (word == "from") entry.reversed = (words >> word) && word == "right"; + else if (word == "braking") + words >> entry.braking; else if (word == "anchor") words >> entry.anchor; else if (word == "name") @@ -301,7 +525,8 @@ void srk_tool::store() continue; if (lines.empty()) lines.emplace_back("# stations described in the track editor, the simulation skips these lines"); - lines.push_back(format("station %s line %s km %s from %s anchor %s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), word_of(entry.km).c_str(), entry.reversed ? "right" : "left", anchor_of(entry).c_str(), entry.name.c_str())); + auto const braking{entry.braking > 0 ? format(" braking %d", entry.braking) : std::string{}}; + lines.push_back(format("station %s line %s km %s from %s anchor %s%s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), word_of(entry.km).c_str(), entry.reversed ? "right" : "left", anchor_of(entry).c_str(), braking.c_str(), entry.name.c_str())); } scene::Layers.mark(layer, kMark, std::move(lines)); } @@ -539,9 +764,20 @@ void srk_tool::draw_overlay() const ImVec2 screen; if (false == near_enough(mark.head, kLabelRange) || false == projection.project(mark.head, screen)) continue; - auto const size{ImGui::CalcTextSize(mark.text.c_str())}; - drawlist->AddRectFilled(ImVec2(screen.x - size.x * 0.5f - 4.0f, screen.y - size.y - 4.0f), ImVec2(screen.x + size.x * 0.5f + 4.0f, screen.y), kSignal, 3.0f); - drawlist->AddText(ImVec2(screen.x - size.x * 0.5f, screen.y - size.y - 2.0f), IM_COL32(255, 255, 255, 255), mark.text.c_str()); + auto const text{mark.unfit ? mark.text + " !" : mark.text}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + drawlist->AddRectFilled(ImVec2(screen.x - size.x * 0.5f - 4.0f, screen.y - size.y - 4.0f), ImVec2(screen.x + size.x * 0.5f + 4.0f, screen.y), mark.unfit ? kUnfit : kSignal, 3.0f); + drawlist->AddText(ImVec2(screen.x - size.x * 0.5f, screen.y - size.y - 2.0f), mark.unfit ? IM_COL32(0, 0, 0, 255) : IM_COL32(255, 255, 255, 255), text.c_str()); + } + if (m_head_hover >= 0 && m_head_hover < static_cast(m_heads.size())) + { + auto const &head{m_heads[m_head_hover]}; + ImVec2 foot, top; + if (projection.project(head.position, foot) && projection.project(head.position + glm::dvec3{0.0, 6.0, 0.0}, top)) + { + drawlist->AddLine(foot, top, kHovered, 3.0f); + drawlist->AddCircle(top, 9.0f, kHovered, 0, 3.0f); + } } } @@ -695,6 +931,10 @@ void srk_tool::render_options() if (ImGui::CollapsingHeader(format("%s (%zu)###srklengths", STR_C("Track lengths"), m_lengths.size()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) render_lengths(); ImGui::Spacing(); + m_head_hover = -1; + if (ImGui::CollapsingHeader(format("%s (%zu)###srkheads", STR_C("Signal heads"), static_cast(std::count_if(m_heads.begin(), m_heads.end(), [](head_check const &Head) { return Head.station; }))).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + render_heads(); + ImGui::Spacing(); if (ImGui::CollapsingHeader(format("%s (%zu)###srkswitches", STR_C("Switches"), layout.switches.size()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::BeginTable("##srkswitchlist", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(320.0f, ImGui::GetTextLineHeightWithSpacing() * (layout.switches.size() + 1.5f))))) @@ -847,6 +1087,186 @@ void srk_tool::render_lengths() dimmed(STR_C("The useful length: where a train stands, from the signal it stops by back to the joint, the fouling point or the level crossing behind it, between those where no signal stands (dimmed). The longest train leaves 25 m of it: 10 m to stop, 10 m to see the signal, 5 m to the joint")); } +void srk_tool::render_heads() +{ + auto const dimmed = [](char const *Text) { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextWrapped("%s", Text); + ImGui::PopStyleColor(); + }; + if (begin_fields("##srkbraking")) + { + field_row("Line speed"); + auto const set{set_speed_of_open()}; + if (set > 0.0) + ImGui::TextWrapped("%s", format(STR_C("%.0f km/h, the highest set on its main tracks"), set).c_str()); + else + ImGui::TextColored(kWrong, "%s", format(STR_C("%.0f km/h taken: no track of the station has a speed set"), speed_of_open()).c_str()); + item_tooltip("The speed of the trains through the station, the highest one its main tracks and the lines out of it are set to in the Track tool, up to 250 km/h: the braking distance and the lights of the signals go by it. Without one 160 km/h is taken, or the speed of the braking distance set by hand"); + field_row("Braking distance"); + auto const by_hand{m_record >= 0 ? m_records[m_record].braking : 0}; + auto const after{format(STR_C("%.0f m, after the speed"), braking_distance(set > 0.0 ? set : 160.0))}; + auto changed{false}; + ImGui::SetNextItemWidth(-1.0f); + if (ImGui::BeginCombo("##srkbrakingset", by_hand > 0 ? format(STR_C("%d m, set by hand"), by_hand).c_str() : after.c_str())) + { + if (ImGui::Selectable(after.c_str(), by_hand == 0)) + { + record_of_open().braking = 0; + changed = true; + } + for (auto const distance : {400, 700, 1000, 1300}) + if (ImGui::Selectable(format(STR_C("%d m, set by hand"), distance).c_str(), by_hand == distance)) + { + record_of_open().braking = distance; + changed = true; + } + ImGui::EndCombo(); + } + item_tooltip("The braking distance of the trains on the line, Ie-4 § 8: 400 m up to 60 km/h, 700 m up to 100, 1000 m up to 140, 1300 m above. The block sections and the warning discs stand no nearer, a signal telling of the next one standing nearer slows the trains. Set it by hand where the line has another one"); + ImGui::EndTable(); + if (changed) + { + m_changed = true; + survey_signals(); + } + } + if (false == m_heads_known) + { + dimmed(STR_C("Looking for the signal templates...")); + return; + } + if (std::none_of(m_heads.begin(), m_heads.end(), [](head_check const &Head) { return Head.station; })) + { + dimmed(STR_C("No main signal stands by the station yet: its lights are checked once one does")); + if (ImGui::Button((std::string{STR_C("Set the signals")} + "##srkheads").c_str(), ImVec2(-1.0f, 0.0f))) + set_signals(); + return; + } + auto const line{speed_of_open()}; + std::vector rows; + std::size_t fine{0}; + for (std::size_t index = 0; index < m_heads.size(); ++index) + { + auto const &head{m_heads[index]}; + if (head.station || false == head.fits || false == head.problems.empty()) + rows.push_back(index); + else + ++fine; + } + if (ImGui::BeginTable("##srkheadlist", 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(320.0f, ImGui::GetTextLineHeightWithSpacing() * (rows.size() + 1.5f))))) + { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn(STR_C("Signal"), ImGuiTableColumnFlags_WidthStretch, 0.25f); + ImGui::TableSetupColumn(STR_C("Speeds"), ImGuiTableColumnFlags_WidthStretch, 0.23f); + ImGui::TableSetupColumn(STR_C("Lights"), ImGuiTableColumnFlags_WidthStretch, 0.25f); + ImGui::TableSetupColumn(STR_C("State"), ImGuiTableColumnFlags_WidthStretch, 0.27f); + ImGui::TableHeadersRow(); + for (auto const index : rows) + { + auto const &head{m_heads[index]}; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + if (ImGui::Selectable((head.name + "##srkhead" + std::to_string(index)).c_str(), static_cast(index) == m_head_hover, ImGuiSelectableFlags_SpanAllColumns)) + m_view.fly_to(head.position + glm::dvec3{0.0, 4.0, 0.0}, glm::normalize(glm::dvec3{head.travel.x, -0.3, head.travel.y}), 30.0); + if (ImGui::IsItemHovered()) + { + m_head_hover = static_cast(index); + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 32.0f); + auto const *kind{head.kind == "main" ? STR_C("Main signal %s") : head.kind == "block" ? STR_C("Block signal %s") : STR_C("Warning disc %s")}; + ImGui::TextUnformatted(format(kind, head.name.c_str()).c_str()); + ImGui::TextDisabled("%s", head.file.c_str()); + for (std::size_t k = 0; k < head.routes.size(); ++k) + { + auto const &route{head.routes[k]}; + std::string text; + if (route.next >= 0) + text = format(STR_C("To %s: %.0f m"), head.nexts[k].c_str(), route.length); + else if (route.ended) + text = format(STR_C("To the end of the tracks: %.0f m"), route.length); + else + text = format(STR_C("No signal within %.0f m"), route.length); + if (head.kind == "main") + text += ", " + (route.speed > 0.0 && shown_speed(route.speed, line) > 0 ? format(STR_C("%d km/h through the switches"), shown_speed(route.speed, line)) : format(STR_C("%.0f km/h"), line)); + std::string turnouts; + for (auto const *turnout : route.turnouts) + turnouts += (turnouts.empty() ? "" : ", ") + without_stem(turnout->name(), stem_of_open()); + if (false == turnouts.empty()) + text += ", " + format(STR_C("turning at %s"), turnouts.c_str()); + if (route.warning >= 0) + text += ", " + STR("past a warning disc"); + ImGui::BulletText("%s", text.c_str()); + } + if (head.kind != "warning") + { + head_need need; + need.speeds = head.speeds; + need.informs = head.informs; + need.white = head.white; + ImGui::TextUnformatted(format(STR_C("Needs: %s"), need_text(need).c_str()).c_str()); + } + if (head.shortened > 0) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", format(STR_C("The next signal stands nearer than the braking distance: with it at stop this one slows the trains to %d km/h, Ie-4 § 8 ust. 5"), head.shortened).c_str()); + if (head.indicator) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", STR_C("Give it a W21 indicator \"14\": 140 km/h with the next signal at stop, Ie-4 § 8 ust. 5")); + if (head.lamps.empty() && head.kind != "warning") + ImGui::TextDisabled("%s", STR_C("A semaphore with arms: its lights aren't checked")); + else if (false == head.fits && head.fit.empty()) + ImGui::TextColored(kWrong, "%s", STR_C("No template of its kind and mount has these lights")); + else if (false == head.fits) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", format(STR_C("Its lights don't show all of it: %s does"), head.fit.c_str()).c_str()); + else if (head.more) + ImGui::TextDisabled("%s", format(STR_C("It has more lights than it needs: %s will do"), head.fit.c_str()).c_str()); + for (auto const &problem : head.problems) + ImGui::TextColored(kWrong, "%s", problem.c_str()); + ImGui::TextDisabled("%s", STR_C("LMB: fly to it")); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + ImGui::TableNextColumn(); + if (head.kind == "main") + { + auto text{format("%.0f", line)}; + for (auto speed{head.speeds.rbegin()}; speed != head.speeds.rend(); ++speed) + text += ", " + std::to_string(*speed); + ImGui::TextUnformatted(text.c_str()); + } + else + ImGui::TextDisabled("-"); + ImGui::TableNextColumn(); + if (head.lamps.empty()) + ImGui::TextDisabled("%s", head.kind == "warning" ? STR_C("disc") : STR_C("arms")); + else + lamp_row(head.lamps, head.own); + ImGui::TableNextColumn(); + if (head.lamps.empty()) + ImGui::TextDisabled("-"); + else if (false == head.fits) + ImGui::TextColored(head.fit.empty() ? kWrong : ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", head.fit.empty() ? STR_C("no template") : STR_C("lights missing")); + else if (head.more) + ImGui::TextDisabled("%s", STR_C("more than needed")); + else + ImGui::TextColored(ImVec4(0.45f, 0.85f, 0.45f, 1.0f), "%s", STR_C("fits")); + if (false == head.problems.empty()) + { + ImGui::SameLine(); + ImGui::TextColored(kWrong, "%s", STR_C("too near")); + } + } + ImGui::EndTable(); + } + if (fine > 0) + ImGui::TextDisabled("%s", format(STR_P("%zu signal of the lines fits", "%zu signals of the lines fit", fine), fine).c_str()); + auto const fitting{static_cast(std::count_if(m_heads.begin(), m_heads.end(), [](head_check const &Head) { return false == Head.fit.empty() && Head.fit != Head.file && (false == Head.fits || Head.more); }))}; + ImGui::BeginDisabled(fitting == 0); + if (main_button(format("%s (%zu)###srkfitheads", STR_C("Fit the lights"), fitting).c_str(), ImVec2(-1.0f, 0.0f))) + fit_heads(); + ImGui::EndDisabled(); + item_tooltip("Gives each signal marked the template of its kind and mount with the lights its routes need and the least beyond them, the name, the plate and the place kept, the trains reading it as before. One step of undo"); + dimmed(STR_C("The lights go by the routes of the trains: 40 km/h through the switches 1:9, 60 through 1:12 and 1:14, 100 through 1:18.5, by their radii, for these a lower orange light, with an orange strip for 60, a green one for 100, Ie-1; an upper orange one where the next signal can show stop; the white one of the substitute signal. A signal telling of the next one standing nearer than the braking distance slows the trains with it at stop, Ie-4 § 8 ust. 5")); +} + std::string srk_tool::lengths_table() const { auto const stem{stem_of_open()}; @@ -1408,12 +1828,53 @@ void srk_tool::set_signals() m_status = STR_C("The signals are set already"); return; } + // the lights of the main signals placed after the routes of their trains, to the signals standing and to these + std::vector unfit; + std::vector heads; + m_signals.signal_heads(heads, true); + auto const chosen{std::find_if(heads.begin(), heads.end(), [](signal_head const &Head) { return Head.kind == "main" && Head.chosen; })}; + if (chosen != heads.end()) + { + std::vector everywhere; + m_signals.standing_signals(everywhere, true); + std::vector posts; + for (auto const &signal : everywhere) + if (auto const post{post_of(signal)}; post.track != nullptr) + posts.push_back(post); + std::vector from; + for (auto const &order : placed) + { + if (order.kind == "main") + from.push_back(posts.size()); + posts.push_back({order.track, order.facing, order.point, order.kind == "shunt" ? station::signal::role::shunting : station::signal::role::entry}); + } + auto const found{station::routes(posts, from, route_reach(braking_of_open()))}; + auto const line{speed_of_open()}; + std::size_t next{0}; + for (auto &order : placed) + { + if (order.kind != "main") + continue; + auto const need{need_of(found[next++], true, line, braking_of_open())}; + if (auto const best{best_head(heads, "main", chosen->mount, chosen->name, need)}; best >= 0) + order.file = heads[best].file; + else + unfit.push_back(signal_shown(order.name, stem)); + } + } std::string error; - auto const count{m_signals.signal_batch({placed, renamed, {}, {}}, std::move(before), std::move(created), error)}; + auto const count{m_signals.signal_batch({placed, renamed, {}, {}, {}}, std::move(before), std::move(created), error)}; if (false == error.empty()) m_status = format(STR_C("The signals couldn't be placed: %s"), error.c_str()); else m_status = format(STR_P("%zu signal placed", "%zu signals placed", count), count) + ", " + format(STR_P("%zu renamed", "%zu renamed", renamed.size()), renamed.size()); + if (false == unfit.empty()) + { + std::string names; + for (auto const &name : unfit) + names += (names.empty() ? "" : ", ") + name; + m_status += "\n" + format(STR_C("No template has the lights %s need, they got the one chosen in the Signals tool: see Signal heads"), names.c_str()); + } } void srk_tool::set_lines() @@ -1523,13 +1984,15 @@ void srk_tool::set_lines() return (growth > 0) != lines[Index].inbound; return (km_on(Index, 1000.0) > km_on(Index, 0.0)) != lines[Index].inbound; }; - // the braking distance at the highest speed out along the line, at 160 km/h while its tracks have none + // the braking distance at the highest speed out along the line, at 160 km/h while its tracks have none, unless one is set by hand auto const braking = [&](std::size_t const Index) { + if (m_record >= 0 && m_records[m_record].braking > 0) + return static_cast(m_records[m_record].braking); auto speed{0.0}; auto const &line{lines[Index]}; for (std::size_t k = 0; k < line.points.size(); ++k) - if (line.distances[k] >= 0.0) - speed = std::max(speed, line.tracks[line.owners[k]]->VelocityGet()); + if (auto const set{line.tracks[line.owners[k]]->VelocityGet()}; line.distances[k] >= 0.0 && set <= kFastest) + speed = std::max(speed, set); return braking_distance(speed > 0.0 ? speed : 160.0); }; for (std::size_t index = 0; index < lines.size(); ++index) @@ -1914,10 +2377,13 @@ void srk_tool::survey_signals() m_signals_known = m_layout.has_value() && m_signals.standing_signals(standing, false); if (false == m_signals_known) { + m_heads.clear(); + m_heads_known = false; if (false == m_measured) measure({}); return; } + check_heads(standing); auto const stem{stem_of_open()}; std::vector stops; for (auto const &signal : standing) @@ -1927,7 +2393,8 @@ void srk_tool::survey_signals() auto const travel{travel_of(signal.yaw)}; auto const at{station::project(*m_layout, signal.position)}; auto const ahead{station::project(*m_layout, signal.position + glm::dvec3{travel.x, 0.0, travel.y} * 5.0)}; - m_signal_marks.push_back({at, ahead.x > at.x, signal_shown(signal.name, stem), signal.position + glm::dvec3{0.0, signal.kind == "shunt" ? 3.0 : 7.0, 0.0}}); + auto const unfit{std::any_of(m_heads.begin(), m_heads.end(), [&](head_check const &Head) { return Head.instance == signal.instance && (false == Head.fits || false == Head.problems.empty()); })}; + m_signal_marks.push_back({at, ahead.x > at.x, signal_shown(signal.name, stem), signal.position + glm::dvec3{0.0, signal.kind == "shunt" ? 3.0 : 7.0, 0.0}, unfit}); if (signal.kind != "main" && signal.kind != "shunt") continue; station::signal stop; @@ -1954,6 +2421,211 @@ void srk_tool::measure(std::vector const &Signals) }); } +double srk_tool::set_speed_of_open() const +{ + if (false == m_layout.has_value()) + return 0.0; + auto const highest = [](auto const &Tracks) { + auto speed{0.0}; + for (auto *track : Tracks) + if (auto const set{track->VelocityGet()}; track->eType == tt_Normal && set <= kFastest) + speed = std::max(speed, set); + return speed; + }; + std::vector through{m_layout->main}; + through.insert(through.end(), m_layout->second.begin(), m_layout->second.end()); + for (auto const &path : m_layout->paths) + if (path.approach) + through.push_back(path.track); + if (auto const speed{highest(through)}; speed > 0.0) + return speed; + std::vector all; + for (auto const &path : m_layout->paths) + all.push_back(path.track); + return highest(all); +} + +double srk_tool::speed_of_open() const +{ + if (auto const speed{set_speed_of_open()}; speed > 0.0) + return speed; + auto const set{m_record >= 0 ? m_records[m_record].braking : 0}; + return set <= 0 ? 160.0 : set <= 400 ? 60.0 : set <= 700 ? 100.0 : set <= 1000 ? 140.0 : 160.0; +} + +double srk_tool::braking_of_open() const +{ + auto const set{m_record >= 0 ? m_records[m_record].braking : 0}; + return set > 0 ? static_cast(set) : braking_distance(speed_of_open()); +} + +void srk_tool::check_heads(std::vector const &Standing) +{ + using role = station::signal::role; + m_heads.clear(); + std::vector heads; + m_heads_known = m_layout.has_value() && m_signals.signal_heads(heads, false); + if (false == m_heads_known) + return; + auto const &layout{*m_layout}; + auto const stem{stem_of_open()}; + auto const line{speed_of_open()}; + auto const braking{braking_of_open()}; + auto const middle{axis_point(layout, (layout.from + layout.to) * 0.5)}; + std::vector posts; + std::vector owners; + for (std::size_t k = 0; k < Standing.size(); ++k) + if (geometry::plan_distance(Standing[k].position, middle) <= kLineLimit) + if (auto const post{post_of(Standing[k])}; post.track != nullptr) + { + posts.push_back(post); + owners.push_back(k); + } + // the main signals of the station, the block signals and the warning discs of the lines in and out of it + std::vector from; + std::vector own(posts.size(), 0); + for (std::size_t index = 0; index < posts.size(); ++index) + { + if (posts[index].kind == role::entry && gap_to(layout, posts[index].point) <= 8.0) + own[index] = 1; + if (own[index] || posts[index].kind == role::block || posts[index].kind == role::warning) + from.push_back(index); + } + auto const found{station::routes(posts, from, route_reach(braking))}; + std::vector routes_of(posts.size(), -1); + for (std::size_t k = 0; k < from.size(); ++k) + routes_of[from[k]] = static_cast(k); + std::vector checked(own); + std::vector order; + for (std::size_t index = 0; index < posts.size(); ++index) + if (own[index]) + order.push_back(index); + for (std::size_t at = 0; at < order.size(); ++at) + for (auto const &route : found[routes_of[order[at]]]) + for (auto const index : {route.warning, route.next}) + if (index >= 0 && 0 == checked[index] && posts[index].kind != role::entry && routes_of[index] >= 0) + { + checked[index] = 1; + order.push_back(static_cast(index)); + } + for (auto grown{true}; grown;) + { + grown = false; + for (std::size_t index = 0; index < posts.size(); ++index) + { + if (checked[index] || routes_of[index] < 0 || posts[index].kind == role::entry) + continue; + auto const &routes{found[routes_of[index]]}; + if (std::none_of(routes.begin(), routes.end(), [&](station::route const &Route) { return Route.next >= 0 && checked[Route.next]; })) + continue; + checked[index] = 1; + order.push_back(index); + grown = true; + } + } + std::unordered_map by_file; + for (std::size_t index = 0; index < heads.size(); ++index) + by_file.emplace(heads[index].file, index); + for (auto const index : order) + { + auto const &signal{Standing[owners[index]]}; + auto const &post{posts[index]}; + head_check check; + check.instance = signal.instance; + check.name = signal_shown(signal.name, stem); + check.kind = signal.kind; + check.position = signal.position; + check.travel = travel_of(signal.yaw); + check.file = signal.file; + check.station = own[index] != 0; + check.routes = found[routes_of[index]]; + for (auto const &route : check.routes) + check.nexts.push_back(route.next >= 0 ? signal_shown(Standing[owners[route.next]].name, stem) : std::string{}); + auto const known{by_file.find(signal.file)}; + auto const *template_of{known != by_file.end() ? &heads[known->second] : nullptr}; + if (template_of != nullptr) + { + check.lamps = template_of->lamps; + check.own = template_of->speeds; + } + if (post.kind == role::warning) + { + for (std::size_t k = 0; k < check.routes.size(); ++k) + { + auto const &route{check.routes[k]}; + if (route.next < 0) + check.problems.push_back(format(STR_C("No main signal within %.0f m ahead of it to tell of"), route.length)); + else if (route.length < braking - 1.0) + check.problems.push_back(format(STR_C("It stands %.0f m before %s, less than the braking distance of %.0f m: the trains don't stop by the signal in time"), route.length, check.nexts[k].c_str(), braking)); + } + m_heads.push_back(std::move(check)); + continue; + } + auto const main{post.kind == role::entry}; + auto const need{need_of(check.routes, main, line, braking)}; + check.speeds = need.speeds; + check.informs = need.informs; + check.white = need.white; + check.shortened = need.shortened; + check.indicator = need.indicator; + if (false == main) + for (std::size_t k = 0; k < check.routes.size(); ++k) + { + auto const &route{check.routes[k]}; + if (route.next >= 0 && route.warning < 0 && route.length < braking - 1.0) + check.problems.push_back(format(STR_C("The block section to %s is %.0f m long, shorter than the braking distance of %.0f m, Ie-4 § 8 ust. 1"), check.nexts[k].c_str(), route.length, braking)); + } + if (template_of != nullptr) + { + check.fits = shows(*template_of, need); + auto const best{best_head(heads, signal.kind, template_of->mount, template_of->name, need)}; + if (best >= 0) + { + check.fit = heads[best].file; + check.more = check.fits && surplus(heads[best], need) < surplus(*template_of, need); + } + } + m_heads.push_back(std::move(check)); + } + std::stable_sort(m_heads.begin(), m_heads.end(), [&](head_check const &A, head_check const &B) { + if (A.station != B.station) + return A.station; + if (A.station) + return station::project(layout, A.position).x < station::project(layout, B.position).x; + return geometry::plan_distance(A.position, middle) < geometry::plan_distance(B.position, middle); + }); +} + +void srk_tool::fit_heads() +{ + m_status.clear(); + signal_changes changes; + for (auto const &head : m_heads) + if (false == head.fit.empty() && head.fit != head.file && (false == head.fits || head.more)) + changes.files.emplace_back(head.instance, head.fit); + if (changes.files.empty()) + { + m_status = std::any_of(m_heads.begin(), m_heads.end(), [](head_check const &Head) { return false == Head.fits; }) ? STR_C("No template has the lights the signals marked need") : STR_C("The lights of the signals fit already"); + return; + } + std::string error; + m_signals.signal_batch(changes, {}, {}, error); + if (false == error.empty()) + { + m_status = format(STR_C("The signals couldn't be changed: %s"), error.c_str()); + return; + } + std::vector standing; + m_signals.standing_signals(standing, true); + std::size_t changed{0}; + for (auto const &head : m_heads) + changed += static_cast(std::count_if(standing.begin(), standing.end(), [&](standing_signal const &Signal) { return Signal.instance == head.instance && Signal.file != head.file; })); + if (changed == 0) + m_status = STR_C("The lights of the signals fit already"); + else + m_status = format(STR_P("%zu signal got the lights it needs, Ctrl+Z takes it back", "%zu signals got the lights they need, Ctrl+Z takes it back", changed), changed); +} + void srk_tool::survey() { m_circuit_of.assign(m_layout.has_value() ? m_layout->paths.size() : 0, -1); @@ -2259,8 +2931,9 @@ void srk_tool::render_canvas(float const Height) { auto const at{screen_of(mark.at)}; auto const way{mark.ahead ? 1.0f : -1.0f}; - draw->AddTriangleFilled(ImVec2(at.x - 4.0f * way, at.y - 4.0f), ImVec2(at.x - 4.0f * way, at.y + 4.0f), ImVec2(at.x + 4.0f * way, at.y), kSignal); - label(mark.text, kSignal, {ImVec2(at.x, at.y - height - 5.0f), ImVec2(at.x, at.y + 5.0f)}); + auto const colour{mark.unfit ? kUnfit : kSignal}; + draw->AddTriangleFilled(ImVec2(at.x - 4.0f * way, at.y - 4.0f), ImVec2(at.x - 4.0f * way, at.y + 4.0f), ImVec2(at.x + 4.0f * way, at.y), colour); + label(mark.unfit ? mark.text + " !" : mark.text, colour, {ImVec2(at.x, at.y - height - 5.0f), ImVec2(at.x, at.y + 5.0f)}); } std::vector order; for (std::size_t index = 0; index < m_circuits.size(); ++index) @@ -2545,6 +3218,58 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri } return editor_selftest_result::handled; } + if (Command == "srkheads") + { + // srkheads [braking=] [fit=1]: the lights of the signals of the open station checked, fitted + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkheads: no station open"); + return editor_selftest_result::handled; + } + auto const values{selftest_values(Words)}; + if (auto const braking{values.find("braking")}; braking != values.end()) + { + record_of_open().braking = std::atoi(braking->second.c_str()); + m_changed = true; + } + std::vector standing; + m_signals.standing_signals(standing, true); + std::vector heads; + m_signals.signal_heads(heads, true); + refresh(); + if (auto const fit{values.find("fit")}; fit != values.end() && fit->second == "1" && m_layout.has_value()) + { + fit_heads(); + WriteLog("SELFTEST srkheads fit: " + m_status); + refresh(); + } + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkheads: the station is gone"); + return editor_selftest_result::handled; + } + WriteLog(format("SELFTEST srkheads: speed %.0f (set %.0f), braking %.0f, %zu checked, templates %zu", speed_of_open(), set_speed_of_open(), braking_of_open(), m_heads.size(), heads.size())); + auto const list = [](std::vector const &Speeds) { + std::string text; + for (auto const speed : Speeds) + text += (text.empty() ? "" : "/") + std::to_string(speed); + return text.empty() ? std::string{"-"} : text; + }; + for (auto const &head : m_heads) + { + std::string routes; + for (std::size_t k = 0; k < head.routes.size(); ++k) + { + auto const &route{head.routes[k]}; + routes += format(" %s:%.0f:%.0f%s%s", route.next >= 0 ? head.nexts[k].c_str() : route.ended ? "end" : "none", route.length, route.speed, route.warning >= 0 ? ":to" : "", route.turnouts.empty() ? "" : format(":%zut", route.turnouts.size()).c_str()); + } + WriteLog(format("SELFTEST srkheads %s %s%s %s lamps=%s own=%s need=%s%s%s short=%d%s fits=%d more=%d fit=%s routes%s problems=%zu", head.name.c_str(), head.kind.c_str(), head.station ? "" : "(line)", head.file.c_str(), head.lamps.c_str(), list(head.own).c_str(), list(head.speeds).c_str(), head.informs ? "+inform" : "", + head.white ? "+white" : "", head.shortened, head.indicator ? " w21" : "", head.fits ? 1 : 0, head.more ? 1 : 0, head.fit.empty() ? "-" : head.fit.c_str(), routes.c_str(), head.problems.size())); + for (auto const &problem : head.problems) + WriteLog("SELFTEST srkheads " + problem); + } + return editor_selftest_result::handled; + } if (Command == "srkstrip") { // srkstrip shown=0: the schematic hidden, the 3d view whole for a screenshot diff --git a/application/editormodesrk.h b/application/editormodesrk.h index d5c79e34..61a4b890 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -56,6 +56,7 @@ class srk_tool final : public editor_tool std::string line; // the number of the railway line std::string km; // of the axis of the station along the line, empty while unknown bool reversed{false}; // the km rise to the left of the schematic, the switches numbered from there + int braking{0}; // m, the braking distance of the line set by hand, 0: after its speed }; // the schematic: the chainage across, the offsets upwards, exaggerated struct view @@ -104,6 +105,7 @@ class srk_tool final : public editor_tool bool ahead{true}; // the trains it faces run along the chainage std::string text; glm::dvec3 head{0.0}; // above its lamps, in the scenery + bool unfit{false}; // its lights don't show what its routes need }; std::vector m_signal_marks; bool m_signals_known{false}; // the signals standing surveyed, once their templates are found @@ -113,6 +115,33 @@ class srk_tool final : public editor_tool int m_length_hover{-1}; // of the lengths, the row under the mouse int m_length_canvas{-1}; // of the lengths, of the track under the mouse in the schematic int m_length_shown{-1}; // of the lengths, the one drawn: of the row or the track under the mouse, else of the track selected + // a signal of the station or of its lines, its lights as the routes of its trains need them: Ie-1, Ie-4 § 8 + struct head_check + { + scene::instance_handle instance{0}; + std::string name; // shown, without the code of the station + std::string kind; + glm::dvec3 position{0.0}; + glm::dvec2 travel{0.0, 1.0}; + std::string file; // of its template + std::string lamps; + std::vector own; // the speeds of its template + bool station{false}; // stands by the station, not out on a line + std::vector routes; + std::vector nexts; // of the routes, the signals they end at, shown + std::vector speeds; // km/h, the lower ones it must show: 40, 60, 100 + bool informs{false}; // it must tell the next signal shows stop + bool white{false}; // it must give the substitute signal + int shortened{0}; // km/h, it slows the trains to, the next signal nearer than the braking distance: Ie-4 § 8 ust. 5 + bool indicator{false}; // a W21 "14" beside it, for 140 km/h so + bool fits{true}; // its template shows all of it + bool more{false}; // its template has more than it needs + std::string fit; // the template that fits best, empty for none + std::vector problems; // of its place + }; + std::vector m_heads; + bool m_heads_known{false}; // the templates found + int m_head_hover{-1}; // of the heads, the row under the mouse std::string m_status; void load(); @@ -137,6 +166,17 @@ class srk_tool final : public editor_tool void survey_signals(); // the lengths of the station tracks, the signals given by them void measure(std::vector const &Signals); + // km/h, the highest of the speeds the tracks of the open station are set to, 0 while none is + double set_speed_of_open() const; + // km/h, the trains run the station at: the highest set on its tracks, else after the braking distance set by hand, else 160 + double speed_of_open() const; + // m, the braking distance of the line of the open station: set by hand, else after its speed, as Ie-4 § 8 gives it + double braking_of_open() const; + // the lights of the signals checked against the routes of their trains and the places of the signals they tell of + void check_heads(std::vector const &Standing); + void render_heads(); + // the signals given the templates whose lights fit their routes; one step of the history + void fit_heads(); // the lengths of the station tracks as the list of the tracks gives them, the useful ones each way void render_lengths(); // the lengths as a table to paste into a spreadsheet, a line a track, the columns parted by tabs diff --git a/application/editortool.h b/application/editortool.h index 267350ed..6dbecc6a 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -85,6 +85,7 @@ struct signal_order std::string name; std::string plate; // the name on the plate under the head, when the template has one std::string linked{"none"}; // the signal before it, when the template takes one: a block signal shows its aspects by it + std::string file; // the template, of its kind; empty for the one chosen for the kind }; struct signal_rename { @@ -101,6 +102,21 @@ struct standing_signal glm::dvec3 position{0.0}; double yaw{0.0}; std::string linked; // of a template that takes one, else empty + std::string file; // of its template + TTrack *track{nullptr}; // the trains read it on, nullptr while none does + int facing{0}; // the trains it faces run 1: towards the end of the path of the track, -1: towards its start, 0 unknown +}; +// a signal template as the signalling picks one: the lights it has and the speeds its events show +struct signal_head +{ + std::string file; + std::string name; + std::string kind; + std::string mount; + std::string lamps; // its chambers from the top: z green, c red, p orange, b white, n blue, x blind + std::vector speeds; // of the signal itself, km/h: -1 the highest allowed, 20 the substitute signal + bool informs{false}; // it tells the next signal shows stop + bool chosen{false}; // the Signals tool places it for its kind }; // one step of the history of the signalling: signals placed, standing ones renamed, other templates placed with them, the values // of standing ones replaced @@ -110,6 +126,7 @@ struct signal_changes std::vector renamed; std::vector others; std::vector>> values; + std::vector> files; // standing signals given another template, of the same kind, their values kept }; struct nmt_place @@ -195,6 +212,8 @@ class editor_signals // the signals placed by the templates chosen for their kinds and the rest of the changes, with the tracks changed before: one // step of the history. returns: how many signals were placed virtual std::size_t signal_batch(signal_changes const &Changes, std::vector> States, std::vector Created, std::string &Error) = 0; + // the signal templates with lights, false while they are still being looked for + virtual bool signal_heads(std::vector &Heads, bool const Wait) = 0; protected: ~editor_signals() = default; diff --git a/editor/editorIncludeInfo.cpp b/editor/editorIncludeInfo.cpp index 55b7cd4f..64bac01b 100644 --- a/editor/editorIncludeInfo.cpp +++ b/editor/editorIncludeInfo.cpp @@ -501,6 +501,49 @@ int parameter_count(std::string const &File) return read_template(File, text, error) ? count_parameters(text) : 0; } +bool signal_speeds(std::string const &File, std::vector &Speeds, bool &Informs) +{ + Speeds.clear(); + Informs = false; + template_text text; + std::string error; + if (false == read_template(File, text, error)) + { + return false; + } + auto const tokens{tokenize(text.content)}; + auto const speed = [](std::string const &Token, int &Speed) { + char *end{nullptr}; + auto const value{std::strtod(Token.c_str(), &end)}; + if (end == Token.c_str() || *end != '\0') + { + return false; + } + Speed = static_cast(std::lround(value)); + return true; + }; + for (std::size_t idx = 0; idx + 2 < tokens.size(); ++idx) + { + if (tokens[idx] != "setvelocity") + { + continue; + } + int own{0}; + int next{0}; + auto const shown{speed(tokens[idx + 1], own)}; + if (shown && own != 0 && std::find(Speeds.begin(), Speeds.end(), own) == Speeds.end()) + { + Speeds.push_back(own); + } + if (speed(tokens[idx + 2], next) && next == 0 && (tokens[idx + 1] == "*" || (shown && own != 0 && own != 20))) + { + Informs = true; + } + } + std::sort(Speeds.begin(), Speeds.end()); + return true; +} + void suggest(std::string const &File, include_info &Info) { template_text text; diff --git a/editor/editorIncludeInfo.hpp b/editor/editorIncludeInfo.hpp index 4b70de17..97099fef 100644 --- a/editor/editorIncludeInfo.hpp +++ b/editor/editorIncludeInfo.hpp @@ -68,6 +68,9 @@ bool load(std::string const &File, include_info &Info, std::string &Error); bool save(std::string const &File, include_info const &Info, std::string &Error); // highest number of (pN) placeholder used by specified template, 0 if it has none int parameter_count(std::string const &File); +// the speeds the events of a signal template set it to show, by their SetVelocity: of the signal itself, km/h, -1 the highest +// allowed, 20 the substitute signal. Informs: one of them tells the next signal shows stop. returns: false if the file can't be read +bool signal_speeds(std::string const &File, std::vector &Speeds, bool &Informs); // sets roles of the parameters whose use in the template makes their meaning clear. roles already set are left alone void suggest(std::string const &File, include_info &Info); // true if value of a parameter with specified role is supplied by the editor when the template is placed diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index 042fd4e8..d7d79757 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -2043,4 +2043,190 @@ std::vector track_lengths(layout const &Layout, std::vector c return result; } +namespace +{ + +double constexpr kStraight{5000.0}; // m, a path curved less goes for a straight one +int constexpr kPieces{20000}; // of the tracks passed by the routes of a signal, at most + +struct reach +{ + TTrack *track{nullptr}; + int path{0}; + int facing{1}; + double from{0.0}; // m, along the path from its start, where the route comes in + double length{0.0}; // of the route up to there + double speed{0.0}; + int warning{-1}; + std::vector turnouts; + std::vector> passed; +}; + +} // namespace + +double path_speed(TTrack const &Track, int const Path) +{ + bezier const curve{Track.m_paths[Path]}; + int constexpr count{32}; + int constexpr span{4}; + auto least{std::numeric_limits::max()}; + for (int i = 0; i + 2 * span <= count; ++i) + { + auto const a{map_of(curve.point(static_cast(i) / count))}; + auto const b{map_of(curve.point(static_cast(i + span) / count))}; + auto const c{map_of(curve.point(static_cast(i + 2 * span) / count))}; + auto const twice{std::abs((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x))}; + if (twice > 1e-9) + least = std::min(least, glm::distance(a, b) * glm::distance(b, c) * glm::distance(c, a) / (2.0 * twice)); + } + return least > kStraight ? 0.0 : std::sqrt(8.424 * least); +} + +std::vector> routes(std::vector const &Posts, std::vector const &From, double const Limit) +{ + std::vector> all; + struct along + { + std::vector points; + std::vector distances; + }; + std::map, along> paths; + auto const along_of = [&](TTrack const &Track, int const Path) -> along const & { + auto &found{paths[{&Track, Path}]}; + if (found.points.empty()) + { + double length{0.0}; + found.points = points_of(Track, Path, length); + found.distances.assign(1, 0.0); + for (std::size_t k = 1; k < found.points.size(); ++k) + found.distances.push_back(found.distances.back() + plan_distance(found.points[k - 1], found.points[k])); + } + return found; + }; + auto const project_on = [&](along const &Path, glm::dvec3 const &Point, double &Gap) { + auto best{std::numeric_limits::max()}; + double at{0.0}; + for (std::size_t k = 0; k + 1 < Path.points.size(); ++k) + { + auto const a{map_of(Path.points[k])}; + auto const ab{map_of(Path.points[k + 1]) - a}; + auto const length2{glm::dot(ab, ab)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(map_of(Point) - a, ab) / length2, 0.0, 1.0) : 0.0}; + if (auto const distance{glm::distance(a + ab * t, map_of(Point))}; distance < best) + { + best = distance; + at = Path.distances[k] + (Path.distances[k + 1] - Path.distances[k]) * t; + } + } + Gap = best; + return at; + }; + std::unordered_map> by_track; + for (std::size_t index = 0; index < Posts.size(); ++index) + if (Posts[index].track != nullptr) + by_track[Posts[index].track].push_back(index); + auto const routes_from = [&](std::size_t const Start) { + std::vector result; + auto const &start{Posts[Start]}; + auto path{0}; + auto nearest{std::numeric_limits::max()}; + double from{0.0}; + for (int candidate = 0; candidate < static_cast(start.track->m_paths.size()); ++candidate) + { + double gap{0.0}; + auto const at{project_on(along_of(*start.track, candidate), start.point, gap)}; + if (gap < nearest - 1e-3) + { + nearest = gap; + path = candidate; + from = at; + } + } + std::vector stack{{start.track, path, start.facing, from, 0.0, 0.0, -1, {}, {}}}; + auto pieces{0}; + while (false == stack.empty() && pieces++ < kPieces) + { + auto here{std::move(stack.back())}; + stack.pop_back(); + auto const &track{*here.track}; + auto const &line{along_of(track, here.path)}; + auto const total{line.distances.back()}; + here.passed.emplace_back(&track, here.path * 2 + (here.facing > 0 ? 1 : 0)); + if (node(track)) + { + auto const speed{path_speed(track, here.path)}; + auto limit{speed}; + auto const set{track.SwitchExtension != nullptr ? std::abs(static_cast(track.SwitchExtension->fVelocity)) : here.track->VelocityGet()}; + if (speed > 0.0 && set >= 2.0) + limit = std::min(speed, set); + if (limit > 0.0 && (here.speed <= 0.0 || limit < here.speed)) + here.speed = limit; + if (speed > 0.0 && (track.eType != tt_Switch || here.path != straighter(track))) + here.turnouts.push_back(here.track); + } + // the signals ahead on the path, the nearest first + std::vector> ahead; + if (auto const found{by_track.find(&track)}; found != by_track.end()) + for (auto const index : found->second) + { + auto const &other{Posts[index]}; + if (index == Start || other.facing != here.facing || other.kind == signal::role::shunting) + continue; + double gap{0.0}; + auto const at{project_on(line, other.point, gap)}; + auto const distance{(at - here.from) * here.facing}; + if (gap < 4.5 && distance > 0.1) + ahead.emplace_back(distance, index); + } + std::sort(ahead.begin(), ahead.end()); + auto stopped{false}; + for (auto const &[distance, index] : ahead) + { + if (here.length + distance > Limit) + break; + if (Posts[index].kind == signal::role::warning) + { + if (here.warning < 0) + here.warning = static_cast(index); + continue; + } + result.push_back({static_cast(index), here.warning, here.length + distance, here.speed, here.turnouts, false}); + stopped = true; + break; + } + if (stopped) + continue; + auto const length{here.length + (here.facing > 0 ? total - here.from : here.from)}; + if (length > Limit) + { + result.push_back({-1, here.warning, Limit, here.speed, here.turnouts, false}); + continue; + } + auto const atend{here.facing > 0}; + auto const point{end_of(track, here.path, atend)}; + auto *next{neighbour(track, here.path, atend)}; + auto went{false}; + if (next != nullptr && rail(*next)) + for (int other = 0; other < static_cast(next->m_paths.size()); ++other) + for (auto const end : {false, true}) + { + if (glm::distance(end_of(*next, other, end), point) >= same_point) + continue; + auto const facing{end ? -1 : 1}; + if (std::find(here.passed.begin(), here.passed.end(), std::make_pair(static_cast(next), other * 2 + (facing > 0 ? 1 : 0))) != here.passed.end()) + continue; + auto const &entered{along_of(*next, other)}; + stack.push_back({next, other, facing, end ? entered.distances.back() : 0.0, length, here.speed, here.warning, here.turnouts, here.passed}); + went = true; + } + if (false == went) + result.push_back({-1, here.warning, length, here.speed, here.turnouts, true}); + } + return result; + }; + for (auto const from : From) + all.push_back(from < Posts.size() && Posts[from].track != nullptr && false == Posts[from].track->m_paths.empty() ? routes_from(from) : std::vector{}); + return all; +} + } // namespace station diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index ff676c15..8272c075 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -226,4 +226,27 @@ struct lengths // the lengths of the station tracks, the signals standing by them among the signals given std::vector track_lengths(layout const &Layout, std::vector const &Tracks, std::vector const &Signals); +// a signal by the track the trains read it on +struct post +{ + TTrack *track{nullptr}; + int facing{1}; // the trains it faces run 1: towards the end of the path, -1: towards its start + glm::dvec3 point{0.0}; + signal::role kind{signal::role::entry}; // entry for any main signal +}; +// the way of the trains from a signal on to the next main or block signal facing them +struct route +{ + int next{-1}; // of the posts, -1 for none within the limit + int warning{-1}; // the warning disc passed on the way, of the posts + double length{0.0}; // m, in the plan + double speed{0.0}; // km/h, the least the switches on it allow on their curved paths, by the radius and by the speed set; 0 for no limit + std::vector turnouts; // the switches it turns off the straight at + bool ended{false}; // at the end of the tracks, before the limit +}; +// km/h, a train runs the path at by its least radius, 0.65 m/s² of the acceleration left unbalanced without cant; 0 for a straight one +double path_speed(TTrack const &Track, int const Path); +// the routes of the trains from each of the posts given on through the switches, none longer than the limit +std::vector> routes(std::vector const &Posts, std::vector const &From, double const Limit); + } // namespace station diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 7178f8c3..8b55cf7a 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10885,3 +10885,180 @@ msgstr "Tor\tDługość budowlana [m]\tUżyteczna w prawo [m]\tod\tdo\tUżyteczn msgid "Track %s, useful length %.0f m to the right, %.0f m to the left" msgstr "Tor %s, długość użyteczna %.0f m w prawo, %.0f m w lewo" +msgid "green" +msgstr "zielone" + +msgid "two orange" +msgstr "dwa pomarańczowe" + +msgid "red" +msgstr "czerwone" + +msgid "%s km/h" +msgstr "%s km/h" + +msgid "an orange strip" +msgstr "pomarańczowy pas świetlny" + +msgid "a green strip" +msgstr "zielony pas świetlny" + +msgid "Signal heads" +msgstr "Komory semaforów" + +msgid "Line speed" +msgstr "Prędkość linii" + +msgid "%.0f km/h, the highest set on its main tracks" +msgstr "%.0f km/h, najwyższa ustawiona na torach głównych" + +msgid "%.0f km/h taken: no track of the station has a speed set" +msgstr "przyjęto %.0f km/h: żaden tor stacji nie ma ustawionej prędkości" + +msgid "The speed of the trains through the station, the highest one its main tracks and the lines out of it are set to in the Track tool, up to 250 km/h: the braking distance and the lights of the signals go by it. Without one 160 km/h is taken, or the speed of the braking distance set by hand" +msgstr "Prędkość pociągów przez stację: najwyższa, jaką jej tory główne i wychodzące z niej linie mają ustawioną w narzędziu Tor, do 250 km/h. Od niej zależą droga hamowania i światła semaforów. Gdy żaden tor jej nie ma, przyjmuje się 160 km/h albo prędkość drogi hamowania ustawionej ręcznie" + +msgid "Braking distance" +msgstr "Droga hamowania" + +msgid "%.0f m, after the speed" +msgstr "%.0f m, według prędkości" + +msgid "%d m, set by hand" +msgstr "%d m, ustawiona ręcznie" + +msgid "The braking distance of the trains on the line, Ie-4 § 8: 400 m up to 60 km/h, 700 m up to 100, 1000 m up to 140, 1300 m above. The block sections and the warning discs stand no nearer, a signal telling of the next one standing nearer slows the trains. Set it by hand where the line has another one" +msgstr "Droga hamowania pociągów na linii, Ie-4 § 8: 400 m do 60 km/h, 700 m do 100, 1000 m do 140, 1300 m powyżej. Odstępy blokady i tarcze ostrzegawcze nie są krótsze, a semafor zapowiadający następny, który stoi bliżej, zmniejsza prędkość pociągów. Ustaw ją ręcznie, gdy linia ma inną" + +msgid "Looking for the signal templates..." +msgstr "Szukam szablonów semaforów..." + +msgid "No main signal stands by the station yet: its lights are checked once one does" +msgstr "Przy stacji nie stoi jeszcze żaden semafor: światła sprawdzę, gdy jakiś stanie" + +msgid "Speeds" +msgstr "Prędkości" + +msgid "Lights" +msgstr "Światła" + +msgid "State" +msgstr "Stan" + +msgid "Main signal %s" +msgstr "Semafor %s" + +msgid "Block signal %s" +msgstr "Semafor SBL %s" + +msgid "Warning disc %s" +msgstr "Tarcza ostrzegawcza %s" + +msgid "To %s: %.0f m" +msgstr "Do %s: %.0f m" + +msgid "To the end of the tracks: %.0f m" +msgstr "Do końca torów: %.0f m" + +msgid "No signal within %.0f m" +msgstr "Żadnego semafora przez %.0f m" + +msgid "%d km/h through the switches" +msgstr "%d km/h przez rozjazdy" + +msgid "%.0f km/h" +msgstr "%.0f km/h" + +msgid "turning at %s" +msgstr "zwrot na %s" + +msgid "past a warning disc" +msgstr "za tarczą ostrzegawczą" + +msgid "Needs: %s" +msgstr "Potrzebne: %s" + +msgid "The next signal stands nearer than the braking distance: with it at stop this one slows the trains to %d km/h, Ie-4 § 8 ust. 5" +msgstr "Następny semafor stoi bliżej niż droga hamowania: gdy on pokazuje stój, ten zmniejsza prędkość pociągów do %d km/h, Ie-4 § 8 ust. 5" + +msgid "Give it a W21 indicator \"14\": 140 km/h with the next signal at stop, Ie-4 § 8 ust. 5" +msgstr "Dostaw mu wskaźnik W21 „14”: 140 km/h, gdy następny semafor pokazuje stój, Ie-4 § 8 ust. 5" + +msgid "A semaphore with arms: its lights aren't checked" +msgstr "Semafor kształtowy: jego świateł nie sprawdzam" + +msgid "No template of its kind and mount has these lights" +msgstr "Żaden szablon tego rodzaju i mocowania nie ma tych świateł" + +msgid "Its lights don't show all of it: %s does" +msgstr "Jego światła nie pokażą wszystkiego, %s pokaże" + +msgid "It has more lights than it needs: %s will do" +msgstr "Ma więcej świateł, niż trzeba: wystarczy %s" + +msgid "LMB: fly to it" +msgstr "LPM: leć do niego" + +msgid "disc" +msgstr "tarcza" + +msgid "arms" +msgstr "ramiona" + +msgid "no template" +msgstr "brak szablonu" + +msgid "lights missing" +msgstr "brak świateł" + +msgid "more than needed" +msgstr "za dużo" + +msgid "fits" +msgstr "pasuje" + +msgid "too near" +msgstr "za blisko" + +msgid "Fit the lights" +msgstr "Dopasuj światła" + +msgid "Gives each signal marked the template of its kind and mount with the lights its routes need and the least beyond them, the name, the plate and the place kept, the trains reading it as before. One step of undo" +msgstr "Daje każdemu zaznaczonemu semaforowi szablon tego samego rodzaju i mocowania ze światłami, jakich wymagają jego przebiegi, i jak najmniejszym nadmiarem. Nazwa, tablica i miejsce zostają, pociągi odczytują go jak dotąd. Jeden krok cofania" + +msgid "The lights go by the routes of the trains: 40 km/h through the switches 1:9, 60 through 1:12 and 1:14, 100 through 1:18.5, by their radii, for these a lower orange light, with an orange strip for 60, a green one for 100, Ie-1; an upper orange one where the next signal can show stop; the white one of the substitute signal. A signal telling of the next one standing nearer than the braking distance slows the trains with it at stop, Ie-4 § 8 ust. 5" +msgstr "Światła wynikają z przebiegów pociągów: 40 km/h przez rozjazdy 1:9, 60 przez 1:12 i 1:14, 100 przez 1:18,5, liczone z ich promieni; dla nich dolne światło pomarańczowe, z pomarańczowym pasem dla 60 i zielonym dla 100, Ie-1; górne pomarańczowe, gdy następny semafor może pokazać stój; białe dla sygnału zastępczego. Semafor zapowiadający następny, który stoi bliżej niż droga hamowania, zmniejsza prędkość pociągów, gdy tamten pokazuje stój, Ie-4 § 8 ust. 5" + +msgid "No template has the lights %s need, they got the one chosen in the Signals tool: see Signal heads" +msgstr "Żaden szablon nie ma świateł potrzebnych dla %s, dostały ten wybrany w narzędziu Sygnalizatory: zobacz Komory semaforów" + +msgid "No main signal within %.0f m ahead of it to tell of" +msgstr "Przez %.0f m przed nią nie ma semafora, który by zapowiadała" + +msgid "It stands %.0f m before %s, less than the braking distance of %.0f m: the trains don't stop by the signal in time" +msgstr "Stoi %.0f m przed %s, mniej niż droga hamowania %.0f m: pociąg nie zdąży się zatrzymać przed semaforem" + +msgid "The block section to %s is %.0f m long, shorter than the braking distance of %.0f m, Ie-4 § 8 ust. 1" +msgstr "Odstęp do %s ma %.0f m, mniej niż droga hamowania %.0f m, Ie-4 § 8 ust. 1" + +msgid "No template has the lights the signals marked need" +msgstr "Żaden szablon nie ma świateł potrzebnych zaznaczonym semaforom" + +msgid "The lights of the signals fit already" +msgstr "Światła semaforów już pasują" + +msgid "The signals couldn't be changed: %s" +msgstr "Nie udało się zmienić semaforów: %s" + +msgid "%zu signal of the lines fits" +msgid_plural "%zu signals of the lines fit" +msgstr[0] "%zu semafor linii pasuje" +msgstr[1] "%zu semafory linii pasują" +msgstr[2] "%zu semaforów linii pasuje" + +msgid "%zu signal got the lights it needs, Ctrl+Z takes it back" +msgid_plural "%zu signals got the lights they need, Ctrl+Z takes it back" +msgstr[0] "%zu semafor dostał potrzebne światła, Ctrl+Z cofa" +msgstr[1] "%zu semafory dostały potrzebne światła, Ctrl+Z cofa" +msgstr[2] "%zu semaforów dostało potrzebne światła, Ctrl+Z cofa" + From cbbf373aab32a6c823d92eb0d248a7767fbfb2ea Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 17:55:05 +0200 Subject: [PATCH 063/145] fix(editor): the templates of the signals swapped or placed without a track changed are a step of the history of their own: they went into the step before, the block signals set say, undone with it, and the SRK tool, missing a new step, kept showing the lights as they were --- application/editormodesignal.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 7bccb392..81910a3a 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -1077,7 +1077,17 @@ std::size_t editor_mode::signal_batch(signal_changes const &Changes, std::vector auto const count{instances.size()}; if (step.tracks.empty() && Created.empty() && instances.empty() && directives.empty()) return 0; - push_track_snapshot(std::move(step.tracks), std::move(Created)); + if (step.tracks.empty() && Created.empty()) + { + EditorSnapshot only; + auto const *file{scene::Layers.instance(directives.empty() ? instances.front() : directives.front().first).file}; + only.node_name = file != nullptr ? *file : std::string{}; + trim_history(); + m_history.push_back(std::move(only)); + g_redo.clear(); + } + else + push_track_snapshot(std::move(step.tracks), std::move(Created)); auto &pushed{m_history.back()}; pushed.instances = std::move(instances); pushed.directives.insert(pushed.directives.end(), directives.begin(), directives.end()); From a811486b538aec91113193461c65f36c18092780 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 17:55:05 +0200 Subject: [PATCH 064/145] fix(editor): the pin of the signal under the mouse in the list of the signal heads stands over its name, clear of its lamps; the routes name the switches they turn off the straight at, a plural for several, and the Polish of the tooltip reads plainer --- application/editormodesrk.cpp | 10 ++++++---- lang/editor_pl.po | 15 +++++++++------ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 5ba0c5f4..ffb08000 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -772,11 +772,13 @@ void srk_tool::draw_overlay() const if (m_head_hover >= 0 && m_head_hover < static_cast(m_heads.size())) { auto const &head{m_heads[m_head_hover]}; - ImVec2 foot, top; - if (projection.project(head.position, foot) && projection.project(head.position + glm::dvec3{0.0, 6.0, 0.0}, top)) + ImVec2 label; + if (projection.project(head.position + glm::dvec3{0.0, 7.0, 0.0}, label)) { + ImVec2 const foot{label.x, label.y - ImGui::GetTextLineHeight() - 8.0f}; + ImVec2 const top{label.x, foot.y - 30.0f}; drawlist->AddLine(foot, top, kHovered, 3.0f); - drawlist->AddCircle(top, 9.0f, kHovered, 0, 3.0f); + drawlist->AddCircle(ImVec2(top.x, top.y - 9.0f), 9.0f, kHovered, 0, 3.0f); } } } @@ -1193,7 +1195,7 @@ void srk_tool::render_heads() for (auto const *turnout : route.turnouts) turnouts += (turnouts.empty() ? "" : ", ") + without_stem(turnout->name(), stem_of_open()); if (false == turnouts.empty()) - text += ", " + format(STR_C("turning at %s"), turnouts.c_str()); + text += ", " + format(STR_P("off the straight at switch %s", "off the straight at switches %s", route.turnouts.size()), turnouts.c_str()); if (route.warning >= 0) text += ", " + STR("past a warning disc"); ImGui::BulletText("%s", text.c_str()); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 8b55cf7a..4202c641 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10961,7 +10961,7 @@ msgid "To the end of the tracks: %.0f m" msgstr "Do końca torów: %.0f m" msgid "No signal within %.0f m" -msgstr "Żadnego semafora przez %.0f m" +msgstr "Brak semafora na %.0f m" msgid "%d km/h through the switches" msgstr "%d km/h przez rozjazdy" @@ -10969,8 +10969,11 @@ msgstr "%d km/h przez rozjazdy" msgid "%.0f km/h" msgstr "%.0f km/h" -msgid "turning at %s" -msgstr "zwrot na %s" +msgid "off the straight at switch %s" +msgid_plural "off the straight at switches %s" +msgstr[0] "w bok na rozjeździe %s" +msgstr[1] "w bok na rozjazdach %s" +msgstr[2] "w bok na rozjazdach %s" msgid "past a warning disc" msgstr "za tarczą ostrzegawczą" @@ -10991,13 +10994,13 @@ msgid "No template of its kind and mount has these lights" msgstr "Żaden szablon tego rodzaju i mocowania nie ma tych świateł" msgid "Its lights don't show all of it: %s does" -msgstr "Jego światła nie pokażą wszystkiego, %s pokaże" +msgstr "Brakuje mu świateł, pasuje %s" msgid "It has more lights than it needs: %s will do" msgstr "Ma więcej świateł, niż trzeba: wystarczy %s" msgid "LMB: fly to it" -msgstr "LPM: leć do niego" +msgstr "LPM: pokaż go w scenerii" msgid "disc" msgstr "tarcza" @@ -11033,7 +11036,7 @@ msgid "No template has the lights %s need, they got the one chosen in the Signal msgstr "Żaden szablon nie ma świateł potrzebnych dla %s, dostały ten wybrany w narzędziu Sygnalizatory: zobacz Komory semaforów" msgid "No main signal within %.0f m ahead of it to tell of" -msgstr "Przez %.0f m przed nią nie ma semafora, który by zapowiadała" +msgstr "Na %.0f m przed nią nie ma semafora do zapowiadania" msgid "It stands %.0f m before %s, less than the braking distance of %.0f m: the trains don't stop by the signal in time" msgstr "Stoi %.0f m przed %s, mniej niż droga hamowania %.0f m: pociąg nie zdąży się zatrzymać przed semaforem" From 9c124b23bbae97af77034dcf5439c5fbc47b2161 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 18:54:54 +0200 Subject: [PATCH 065/145] refactor(editor): a signal head tells the next one shows stop by its chambers alone, an orange light above the red one, not by the events of its template reading the next signal, which the templates of the old sceneries lack --- application/editormode.h | 1 - application/editormodesignal.cpp | 4 ++-- application/editormodesrk.cpp | 7 ++++--- application/editortool.h | 1 - editor/editorIncludeInfo.cpp | 13 +++---------- editor/editorIncludeInfo.hpp | 4 ++-- 6 files changed, 11 insertions(+), 19 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 8cb25919..13d42f3a 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1920,7 +1920,6 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool plate{false}; bool linked{false}; std::vector speeds; - bool informs{false}; }; struct signal_spot { diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 81910a3a..189bf092 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -264,7 +264,7 @@ bool editor_mode::signal_scan(bool const Wait) if (parameter.id == 7) item.linked = true; } - editor_includes::signal_speeds(entry.file, item.speeds, item.informs); + editor_includes::signal_speeds(entry.file, item.speeds); tool.templates.push_back(std::move(item)); } std::sort(tool.templates.begin(), tool.templates.end(), [](signal_template const &A, signal_template const &B) { @@ -912,7 +912,7 @@ bool editor_mode::signal_heads(std::vector &Heads, bool const Wait) if (item.lamps.empty()) continue; auto const kind{kind_index(item.kind)}; - Heads.push_back({item.file, item.name, item.kind, item.mount, item.lamps, item.speeds, item.informs, kind >= 0 && m_signal.chosen[kind] == index}); + Heads.push_back({item.file, item.name, item.kind, item.mount, item.lamps, item.speeds, kind >= 0 && m_signal.chosen[kind] == index}); } return true; } diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index ffb08000..667a3d8b 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -302,15 +302,16 @@ bool has_speed(std::vector const &Speeds, int const Speed) return std::find(Speeds.begin(), Speeds.end(), Speed) != Speeds.end(); } -// the template has the lights the need takes: a lower orange one for a lower speed, a strip for 60 and 100, Ie-1 § 7, two orange -// ones to show a lower speed with the next signal at stop +// the template has the lights the need takes, by its chambers: an orange one to tell the next signal shows stop, a lower orange one +// for a lower speed, a strip for 60 and 100 by the speeds its events set, Ie-1 § 7, two orange ones to show a lower speed with the +// next signal at stop bool shows(signal_head const &Head, head_need const &Need) { auto const &lamps{Head.lamps}; auto const count = [&](char const Lamp) { return std::count(lamps.begin(), lamps.end(), Lamp); }; if (count('z') == 0 || count('c') == 0 || (Need.white && count('b') == 0)) return false; - if (Need.informs && (false == Head.informs || count('p') == 0)) + if (Need.informs && count('p') == 0) return false; if (Need.speeds.empty()) return true; diff --git a/application/editortool.h b/application/editortool.h index 6dbecc6a..70a8eaef 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -115,7 +115,6 @@ struct signal_head std::string mount; std::string lamps; // its chambers from the top: z green, c red, p orange, b white, n blue, x blind std::vector speeds; // of the signal itself, km/h: -1 the highest allowed, 20 the substitute signal - bool informs{false}; // it tells the next signal shows stop bool chosen{false}; // the Signals tool places it for its kind }; // one step of the history of the signalling: signals placed, standing ones renamed, other templates placed with them, the values diff --git a/editor/editorIncludeInfo.cpp b/editor/editorIncludeInfo.cpp index 64bac01b..11092df2 100644 --- a/editor/editorIncludeInfo.cpp +++ b/editor/editorIncludeInfo.cpp @@ -501,10 +501,9 @@ int parameter_count(std::string const &File) return read_template(File, text, error) ? count_parameters(text) : 0; } -bool signal_speeds(std::string const &File, std::vector &Speeds, bool &Informs) +bool signal_speeds(std::string const &File, std::vector &Speeds) { Speeds.clear(); - Informs = false; template_text text; std::string error; if (false == read_template(File, text, error)) @@ -522,23 +521,17 @@ bool signal_speeds(std::string const &File, std::vector &Speeds, bool &Info Speed = static_cast(std::lround(value)); return true; }; - for (std::size_t idx = 0; idx + 2 < tokens.size(); ++idx) + for (std::size_t idx = 0; idx + 1 < tokens.size(); ++idx) { if (tokens[idx] != "setvelocity") { continue; } int own{0}; - int next{0}; - auto const shown{speed(tokens[idx + 1], own)}; - if (shown && own != 0 && std::find(Speeds.begin(), Speeds.end(), own) == Speeds.end()) + if (speed(tokens[idx + 1], own) && own != 0 && std::find(Speeds.begin(), Speeds.end(), own) == Speeds.end()) { Speeds.push_back(own); } - if (speed(tokens[idx + 2], next) && next == 0 && (tokens[idx + 1] == "*" || (shown && own != 0 && own != 20))) - { - Informs = true; - } } std::sort(Speeds.begin(), Speeds.end()); return true; diff --git a/editor/editorIncludeInfo.hpp b/editor/editorIncludeInfo.hpp index 97099fef..d5340c03 100644 --- a/editor/editorIncludeInfo.hpp +++ b/editor/editorIncludeInfo.hpp @@ -69,8 +69,8 @@ bool save(std::string const &File, include_info const &Info, std::string &Error) // highest number of (pN) placeholder used by specified template, 0 if it has none int parameter_count(std::string const &File); // the speeds the events of a signal template set it to show, by their SetVelocity: of the signal itself, km/h, -1 the highest -// allowed, 20 the substitute signal. Informs: one of them tells the next signal shows stop. returns: false if the file can't be read -bool signal_speeds(std::string const &File, std::vector &Speeds, bool &Informs); +// allowed, 20 the substitute signal. returns: false if the file can't be read +bool signal_speeds(std::string const &File, std::vector &Speeds); // sets roles of the parameters whose use in the template makes their meaning clear. roles already set are left alone void suggest(std::string const &File, include_info &Info); // true if value of a parameter with specified role is supplied by the editor when the template is placed From edb4f7c60d59ab6b1a3398186dd11a0eae8d6fe1 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 18:55:13 +0200 Subject: [PATCH 066/145] feat(editor): the panel of a signal picked swaps its template for another of its kind and mount, the name, the plate, the place and the read event kept, a head leaning to one side for its counterpart leaning towards the track; one step of undo, the selftest signalswap --- application/editormode.h | 1 + application/editormodeselftestsignal.cpp | 20 ++++++++++- application/editormodesignal.cpp | 43 ++++++++++++++++++++++++ lang/editor_pl.po | 12 +++++++ 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/application/editormode.h b/application/editormode.h index 13d42f3a..12efab27 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1987,6 +1987,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool signal_move_to(signal_standing const &Standing, signal_spot const &Spot, char const *Read, char const *Unread); bool signal_flip(scene::instance_handle const Instance); bool signal_turn(scene::instance_handle const Instance); + bool signal_swap(scene::instance_handle const Instance, std::string const &File); signal_standing const *signal_standing_of(scene::instance_handle const Instance) const; bool signal_delete(scene::instance_handle const Instance); void signal_detach_events(std::string const &Read, std::vector> &States); diff --git a/application/editormodeselftestsignal.cpp b/application/editormodeselftestsignal.cpp index db7c1624..aab2d651 100644 --- a/application/editormodeselftestsignal.cpp +++ b/application/editormodeselftestsignal.cpp @@ -50,7 +50,7 @@ http://mozilla.org/MPL/2.0/. editor_mode::selftest_result editor_mode::selftest_signal(std::string const &command, std::istringstream &words) { - if (command == "signal" || command == "signalmove" || command == "signaldelete" || command == "signalrename" || command == "signalprobe" || command == "signalflip" || command == "signalturn") + if (command == "signal" || command == "signalmove" || command == "signaldelete" || command == "signalrename" || command == "signalprobe" || command == "signalflip" || command == "signalturn" || command == "signalselect" || command == "signalswap") { auto values{selftest_values(words)}; show_track_tab(track_tab::signals); @@ -141,6 +141,24 @@ editor_mode::selftest_result editor_mode::selftest_signal(std::string const &com auto const done{standing != nullptr && (command == "signalflip" ? signal_flip(standing->include.instance) : signal_turn(standing->include.instance))}; WriteLog("SELFTEST " + command + " " + values["name"] + (done ? ": " : ": failed, ") + tool.status); } + else if (command == "signalselect" || command == "signalswap") + { + auto const *standing{standing_named(values["name"])}; + if (standing == nullptr) + { + WriteLog("SELFTEST " + command + ": no signal " + values["name"]); + return selftest_result::handled; + } + auto const instance{standing->include.instance}; + signal_select(instance); + if (command == "signalswap") + { + auto const done{signal_swap(instance, values["file"])}; + WriteLog("SELFTEST signalswap " + values["name"] + (done ? ": " : ": failed, ") + tool.status); + } + else + WriteLog("SELFTEST signalselect " + values["name"] + ": " + standing->include.file); + } else if (command == "signaldelete") { auto const *standing{standing_named(values["name"])}; diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 189bf092..c8181c35 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -689,6 +689,30 @@ bool editor_mode::signal_turn(scene::instance_handle const Instance) return signal_move_to(*standing, spot, STR_C("Signal %s turned to the trains of the other direction, read by %s %s of %s, Ctrl+Z takes it back"), STR_C("Signal %s turned to the trains of the other direction, Ctrl+Z takes it back")); } +bool editor_mode::signal_swap(scene::instance_handle const Instance, std::string const &File) +{ + auto &tool{m_signal}; + auto const *standing{signal_standing_of(Instance)}; + if (standing == nullptr) + return false; + auto const name{standing->name}; + auto const before{standing->include.file}; + signal_changes changes; + changes.files.emplace_back(Instance, File); + std::string error; + signal_batch(changes, {}, {}, error); + signal_refresh(true); + auto const *swapped{signal_standing_of(Instance)}; + if (false == error.empty() || swapped == nullptr || swapped->include.file == before) + { + tool.status = error.empty() ? format(STR_C("Signal %s can't take the template %s"), name.c_str(), File.c_str()) : error; + return false; + } + signal_select(Instance); + tool.status = format(STR_C("Signal %s got the template %s, Ctrl+Z takes it back"), name.c_str(), swapped->include.file.c_str()); + return true; +} + std::string editor_mode::signal_variant(signal_template const &Template, signal_spot const &Spot) const { auto file{Template.file}; @@ -1217,6 +1241,25 @@ void editor_mode::render_signal_selected() else ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), STR_C("No path has %s among its events: the train doesn't see it"), found->read.c_str()); auto const instance{found->include.instance}; + if (auto const *item{signal_template_of(found->include.file)}; item != nullptr) + { + auto const shown = [](signal_template const &Template) { return Template.lamps.empty() ? Template.name : Template.name + " " + Template.lamps; }; + std::string swap; + ImGui::SetNextItemWidth(-1.0f); + if (ImGui::BeginCombo("##signalswap", format(STR_C("Template: %s"), shown(*item).c_str()).c_str(), ImGuiComboFlags_HeightLarge)) + { + for (auto const &other : m_signal.templates) + if (other.kind == item->kind && other.mount == item->mount && ImGui::Selectable((shown(other) + "##" + other.file).c_str(), other.file == item->file) && other.file != item->file) + swap = other.file; + ImGui::EndCombo(); + } + item_tooltip("Another template of its kind and mount for the signal: the name, the plate, the place and the event the train reads it by stay. A head leaning to one side is swapped for its counterpart leaning towards the track. Ctrl+Z takes it back"); + if (false == swap.empty()) + { + signal_swap(instance, swap); + return; + } + } auto const width{(ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f}; if (ImGui::Button(STR_C("To the other side"), ImVec2(width, 0.0f))) signal_flip(instance); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 4202c641..10f5d0f8 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11065,3 +11065,15 @@ msgstr[0] "%zu semafor dostał potrzebne światła, Ctrl+Z cofa" msgstr[1] "%zu semafory dostały potrzebne światła, Ctrl+Z cofa" msgstr[2] "%zu semaforów dostało potrzebne światła, Ctrl+Z cofa" + +msgid "Template: %s" +msgstr "Szablon: %s" + +msgid "Another template of its kind and mount for the signal: the name, the plate, the place and the event the train reads it by stay. A head leaning to one side is swapped for its counterpart leaning towards the track. Ctrl+Z takes it back" +msgstr "Inny szablon tego samego rodzaju i mocowania dla semafora: nazwa, tablica, miejsce i event, po którym pociąg go odczytuje, zostają. Głowica pochylona w bok zamienia się na odpowiednik pochylony ku torowi. Ctrl+Z cofa" + +msgid "Signal %s got the template %s, Ctrl+Z takes it back" +msgstr "Semafor %s dostał szablon %s, Ctrl+Z cofa" + +msgid "Signal %s can't take the template %s" +msgstr "Semafor %s nie może dostać szablonu %s" From fc6d1158bd0db6335f9bb9d931f8fa800743fa1a Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 18:55:21 +0200 Subject: [PATCH 067/145] feat(editor): the SRK tool shows the block of each line out of the station and places the SHP resonators: the automatic or the semi-automatic block chosen in a table of the lines, by hand or by the block signals standing along it, a double-track line taking the automatic one, a single-track one the other, kept in the record of the station; the block signals placed only on the lines of the automatic block, the warning discs only on those of the semi-automatic one. The heads checked for their resonators 200 m before an entry signal, a block signal and a warning disc and at an exit signal telling of the next one, 5 m either way, those of the old sceneries found by any event ending with _shp within 100 m; the ones missing placed as rezonator.inc with their events on the tracks the trains run into, at a joint within 5 m, else the track cut, in one step of undo. A signal stands by the track of its read event only when it lies near it, else by the nearest track, so a read event put on a track before the signal no longer shifts its routes --- application/editormodelineside.cpp | 3 +- application/editormodesrk.cpp | 830 +++++++++++++++++++++++++---- application/editormodesrk.h | 46 ++ editor/editorStation.cpp | 61 +++ editor/editorStation.hpp | 14 + lang/editor_pl.po | 186 ++++++- 6 files changed, 1015 insertions(+), 125 deletions(-) diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp index 20c5294e..fdfc030e 100644 --- a/application/editormodelineside.cpp +++ b/application/editormodelineside.cpp @@ -278,7 +278,8 @@ std::size_t editor_mode::place_templates(std::vector const &Items } else { - std::vector values{"none", number(local.x, 3), number(local.y, 3), number(local.z, 3), number(item.yaw, 2)}; + auto const name{item.values.find("name")}; + std::vector values{name != item.values.end() ? name->second : std::string{"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); } diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 667a3d8b..138cb452 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -54,6 +54,12 @@ double constexpr kBlockShare{1.1}; // of the braking distance, the block section double constexpr kPredicted{305.0}; // m, out from the next station to its entry signal, while it has none // the events of a block section, switching the block signal guarding it char const *const kControl{"sbl_odstep.inc"}; +// the SHP track resonator: its model on the axis, 0.2 m below it, turned as a signal facing the trains, and its event +char const *const kResonator{"rezonator.inc"}; +double constexpr kShpBefore{200.0}; // m, of the resonator before an entry signal, a block signal and a warning disc +double constexpr kShpTolerance{5.0}; // m, either way +double constexpr kShpWindow{100.0}; // m, either way of its place, a resonator standing is taken for the one of the signal +double constexpr kResonatorDrop{0.2}; // m, of its origin below the axis // m, of a train at the speed, as Ie-4 § 8 takes it: the least block section of the automatic block, a warning disc that far before // its signal @@ -419,6 +425,21 @@ double route_reach(double const Braking) return std::max(3000.0, 3.0 * Braking); } +// how far the point lies from the path in the plan, Parameter: of the nearest point of it, a metre or so apart +double nearest_of(TTrack const &Track, int const Path, glm::dvec3 const &Point, double &Parameter) +{ + geometry::bezier const curve{Track.m_paths[Path]}; + auto const count{std::clamp(static_cast(curve.plan_length(16)), 64, 4000)}; + auto best{std::numeric_limits::max()}; + for (int k = 0; k <= count; ++k) + if (auto const distance{geometry::plan_distance(curve.point(static_cast(k) / count), Point)}; distance < best) + { + best = distance; + Parameter = static_cast(k) / count; + } + return best; +} + // a standing signal by the track its trains read it on, no track for a signal of another kind or by none station::post post_of(standing_signal const &Signal) { @@ -435,31 +456,123 @@ station::post post_of(standing_signal const &Signal) else return post; post.point = Signal.position; + // the track it's read on, where it stands by it: an event put on a track before the signal leaves the track it stands by if (Signal.track != nullptr && Signal.facing != 0 && false == Signal.track->m_editorremoved) - { - post.track = Signal.track; - post.facing = Signal.facing; - return post; - } + for (int candidate = 0; candidate < static_cast(Signal.track->m_paths.size()); ++candidate) + if (double parameter{0.0}; nearest_of(*Signal.track, candidate, Signal.position, parameter) < 4.5) + { + post.track = Signal.track; + post.facing = Signal.facing; + return post; + } int path{0}; post.track = kilometrage::track_near(Signal.position, 4.5, path); if (post.track == nullptr) return post; geometry::bezier const curve{post.track->m_paths[path]}; auto parameter{0.0}; - auto best{std::numeric_limits::max()}; - for (int k = 0; k <= 64; ++k) - if (auto const distance{geometry::plan_distance(curve.point(k / 64.0), Signal.position)}; distance < best) - { - best = distance; - parameter = k / 64.0; - } + nearest_of(*post.track, path, Signal.position, parameter); auto const tangent{curve.first(parameter)}; auto const travel{travel_of(Signal.yaw)}; post.facing = travel.x * tangent.x + travel.y * tangent.z >= 0.0 ? 1 : -1; return post; } +// the entry signal of the next station along the line: the first main signal 100 m out or more facing out, of the standing ones, +// -1 for none; Distance: out to it, else to where the signals of the line end +int next_entry(station::line const &Line, std::vector const &Standing, double &Distance) +{ + auto found{-1}; + for (std::size_t k = 0; k < Standing.size(); ++k) + { + if (Standing[k].kind != "main") + continue; + double gap{0.0}; + auto const distance{station::distance_on(Line, Standing[k].position, gap)}; + if (gap > 4.5 || distance < 100.0 || distance > Line.length() - 1.0) + continue; + auto const way{station::point_on(Line, distance).way}; + if (glm::dot(travel_of(Standing[k].yaw), glm::dvec2{way.x, way.z}) < 0.87) + continue; + if (found < 0 || distance < Distance) + { + found = static_cast(k); + Distance = distance; + } + } + if (found < 0) + Distance = Line.station ? Line.length() - kPredicted : Line.length(); + return found; +} + +// where the trains run into the event of an SHP track resonator: the end of the track it's put on, the way they run +struct shp_trigger +{ + glm::dvec3 point{0.0}; + glm::dvec3 way{0.0, 0.0, 1.0}; +}; + +bool shp_event(std::string const &Name) +{ + return Name.size() > 4 && ToLower(Name.substr(Name.size() - 4)) == "_shp"; +} + +std::vector shp_triggers() +{ + std::vector result; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) + continue; + for (auto const list : {1, 2, 4, 5}) + for (auto const &event : editor_track::events(*track, list)) + if (shp_event(event.first)) + { + auto const towards_end{list % 3 == 2}; + geometry::bezier const curve{track->m_paths.front()}; + auto const tangent{curve.first(towards_end ? 0.0 : 1.0)}; + glm::dvec3 way{tangent.x, 0.0, tangent.z}; + way = glm::length(way) > 1e-9 ? glm::normalize(way) * (towards_end ? 1.0 : -1.0) : glm::dvec3{0.0, 0.0, 1.0}; + result.push_back({curve.point(towards_end ? 0.0 : 1.0), way}); + } + } + return result; +} + +// m before the signal the trains run into the event of an SHP resonator, the one nearest the place wanted within kShpWindow of it; +// -1 for none +double shp_before(std::vector const &Way, glm::dvec3 const &Signal, glm::dvec3 const &Travel, std::vector const &Triggers, double const Wanted) +{ + auto best{-1.0}; + auto miss{std::numeric_limits::max()}; + for (auto const &trigger : Triggers) + { + if (geometry::plan_distance(trigger.point, Signal) > Wanted + kShpWindow + 10.0 || glm::dot(trigger.way, Travel) < 0.7) + continue; + auto before{std::numeric_limits::lowest()}; + if (geometry::plan_distance(trigger.point, Signal) < 12.0) + before = glm::dot(glm::dvec3{Signal.x - trigger.point.x, 0.0, Signal.z - trigger.point.z}, Travel); + else + { + auto nearest{2.5}; + for (auto const &point : Way) + if (auto const gap{geometry::plan_distance(point.point, trigger.point)}; gap < nearest && glm::dot(point.way, trigger.way) > 0.7) + { + nearest = gap; + before = point.distance; + } + } + if (before < -10.0 || std::abs(before - Wanted) > kShpWindow) + continue; + if (std::abs(before - Wanted) < miss) + { + miss = std::abs(before - Wanted); + best = std::max(0.0, before); + } + } + return best; +} + } // namespace void srk_tool::load() @@ -490,6 +603,15 @@ void srk_tool::load() entry.reversed = (words >> word) && word == "right"; else if (word == "braking") words >> entry.braking; + else if (word == "blocks" && words >> word) + for (std::size_t from = 0; from < word.size();) + { + auto const comma{std::min(word.find(',', from), word.size())}; + auto const item{word.substr(from, comma - from)}; + if (auto const equals{item.find('=')}; equals != std::string::npos && equals > 0) + entry.blocks[item.substr(0, equals)] = item.substr(equals + 1) == "sbl"; + from = comma + 1; + } else if (word == "anchor") words >> entry.anchor; else if (word == "name") @@ -527,7 +649,11 @@ void srk_tool::store() if (lines.empty()) lines.emplace_back("# stations described in the track editor, the simulation skips these lines"); auto const braking{entry.braking > 0 ? format(" braking %d", entry.braking) : std::string{}}; - lines.push_back(format("station %s line %s km %s from %s anchor %s%s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), word_of(entry.km).c_str(), entry.reversed ? "right" : "left", anchor_of(entry).c_str(), braking.c_str(), entry.name.c_str())); + std::string blocks; + for (auto const &[signal, automatic] : entry.blocks) + if (false == signal.empty() && signal.find_first_of(" ,=\t") == std::string::npos) + blocks += (blocks.empty() ? " blocks " : ",") + signal + (automatic ? "=sbl" : "=semi"); + lines.push_back(format("station %s line %s km %s from %s anchor %s%s%s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), word_of(entry.km).c_str(), entry.reversed ? "right" : "left", anchor_of(entry).c_str(), braking.c_str(), blocks.c_str(), entry.name.c_str())); } scene::Layers.mark(layer, kMark, std::move(lines)); } @@ -874,7 +1000,40 @@ void srk_tool::render_options() }); item_tooltip("The km of the middle of the station along the line, rising the way the switches are numbered: the block signals of the lines are numbered after the km"); } + auto changed{false}; + field_row("Line speed"); + auto const set{set_speed_of_open()}; + if (set > 0.0) + ImGui::TextWrapped("%s", format(STR_C("%.0f km/h, the highest set on its main tracks"), set).c_str()); + else + ImGui::TextColored(kWrong, "%s", format(STR_C("%.0f km/h taken: no track of the station has a speed set"), speed_of_open()).c_str()); + item_tooltip("The speed of the trains through the station, the highest one its main tracks and the lines out of it are set to in the Track tool, up to 250 km/h: the braking distance and the lights of the signals go by it. Without one 160 km/h is taken, or the speed of the braking distance set by hand"); + field_row("Braking distance"); + auto const by_hand{m_record >= 0 ? m_records[m_record].braking : 0}; + auto const after{format(STR_C("%.0f m, after the speed"), braking_distance(set > 0.0 ? set : 160.0))}; + ImGui::SetNextItemWidth(-1.0f); + if (ImGui::BeginCombo("##srkbrakingset", by_hand > 0 ? format(STR_C("%d m, set by hand"), by_hand).c_str() : after.c_str())) + { + if (ImGui::Selectable(after.c_str(), by_hand == 0)) + { + record_of_open().braking = 0; + changed = true; + } + for (auto const distance : {400, 700, 1000, 1300}) + if (ImGui::Selectable(format(STR_C("%d m, set by hand"), distance).c_str(), by_hand == distance)) + { + record_of_open().braking = distance; + changed = true; + } + ImGui::EndCombo(); + } + item_tooltip("The braking distance of the trains on the line, Ie-4 § 8: 400 m up to 60 km/h, 700 m up to 100, 1000 m up to 140, 1300 m above. The block sections and the warning discs stand no nearer, a signal telling of the next one standing nearer slows the trains. Set it by hand where the line has another one"); ImGui::EndTable(); + if (changed) + { + m_changed = true; + survey_signals(); + } } step_heading(1, STR_C("Switch numbers")); int from{m_reversed ? 1 : 0}; @@ -907,15 +1066,50 @@ void srk_tool::render_options() "P2 towards the end; a shunting signal Tm1, Tm2... at the end of a shorter track. A signal standing by the place of one gets " "its name, none is doubled. The kinds of signals chosen in the Signals tool. One step of undo"); step_heading(4, STR_C("Lines")); + render_lines(); if (ImGui::Button(STR_C("Set the line signals"), ImVec2(-1.0f, 0.0f))) set_lines(); - item_tooltip("Places the signals of the lines out of the station, from its entry signals on. A double-track line gets the automatic block: " - "block signals evenly to the entry signals of the next station, a section no shorter than the braking distance, numbered " - "after the hectometre ahead of them, odd on track 1, even on track 2, and the block sections from 15 m beyond them named as " - "on the lines of PKP PLK, 2411-2571@LK2.1. A single-track line gets a warning disc ToA the braking distance before the entry " - "signal. The braking distance after the speed of the line, as Ie-4 gives it: 400 m up to 60 km/h, 700 m up to 100, 1000 m up " - "to 140, 1300 m above. Needs the line and the km of the station. Run it at the next station too, it keeps what stands. One " - "step of undo"); + item_tooltip("Places the signals of the lines out of the station, from its entry signals on, by the block chosen for each line above. " + "The automatic block: block signals evenly to the entry signals of the next station, a section no shorter than the braking " + "distance, numbered after the hectometre ahead of them, odd on track 1, even on track 2, and the block sections from 15 m " + "beyond them named as on the lines of PKP PLK, 2411-2571@LK2.1. The semi-automatic block: a warning disc ToA the braking " + "distance before the entry signal. The braking distance after the speed of the line, as Ie-4 gives it: 400 m up to 60 km/h, " + "700 m up to 100, 1000 m up to 140, 1300 m above. Needs the line and the km of the station. Run it at the next station too, it " + "keeps what stands. One step of undo"); + step_heading(5, STR_C("SHP")); + std::size_t needed{0}; + std::size_t found{0}; + std::size_t off{0}; + std::size_t nowhere{0}; + for (auto const &head : m_heads) + if (head.shp >= 0.0) + { + ++needed; + if (head.shp_found >= 0.0) + { + ++found; + if (std::abs(head.shp_found - head.shp) > kShpTolerance) + ++off; + } + else if (head.shp_short) + ++nowhere; + } + ImGui::BeginDisabled(found + nowhere == needed); + if (ImGui::Button(format("%s (%zu)###srkshp", STR_C("Place the SHP resonators"), needed - found - nowhere).c_str(), ImVec2(-1.0f, 0.0f))) + set_shp(); + ImGui::EndDisabled(); + item_tooltip("Places the SHP track resonators the signals checked lack, rezonator.inc, as PKP PLK places them: 200 m before an entry " + "signal, a block signal and a warning disc, at an exit signal telling of the next one, as one onto a line of the automatic " + "block, 5 m either way. Its event goes on the track the trains run into there, at a joint of the tracks within 5 m, else the " + "track is cut. A resonator standing is one whose event ends with _shp, put on a track the trains run into within 100 m of its " + "place, so the ones of the old sceneries count too. One step of undo"); + if (m_heads_known && needed > 0) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextWrapped("%s", (format(STR_P("%zu of %zu signal has its SHP resonator", "%zu of %zu signals have their SHP resonators", needed), found, needed) + (off > 0 ? ", " + format(STR_P("%zu farther than 5 m from its place", "%zu farther than 5 m from their places", off), off) : std::string{}) + + (nowhere > 0 ? ", " + format(STR_P("%zu with the tracks ending short of the place of its resonator", "%zu with the tracks ending short of the places of their resonators", nowhere), nowhere) : std::string{})).c_str()); + ImGui::PopStyleColor(); + } if (auto const *selected{m_selection.selected_track()}; selected != nullptr && false == selected->Isolated.empty()) if (auto const number{track_of(selected->Isolated.front()->asName, stem_of_open())}; false == number.empty() && begin_fields("##srktrack")) { @@ -984,6 +1178,112 @@ void srk_tool::render_options() item_tooltip("Leaves this station; LMB on a switch opens another one"); } +void srk_tool::render_lines() +{ + if (m_lines.empty()) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextWrapped("%s", m_lines_problem.empty() ? STR_C("No line leads out of the station beyond its entry signals") : m_lines_problem.c_str()); + ImGui::PopStyleColor(); + return; + } + auto const reason = [](block_reason const Why) { + switch (Why) + { + case block_reason::hand: return STR_C("set by hand"); + case block_reason::standing: return STR_C("block signals stand along it"); + case block_reason::double_track: return STR_C("a double-track line"); + default: return STR_C("a single-track line"); + } + }; + auto const kind = [](bool const Automatic) { return Automatic ? STR_C("automatic (SBL)") : STR_C("semi-automatic"); }; + auto const set = [&](line_row const &Row, std::optional const Automatic) { + auto &entry{record_of_open()}; + for (auto const *row : std::initializer_list{&Row, Row.partner >= 0 && Row.partner < static_cast(m_lines.size()) ? &m_lines[Row.partner] : nullptr}) + if (row != nullptr) + { + if (Automatic.has_value()) + entry.blocks[row->entry] = *Automatic; + else + entry.blocks.erase(row->entry); + } + m_changed = true; + }; + auto changed{false}; + if (ImGui::BeginTable("##srklinelist", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn(STR_C("Line"), ImGuiTableColumnFlags_WidthStretch, 0.27f); + ImGui::TableSetupColumn(STR_C("To"), ImGuiTableColumnFlags_WidthStretch, 0.33f); + ImGui::TableSetupColumn(STR_C("Block"), ImGuiTableColumnFlags_WidthStretch, 0.40f); + ImGui::TableHeadersRow(); + for (std::size_t index = 0; index < m_lines.size() && false == changed; ++index) + { + auto const &row{m_lines[index]}; + ImGui::PushID(static_cast(index)); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + auto const track{row.number == 0 ? std::string{STR_C("single track")} : format(STR_C("track %d"), row.number)}; + if (ImGui::Selectable(format("%s, %s##srkline", row.entry.c_str(), track.c_str()).c_str(), false, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) + m_view.fly_to(row.position + glm::dvec3{0.0, 4.0, 0.0}, glm::normalize(glm::dvec3{row.travel.x, -0.3, row.travel.y}), 30.0); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 30.0f); + ImGui::TextUnformatted(format(STR_C("The line out beyond the entry signal %s"), row.entry.c_str()).c_str()); + if (row.number > 0) + ImGui::TextUnformatted((format(STR_C("Track %d of a double-track line"), row.number) + ", " + (row.inbound ? STR("its trains run in to the station") : STR("its trains run out of the station"))).c_str()); + else + ImGui::TextUnformatted(STR_C("A single-track line")); + ImGui::TextUnformatted(format(STR_P("%zu block signal stands along it", "%zu block signals stand along it", row.blocks), row.blocks).c_str()); + ImGui::TextUnformatted(format(STR_C("Block: %s, %s"), kind(row.automatic), reason(row.why)).c_str()); + ImGui::TextDisabled("%s", STR_C("LMB: fly to the entry signal")); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + ImGui::TableNextColumn(); + auto const where{false == row.next.empty() ? row.next : row.station ? std::string{STR_C("station")} : std::string{STR_C("track end")}}; + ImGui::TextUnformatted(format("%s %.1f km", where.c_str(), row.length / 1000.0).c_str()); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", (false == row.next.empty() ? format(STR_C("To the entry signal %s of the next station"), row.next.c_str()) : row.station ? std::string{STR_C("To the next station, its entry signal not standing yet")} : std::string{STR_C("To the end of the tracks")}).c_str()); + ImGui::TableNextColumn(); + auto const automatic_by_itself{row.blocks > 0 || row.number > 0}; + auto const by_itself{row.blocks > 0 ? block_reason::standing : row.number > 0 ? block_reason::double_track : block_reason::single}; + ImGui::SetNextItemWidth(-1.0f); + auto const preview{row.why == block_reason::hand ? std::string{kind(row.automatic)} : format(STR_C("%s, auto"), kind(row.automatic))}; + if (ImGui::BeginCombo("##srkblock", preview.c_str())) + { + if (ImGui::Selectable(format(STR_C("Auto: %s, %s"), kind(automatic_by_itself), reason(by_itself)).c_str(), row.why != block_reason::hand)) + { + set(row, std::nullopt); + changed = true; + } + ImGui::BeginDisabled(row.number == 0); + if (ImGui::Selectable(STR_C("Automatic block (SBL)"), row.why == block_reason::hand && row.automatic)) + { + set(row, true); + changed = true; + } + ImGui::EndDisabled(); + if (row.number == 0) + item_tooltip("Placed on a double-track line only: on a single track the block signals would face both ways, the editor leaves those standing as they are"); + if (ImGui::Selectable(STR_C("Semi-automatic block"), row.why == block_reason::hand && false == row.automatic)) + { + set(row, false); + changed = true; + } + ImGui::EndCombo(); + } + item_tooltip("The block of the line, both tracks of a double-track one alike. Automatic (SBL): block signals with block sections, no " + "warning disc. Semi-automatic: a warning disc ToA the braking distance before the entry signal. By itself: the automatic " + "one where block signals stand along the line or it's double-track, else the semi-automatic one"); + ImGui::PopID(); + } + ImGui::EndTable(); + } + if (changed) + survey_signals(); +} + void srk_tool::render_lengths() { auto const dimmed = [](char const *Text) { @@ -1097,43 +1397,6 @@ void srk_tool::render_heads() ImGui::TextWrapped("%s", Text); ImGui::PopStyleColor(); }; - if (begin_fields("##srkbraking")) - { - field_row("Line speed"); - auto const set{set_speed_of_open()}; - if (set > 0.0) - ImGui::TextWrapped("%s", format(STR_C("%.0f km/h, the highest set on its main tracks"), set).c_str()); - else - ImGui::TextColored(kWrong, "%s", format(STR_C("%.0f km/h taken: no track of the station has a speed set"), speed_of_open()).c_str()); - item_tooltip("The speed of the trains through the station, the highest one its main tracks and the lines out of it are set to in the Track tool, up to 250 km/h: the braking distance and the lights of the signals go by it. Without one 160 km/h is taken, or the speed of the braking distance set by hand"); - field_row("Braking distance"); - auto const by_hand{m_record >= 0 ? m_records[m_record].braking : 0}; - auto const after{format(STR_C("%.0f m, after the speed"), braking_distance(set > 0.0 ? set : 160.0))}; - auto changed{false}; - ImGui::SetNextItemWidth(-1.0f); - if (ImGui::BeginCombo("##srkbrakingset", by_hand > 0 ? format(STR_C("%d m, set by hand"), by_hand).c_str() : after.c_str())) - { - if (ImGui::Selectable(after.c_str(), by_hand == 0)) - { - record_of_open().braking = 0; - changed = true; - } - for (auto const distance : {400, 700, 1000, 1300}) - if (ImGui::Selectable(format(STR_C("%d m, set by hand"), distance).c_str(), by_hand == distance)) - { - record_of_open().braking = distance; - changed = true; - } - ImGui::EndCombo(); - } - item_tooltip("The braking distance of the trains on the line, Ie-4 § 8: 400 m up to 60 km/h, 700 m up to 100, 1000 m up to 140, 1300 m above. The block sections and the warning discs stand no nearer, a signal telling of the next one standing nearer slows the trains. Set it by hand where the line has another one"); - ImGui::EndTable(); - if (changed) - { - m_changed = true; - survey_signals(); - } - } if (false == m_heads_known) { dimmed(STR_C("Looking for the signal templates...")); @@ -1152,18 +1415,19 @@ void srk_tool::render_heads() for (std::size_t index = 0; index < m_heads.size(); ++index) { auto const &head{m_heads[index]}; - if (head.station || false == head.fits || false == head.problems.empty()) + if (head.station || false == head.fits || false == head.problems.empty() || (head.shp >= 0.0 && (head.shp_found < 0.0 || std::abs(head.shp_found - head.shp) > kShpTolerance))) rows.push_back(index); else ++fine; } - if (ImGui::BeginTable("##srkheadlist", 4, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(320.0f, ImGui::GetTextLineHeightWithSpacing() * (rows.size() + 1.5f))))) + if (ImGui::BeginTable("##srkheadlist", 5, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY, ImVec2(0.0f, std::min(320.0f, ImGui::GetTextLineHeightWithSpacing() * (rows.size() + 1.5f))))) { ImGui::TableSetupScrollFreeze(0, 1); - ImGui::TableSetupColumn(STR_C("Signal"), ImGuiTableColumnFlags_WidthStretch, 0.25f); - ImGui::TableSetupColumn(STR_C("Speeds"), ImGuiTableColumnFlags_WidthStretch, 0.23f); - ImGui::TableSetupColumn(STR_C("Lights"), ImGuiTableColumnFlags_WidthStretch, 0.25f); - ImGui::TableSetupColumn(STR_C("State"), ImGuiTableColumnFlags_WidthStretch, 0.27f); + ImGui::TableSetupColumn(STR_C("Signal"), ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableSetupColumn(STR_C("Speeds"), ImGuiTableColumnFlags_WidthStretch, 0.2f); + ImGui::TableSetupColumn(STR_C("Lights"), ImGuiTableColumnFlags_WidthStretch, 0.21f); + ImGui::TableSetupColumn(STR_C("State"), ImGuiTableColumnFlags_WidthStretch, 0.24f); + ImGui::TableSetupColumn(STR_C("SHP"), ImGuiTableColumnFlags_WidthStretch, 0.15f); ImGui::TableHeadersRow(); for (auto const index : rows) { @@ -1223,6 +1487,14 @@ void srk_tool::render_heads() ImGui::TextDisabled("%s", format(STR_C("It has more lights than it needs: %s will do"), head.fit.c_str()).c_str()); for (auto const &problem : head.problems) ImGui::TextColored(kWrong, "%s", problem.c_str()); + if (head.shp >= 0.0) + { + auto const wanted{head.shp > 0.0 ? format(STR_C("its SHP resonator belongs %.0f m before it"), head.shp) : std::string{STR_C("its SHP resonator belongs at it, it tells of the next signal")}}; + if (head.shp_found < 0.0) + ImGui::TextColored(kWrong, "%s", (wanted + ", " + (head.shp_short ? STR("the tracks before it end short of it") : STR("none stands"))).c_str()); + else + ImGui::TextColored(std::abs(head.shp_found - head.shp) > kShpTolerance ? ImVec4(1.0f, 0.75f, 0.3f, 1.0f) : ImVec4(0.45f, 0.85f, 0.45f, 1.0f), "%s", (wanted + ", " + format(STR_C("its event %.0f m before it"), head.shp_found)).c_str()); + } ImGui::TextDisabled("%s", STR_C("LMB: fly to it")); ImGui::PopTextWrapPos(); ImGui::EndTooltip(); @@ -1256,11 +1528,20 @@ void srk_tool::render_heads() ImGui::SameLine(); ImGui::TextColored(kWrong, "%s", STR_C("too near")); } + ImGui::TableNextColumn(); + if (head.shp < 0.0) + ImGui::TextDisabled("-"); + else if (head.shp_found < 0.0) + ImGui::TextColored(kWrong, "%s", head.shp_short ? STR_C("no track") : STR_C("none")); + else if (std::abs(head.shp_found - head.shp) > kShpTolerance) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%.0f m", head.shp_found); + else + ImGui::TextColored(ImVec4(0.45f, 0.85f, 0.45f, 1.0f), "%s", head.shp > 0.0 ? format("%.0f m", head.shp_found).c_str() : STR_C("at it")); } ImGui::EndTable(); } if (fine > 0) - ImGui::TextDisabled("%s", format(STR_P("%zu signal of the lines fits", "%zu signals of the lines fit", fine), fine).c_str()); + ImGui::TextDisabled("%s", format(STR_P("%zu signal of the lines fits, its SHP too", "%zu signals of the lines fit, their SHP too", fine), fine).c_str()); auto const fitting{static_cast(std::count_if(m_heads.begin(), m_heads.end(), [](head_check const &Head) { return false == Head.fit.empty() && Head.fit != Head.file && (false == Head.fits || Head.more); }))}; ImGui::BeginDisabled(fitting == 0); if (main_button(format("%s (%zu)###srkfitheads", STR_C("Fit the lights"), fitting).c_str(), ImVec2(-1.0f, 0.0f))) @@ -1880,62 +2161,140 @@ void srk_tool::set_signals() } } -void srk_tool::set_lines() +bool srk_tool::lines_of_open(std::vector const &Standing, std::vector &Lines, std::vector &Entries, std::vector &Owners, std::string &Problem) const { - m_status.clear(); + Lines.clear(); + Entries.clear(); + Owners.clear(); auto const stem{stem_of_open()}; + if (false == m_layout.has_value()) + return false; if (stem.empty()) { - m_status = STR_C("Give the station its code first: the signals are named after it"); - return; + Problem = STR_C("Give the station its code first: the signals are named after it"); + return false; } - auto const &layout{*m_layout}; std::vector tracks; std::unordered_set inside; if (false == station_tracks(tracks, inside)) { - m_status = STR_C("Set the track circuits first: the signals stand at their joints"); - return; + Problem = STR_C("Set the track circuits first: the signals stand at their joints"); + return false; } - auto const plan{station::signals(layout, tracks, [&](TTrack const &Track) { return inside.count(&Track) > 0; })}; - std::vector standing; - m_signals.standing_signals(standing, true); + auto const plan{station::signals(*m_layout, tracks, [&](TTrack const &Track) { return inside.count(&Track) > 0; })}; // the lines begin at the entry signals standing - std::vector entries; - std::vector entry_standing; for (auto const &planned : plan) { if (planned.kind != station::signal::role::entry) continue; - auto const found{std::find_if(standing.begin(), standing.end(), [&](standing_signal const &Signal) { return Signal.kind == "main" && ToLower(Signal.name) == stem + ToLower(planned.name); })}; - if (found == standing.end()) + auto const found{std::find_if(Standing.begin(), Standing.end(), [&](standing_signal const &Signal) { return Signal.kind == "main" && ToLower(Signal.name) == stem + ToLower(planned.name); })}; + if (found == Standing.end()) continue; - entries.push_back(planned); - entry_standing.push_back(static_cast(found - standing.begin())); + Entries.push_back(planned); + Owners.push_back(static_cast(found - Standing.begin())); } - if (entries.empty()) + if (Entries.empty()) { - m_status = STR_C("Set the signals first: the lines begin at the entry signals"); - return; + Problem = STR_C("Set the signals first: the lines begin at the entry signals"); + return false; } - auto lines{station::lines_out(layout, entries, kLineLimit)}; - if (lines.empty()) + Lines = station::lines_out(*m_layout, Entries, kLineLimit); + if (Lines.empty()) { - m_status = STR_C("No line leads out of the station beyond its entry signals"); - return; + Problem = STR_C("No line leads out of the station beyond its entry signals"); + return false; } // the distances from the entry signals as they stand - for (auto &line : lines) + for (auto &line : Lines) { double gap{0.0}; - auto const at{station::distance_on(line, standing[entry_standing[line.entry]].position, gap)}; + auto const at{station::distance_on(line, Standing[Owners[line.entry]].position, gap)}; if (gap < 4.5) for (auto &distance : line.distances) distance -= at; } + return true; +} + +bool srk_tool::automatic_block(station::line const &Line, std::string const &Entry, std::vector const &Standing, block_reason &Why, std::size_t &Blocks) const +{ + Blocks = 0; + for (auto const &signal : Standing) + { + if (signal.kind != "block") + continue; + double gap{0.0}; + auto const distance{station::distance_on(Line, signal.position, gap)}; + if (gap < 4.5 && distance > 50.0 && distance < Line.length() - 1.0) + ++Blocks; + } + if (m_record >= 0) + if (auto const set{m_records[m_record].blocks.find(Entry)}; set != m_records[m_record].blocks.end()) + { + Why = block_reason::hand; + return set->second; + } + if (Blocks > 0) + { + Why = block_reason::standing; + return true; + } + Why = Line.number > 0 ? block_reason::double_track : block_reason::single; + return Line.number > 0; +} + +void srk_tool::survey_lines(std::vector const &Standing) +{ + m_lines.clear(); + m_lines_problem.clear(); + std::vector lines; + std::vector entries; + std::vector owners; + if (false == lines_of_open(Standing, lines, entries, owners, m_lines_problem)) + return; + auto const stem{stem_of_open()}; + for (auto const &line : lines) + { + line_row row; + row.entry = entries[line.entry].name; + row.number = line.number; + row.inbound = line.inbound; + row.partner = line.partner; + row.station = line.station; + row.position = Standing[owners[line.entry]].position; + row.travel = travel_of(Standing[owners[line.entry]].yaw); + row.automatic = automatic_block(line, row.entry, Standing, row.why, row.blocks); + auto const next{next_entry(line, Standing, row.length)}; + row.next = next >= 0 ? signal_shown(Standing[next].name, stem) : std::string{}; + m_lines.push_back(std::move(row)); + } +} + +void srk_tool::set_lines() +{ + m_status.clear(); + auto const stem{stem_of_open()}; + auto const &layout{*m_layout}; + std::vector standing; + m_signals.standing_signals(standing, true); + std::vector lines; + std::vector entries; + std::vector entry_standing; + if (false == lines_of_open(standing, lines, entries, entry_standing, m_status)) + return; + // of each line, 1: the automatic block, placed on a double-track line only, 2: the automatic block on a single-track line, its + // signals facing both ways on a track left as they stand, 0: the semi-automatic one + std::vector automatic(lines.size(), 0); + for (std::size_t index = 0; index < lines.size(); ++index) + { + auto why{block_reason::single}; + std::size_t blocks{0}; + if (automatic_block(lines[index], entries[lines[index].entry].name, standing, why, blocks)) + automatic[index] = lines[index].number > 0 ? 1 : 2; + } auto const described{line_of_open()}; double known{0.0}; - if (std::any_of(lines.begin(), lines.end(), [](station::line const &Line) { return Line.number > 0; }) && (false == km_of_open(0.0, known) || described.empty())) + if (std::any_of(automatic.begin(), automatic.end(), [](char const Automatic) { return Automatic == 1; }) && (false == km_of_open(0.0, known) || described.empty())) { m_status = STR_C("Give the station its line and its km first: the block signals and the block sections are named after them"); return; @@ -1999,34 +2358,14 @@ void srk_tool::set_lines() return braking_distance(speed > 0.0 ? speed : 160.0); }; for (std::size_t index = 0; index < lines.size(); ++index) - { - auto const &line{lines[index]}; - auto &end{ends[index]}; - // the entry signal of the next station: the first main signal along the line facing out - for (std::size_t k = 0; k < standing.size(); ++k) - { - if (standing[k].kind != "main") - continue; - double gap{0.0}; - auto const distance{station::distance_on(line, standing[k].position, gap)}; - if (gap > 4.5 || distance < 100.0 || distance > line.length() - 1.0 || facing_out(line, standing[k], distance) < 0.87) - continue; - if (end.signal < 0 || distance < end.distance) - { - end.signal = static_cast(k); - end.distance = distance; - } - } - if (end.signal < 0) - end.distance = line.station ? line.length() - kPredicted : line.length(); - } + ends[index].signal = next_entry(lines[index], standing, ends[index].distance); for (std::size_t index = 0; index < lines.size(); ++index) { auto const &line{lines[index]}; auto const &end{ends[index]}; auto const &entry{entries[line.entry]}; auto const least{braking(index)}; - if (line.number > 0) + if (automatic[index] == 1) { // the block signals, Ie-4 § 51: the sections no shorter than the braking distance and no longer than twice it, the line to the // next station parted evenly, towards the end of the tracks a little over the braking distance each @@ -2053,7 +2392,7 @@ void srk_tool::set_lines() planned.push_back(std::move(signal)); } } - else + else if (automatic[index] == 0) { // the warning disc of the entry signal, the braking distance before it auto const distance{std::min(least, end.distance - 100.0)}; @@ -2141,7 +2480,7 @@ void srk_tool::set_lines() for (std::size_t index = 0; index < lines.size(); ++index) { auto const &line{lines[index]}; - if (line.number == 0) + if (automatic[index] != 1) continue; struct stop { @@ -2387,6 +2726,7 @@ void srk_tool::survey_signals() return; } check_heads(standing); + survey_lines(standing); auto const stem{stem_of_open()}; std::vector stops; for (auto const &signal : standing) @@ -2529,6 +2869,7 @@ void srk_tool::check_heads(std::vector const &Standing) std::unordered_map by_file; for (std::size_t index = 0; index < heads.size(); ++index) by_file.emplace(heads[index].file, index); + auto const triggers{shp_triggers()}; for (auto const index : order) { auto const &signal{Standing[owners[index]]}; @@ -2551,6 +2892,24 @@ void srk_tool::check_heads(std::vector const &Standing) check.lamps = template_of->lamps; check.own = template_of->speeds; } + glm::dvec3 const travel{check.travel.x, 0.0, check.travel.y}; + if (post.kind != role::entry) + check.shp = kShpBefore; + else if (own[index]) + { + auto const along{station::project(layout, signal.position).x}; + auto const ahead{station::project(layout, signal.position + travel * 10.0).x}; + if ((along < layout.from && ahead > along) || (along > layout.to && ahead < along)) + check.shp = kShpBefore; + else if (std::any_of(check.routes.begin(), check.routes.end(), [](station::route const &Route) { return Route.next >= 0 && Route.warning < 0; })) + check.shp = 0.0; + } + if (check.shp >= 0.0) + { + auto const way{station::approach(*post.track, post.point, post.facing, check.shp + kShpWindow)}; + check.shp_found = shp_before(way, signal.position, travel, triggers, check.shp); + check.shp_short = way.empty() || way.back().distance < check.shp - kShpTolerance; + } if (post.kind == role::warning) { for (std::size_t k = 0; k < check.routes.size(); ++k) @@ -2629,6 +2988,196 @@ void srk_tool::fit_heads() m_status = format(STR_P("%zu signal got the lights it needs, Ctrl+Z takes it back", "%zu signals got the lights they need, Ctrl+Z takes it back", changed), changed); } +void srk_tool::set_shp() +{ + m_status.clear(); + if (false == m_layout.has_value()) + return; + include_info resonator; + std::string unreadable; + if (false == editor_includes::load(kResonator, resonator, unreadable)) + { + m_status = format(STR_C("%s isn't in the scenery folder: the SHP resonators can't be placed"), kResonator); + return; + } + std::vector standing; + m_signals.standing_signals(standing, true); + check_heads(standing); + std::unordered_set taken; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved) + for (int list = 0; list < 6; ++list) + for (auto const &event : editor_track::events(*track, list)) + taken.insert(ToLower(event.first)); + struct placing + { + std::string name; // of the resonator, its event is _shp + TTrack *track{nullptr}; // the trains run into its event on, where a joint of the tracks lies + TTrack *cut{nullptr}; // the track cut at the point, else + int facing{1}; // the trains run on the track, 1: towards the end of its path + glm::dvec3 point{0.0}; + glm::dvec3 way{0.0, 0.0, 1.0}; + glm::dvec3 inside{0.0}; // of the track the trains run into, a metre on from the point + }; + std::vector places; + std::vector problems; + for (auto const &head : m_heads) + { + if (head.shp < 0.0 || head.shp_found >= 0.0) + continue; + if (head.shp_short) + { + problems.push_back(format(STR_C("%s: the tracks before it end short of %.0f m"), head.name.c_str(), head.shp)); + continue; + } + auto const found{std::find_if(standing.begin(), standing.end(), [&](standing_signal const &Signal) { return Signal.instance == head.instance; })}; + if (found == standing.end()) + continue; + auto const post{post_of(*found)}; + if (post.track == nullptr) + { + problems.push_back(format(STR_C("%s stands by no track"), head.name.c_str())); + continue; + } + auto const way{station::approach(*post.track, post.point, post.facing, head.shp + kShpTolerance + 1.0)}; + if (way.empty() || way.back().distance < head.shp - kShpTolerance) + { + problems.push_back(format(STR_C("%s: the tracks before it end short of %.0f m"), head.name.c_str(), head.shp)); + continue; + } + // a joint of the tracks within the tolerance takes the event on the track the trains run into there, else the track is cut + auto best{-1}; + auto joint{false}; + auto miss{kShpTolerance + 1e-6}; + for (std::size_t k = 0; k + 1 < way.size(); ++k) + if (way[k + 1].track != way[k].track && way[k].track->eType == tt_Normal && std::abs(way[k].distance - head.shp) < miss) + { + miss = std::abs(way[k].distance - head.shp); + best = static_cast(k); + joint = true; + } + if (best < 0) + for (std::size_t k = 0; k < way.size(); ++k) + { + auto const &ends{way[k].track->m_paths.front().points}; + if (way[k].track->eType != tt_Normal || std::min(glm::distance(way[k].point, ends[segment_data::point::start]), glm::distance(way[k].point, ends[segment_data::point::end])) <= 0.5) + continue; + if (std::abs(way[k].distance - head.shp) < miss) + { + miss = std::abs(way[k].distance - head.shp); + best = static_cast(k); + } + } + if (best < 0) + { + problems.push_back(format(STR_C("%s: no plain track within %.0f m of the place of its SHP resonator"), head.name.c_str(), kShpTolerance)); + continue; + } + auto const &at{way[best]}; + std::string reason; + if (false == scene::Layers.editable(at.track, &reason) || (false == joint && false == editor_track::can_edit_geometry(*at.track, reason))) + { + problems.push_back(format(STR_C("%s can't be changed: %s"), name_of(*at.track).c_str(), reason.c_str())); + continue; + } + if (false == at.track->Dynamics.empty()) + { + problems.push_back(format(STR_C("%s: vehicles stand on %s"), head.name.c_str(), name_of(*at.track).c_str())); + continue; + } + placing entry; + auto name{found->name + "_rez"}; + for (int number = 2; taken.count(ToLower(name) + "_shp") > 0 && number < 100; ++number) + name = found->name + "_rez" + std::to_string(number); + taken.insert(ToLower(name) + "_shp"); + entry.name = name; + entry.facing = at.facing; + entry.point = at.point; + entry.way = at.way; + if (joint) + entry.track = at.track; + else + { + entry.cut = at.track; + entry.inside = best > 0 ? way[best - 1].point : at.point + at.way; + } + places.push_back(std::move(entry)); + } + if (places.empty()) + { + m_status = problems.empty() ? STR_C("The signals have their SHP resonators already") : problems.front(); + return; + } + std::vector> before; + std::vector created; + std::unordered_set captured; + std::unordered_map> pieces; // of each track cut, itself and the pieces cut off it + auto const piece_at = [&](TTrack *Track, glm::dvec3 const &Point) { + auto found{pieces.find(Track)}; + if (found == pieces.end()) + return Track; + TTrack *nearest{Track}; + auto gap{std::numeric_limits::max()}; + for (auto *piece : found->second) + if (auto const distance{glm::distance(editor_track::point_at(*piece, editor_track::nearest_parameter(*piece, Point)), Point)}; distance < gap) + { + gap = distance; + nearest = piece; + } + return nearest; + }; + std::size_t cuts{0}; + for (auto const &entry : places) + { + if (entry.cut == nullptr) + continue; + auto *track{piece_at(entry.cut, entry.point)}; + auto const &ends{track->m_paths.front().points}; + if (std::min(glm::distance(entry.point, ends[segment_data::point::start]), glm::distance(entry.point, ends[segment_data::point::end])) <= 0.3) + continue; + if (captured.insert(track).second) + before.emplace_back(track, editor_track::capture(*track)); + if (auto *piece{editor_track::split_path(*track, editor_track::nearest_parameter(*track, entry.point))}) + { + created.push_back(piece); + captured.insert(piece); + auto &list{pieces[entry.cut]}; + if (list.empty()) + list.push_back(entry.cut); + list.push_back(piece); + ++cuts; + } + } + signal_changes changes; + for (auto const &entry : places) + { + auto *track{entry.track != nullptr ? entry.track : piece_at(entry.cut, entry.inside)}; + if (captured.insert(track).second) + before.emplace_back(track, editor_track::capture(*track)); + editor_track::events(*track, entry.facing > 0 ? 2 : 1).emplace_back(ToLower(entry.name) + "_shp", nullptr); + std::string missing; + editor_track::bind_events(*track, missing); + track->mark_dirty(); + template_item item; + item.file = kResonator; + item.location = entry.point - glm::dvec3{0.0, kResonatorDrop, 0.0}; + auto const yaw{geometry::heading_of({entry.way.x, entry.way.z}) + 180.0}; + item.yaw = yaw >= 360.0 ? yaw - 360.0 : yaw; + item.values["name"] = entry.name; + changes.others.push_back(std::move(item)); + } + std::string error; + m_signals.signal_batch(changes, std::move(before), std::move(created), error); + if (false == error.empty()) + { + m_status = format(STR_C("The SHP resonators couldn't be placed: %s"), error.c_str()); + return; + } + m_status = format(STR_P("%zu SHP resonator placed", "%zu SHP resonators placed", places.size()), places.size()) + ", " + format(STR_P("%zu track cut", "%zu tracks cut", cuts), cuts) + ". " + STR("Ctrl+Z takes it back, the resonators work after the scenery is saved and opened again"); + for (auto const &problem : problems) + m_status += "\n" + problem; +} + void srk_tool::survey() { m_circuit_of.assign(m_layout.has_value() ? m_layout->paths.size() : 0, -1); @@ -3266,13 +3815,62 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri auto const &route{head.routes[k]}; routes += format(" %s:%.0f:%.0f%s%s", route.next >= 0 ? head.nexts[k].c_str() : route.ended ? "end" : "none", route.length, route.speed, route.warning >= 0 ? ":to" : "", route.turnouts.empty() ? "" : format(":%zut", route.turnouts.size()).c_str()); } - WriteLog(format("SELFTEST srkheads %s %s%s %s lamps=%s own=%s need=%s%s%s short=%d%s fits=%d more=%d fit=%s routes%s problems=%zu", head.name.c_str(), head.kind.c_str(), head.station ? "" : "(line)", head.file.c_str(), head.lamps.c_str(), list(head.own).c_str(), list(head.speeds).c_str(), head.informs ? "+inform" : "", - head.white ? "+white" : "", head.shortened, head.indicator ? " w21" : "", head.fits ? 1 : 0, head.more ? 1 : 0, head.fit.empty() ? "-" : head.fit.c_str(), routes.c_str(), head.problems.size())); + WriteLog(format("SELFTEST srkheads %s %s%s %s lamps=%s own=%s need=%s%s%s short=%d%s fits=%d more=%d fit=%s shp=%.0f/%.0f routes%s problems=%zu", head.name.c_str(), head.kind.c_str(), head.station ? "" : "(line)", head.file.c_str(), head.lamps.c_str(), list(head.own).c_str(), list(head.speeds).c_str(), head.informs ? "+inform" : "", + head.white ? "+white" : "", head.shortened, head.indicator ? " w21" : "", head.fits ? 1 : 0, head.more ? 1 : 0, head.fit.empty() ? "-" : head.fit.c_str(), head.shp, head.shp_found, routes.c_str(), head.problems.size())); for (auto const &problem : head.problems) WriteLog("SELFTEST srkheads " + problem); } return editor_selftest_result::handled; } + if (Command == "srkshp") + { + // srkshp: the SHP resonators of the open station placed, the signals listed with them + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkshp: no station open"); + return editor_selftest_result::handled; + } + set_shp(); + WriteLog("SELFTEST srkshp: " + m_status); + refresh(); + if (false == m_layout.has_value()) + return editor_selftest_result::handled; + std::vector standing; + m_signals.standing_signals(standing, true); + check_heads(standing); + for (auto const &head : m_heads) + if (head.shp >= 0.0) + WriteLog(format("SELFTEST srkshp %s %s wanted %.0f found %.0f", head.name.c_str(), head.kind.c_str(), head.shp, head.shp_found)); + for (auto const &include : m_templates.standing_templates([](std::string const &File) { return File == kResonator; })) + WriteLog(format("SELFTEST srkshp resonator %s at %.2f %.2f %.2f yaw %.1f", include.values.empty() ? "-" : include.values.front().c_str(), include.location.x, include.location.y, include.location.z, include.yaw)); + return editor_selftest_result::handled; + } + if (Command == "srkblock") + { + // srkblock entry= kind=sbl|semi|auto: the block of the line out beyond the entry signal, the lines listed + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkblock: no station open"); + return editor_selftest_result::handled; + } + auto const values{selftest_values(Words)}; + auto const entry{values.find("entry")}; + auto const kind{values.find("kind")}; + if (entry != values.end() && kind != values.end()) + { + if (kind->second == "auto") + record_of_open().blocks.erase(entry->second); + else + record_of_open().blocks[entry->second] = kind->second == "sbl"; + m_changed = true; + } + survey_signals(); + if (false == m_lines_problem.empty()) + WriteLog("SELFTEST srkblock: " + m_lines_problem); + for (auto const &row : m_lines) + WriteLog(format("SELFTEST srkblock %s track %d %s to %s %.0f m blocks %zu: %s why %d", row.entry.c_str(), row.number, row.inbound ? "in" : "out", row.next.empty() ? "-" : row.next.c_str(), row.length, row.blocks, row.automatic ? "sbl" : "semi", static_cast(row.why))); + return editor_selftest_result::handled; + } if (Command == "srkstrip") { // srkstrip shown=0: the schematic hidden, the 3d view whole for a screenshot diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 61a4b890..5e7494ac 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "imgui/imgui.h" +#include #include #include #include @@ -45,6 +46,14 @@ class srk_tool final : public editor_tool editor_history &m_history; editor_templates &m_templates; editor_signals &m_signals; + // where the block of a line out of the station comes from + enum class block_reason + { + hand, // set by hand + standing, // block signals stand along it + double_track, // the automatic one on a double-track line + single // the semi-automatic one on a single-track line + }; // a station described: found again by a switch of it struct record { @@ -57,6 +66,7 @@ class srk_tool final : public editor_tool std::string km; // of the axis of the station along the line, empty while unknown bool reversed{false}; // the km rise to the left of the schematic, the switches numbered from there int braking{0}; // m, the braking distance of the line set by hand, 0: after its speed + std::map blocks; // of the lines by their entry signals: the automatic block on it, as set by hand }; // the schematic: the chainage across, the offsets upwards, exaggerated struct view @@ -138,10 +148,33 @@ class srk_tool final : public editor_tool bool more{false}; // its template has more than it needs std::string fit; // the template that fits best, empty for none std::vector problems; // of its place + // m before it the SHP track resonator belongs, -1 for none: 200 m before an entry signal, a block signal and a warning disc, + // at an exit signal onto a line of the automatic block + double shp{-1.0}; + double shp_found{-1.0}; // m before it the trains run into the event of the SHP resonator standing, -1 for none + bool shp_short{false}; // the tracks before it end short of the place of its resonator }; std::vector m_heads; bool m_heads_known{false}; // the templates found int m_head_hover{-1}; // of the heads, the row under the mouse + // a line out of the open station, from an entry signal of it + struct line_row + { + std::string entry; // the entry signal, shown + glm::dvec3 position{0.0}; // of the entry signal + glm::dvec2 travel{0.0, 1.0}; // the trains it faces run, in the plan + int number{0}; // of the track of a double-track line, 0 on a single-track one + bool inbound{true}; // its trains run towards the station + int partner{-1}; // the other track of a double-track line, of the lines + double length{0.0}; // to the entry signal of the next station, or to the end of the tracks + std::string next; // the entry signal of the next station facing out along it, shown; empty for none + bool station{false}; // ends at the next station rather than at the end of the tracks + std::size_t blocks{0}; // the block signals standing along it + bool automatic{false}; // of the automatic block + block_reason why{block_reason::single}; // the block it has, so + }; + std::vector m_lines; + std::string m_lines_problem; // why the station has no lines listed std::string m_status; void load(); @@ -186,6 +219,19 @@ class srk_tool final : public editor_tool // the signals of the lines out of the station: on a double-track line the block signals to the next station and the block // sections between them, on a single-track one a warning disc before the entry signal; one step of the history void set_lines(); + // the lines out of the open station from its entry signals standing, Entries: the signals of the plan they begin at, Owners: + // the standing ones; false with the reason while it has none + bool lines_of_open(std::vector const &Standing, std::vector &Lines, std::vector &Entries, std::vector &Owners, std::string &Problem) const; + // the line has the automatic block: as set by hand for its entry signal, else where block signals stand along it, else on a + // double-track line + bool automatic_block(station::line const &Line, std::string const &Entry, std::vector const &Standing, block_reason &Why, std::size_t &Blocks) const; + // the lines out of the open station, for the panel + void survey_lines(std::vector const &Standing); + void render_lines(); + // the SHP track resonators placed where the signals checked need them and have none: 200 m before an entry signal, a block + // signal and a warning disc, at an exit signal onto a line of the automatic block, the track cut where none of its joints lies + // within 5 m, its event put on the track the trains run into there; one step of the history + void set_shp(); // the names of the switches and the circuits of the open station begin with it, empty while it has no code std::string stem_of_open() const; // the km at the chainage of the open station: of the kilometrage of its line, else of its record, else of its switches; false diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index d7d79757..85b1f640 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -2229,4 +2229,65 @@ std::vector> routes(std::vector const &Posts, std::vect return all; } +std::vector approach(TTrack &Track, glm::dvec3 const &Point, int const Facing, double const Distance) +{ + std::vector result; + if (false == rail(Track)) + return result; + auto *track{&Track}; + int path{0}; + long at{0}; + std::vector points; + auto nearest{std::numeric_limits::max()}; + for (int candidate = 0; candidate < static_cast(Track.m_paths.size()); ++candidate) + { + double length{0.0}; + auto dense{dense_points(Track, candidate, length)}; + for (std::size_t k = 0; k < dense.size(); ++k) + if (auto const gap{plan_distance(dense[k], Point)}; gap < nearest - 1e-6) + { + nearest = gap; + path = candidate; + at = static_cast(k); + } + if (path == candidate) + points = std::move(dense); + } + auto facing{Facing > 0 ? 1 : -1}; + auto last{points[at]}; + double distance{0.0}; + std::unordered_set seen{track}; + while (true) + { + auto const count{static_cast(points.size())}; + auto const step{facing > 0 ? -1L : 1L}; + for (auto k{at}; k >= 0 && k < count; k += step) + { + distance += plan_distance(last, points[k]); + last = points[k]; + auto const ahead{points[std::clamp(k + 1, 0L, count - 1)] - points[std::clamp(k - 1, 0L, count - 1)]}; + glm::dvec3 way{ahead.x * facing, 0.0, ahead.z * facing}; + way = glm::length(way) > 1e-9 ? glm::normalize(way) : glm::dvec3{0.0, 0.0, 1.0}; + result.push_back({track, path, facing, points[k], way, distance}); + if (distance >= Distance) + return result; + } + auto const atend{facing < 0}; + auto const end{end_of(*track, path, atend)}; + auto *before{neighbour(*track, path, atend)}; + int entered{0}; + bool from_end{false}; + if (before == nullptr || false == rail(*before) || false == seen.insert(before).second || false == end_at(*before, end, entered, from_end)) + return result; + if (before->eType == tt_Switch && false == from_end) + entered = straighter(*before); + track = before; + path = entered; + double length{0.0}; + points = dense_points(*track, path, length); + facing = from_end ? 1 : -1; + at = from_end ? static_cast(points.size()) - 2 : 1; + } +} + } // namespace station diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index 8272c075..eb97b20e 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -249,4 +249,18 @@ double path_speed(TTrack const &Track, int const Path); // the routes of the trains from each of the posts given on through the switches, none longer than the limit std::vector> routes(std::vector const &Posts, std::vector const &From, double const Limit); +// a point of the way of the trains up to a signal +struct approach_point +{ + TTrack *track{nullptr}; + int path{0}; + int facing{1}; // the trains run 1: towards the end of the path, -1: towards its start + glm::dvec3 point{0.0}; + glm::dvec3 way{0.0, 0.0, 1.0}; // the trains run, in the plan + double distance{0.0}; // m, before the signal +}; +// the way of the trains running past the point of the track the way given, back along the tracks they come by up to the distance +// before it or the end of the tracks, a metre or so apart: into a switch from its start along its straighter path +std::vector approach(TTrack &Track, glm::dvec3 const &Point, int const Facing, double const Distance); + } // namespace station diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 10f5d0f8..b03b9add 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10494,8 +10494,8 @@ msgstr "Szlaki" msgid "Set the line signals" msgstr "Ustaw sygnalizatory szlakowe" -msgid "Places the signals of the lines out of the station, from its entry signals on. A double-track line gets the automatic block: block signals evenly to the entry signals of the next station, a section no shorter than the braking distance, numbered after the hectometre ahead of them, odd on track 1, even on track 2, and the block sections from 15 m beyond them named as on the lines of PKP PLK, 2411-2571@LK2.1. A single-track line gets a warning disc ToA the braking distance before the entry signal. The braking distance after the speed of the line, as Ie-4 gives it: 400 m up to 60 km/h, 700 m up to 100, 1000 m up to 140, 1300 m above. Needs the line and the km of the station. Run it at the next station too, it keeps what stands. One step of undo" -msgstr "Stawia sygnalizatory szlaków wychodzących ze stacji, od jej semaforów wjazdowych. Szlak dwutorowy dostaje samoczynną blokadę liniową: semafory SBL rozstawione równo do semaforów wjazdowych następnej stacji, odstęp nie krótszy od drogi hamowania, numerowane według najbliższego hektometra przed nimi, nieparzyste na torze 1, parzyste na torze 2, a odstępy od 15 m za nimi nazwane jak na liniach PKP PLK, 2411-2571@LK2.1. Szlak jednotorowy dostaje tarczę ostrzegawczą ToA w odległości drogi hamowania przed semaforem wjazdowym. Droga hamowania według prędkości szlaku, jak w Ie-4: 400 m do 60 km/h, 700 m do 100, 1000 m do 140, 1300 m powyżej. Potrzebna linia i kilometr stacji. Uruchom to też na następnej stacji, zostawia to, co już stoi. Jeden krok cofania" +msgid "Places the signals of the lines out of the station, from its entry signals on, by the block chosen for each line above. The automatic block: block signals evenly to the entry signals of the next station, a section no shorter than the braking distance, numbered after the hectometre ahead of them, odd on track 1, even on track 2, and the block sections from 15 m beyond them named as on the lines of PKP PLK, 2411-2571@LK2.1. The semi-automatic block: a warning disc ToA the braking distance before the entry signal. The braking distance after the speed of the line, as Ie-4 gives it: 400 m up to 60 km/h, 700 m up to 100, 1000 m up to 140, 1300 m above. Needs the line and the km of the station. Run it at the next station too, it keeps what stands. One step of undo" +msgstr "Stawia sygnalizatory szlaków wychodzących ze stacji, od jej semaforów wjazdowych, według blokady wybranej wyżej dla każdego szlaku. Blokada samoczynna: semafory SBL rozstawione równo do semaforów wjazdowych następnej stacji, odstęp nie krótszy od drogi hamowania, numerowane według najbliższego hektometra przed nimi, nieparzyste na torze 1, parzyste na torze 2, a odstępy od 15 m za nimi nazwane jak na liniach PKP PLK, 2411-2571@LK2.1. Blokada półsamoczynna: tarcza ostrzegawcza ToA w odległości drogi hamowania przed semaforem wjazdowym. Droga hamowania według prędkości szlaku, jak w Ie-4: 400 m do 60 km/h, 700 m do 100, 1000 m do 140, 1300 m powyżej. Potrzebna linia i kilometr stacji. Uruchom to też na następnej stacji, zostawia to, co już stoi. Jeden krok cofania" msgid "Km" msgstr "Kilometr" @@ -11053,11 +11053,11 @@ msgstr "Światła semaforów już pasują" msgid "The signals couldn't be changed: %s" msgstr "Nie udało się zmienić semaforów: %s" -msgid "%zu signal of the lines fits" -msgid_plural "%zu signals of the lines fit" -msgstr[0] "%zu semafor linii pasuje" -msgstr[1] "%zu semafory linii pasują" -msgstr[2] "%zu semaforów linii pasuje" +msgid "%zu signal of the lines fits, its SHP too" +msgid_plural "%zu signals of the lines fit, their SHP too" +msgstr[0] "%zu semafor szlaku pasuje, jego SHP też" +msgstr[1] "%zu semafory szlaku pasują, ich SHP też" +msgstr[2] "%zu semaforów szlaku pasuje, ich SHP też" msgid "%zu signal got the lights it needs, Ctrl+Z takes it back" msgid_plural "%zu signals got the lights they need, Ctrl+Z takes it back" @@ -11065,7 +11065,6 @@ msgstr[0] "%zu semafor dostał potrzebne światła, Ctrl+Z cofa" msgstr[1] "%zu semafory dostały potrzebne światła, Ctrl+Z cofa" msgstr[2] "%zu semaforów dostało potrzebne światła, Ctrl+Z cofa" - msgid "Template: %s" msgstr "Szablon: %s" @@ -11077,3 +11076,174 @@ msgstr "Semafor %s dostał szablon %s, Ctrl+Z cofa" msgid "Signal %s can't take the template %s" msgstr "Semafor %s nie może dostać szablonu %s" + +msgid "SHP" +msgstr "SHP" + +msgid "Place the SHP resonators" +msgstr "Postaw rezonatory SHP" + +msgid "Places the SHP track resonators the signals checked lack, rezonator.inc, as PKP PLK places them: 200 m before an entry signal, a block signal and a warning disc, at an exit signal telling of the next one, as one onto a line of the automatic block, 5 m either way. Its event goes on the track the trains run into there, at a joint of the tracks within 5 m, else the track is cut. A resonator standing is one whose event ends with _shp, put on a track the trains run into within 100 m of its place, so the ones of the old sceneries count too. One step of undo" +msgstr "Stawia rezonatory SHP (rezonator.inc) tam, gdzie sprawdzonym semaforom ich brakuje, tak jak PKP PLK: 200 m przed semaforem wjazdowym, semaforem SBL i tarczą ostrzegawczą, a przy semaforze wyjazdowym, który zapowiada następny semafor, np. przy wyjeździe na szlak z SBL; ±5 m. Event rezonatora trafia na tor, na który pociąg tam wjeżdża: na styku torów, jeśli jakiś jest w promieniu 5 m, inaczej tor zostaje przecięty. Za stojący rezonator uznaje się każdy event kończący się na _shp na torze w promieniu 100 m od miejsca rezonatora, więc liczą się też te ze starych scenerii. Jeden krok cofania" + +msgid "%zu of %zu signal has its SHP resonator" +msgid_plural "%zu of %zu signals have their SHP resonators" +msgstr[0] "%zu z %zu semafora ma rezonator SHP" +msgstr[1] "%zu z %zu semaforów ma rezonatory SHP" +msgstr[2] "%zu z %zu semaforów ma rezonatory SHP" + +msgid "%zu farther than 5 m from its place" +msgid_plural "%zu farther than 5 m from their places" +msgstr[0] "%zu dalej niż 5 m od swojego miejsca" +msgstr[1] "%zu dalej niż 5 m od swoich miejsc" +msgstr[2] "%zu dalej niż 5 m od swoich miejsc" + +msgid "set by hand" +msgstr "ustawiona ręcznie" + +msgid "block signals stand along it" +msgstr "stoją na nim semafory SBL" + +msgid "a double-track line" +msgstr "linia dwutorowa" + +msgid "a single-track line" +msgstr "linia jednotorowa" + +msgid "automatic (SBL)" +msgstr "samoczynna (SBL)" + +msgid "semi-automatic" +msgstr "półsamoczynna" + +msgid "To" +msgstr "Dokąd" + +msgid "Block" +msgstr "Blokada" + +msgid "single track" +msgstr "jednotorowy" + +msgid "track %d" +msgstr "tor %d" + +msgid "The line out beyond the entry signal %s" +msgstr "Szlak za semaforem wjazdowym %s" + +msgid "Track %d of a double-track line" +msgstr "Tor %d linii dwutorowej" + +msgid "its trains run in to the station" +msgstr "pociągi jadą nim do stacji" + +msgid "its trains run out of the station" +msgstr "pociągi jadą nim ze stacji" + +msgid "A single-track line" +msgstr "Linia jednotorowa" + +msgid "%zu block signal stands along it" +msgid_plural "%zu block signals stand along it" +msgstr[0] "Stoi na nim %zu semafor SBL" +msgstr[1] "Stoją na nim %zu semafory SBL" +msgstr[2] "Stoi na nim %zu semaforów SBL" + +msgid "Block: %s, %s" +msgstr "Blokada: %s, %s" + +msgid "LMB: fly to the entry signal" +msgstr "LPM: leć do semafora wjazdowego" + +msgid "station" +msgstr "stacja" + +msgid "track end" +msgstr "koniec toru" + +msgid "To the entry signal %s of the next station" +msgstr "Do semafora wjazdowego %s następnej stacji" + +msgid "To the next station, its entry signal not standing yet" +msgstr "Do następnej stacji, jej semafor wjazdowy jeszcze nie stoi" + +msgid "To the end of the tracks" +msgstr "Do końca torów" + +msgid "%s, auto" +msgstr "%s, auto" + +msgid "Auto: %s, %s" +msgstr "Auto: %s, %s" + +msgid "Automatic block (SBL)" +msgstr "Blokada samoczynna (SBL)" + +msgid "Placed on a double-track line only: on a single track the block signals would face both ways, the editor leaves those standing as they are" +msgstr "Edytor stawia ją tylko na linii dwutorowej: na jednym torze semafory SBL musiałyby stać w obu kierunkach, takie, które już stoją, zostawia bez zmian" + +msgid "Semi-automatic block" +msgstr "Blokada półsamoczynna" + +msgid "The block of the line, both tracks of a double-track one alike. Automatic (SBL): block signals with block sections, no warning disc. Semi-automatic: a warning disc ToA the braking distance before the entry signal. By itself: the automatic one where block signals stand along the line or it's double-track, else the semi-automatic one" +msgstr "Blokada liniowa szlaku, na dwutorowym taka sama na obu torach. Samoczynna (SBL): semafory SBL z odstępami blokowymi, bez tarczy ostrzegawczej. Półsamoczynna: tarcza ostrzegawcza ToA w odległości drogi hamowania przed semaforem wjazdowym. Auto: samoczynna, gdy na szlaku stoją semafory SBL albo linia jest dwutorowa, inaczej półsamoczynna" + +msgid "its SHP resonator belongs %.0f m before it" +msgstr "rezonator SHP powinien stać %.0f m przed nim" + +msgid "its SHP resonator belongs at it, it tells of the next signal" +msgstr "rezonator SHP powinien stać przy nim, bo zapowiada następny semafor" + +msgid "none stands" +msgstr "nie ma go" + +msgid "its event %.0f m before it" +msgstr "jego event jest %.0f m przed nim" + +msgid "%s isn't in the scenery folder: the SHP resonators can't be placed" +msgstr "Brak %s w folderze scenerii: nie da się postawić rezonatorów SHP" + +msgid "%s stands by no track" +msgstr "%s nie stoi przy żadnym torze" + +msgid "%s: the tracks before it end short of %.0f m" +msgstr "%s: tory przed nim kończą się przed %.0f m" + +msgid "%s: no plain track within %.0f m of the place of its SHP resonator" +msgstr "%s: w miejscu rezonatora SHP (±%.0f m) nie ma zwykłego toru, same rozjazdy" + +msgid "%s: vehicles stand on %s" +msgstr "%s: na %s stoją pojazdy" + +msgid "The signals have their SHP resonators already" +msgstr "Semafory mają już rezonatory SHP" + +msgid "%zu SHP resonator placed" +msgid_plural "%zu SHP resonators placed" +msgstr[0] "Postawiono %zu rezonator SHP" +msgstr[1] "Postawiono %zu rezonatory SHP" +msgstr[2] "Postawiono %zu rezonatorów SHP" + +msgid "%zu track cut" +msgid_plural "%zu tracks cut" +msgstr[0] "przecięto %zu tor" +msgstr[1] "przecięto %zu tory" +msgstr[2] "przecięto %zu torów" + +msgid "Ctrl+Z takes it back, the resonators work after the scenery is saved and opened again" +msgstr "Ctrl+Z cofa, rezonatory działają po zapisaniu i ponownym otwarciu scenerii" + +msgid "The SHP resonators couldn't be placed: %s" +msgstr "Nie udało się postawić rezonatorów SHP: %s" + +msgid "at it" +msgstr "przy nim" + +msgid "the tracks before it end short of it" +msgstr "tory przed nim kończą się wcześniej" + +msgid "%zu with the tracks ending short of the place of its resonator" +msgid_plural "%zu with the tracks ending short of the places of their resonators" +msgstr[0] "%zu ma tory kończące się przed miejscem rezonatora" +msgstr[1] "%zu mają tory kończące się przed miejscem rezonatora" +msgstr[2] "%zu ma tory kończące się przed miejscem rezonatora" From 9ea362866119606e9eb464aec8bbec304040744b Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 19:06:49 +0200 Subject: [PATCH 068/145] refactor(editor): the SRK tool judges the SHP resonator of a signal in one place, missing, beyond the tracks, off its place or placed, and picks the place of a resonator, a joint or a cut, apart from placing it; the nearest point of a track to a signal found coarsely and narrowed down rather than by up to 4000 points. A resonator whose event would land away from its place, the track not cut there, is left out with a note, and the status after a failed placing tells Ctrl+Z takes the cuts back; the lines out of a station emptied while the signal templates are looked for, the layout of the station read only once it's known open --- application/editormodesrk.cpp | 184 +++++++++++++++++++++------------- application/editormodesrk.h | 10 ++ lang/editor_pl.po | 7 +- 3 files changed, 128 insertions(+), 73 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 138cb452..b6a11e1d 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -80,6 +80,8 @@ double constexpr kUnequal{20.0}; // m, the useful lengths of the two ways differ ImU32 constexpr kUnfit{IM_COL32(255, 190, 40, 255)}; double constexpr kFastest{250.0}; // km/h, a track set faster is taken for one without a limit ImVec4 constexpr kWrong{1.0f, 0.42f, 0.36f, 1.0f}; +ImVec4 constexpr kDoubtful{1.0f, 0.75f, 0.3f, 1.0f}; +ImVec4 constexpr kRight{0.45f, 0.85f, 0.45f, 1.0f}; bool is_switch(TTrack const &Track) { @@ -425,18 +427,36 @@ double route_reach(double const Braking) return std::max(3000.0, 3.0 * Braking); } -// how far the point lies from the path in the plan, Parameter: of the nearest point of it, a metre or so apart +// how far the point lies from the path in the plan, Parameter: of the nearest point of it, found among points 8 m or so apart and +// narrowed down between the two by them double nearest_of(TTrack const &Track, int const Path, glm::dvec3 const &Point, double &Parameter) { geometry::bezier const curve{Track.m_paths[Path]}; - auto const count{std::clamp(static_cast(curve.plan_length(16)), 64, 4000)}; + auto const gap = [&](double const At) { return geometry::plan_distance(curve.point(At), Point); }; + auto const count{std::clamp(static_cast(curve.plan_length(16) / 8.0), 16, 512)}; auto best{std::numeric_limits::max()}; for (int k = 0; k <= count; ++k) - if (auto const distance{geometry::plan_distance(curve.point(static_cast(k) / count), Point)}; distance < best) + if (auto const distance{gap(static_cast(k) / count)}; distance < best) { best = distance; Parameter = static_cast(k) / count; } + auto low{std::max(0.0, Parameter - 1.0 / count)}; + auto high{std::min(1.0, Parameter + 1.0 / count)}; + for (int step = 0; step < 40; ++step) + { + auto const first{low + (high - low) / 3.0}; + auto const second{high - (high - low) / 3.0}; + if (gap(first) < gap(second)) + high = second; + else + low = first; + } + if (auto const narrowed{(low + high) * 0.5}; gap(narrowed) < best) + { + best = gap(narrowed); + Parameter = narrowed; + } return best; } @@ -573,8 +593,50 @@ double shp_before(std::vector const &Way, glm::dvec3 co return best; } +// m from the point to the nearer end of the track +double end_gap(TTrack const &Track, glm::dvec3 const &Point) +{ + auto const &ends{Track.m_paths.front().points}; + return std::min(glm::distance(Point, ends[segment_data::point::start]), glm::distance(Point, ends[segment_data::point::end])); +} + +// of the way of the trains, the point the SHP resonator wanted m before the signal goes at, within kShpTolerance: a joint of the +// tracks, Joint, its event put on the plain track the trains run into there, else a point of a plain track away from its ends, the +// track cut at it; -1 for none +int resonator_at(std::vector const &Way, double const Wanted, bool &Joint) +{ + auto best{-1}; + auto miss{kShpTolerance + 1e-6}; + Joint = false; + for (std::size_t k = 0; k + 1 < Way.size(); ++k) + if (Way[k + 1].track != Way[k].track && Way[k].track->eType == tt_Normal && std::abs(Way[k].distance - Wanted) < miss) + { + miss = std::abs(Way[k].distance - Wanted); + best = static_cast(k); + Joint = true; + } + if (best >= 0) + return best; + for (std::size_t k = 0; k < Way.size(); ++k) + if (Way[k].track->eType == tt_Normal && end_gap(*Way[k].track, Way[k].point) > 0.5 && std::abs(Way[k].distance - Wanted) < miss) + { + miss = std::abs(Way[k].distance - Wanted); + best = static_cast(k); + } + return best; +} + } // namespace +srk_tool::shp_state srk_tool::head_check::shp_of() const +{ + if (shp < 0.0) + return shp_state::needless; + if (shp_found < 0.0) + return shp_short ? shp_state::beyond : shp_state::missing; + return std::abs(shp_found - shp) > kShpTolerance ? shp_state::off : shp_state::placed; +} + void srk_tool::load() { if (m_loaded) @@ -950,7 +1012,7 @@ void srk_tool::render_options() ImGui::Spacing(); gesture_rows({{"LMB on a switch", "opens its station"}}); if (false == m_status.empty()) - ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); + ImGui::TextColored(kDoubtful, "%s", m_status.c_str()); ImGui::Spacing(); render_found(); return; @@ -1077,25 +1139,16 @@ void srk_tool::render_options() "700 m up to 100, 1000 m up to 140, 1300 m above. Needs the line and the km of the station. Run it at the next station too, it " "keeps what stands. One step of undo"); step_heading(5, STR_C("SHP")); - std::size_t needed{0}; - std::size_t found{0}; - std::size_t off{0}; - std::size_t nowhere{0}; + std::array states{}; for (auto const &head : m_heads) - if (head.shp >= 0.0) - { - ++needed; - if (head.shp_found >= 0.0) - { - ++found; - if (std::abs(head.shp_found - head.shp) > kShpTolerance) - ++off; - } - else if (head.shp_short) - ++nowhere; - } - ImGui::BeginDisabled(found + nowhere == needed); - if (ImGui::Button(format("%s (%zu)###srkshp", STR_C("Place the SHP resonators"), needed - found - nowhere).c_str(), ImVec2(-1.0f, 0.0f))) + ++states[static_cast(head.shp_of())]; + auto const of = [&](shp_state const State) { return states[static_cast(State)]; }; + auto const needed{m_heads.size() - of(shp_state::needless)}; + auto const found{of(shp_state::off) + of(shp_state::placed)}; + auto const off{of(shp_state::off)}; + auto const nowhere{of(shp_state::beyond)}; + ImGui::BeginDisabled(of(shp_state::missing) == 0); + if (ImGui::Button(format("%s (%zu)###srkshp", STR_C("Place the SHP resonators"), of(shp_state::missing)).c_str(), ImVec2(-1.0f, 0.0f))) set_shp(); ImGui::EndDisabled(); item_tooltip("Places the SHP track resonators the signals checked lack, rezonator.inc, as PKP PLK places them: 200 m before an entry " @@ -1122,7 +1175,7 @@ void srk_tool::render_options() ImGui::EndTable(); } if (false == m_status.empty()) - ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", m_status.c_str()); + ImGui::TextColored(kDoubtful, "%s", m_status.c_str()); ImGui::Spacing(); m_length_hover = -1; if (ImGui::CollapsingHeader(format("%s (%zu)###srklengths", STR_C("Track lengths"), m_lengths.size()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) @@ -1354,7 +1407,7 @@ void srk_tool::render_lengths() } ImGui::TextDisabled(STR_C("Construction length %.0f m: between the switches at its ends, the switches within it left out"), track.construction); if (std::abs(track.ways[0].length - track.ways[1].length) > kUnequal) - ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", STR_C("The two ways differ: their trains stop on other stretches of the track, between other switches or signals")); + ImGui::TextColored(kDoubtful, "%s", STR_C("The two ways differ: their trains stop on other stretches of the track, between other switches or signals")); ImGui::TextDisabled("%s", STR_C("LMB: show it")); ImGui::EndTooltip(); } @@ -1415,7 +1468,7 @@ void srk_tool::render_heads() for (std::size_t index = 0; index < m_heads.size(); ++index) { auto const &head{m_heads[index]}; - if (head.station || false == head.fits || false == head.problems.empty() || (head.shp >= 0.0 && (head.shp_found < 0.0 || std::abs(head.shp_found - head.shp) > kShpTolerance))) + if (head.station || false == head.fits || false == head.problems.empty() || (head.shp_of() != shp_state::needless && head.shp_of() != shp_state::placed)) rows.push_back(index); else ++fine; @@ -1474,26 +1527,26 @@ void srk_tool::render_heads() ImGui::TextUnformatted(format(STR_C("Needs: %s"), need_text(need).c_str()).c_str()); } if (head.shortened > 0) - ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", format(STR_C("The next signal stands nearer than the braking distance: with it at stop this one slows the trains to %d km/h, Ie-4 § 8 ust. 5"), head.shortened).c_str()); + ImGui::TextColored(kDoubtful, "%s", format(STR_C("The next signal stands nearer than the braking distance: with it at stop this one slows the trains to %d km/h, Ie-4 § 8 ust. 5"), head.shortened).c_str()); if (head.indicator) - ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", STR_C("Give it a W21 indicator \"14\": 140 km/h with the next signal at stop, Ie-4 § 8 ust. 5")); + ImGui::TextColored(kDoubtful, "%s", STR_C("Give it a W21 indicator \"14\": 140 km/h with the next signal at stop, Ie-4 § 8 ust. 5")); if (head.lamps.empty() && head.kind != "warning") ImGui::TextDisabled("%s", STR_C("A semaphore with arms: its lights aren't checked")); else if (false == head.fits && head.fit.empty()) ImGui::TextColored(kWrong, "%s", STR_C("No template of its kind and mount has these lights")); else if (false == head.fits) - ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", format(STR_C("Its lights don't show all of it: %s does"), head.fit.c_str()).c_str()); + ImGui::TextColored(kDoubtful, "%s", format(STR_C("Its lights don't show all of it: %s does"), head.fit.c_str()).c_str()); else if (head.more) ImGui::TextDisabled("%s", format(STR_C("It has more lights than it needs: %s will do"), head.fit.c_str()).c_str()); for (auto const &problem : head.problems) ImGui::TextColored(kWrong, "%s", problem.c_str()); - if (head.shp >= 0.0) + if (auto const state{head.shp_of()}; state != shp_state::needless) { auto const wanted{head.shp > 0.0 ? format(STR_C("its SHP resonator belongs %.0f m before it"), head.shp) : std::string{STR_C("its SHP resonator belongs at it, it tells of the next signal")}}; - if (head.shp_found < 0.0) - ImGui::TextColored(kWrong, "%s", (wanted + ", " + (head.shp_short ? STR("the tracks before it end short of it") : STR("none stands"))).c_str()); + if (state == shp_state::missing || state == shp_state::beyond) + ImGui::TextColored(kWrong, "%s", (wanted + ", " + (state == shp_state::beyond ? STR("the tracks before it end short of it") : STR("none stands"))).c_str()); else - ImGui::TextColored(std::abs(head.shp_found - head.shp) > kShpTolerance ? ImVec4(1.0f, 0.75f, 0.3f, 1.0f) : ImVec4(0.45f, 0.85f, 0.45f, 1.0f), "%s", (wanted + ", " + format(STR_C("its event %.0f m before it"), head.shp_found)).c_str()); + ImGui::TextColored(state == shp_state::off ? kDoubtful : kRight, "%s", (wanted + ", " + format(STR_C("its event %.0f m before it"), head.shp_found)).c_str()); } ImGui::TextDisabled("%s", STR_C("LMB: fly to it")); ImGui::PopTextWrapPos(); @@ -1518,25 +1571,25 @@ void srk_tool::render_heads() if (head.lamps.empty()) ImGui::TextDisabled("-"); else if (false == head.fits) - ImGui::TextColored(head.fit.empty() ? kWrong : ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", head.fit.empty() ? STR_C("no template") : STR_C("lights missing")); + ImGui::TextColored(head.fit.empty() ? kWrong : kDoubtful, "%s", head.fit.empty() ? STR_C("no template") : STR_C("lights missing")); else if (head.more) ImGui::TextDisabled("%s", STR_C("more than needed")); else - ImGui::TextColored(ImVec4(0.45f, 0.85f, 0.45f, 1.0f), "%s", STR_C("fits")); + ImGui::TextColored(kRight, "%s", STR_C("fits")); if (false == head.problems.empty()) { ImGui::SameLine(); ImGui::TextColored(kWrong, "%s", STR_C("too near")); } ImGui::TableNextColumn(); - if (head.shp < 0.0) - ImGui::TextDisabled("-"); - else if (head.shp_found < 0.0) - ImGui::TextColored(kWrong, "%s", head.shp_short ? STR_C("no track") : STR_C("none")); - else if (std::abs(head.shp_found - head.shp) > kShpTolerance) - ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%.0f m", head.shp_found); - else - ImGui::TextColored(ImVec4(0.45f, 0.85f, 0.45f, 1.0f), "%s", head.shp > 0.0 ? format("%.0f m", head.shp_found).c_str() : STR_C("at it")); + switch (head.shp_of()) + { + case shp_state::needless: ImGui::TextDisabled("-"); break; + case shp_state::missing: ImGui::TextColored(kWrong, "%s", STR_C("none")); break; + case shp_state::beyond: ImGui::TextColored(kWrong, "%s", STR_C("no track")); break; + case shp_state::off: ImGui::TextColored(kDoubtful, "%.0f m", head.shp_found); break; + default: ImGui::TextColored(kRight, "%s", head.shp > 0.0 ? format("%.0f m", head.shp_found).c_str() : STR_C("at it")); break; + } } ImGui::EndTable(); } @@ -2274,7 +2327,6 @@ void srk_tool::set_lines() { m_status.clear(); auto const stem{stem_of_open()}; - auto const &layout{*m_layout}; std::vector standing; m_signals.standing_signals(standing, true); std::vector lines; @@ -2282,6 +2334,7 @@ void srk_tool::set_lines() std::vector entry_standing; if (false == lines_of_open(standing, lines, entries, entry_standing, m_status)) return; + auto const &layout{*m_layout}; // of each line, 1: the automatic block, placed on a double-track line only, 2: the automatic block on a single-track line, its // signals facing both ways on a track left as they stand, 0: the semi-automatic one std::vector automatic(lines.size(), 0); @@ -2721,6 +2774,8 @@ void srk_tool::survey_signals() { m_heads.clear(); m_heads_known = false; + m_lines.clear(); + m_lines_problem = STR("Looking for the signal templates..."); if (false == m_measured) measure({}); return; @@ -3011,6 +3066,7 @@ void srk_tool::set_shp() taken.insert(ToLower(event.first)); struct placing { + std::string signal; // shown std::string name; // of the resonator, its event is _shp TTrack *track{nullptr}; // the trains run into its event on, where a joint of the tracks lies TTrack *cut{nullptr}; // the track cut at the point, else @@ -3023,9 +3079,9 @@ void srk_tool::set_shp() std::vector problems; for (auto const &head : m_heads) { - if (head.shp < 0.0 || head.shp_found >= 0.0) + if (head.shp_of() != shp_state::missing && head.shp_of() != shp_state::beyond) continue; - if (head.shp_short) + if (head.shp_of() == shp_state::beyond) { problems.push_back(format(STR_C("%s: the tracks before it end short of %.0f m"), head.name.c_str(), head.shp)); continue; @@ -3045,29 +3101,8 @@ void srk_tool::set_shp() problems.push_back(format(STR_C("%s: the tracks before it end short of %.0f m"), head.name.c_str(), head.shp)); continue; } - // a joint of the tracks within the tolerance takes the event on the track the trains run into there, else the track is cut - auto best{-1}; auto joint{false}; - auto miss{kShpTolerance + 1e-6}; - for (std::size_t k = 0; k + 1 < way.size(); ++k) - if (way[k + 1].track != way[k].track && way[k].track->eType == tt_Normal && std::abs(way[k].distance - head.shp) < miss) - { - miss = std::abs(way[k].distance - head.shp); - best = static_cast(k); - joint = true; - } - if (best < 0) - for (std::size_t k = 0; k < way.size(); ++k) - { - auto const &ends{way[k].track->m_paths.front().points}; - if (way[k].track->eType != tt_Normal || std::min(glm::distance(way[k].point, ends[segment_data::point::start]), glm::distance(way[k].point, ends[segment_data::point::end])) <= 0.5) - continue; - if (std::abs(way[k].distance - head.shp) < miss) - { - miss = std::abs(way[k].distance - head.shp); - best = static_cast(k); - } - } + auto const best{resonator_at(way, head.shp, joint)}; if (best < 0) { problems.push_back(format(STR_C("%s: no plain track within %.0f m of the place of its SHP resonator"), head.name.c_str(), kShpTolerance)); @@ -3090,6 +3125,7 @@ void srk_tool::set_shp() for (int number = 2; taken.count(ToLower(name) + "_shp") > 0 && number < 100; ++number) name = found->name + "_rez" + std::to_string(number); taken.insert(ToLower(name) + "_shp"); + entry.signal = head.name; entry.name = name; entry.facing = at.facing; entry.point = at.point; @@ -3132,8 +3168,7 @@ void srk_tool::set_shp() if (entry.cut == nullptr) continue; auto *track{piece_at(entry.cut, entry.point)}; - auto const &ends{track->m_paths.front().points}; - if (std::min(glm::distance(entry.point, ends[segment_data::point::start]), glm::distance(entry.point, ends[segment_data::point::end])) <= 0.3) + if (end_gap(*track, entry.point) <= 0.3) continue; if (captured.insert(track).second) before.emplace_back(track, editor_track::capture(*track)); @@ -3152,6 +3187,12 @@ void srk_tool::set_shp() for (auto const &entry : places) { auto *track{entry.track != nullptr ? entry.track : piece_at(entry.cut, entry.inside)}; + // the trains run into the track at the point: at the start of its path running towards its end + if (glm::distance(track->m_paths.front().points[entry.facing > 0 ? segment_data::point::start : segment_data::point::end], entry.point) > 1.0) + { + problems.push_back(format(STR_C("%s: the track couldn't be cut at the place of its SHP resonator"), entry.signal.c_str())); + continue; + } if (captured.insert(track).second) before.emplace_back(track, editor_track::capture(*track)); editor_track::events(*track, entry.facing > 0 ? 2 : 1).emplace_back(ToLower(entry.name) + "_shp", nullptr); @@ -3167,13 +3208,14 @@ void srk_tool::set_shp() changes.others.push_back(std::move(item)); } std::string error; + auto const placed{changes.others.size()}; m_signals.signal_batch(changes, std::move(before), std::move(created), error); if (false == error.empty()) { - m_status = format(STR_C("The SHP resonators couldn't be placed: %s"), error.c_str()); + m_status = format(STR_C("The SHP resonators couldn't be placed: %s. Ctrl+Z takes back the tracks cut"), error.c_str()); return; } - m_status = format(STR_P("%zu SHP resonator placed", "%zu SHP resonators placed", places.size()), places.size()) + ", " + format(STR_P("%zu track cut", "%zu tracks cut", cuts), cuts) + ". " + STR("Ctrl+Z takes it back, the resonators work after the scenery is saved and opened again"); + m_status = format(STR_P("%zu SHP resonator placed", "%zu SHP resonators placed", placed), placed) + ", " + format(STR_P("%zu track cut", "%zu tracks cut", cuts), cuts) + ". " + STR("Ctrl+Z takes it back, the resonators work after the scenery is saved and opened again"); for (auto const &problem : problems) m_status += "\n" + problem; } diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 5e7494ac..25fb225b 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -125,6 +125,15 @@ class srk_tool final : public editor_tool int m_length_hover{-1}; // of the lengths, the row under the mouse int m_length_canvas{-1}; // of the lengths, of the track under the mouse in the schematic int m_length_shown{-1}; // of the lengths, the one drawn: of the row or the track under the mouse, else of the track selected + // how the SHP track resonator of a signal stands + enum class shp_state + { + needless, // the signal needs none + missing, + beyond, // missing, the tracks before the signal end short of its place + off, // farther from its place than allowed + placed + }; // a signal of the station or of its lines, its lights as the routes of its trains need them: Ie-1, Ie-4 § 8 struct head_check { @@ -153,6 +162,7 @@ class srk_tool final : public editor_tool double shp{-1.0}; double shp_found{-1.0}; // m before it the trains run into the event of the SHP resonator standing, -1 for none bool shp_short{false}; // the tracks before it end short of the place of its resonator + shp_state shp_of() const; }; std::vector m_heads; bool m_heads_known{false}; // the templates found diff --git a/lang/editor_pl.po b/lang/editor_pl.po index b03b9add..00adaabe 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11233,8 +11233,11 @@ msgstr[2] "przecięto %zu torów" msgid "Ctrl+Z takes it back, the resonators work after the scenery is saved and opened again" msgstr "Ctrl+Z cofa, rezonatory działają po zapisaniu i ponownym otwarciu scenerii" -msgid "The SHP resonators couldn't be placed: %s" -msgstr "Nie udało się postawić rezonatorów SHP: %s" +msgid "The SHP resonators couldn't be placed: %s. Ctrl+Z takes back the tracks cut" +msgstr "Nie udało się postawić rezonatorów SHP: %s. Ctrl+Z cofa przecięcie torów" + +msgid "%s: the track couldn't be cut at the place of its SHP resonator" +msgstr "%s: nie udało się przeciąć toru w miejscu rezonatora SHP" msgid "at it" msgstr "przy nim" From fc0b9b46e8898e8fa117e794c314dc05f27252ac Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 19:06:52 +0200 Subject: [PATCH 069/145] feat(editor): the table of the lines out of a station marks the signals of the other block left standing along a line, warning discs on one of the automatic block, block signals on one of the semi-automatic one, a click flying to the first of them --- application/editormodesrk.cpp | 38 +++++++++++++++++++++++++++++++---- application/editormodesrk.h | 5 +++++ lang/editor_pl.po | 15 ++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index b6a11e1d..25edf47e 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -1276,8 +1276,17 @@ void srk_tool::render_lines() ImGui::TableNextRow(); ImGui::TableNextColumn(); auto const track{row.number == 0 ? std::string{STR_C("single track")} : format(STR_C("track %d"), row.number)}; - if (ImGui::Selectable(format("%s, %s##srkline", row.entry.c_str(), track.c_str()).c_str(), false, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) - m_view.fly_to(row.position + glm::dvec3{0.0, 4.0, 0.0}, glm::normalize(glm::dvec3{row.travel.x, -0.3, row.travel.y}), 30.0); + if (row.leftovers > 0) + ImGui::PushStyleColor(ImGuiCol_Text, kDoubtful); + auto const clicked{ImGui::Selectable(format("%s, %s##srkline", row.entry.c_str(), track.c_str()).c_str(), false, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)}; + if (row.leftovers > 0) + ImGui::PopStyleColor(); + if (clicked) + { + auto const &position{row.leftovers > 0 ? row.leftover : row.position}; + auto const &travel{row.leftovers > 0 ? row.leftover_travel : row.travel}; + m_view.fly_to(position + glm::dvec3{0.0, 4.0, 0.0}, glm::normalize(glm::dvec3{travel.x, -0.3, travel.y}), 30.0); + } if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); @@ -1289,7 +1298,14 @@ void srk_tool::render_lines() ImGui::TextUnformatted(STR_C("A single-track line")); ImGui::TextUnformatted(format(STR_P("%zu block signal stands along it", "%zu block signals stand along it", row.blocks), row.blocks).c_str()); ImGui::TextUnformatted(format(STR_C("Block: %s, %s"), kind(row.automatic), reason(row.why)).c_str()); - ImGui::TextDisabled("%s", STR_C("LMB: fly to the entry signal")); + if (row.leftovers > 0) + { + ImGui::TextColored(kDoubtful, "%s", (row.automatic ? format(STR_P("%zu warning disc stands along it, of the semi-automatic block: delete it in the Signals tool", "%zu warning discs stand along it, of the semi-automatic block: delete them in the Signals tool", row.leftovers), row.leftovers) + : format(STR_P("%zu block signal stands along it, of the automatic block: delete it in the Signals tool", "%zu block signals stand along it, of the automatic block: delete them in the Signals tool", row.leftovers), row.leftovers)).c_str()); + ImGui::TextDisabled("%s", STR_C("LMB: fly to the first of them")); + } + else + ImGui::TextDisabled("%s", STR_C("LMB: fly to the entry signal")); ImGui::PopTextWrapPos(); ImGui::EndTooltip(); } @@ -2317,6 +2333,20 @@ void srk_tool::survey_lines(std::vector const &Standing) row.position = Standing[owners[line.entry]].position; row.travel = travel_of(Standing[owners[line.entry]].yaw); row.automatic = automatic_block(line, row.entry, Standing, row.why, row.blocks); + for (auto const &signal : Standing) + { + if (signal.kind != (row.automatic ? "warning" : "block")) + continue; + double gap{0.0}; + auto const distance{station::distance_on(line, signal.position, gap)}; + if (gap > 4.5 || distance < 50.0 || distance > line.length() - 1.0) + continue; + if (row.leftovers++ == 0) + { + row.leftover = signal.position; + row.leftover_travel = travel_of(signal.yaw); + } + } auto const next{next_entry(line, Standing, row.length)}; row.next = next >= 0 ? signal_shown(Standing[next].name, stem) : std::string{}; m_lines.push_back(std::move(row)); @@ -3910,7 +3940,7 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri if (false == m_lines_problem.empty()) WriteLog("SELFTEST srkblock: " + m_lines_problem); for (auto const &row : m_lines) - WriteLog(format("SELFTEST srkblock %s track %d %s to %s %.0f m blocks %zu: %s why %d", row.entry.c_str(), row.number, row.inbound ? "in" : "out", row.next.empty() ? "-" : row.next.c_str(), row.length, row.blocks, row.automatic ? "sbl" : "semi", static_cast(row.why))); + WriteLog(format("SELFTEST srkblock %s track %d %s to %s %.0f m blocks %zu: %s why %d leftovers %zu", row.entry.c_str(), row.number, row.inbound ? "in" : "out", row.next.empty() ? "-" : row.next.c_str(), row.length, row.blocks, row.automatic ? "sbl" : "semi", static_cast(row.why), row.leftovers)); return editor_selftest_result::handled; } if (Command == "srkstrip") diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 25fb225b..fae49384 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -182,6 +182,11 @@ class srk_tool final : public editor_tool std::size_t blocks{0}; // the block signals standing along it bool automatic{false}; // of the automatic block block_reason why{block_reason::single}; // the block it has, so + // the signals of the other block standing along it: warning discs on a line of the automatic block, block signals on one of the + // semi-automatic one, the first of them + std::size_t leftovers{0}; + glm::dvec3 leftover{0.0}; + glm::dvec2 leftover_travel{0.0, 1.0}; }; std::vector m_lines; std::string m_lines_problem; // why the station has no lines listed diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 00adaabe..37919193 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11250,3 +11250,18 @@ msgid_plural "%zu with the tracks ending short of the places of their resonators msgstr[0] "%zu ma tory kończące się przed miejscem rezonatora" msgstr[1] "%zu mają tory kończące się przed miejscem rezonatora" msgstr[2] "%zu ma tory kończące się przed miejscem rezonatora" + +msgid "%zu warning disc stands along it, of the semi-automatic block: delete it in the Signals tool" +msgid_plural "%zu warning discs stand along it, of the semi-automatic block: delete them in the Signals tool" +msgstr[0] "Stoi na nim %zu tarcza ostrzegawcza, z blokady półsamoczynnej: usuń ją w narzędziu Sygnalizatory (H)" +msgstr[1] "Stoją na nim %zu tarcze ostrzegawcze, z blokady półsamoczynnej: usuń je w narzędziu Sygnalizatory (H)" +msgstr[2] "Stoi na nim %zu tarcz ostrzegawczych, z blokady półsamoczynnej: usuń je w narzędziu Sygnalizatory (H)" + +msgid "%zu block signal stands along it, of the automatic block: delete it in the Signals tool" +msgid_plural "%zu block signals stand along it, of the automatic block: delete them in the Signals tool" +msgstr[0] "Stoi na nim %zu semafor SBL, z blokady samoczynnej: usuń go w narzędziu Sygnalizatory (H)" +msgstr[1] "Stoją na nim %zu semafory SBL, z blokady samoczynnej: usuń je w narzędziu Sygnalizatory (H)" +msgstr[2] "Stoi na nim %zu semaforów SBL, z blokady samoczynnej: usuń je w narzędziu Sygnalizatory (H)" + +msgid "LMB: fly to the first of them" +msgstr "LPM: leć do pierwszego z nich" From b92d1283acff01b70ca22c25c1cbb8fa9d92faa5 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 19:26:25 +0200 Subject: [PATCH 070/145] feat(editor): a block signal template knows its aspects, 4 where its events give S3, the green light flashing with the next signal at S5, else 3; one step of the signalling takes includes away too, the signals among them with their read events, Ctrl+Z bringing them back with the rest of it --- application/editormode.h | 1 + application/editormodesignal.cpp | 27 +++++++++++++++++++++++++-- application/editortool.h | 2 ++ editor/editorIncludeInfo.cpp | 19 +++++++++++++++++++ editor/editorIncludeInfo.hpp | 3 +++ 5 files changed, 50 insertions(+), 2 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 12efab27..7060ab7d 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1920,6 +1920,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool plate{false}; bool linked{false}; std::vector speeds; + int aspects{0}; // of a block signal, 3 or 4 }; struct signal_spot { diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index c8181c35..2261dd47 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -265,6 +265,8 @@ bool editor_mode::signal_scan(bool const Wait) item.linked = true; } editor_includes::signal_speeds(entry.file, item.speeds); + if (item.kind == "block") + item.aspects = editor_includes::block_aspects(entry.file); tool.templates.push_back(std::move(item)); } std::sort(tool.templates.begin(), tool.templates.end(), [](signal_template const &A, signal_template const &B) { @@ -936,7 +938,7 @@ bool editor_mode::signal_heads(std::vector &Heads, bool const Wait) if (item.lamps.empty()) continue; auto const kind{kind_index(item.kind)}; - Heads.push_back({item.file, item.name, item.kind, item.mount, item.lamps, item.speeds, kind >= 0 && m_signal.chosen[kind] == index}); + Heads.push_back({item.file, item.name, item.kind, item.mount, item.lamps, item.speeds, kind >= 0 && m_signal.chosen[kind] == index, item.aspects}); } return true; } @@ -1072,6 +1074,27 @@ std::size_t editor_mode::signal_batch(signal_changes const &Changes, std::vector if (false == read_before.empty() && false == read_after.empty() && ToLower(read_before) != ToLower(read_after)) signal_renamed(read_before, read_after, step); } + std::unordered_set const created(Created.begin(), Created.end()); + std::vector removed; + for (auto const instance : Changes.removed) + { + if (false == scene::Layers.tracked(instance) || false == scene::Layers.removable(instance) || scene::Layers.instance(instance).removed) + continue; + auto const *standing{signal_standing_of(instance)}; + auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr}; + std::string file; + std::vector values; + if (item != nullptr && false == item->read.empty() && editor_includes::parse_directive(scene::Layers.directive(instance), file, values)) + { + std::vector> states; + signal_detach_events(editor_includes::substitute(item->read, values), states); + for (auto &state : states) + if (created.count(state.first) == 0 && std::none_of(step.tracks.begin(), step.tracks.end(), [&](auto const &Taken) { return Taken.first == state.first; })) + step.tracks.push_back(std::move(state)); + } + scene::Layers.removed(instance, true); + removed.push_back(instance); + } std::vector instances; if (Error.empty() && false == items.empty()) { @@ -1086,7 +1109,6 @@ std::size_t editor_mode::signal_batch(signal_changes const &Changes, std::vector } else reads.clear(); - std::unordered_set const created(Created.begin(), Created.end()); for (auto const &[spot, read] : reads) { if (read.empty() || false == spot.track->Dynamics.empty() || false == scene::Layers.editable(spot.track)) @@ -1099,6 +1121,7 @@ std::size_t editor_mode::signal_batch(signal_changes const &Changes, std::vector spot.track->mark_dirty(); } auto const count{instances.size()}; + instances.insert(instances.end(), removed.begin(), removed.end()); if (step.tracks.empty() && Created.empty() && instances.empty() && directives.empty()) return 0; if (step.tracks.empty() && Created.empty()) diff --git a/application/editortool.h b/application/editortool.h index 70a8eaef..69114202 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -116,6 +116,7 @@ struct signal_head std::string lamps; // its chambers from the top: z green, c red, p orange, b white, n blue, x blind std::vector speeds; // of the signal itself, km/h: -1 the highest allowed, 20 the substitute signal bool chosen{false}; // the Signals tool places it for its kind + int aspects{0}; // of a block signal: 3, or 4 with the warning two signals ahead }; // one step of the history of the signalling: signals placed, standing ones renamed, other templates placed with them, the values // of standing ones replaced @@ -126,6 +127,7 @@ struct signal_changes std::vector others; std::vector>> values; std::vector> files; // standing signals given another template, of the same kind, their values kept + std::vector removed; // includes taken away, the signals among them with their read events }; struct nmt_place diff --git a/editor/editorIncludeInfo.cpp b/editor/editorIncludeInfo.cpp index 11092df2..670bd935 100644 --- a/editor/editorIncludeInfo.cpp +++ b/editor/editorIncludeInfo.cpp @@ -537,6 +537,25 @@ bool signal_speeds(std::string const &File, std::vector &Speeds) return true; } +int block_aspects(std::string const &File) +{ + template_text text; + std::string error; + if (false == read_template(File, text, error)) + { + return 0; + } + auto const tokens{tokenize(text.content)}; + for (std::size_t idx = 0; idx + 1 < tokens.size(); ++idx) + { + if (tokens[idx] == "event" && tokens[idx + 1].size() > 3 && tokens[idx + 1].ends_with("_s3")) + { + return 4; + } + } + return 3; +} + void suggest(std::string const &File, include_info &Info) { template_text text; diff --git a/editor/editorIncludeInfo.hpp b/editor/editorIncludeInfo.hpp index d5340c03..09f77daf 100644 --- a/editor/editorIncludeInfo.hpp +++ b/editor/editorIncludeInfo.hpp @@ -71,6 +71,9 @@ int parameter_count(std::string const &File); // the speeds the events of a signal template set it to show, by their SetVelocity: of the signal itself, km/h, -1 the highest // allowed, 20 the substitute signal. returns: false if the file can't be read bool signal_speeds(std::string const &File, std::vector &Speeds); +// the aspects of a block signal of the template: 4 where its events give S3, the green light flashing with the next signal at S5, +// the warning two signals ahead, else 3. returns: 0 if the file can't be read +int block_aspects(std::string const &File); // sets roles of the parameters whose use in the template makes their meaning clear. roles already set are left alone void suggest(std::string const &File, include_info &Info); // true if value of a parameter with specified role is supplied by the editor when the template is placed From a3e1624caf766754dcccd4376046b6a884855d13 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 19:26:26 +0200 Subject: [PATCH 071/145] feat(editor): the lines out of a station take the automatic block of 3 or 4 aspects, the block sections no shorter than the braking distance or than half of it, the block signals placed with the templates of the aspects and those standing swapped for them; block boundaries drawn by hand, a click on a track of a line of the automatic block placing block signals across both tracks there, numbered after the hectometre, the tracks cut beyond them and the block sections named and switched anew, a click on one standing taking its signals and the switching of their sections away, a step of undo each, the lengths of the sections either side shown at the mouse. A line whose block signals stand keeps them as they stand, Set the line signals placing them evenly only on a line without any, and one whose block signals no block section of the editor switches keeps the block of the scenery untouched; the signals of the other block left along a line taken away, but where it shares its track with a line of that block, and the switching of the sections gone from the lines set with them. A block signal of 4 aspects is checked for a section no shorter than half the braking distance, and fitted with templates of its aspects only --- application/editormode.h | 2 +- application/editormodesrk.cpp | 723 ++++++++++++++++++++++++------ application/editormodesrk.h | 61 ++- application/editormodetrackux.cpp | 11 + lang/editor_pl.po | 123 ++++- 5 files changed, 762 insertions(+), 158 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 7060ab7d..161ee849 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1644,7 +1644,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void draw_track_hover() const; struct track_intent { - enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling, measure, route_trace, kilometrage }; + enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling, measure, route_trace, kilometrage, srk_boundary }; kind what{kind::none}; TTrack *track{nullptr}; int path{0}; diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 25edf47e..e844a5f4 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -60,6 +60,8 @@ double constexpr kShpBefore{200.0}; // m, of the resonator before an entry signa double constexpr kShpTolerance{5.0}; // m, either way double constexpr kShpWindow{100.0}; // m, either way of its place, a resonator standing is taken for the one of the signal double constexpr kResonatorDrop{0.2}; // m, of its origin below the axis +double constexpr kBoundaryReach{30.0}; // m, either way of a block signal, a click while drawing the boundaries takes it away +double constexpr kBoundaryEnd{50.0}; // m, of a block boundary drawn from the entry signals at the ends of the line // m, of a train at the speed, as Ie-4 § 8 takes it: the least block section of the automatic block, a warning disc that far before // its signal @@ -340,14 +342,14 @@ int surplus(signal_head const &Head, head_need const &Need) // of the templates of the kind and the mount that fit the need, the one with the least beyond it, the most like the one named; // -1 for none -int best_head(std::vector const &Heads, std::string const &Kind, std::string const &Mount, std::string const &Like, head_need const &Need) +int best_head(std::vector const &Heads, std::string const &Kind, std::string const &Mount, std::string const &Like, int const Aspects, head_need const &Need) { auto best{-1}; std::tuple score; for (int index = 0; index < static_cast(Heads.size()); ++index) { auto const &head{Heads[index]}; - if (head.kind != Kind || head.mount != Mount || false == shows(head, Need)) + if (head.kind != Kind || head.mount != Mount || head.aspects != Aspects || false == shows(head, Need)) continue; std::size_t common{0}; while (common < head.name.size() && common < Like.size() && head.name[common] == Like[common]) @@ -626,6 +628,46 @@ int resonator_at(std::vector const &Way, double const W return best; } +// the template of a block signal of the aspects wanted, Like: the one standing, empty for the one chosen in the Signals tool, taken +// where it has them, else the one of their mount with them, its counterpart by the name first, then of its lights; not the first of +// a line, whose events tell a station signal; empty for none +std::string block_template(std::vector const &Heads, std::string const &Like, int const Aspects) +{ + signal_head const *like{nullptr}; + for (auto const &head : Heads) + if (head.kind == "block" && (Like.empty() ? head.chosen : head.file == Like)) + like = &head; + if (like != nullptr && like->aspects == Aspects) + return like->file; + auto counterpart{like != nullptr ? like->name : std::string{}}; + if (auto const digit{counterpart.find_first_of("34")}; digit != std::string::npos) + counterpart[digit] = counterpart[digit] == '3' ? '4' : '3'; + signal_head const *best{nullptr}; + auto rank{std::numeric_limits::max()}; + for (auto const &head : Heads) + { + if (head.kind != "block" || head.aspects != Aspects || ToLower(head.name).find("pierwszy") != std::string::npos) + continue; + auto const score{like == nullptr ? 0 : (head.mount != like->mount ? 4 : 0) + (head.name != counterpart ? 2 : 0) + (head.lamps != like->lamps ? 1 : 0)}; + if (score < rank) + { + rank = score; + best = &head; + } + } + return best != nullptr ? best->file : std::string{}; +} + +// the block signals switched by the block sections of the editor, lower case +std::unordered_set guarded(std::vector const &Controls) +{ + std::unordered_set result; + for (auto const &control : Controls) + if (control.values.size() > 5) + result.insert(ToLower(control.values[5])); + return result; +} + } // namespace srk_tool::shp_state srk_tool::head_check::shp_of() const @@ -671,7 +713,10 @@ void srk_tool::load() auto const comma{std::min(word.find(',', from), word.size())}; auto const item{word.substr(from, comma - from)}; if (auto const equals{item.find('=')}; equals != std::string::npos && equals > 0) - entry.blocks[item.substr(0, equals)] = item.substr(equals + 1) == "sbl"; + { + auto const kind{item.substr(equals + 1)}; + entry.blocks[item.substr(0, equals)] = kind == "sbl4" ? block_kind::four : kind == "sbl" || kind == "sbl3" ? block_kind::three : block_kind::semi; + } from = comma + 1; } else if (word == "anchor") @@ -712,9 +757,9 @@ void srk_tool::store() lines.emplace_back("# stations described in the track editor, the simulation skips these lines"); auto const braking{entry.braking > 0 ? format(" braking %d", entry.braking) : std::string{}}; std::string blocks; - for (auto const &[signal, automatic] : entry.blocks) + for (auto const &[signal, kind] : entry.blocks) if (false == signal.empty() && signal.find_first_of(" ,=\t") == std::string::npos) - blocks += (blocks.empty() ? " blocks " : ",") + signal + (automatic ? "=sbl" : "=semi"); + blocks += (blocks.empty() ? " blocks " : ",") + signal + (kind == block_kind::four ? "=sbl4" : kind == block_kind::three ? "=sbl" : "=semi"); lines.push_back(format("station %s line %s km %s from %s anchor %s%s%s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), word_of(entry.km).c_str(), entry.reversed ? "right" : "left", anchor_of(entry).c_str(), braking.c_str(), blocks.c_str(), entry.name.c_str())); } scene::Layers.mark(layer, kMark, std::move(lines)); @@ -948,6 +993,24 @@ void srk_tool::draw_overlay() const drawlist->AddText(ImVec2(screen.x - size.x * 0.5f, screen.y - size.y - 2.0f), kUseful, text.c_str()); } } + // the block boundary under the mouse while they are drawn: a bar across each track and a post where its signal goes + if (drawing() && m_boundary.valid) + for (std::size_t k = 0; k < m_boundary.places.size(); ++k) + { + auto const colour{m_boundary.remove ? IM_COL32(255, 90, 80, 255) : IM_COL32(110, 230, 120, 255)}; + auto const &way{m_boundary.ways[k]}; + glm::dvec3 const across{-way.z, 0.0, way.x}; + auto const base{m_boundary.places[k] + lift}; + projection.line(drawlist, base - across * 2.0, base + across * 2.0, colour, 4.0f); + projection.line(drawlist, base + across * 2.4, base + across * 2.4 + glm::dvec3{0.0, 6.0, 0.0}, colour, 3.0f); + ImVec2 screen; + if (k >= m_boundary.names.size() || false == projection.project(base + across * 2.4 + glm::dvec3{0.0, 6.5, 0.0}, screen)) + continue; + auto const &text{m_boundary.names[k]}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + drawlist->AddRectFilled(ImVec2(screen.x - size.x * 0.5f - 4.0f, screen.y - size.y - 4.0f), ImVec2(screen.x + size.x * 0.5f + 4.0f, screen.y), colour, 3.0f); + drawlist->AddText(ImVec2(screen.x - size.x * 0.5f, screen.y - size.y - 2.0f), IM_COL32(0, 0, 0, 255), text.c_str()); + } for (auto const &mark : m_signal_marks) { ImVec2 screen; @@ -1249,14 +1312,21 @@ void srk_tool::render_lines() default: return STR_C("a single-track line"); } }; - auto const kind = [](bool const Automatic) { return Automatic ? STR_C("automatic (SBL)") : STR_C("semi-automatic"); }; - auto const set = [&](line_row const &Row, std::optional const Automatic) { + auto const kind = [](block_kind const Kind) { + switch (Kind) + { + case block_kind::three: return STR_C("SBL, 3 aspects"); + case block_kind::four: return STR_C("SBL, 4 aspects"); + default: return STR_C("semi-automatic"); + } + }; + auto const set = [&](line_row const &Row, std::optional const Kind) { auto &entry{record_of_open()}; for (auto const *row : std::initializer_list{&Row, Row.partner >= 0 && Row.partner < static_cast(m_lines.size()) ? &m_lines[Row.partner] : nullptr}) if (row != nullptr) { - if (Automatic.has_value()) - entry.blocks[row->entry] = *Automatic; + if (Kind.has_value()) + entry.blocks[row->entry] = *Kind; else entry.blocks.erase(row->entry); } @@ -1297,11 +1367,13 @@ void srk_tool::render_lines() else ImGui::TextUnformatted(STR_C("A single-track line")); ImGui::TextUnformatted(format(STR_P("%zu block signal stands along it", "%zu block signals stand along it", row.blocks), row.blocks).c_str()); - ImGui::TextUnformatted(format(STR_C("Block: %s, %s"), kind(row.automatic), reason(row.why)).c_str()); + ImGui::TextUnformatted(format(STR_C("Block: %s, %s"), kind(row.block), reason(row.why)).c_str()); + if (row.legacy) + ImGui::TextColored(kDoubtful, "%s", STR_C("No block section of the editor switches its block signals: it keeps the block of the scenery as it stands")); if (row.leftovers > 0) { - ImGui::TextColored(kDoubtful, "%s", (row.automatic ? format(STR_P("%zu warning disc stands along it, of the semi-automatic block: delete it in the Signals tool", "%zu warning discs stand along it, of the semi-automatic block: delete them in the Signals tool", row.leftovers), row.leftovers) - : format(STR_P("%zu block signal stands along it, of the automatic block: delete it in the Signals tool", "%zu block signals stand along it, of the automatic block: delete them in the Signals tool", row.leftovers), row.leftovers)).c_str()); + ImGui::TextColored(kDoubtful, "%s", (row.block != block_kind::semi ? format(STR_P("%zu warning disc stands along it, of the semi-automatic block: Set the line signals takes it away", "%zu warning discs stand along it, of the semi-automatic block: Set the line signals takes them away", row.leftovers), row.leftovers) + : format(STR_P("%zu block signal stands along it, of the automatic block: Set the line signals takes it away", "%zu block signals stand along it, of the automatic block: Set the line signals takes them away", row.leftovers), row.leftovers)).c_str()); ImGui::TextDisabled("%s", STR_C("LMB: fly to the first of them")); } else @@ -1315,42 +1387,162 @@ void srk_tool::render_lines() if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", (false == row.next.empty() ? format(STR_C("To the entry signal %s of the next station"), row.next.c_str()) : row.station ? std::string{STR_C("To the next station, its entry signal not standing yet")} : std::string{STR_C("To the end of the tracks")}).c_str()); ImGui::TableNextColumn(); - auto const automatic_by_itself{row.blocks > 0 || row.number > 0}; - auto const by_itself{row.blocks > 0 ? block_reason::standing : row.number > 0 ? block_reason::double_track : block_reason::single}; ImGui::SetNextItemWidth(-1.0f); - auto const preview{row.why == block_reason::hand ? std::string{kind(row.automatic)} : format(STR_C("%s, auto"), kind(row.automatic))}; + auto const preview{row.legacy ? std::string{STR_C("of the scenery")} : row.why == block_reason::hand ? std::string{kind(row.block)} : format(STR_C("%s, auto"), kind(row.block))}; + ImGui::BeginDisabled(row.legacy); if (ImGui::BeginCombo("##srkblock", preview.c_str())) { - if (ImGui::Selectable(format(STR_C("Auto: %s, %s"), kind(automatic_by_itself), reason(by_itself)).c_str(), row.why != block_reason::hand)) + if (ImGui::Selectable(format(STR_C("Auto: %s, %s"), kind(row.own), reason(row.own_why)).c_str(), row.why != block_reason::hand)) { set(row, std::nullopt); changed = true; } - ImGui::BeginDisabled(row.number == 0); - if (ImGui::Selectable(STR_C("Automatic block (SBL)"), row.why == block_reason::hand && row.automatic)) + for (auto const automatic : {block_kind::three, block_kind::four}) { - set(row, true); - changed = true; + ImGui::BeginDisabled(row.number == 0); + if (ImGui::Selectable(automatic == block_kind::three ? STR_C("Automatic block, 3 aspects (SBL)") : STR_C("Automatic block, 4 aspects (SBL)"), row.why == block_reason::hand && row.block == automatic)) + { + set(row, automatic); + changed = true; + } + ImGui::EndDisabled(); + if (row.number == 0) + item_tooltip("Placed on a double-track line only: on a single track the block signals would face both ways, the editor leaves those standing as they are"); + else + item_tooltip(automatic == block_kind::three ? "A block signal tells of the next one, Ie-1: red, orange for the next at stop, green. The block sections no shorter than the braking distance" : + "A block signal tells of the next two, Ie-1: red, orange for the next at stop, green flashing for the one after it at stop, green. The block sections no shorter than half the braking distance, two of them no shorter than it"); } - ImGui::EndDisabled(); - if (row.number == 0) - item_tooltip("Placed on a double-track line only: on a single track the block signals would face both ways, the editor leaves those standing as they are"); - if (ImGui::Selectable(STR_C("Semi-automatic block"), row.why == block_reason::hand && false == row.automatic)) + if (ImGui::Selectable(STR_C("Semi-automatic block"), row.why == block_reason::hand && row.block == block_kind::semi)) { - set(row, false); + set(row, block_kind::semi); changed = true; } ImGui::EndCombo(); } - item_tooltip("The block of the line, both tracks of a double-track one alike. Automatic (SBL): block signals with block sections, no " - "warning disc. Semi-automatic: a warning disc ToA the braking distance before the entry signal. By itself: the automatic " - "one where block signals stand along the line or it's double-track, else the semi-automatic one"); + ImGui::EndDisabled(); + item_tooltip(row.legacy ? "Block signals stand along the line, none of them switched by a block section of the editor: the line keeps the block of the scenery as it stands" : + "The block of the line, both tracks of a double-track one alike. Automatic (SBL): block signals with block sections, no " + "warning disc, of 3 or 4 aspects, the templates of the block signals standing swapped for those of the aspects. " + "Semi-automatic: a warning disc ToA the braking distance before the entry signal. By itself: the automatic one where " + "block signals stand along the line, of their aspects, or it's double-track, of the aspects of the block signal " + "chosen in the Signals tool, else the semi-automatic one"); ImGui::PopID(); } ImGui::EndTable(); } if (changed) survey_signals(); + ImGui::Checkbox(STR_C("Draw the block boundaries"), &m_drawing); + item_tooltip("LMB on a track of a line of the automatic block, beyond the entry signal: block signals across both tracks there, " + "numbered after the hectometre ahead of them, the tracks cut 15 m beyond them and the new block sections named; LMB on a " + "block boundary standing takes its signals away, the sections either side joined. A line whose block signals stand keeps " + "them: Set the line signals places them evenly only on a line without any. One step of undo each"); + if (m_drawing) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextWrapped("%s", STR_C("LMB on a track of a line beyond the entry signal: a block boundary there; on one standing: taken away")); + ImGui::PopStyleColor(); + } +} + +std::string srk_tool::boundary_hover(glm::dvec3 const &Point, bool &Valid) +{ + m_boundary = {}; + Valid = false; + if (false == drawing()) + return {}; + auto best{-1}; + auto nearest{3.0}; + double at{0.0}; + for (std::size_t index = 0; index < m_lines.size() && index < m_line_ways.size(); ++index) + { + double gap{0.0}; + auto const distance{station::distance_on(m_line_ways[index], Point, gap)}; + if (gap < nearest && distance > 0.0 && distance < m_line_ways[index].length()) + { + nearest = gap; + best = static_cast(index); + at = distance; + } + } + if (best < 0) + return STR("Drawing the block boundaries: LMB on a track of a line out of the station, beyond its entry signal"); + auto const &row{m_lines[best]}; + auto const &line{m_line_ways[best]}; + if (row.legacy) + return format(STR_C("No block section of the editor switches the block signals beyond %s: the line keeps them as they stand"), row.entry.c_str()); + if (row.block == block_kind::semi) + return format(STR_C("The line beyond %s has the semi-automatic block: choose the automatic one for it in the table of the lines"), row.entry.c_str()); + if (row.number == 0) + return format(STR_C("The line beyond %s is single-track: the editor draws the block boundaries of double-track lines only"), row.entry.c_str()); + if (at < kBoundaryEnd || at > row.length - kBoundaryEnd) + return format(STR_C("A block boundary stands %.0f m or more from the entry signals"), kBoundaryEnd); + m_boundary.entry = row.entry; + m_boundary.point = station::point_on(line, at).point; + // the other track across, the block signals standing within reach on both + std::vector> tracks{{static_cast(best), at}}; + if (row.partner >= 0 && row.partner < static_cast(m_line_ways.size())) + if (double gap{0.0}; true) + if (auto const across{station::distance_on(m_line_ways[row.partner], m_boundary.point, gap)}; gap < 12.0) + tracks.emplace_back(static_cast(row.partner), across); + auto const near_stop = [&](line_row const &Row, double const Distance) { + auto const found{std::min_element(Row.stops.begin(), Row.stops.end(), [&](auto const &A, auto const &B) { return std::abs(A.first - Distance) < std::abs(B.first - Distance); })}; + return found != Row.stops.end() && std::abs(found->first - Distance) < kBoundaryReach ? found : Row.stops.end(); + }; + m_boundary.remove = near_stop(row, at) != row.stops.end(); + auto previous{0.0}; + auto next{row.length}; + for (auto const &stop : row.stops) + { + if (m_boundary.remove && std::abs(stop.first - at) < kBoundaryReach) + continue; + if (stop.first < at) + previous = std::max(previous, stop.first); + else + next = std::min(next, stop.first); + } + for (auto const &[index, distance] : tracks) + { + auto const &other{m_lines[index]}; + auto const &way{m_line_ways[index]}; + if (m_boundary.remove) + { + auto const stop{near_stop(other, distance)}; + if (stop == other.stops.end()) + continue; + auto const place{station::point_on(way, stop->first)}; + m_boundary.places.push_back(place.point); + m_boundary.ways.push_back(place.way); + m_boundary.names.push_back(stop->second); + continue; + } + auto const place{station::point_on(way, distance)}; + auto const number{km_line(way)}; + m_boundary.places.push_back(place.point); + m_boundary.ways.push_back(place.way); + m_boundary.names.push_back(station::block_number(km_along(way, number, distance), way.number, km_rising(way, number))); + } + std::string names; + for (auto const &name : m_boundary.names) + names += (names.empty() ? "" : ", ") + name; + m_boundary.valid = true; + Valid = true; + if (m_boundary.remove) + return format(STR_C("Click: take the block signals %s away, the block sections either side joined: %.2f km"), names.c_str(), (next - previous) / 1000.0); + auto const least{row.block == block_kind::four ? row.braking * 0.5 : row.braking}; + auto label{format(STR_C("Click: block signals %s here: the block sections %.2f km and %.2f km"), names.c_str(), (at - previous) / 1000.0, (next - at) / 1000.0)}; + if (std::min(at - previous, next - at) < least) + label += "\n" + format(row.block == block_kind::four ? STR_C("shorter than half the braking distance, %.0f m, of the 4-aspect block") : STR_C("shorter than the braking distance, %.0f m"), least); + return label; +} + +void srk_tool::boundary_click() +{ + if (false == drawing() || false == m_boundary.valid) + return; + block_edit const edit{m_boundary.entry, m_boundary.point, m_boundary.remove}; + m_boundary = {}; + set_lines(&edit); } void srk_tool::render_lengths() @@ -2209,7 +2401,7 @@ void srk_tool::set_signals() if (order.kind != "main") continue; auto const need{need_of(found[next++], true, line, braking_of_open())}; - if (auto const best{best_head(heads, "main", chosen->mount, chosen->name, need)}; best >= 0) + if (auto const best{best_head(heads, "main", chosen->mount, chosen->name, 0, need)}; best >= 0) order.file = heads[best].file; else unfit.push_back(signal_shown(order.name, stem)); @@ -2285,43 +2477,107 @@ bool srk_tool::lines_of_open(std::vector const &Standing, std:: return true; } -bool srk_tool::automatic_block(station::line const &Line, std::string const &Entry, std::vector const &Standing, block_reason &Why, std::size_t &Blocks) const +srk_tool::block_kind srk_tool::block_of(station::line const &Line, std::string const &Entry, std::vector const &Standing, std::vector const &Heads, block_reason &Why, block_kind &Own, block_reason &Own_why, std::size_t &Blocks) const { Blocks = 0; + auto four{false}; for (auto const &signal : Standing) { if (signal.kind != "block") continue; double gap{0.0}; auto const distance{station::distance_on(Line, signal.position, gap)}; - if (gap < 4.5 && distance > 50.0 && distance < Line.length() - 1.0) - ++Blocks; + if (gap > 4.5 || distance < 50.0 || distance > Line.length() - 1.0) + continue; + ++Blocks; + four = four || std::any_of(Heads.begin(), Heads.end(), [&](signal_head const &Head) { return Head.file == signal.file && Head.aspects == 4; }); } + if (Blocks > 0) + { + Own = four ? block_kind::four : block_kind::three; + Own_why = block_reason::standing; + } + else if (Line.number > 0) + { + auto const chosen{std::find_if(Heads.begin(), Heads.end(), [](signal_head const &Head) { return Head.kind == "block" && Head.chosen; })}; + Own = chosen != Heads.end() && chosen->aspects == 4 ? block_kind::four : block_kind::three; + Own_why = block_reason::double_track; + } + else + { + Own = block_kind::semi; + Own_why = block_reason::single; + } + Why = Own_why; if (m_record >= 0) if (auto const set{m_records[m_record].blocks.find(Entry)}; set != m_records[m_record].blocks.end()) { Why = block_reason::hand; return set->second; } - if (Blocks > 0) - { - Why = block_reason::standing; - return true; - } - Why = Line.number > 0 ? block_reason::double_track : block_reason::single; - return Line.number > 0; + return Own; +} + +std::string srk_tool::km_line(station::line const &Line) const +{ + auto const described{line_of_open()}; + auto const out{station::point_on(Line, std::min(2000.0, Line.length() * 0.5)).point}; + double km{0.0}; + if (false == kilometrage::km_at(described, out, km)) + if (auto const found{kilometrage::line_at(out)}; false == found.empty()) + return found; + return described; +} + +double srk_tool::km_along(station::line const &Line, std::string const &Number, double const Distance) const +{ + double km{0.0}; + if (kilometrage::km_at(Number, station::point_on(Line, Distance).point, km) || false == m_layout.has_value()) + return km; + auto const middle{(m_layout->from + m_layout->to) * 0.5}; + auto const chainage{station::project(*m_layout, station::point_on(Line, 0.0).point).x}; + km_of_open(chainage < middle ? chainage - Distance : chainage + Distance, km); + return km; +} + +bool srk_tool::km_rising(station::line const &Line, std::string const &Number) const +{ + auto const at{station::point_on(Line, 100.0)}; + if (auto const growth{kilometrage::growth_at(Number, at.point, {at.way.x, at.way.z})}; growth != 0) + return (growth > 0) != Line.inbound; + return (km_along(Line, Number, 1000.0) > km_along(Line, Number, 0.0)) != Line.inbound; +} + +double srk_tool::line_braking(station::line const &Line) const +{ + if (m_record >= 0 && m_records[m_record].braking > 0) + return static_cast(m_records[m_record].braking); + auto speed{0.0}; + for (std::size_t k = 0; k < Line.points.size() && k < Line.owners.size(); ++k) + if (auto const set{Line.tracks[Line.owners[k]]->VelocityGet()}; Line.distances[k] >= 0.0 && set <= kFastest) + speed = std::max(speed, set); + return braking_distance(speed > 0.0 ? speed : 160.0); } void srk_tool::survey_lines(std::vector const &Standing) { m_lines.clear(); + m_line_ways.clear(); m_lines_problem.clear(); std::vector lines; std::vector entries; std::vector owners; if (false == lines_of_open(Standing, lines, entries, owners, m_lines_problem)) return; + std::vector heads; + m_signals.signal_heads(heads, false); + auto const guards{guarded(m_templates.standing_templates([](std::string const &File) { return File == kControl; }))}; auto const stem{stem_of_open()}; + auto const on_line = [](station::line const &Line, glm::dvec3 const &Point) { + double gap{0.0}; + auto const distance{station::distance_on(Line, Point, gap)}; + return gap < 4.5 && distance > 50.0 && distance < Line.length() - 1.0; + }; for (auto const &line : lines) { line_row row; @@ -2332,28 +2588,48 @@ void srk_tool::survey_lines(std::vector const &Standing) row.station = line.station; row.position = Standing[owners[line.entry]].position; row.travel = travel_of(Standing[owners[line.entry]].yaw); - row.automatic = automatic_block(line, row.entry, Standing, row.why, row.blocks); + row.block = block_of(line, row.entry, Standing, heads, row.why, row.own, row.own_why, row.blocks); + row.braking = line_braking(line); + auto switched{false}; for (auto const &signal : Standing) { - if (signal.kind != (row.automatic ? "warning" : "block")) - continue; double gap{0.0}; auto const distance{station::distance_on(line, signal.position, gap)}; - if (gap > 4.5 || distance < 50.0 || distance > line.length() - 1.0) + if (signal.kind != "block" || gap > 4.5 || distance < 50.0 || distance > line.length() - 1.0) continue; - if (row.leftovers++ == 0) - { - row.leftover = signal.position; - row.leftover_travel = travel_of(signal.yaw); - } + row.stops.emplace_back(distance, signal_shown(signal.name, stem)); + switched = switched || guards.count(ToLower(signal.name)) > 0; } + std::sort(row.stops.begin(), row.stops.end()); + row.legacy = false == row.stops.empty() && false == switched; auto const next{next_entry(line, Standing, row.length)}; row.next = next >= 0 ? signal_shown(Standing[next].name, stem) : std::string{}; m_lines.push_back(std::move(row)); + m_line_ways.push_back(line); + } + // the signals of the other block along a line, but where it shares its track with a line of that block + for (std::size_t index = 0; index < m_lines.size(); ++index) + { + auto &row{m_lines[index]}; + if (row.legacy || (row.number == 0 && row.block != block_kind::semi)) + continue; + auto const automatic{row.block != block_kind::semi}; + for (auto const &signal : Standing) + { + if (signal.kind != (automatic ? "warning" : "block") || false == on_line(m_line_ways[index], signal.position)) + continue; + auto shared{false}; + for (std::size_t other = 0; other < m_lines.size() && false == shared; ++other) + shared = (m_lines[other].block != block_kind::semi) != automatic && on_line(m_line_ways[other], signal.position); + if (shared || row.leftovers++ > 0) + continue; + row.leftover = signal.position; + row.leftover_travel = travel_of(signal.yaw); + } } } -void srk_tool::set_lines() +void srk_tool::set_lines(block_edit const *Edit) { m_status.clear(); auto const stem{stem_of_open()}; @@ -2364,25 +2640,83 @@ void srk_tool::set_lines() std::vector entry_standing; if (false == lines_of_open(standing, lines, entries, entry_standing, m_status)) return; - auto const &layout{*m_layout}; - // of each line, 1: the automatic block, placed on a double-track line only, 2: the automatic block on a single-track line, its - // signals facing both ways on a track left as they stand, 0: the semi-automatic one + std::vector heads; + m_signals.signal_heads(heads, true); + auto const controls{m_templates.standing_templates([](std::string const &File) { return File == kControl; })}; + auto const guards{guarded(controls)}; + auto const facing_out = [&](station::line const &Line, standing_signal const &Signal, double const Distance) { + auto const way{station::point_on(Line, Distance).way}; + return glm::dot(travel_of(Signal.yaw), glm::dvec2{way.x, way.z}); + }; + // of each line, 1: the automatic block, placed on a double-track line only, 2: the automatic block left as it stands, on a + // single-track line its signals facing both ways on a track, or with block signals the block sections of the editor don't switch, + // 0: the semi-automatic one std::vector automatic(lines.size(), 0); + std::vector aspects(lines.size(), 3); + std::vector> along(lines.size()); // the block signals standing along each line facing its trains for (std::size_t index = 0; index < lines.size(); ++index) { + auto const &line{lines[index]}; auto why{block_reason::single}; + auto own{block_kind::semi}; + auto own_why{block_reason::single}; std::size_t blocks{0}; - if (automatic_block(lines[index], entries[lines[index].entry].name, standing, why, blocks)) - automatic[index] = lines[index].number > 0 ? 1 : 2; + auto const kind{block_of(line, entries[line.entry].name, standing, heads, why, own, own_why, blocks)}; + aspects[index] = kind == block_kind::four ? 4 : 3; + auto switched{false}; + for (std::size_t k = 0; k < standing.size(); ++k) + { + if (standing[k].kind != "block") + continue; + double gap{0.0}; + auto const distance{station::distance_on(line, standing[k].position, gap)}; + if (gap > 4.5 || distance < 50.0 || distance > line.length() - 1.0 || (line.inbound ? -1.0 : 1.0) * facing_out(line, standing[k], distance) < 0.87) + continue; + along[index].push_back(k); + switched = switched || guards.count(ToLower(standing[k].name)) > 0; + } + if (kind != block_kind::semi) + automatic[index] = line.number > 0 && (along[index].empty() || switched) ? 1 : 2; + } + // the lines set: all of them, or the one a block boundary is drawn on with the other track of a double-track one + std::vector processed(lines.size(), Edit == nullptr ? 1 : 0); + int edited{-1}; + double drawn{0.0}; + if (Edit != nullptr) + { + for (std::size_t index = 0; index < lines.size(); ++index) + if (entries[lines[index].entry].name == Edit->entry) + edited = static_cast(index); + double gap{0.0}; + if (edited >= 0) + drawn = station::distance_on(lines[edited], Edit->point, gap); + if (edited < 0 || gap > 5.0) + { + m_status = STR_C("The line drawn on isn't there any more"); + return; + } + if (automatic[edited] != 1) + { + m_status = format(STR_C("The line beyond %s has no automatic block the editor sets: choose one for it in the table of the lines"), Edit->entry.c_str()); + return; + } + processed[edited] = 1; + if (auto const partner{lines[edited].partner}; partner >= 0 && automatic[partner] == 1) + processed[partner] = 1; } auto const described{line_of_open()}; double known{0.0}; - if (std::any_of(automatic.begin(), automatic.end(), [](char const Automatic) { return Automatic == 1; }) && (false == km_of_open(0.0, known) || described.empty())) + auto const blocked{[&] { + for (std::size_t index = 0; index < lines.size(); ++index) + if (processed[index] && automatic[index] == 1) + return true; + return false; + }()}; + if (blocked && (false == km_of_open(0.0, known) || described.empty())) { m_status = STR_C("Give the station its line and its km first: the block signals and the block sections are named after them"); return; } - auto const middle{(layout.from + layout.to) * 0.5}; struct planned_signal { station::signal::role kind{station::signal::role::block}; @@ -2392,6 +2726,7 @@ void srk_tool::set_lines() double distance{0.0}; // out along the line std::string linked{"none"}; int match{-1}; // of the standing signals + bool kept{false}; // standing as it is, its name and its plate kept }; struct far_end { @@ -2400,85 +2735,98 @@ void srk_tool::set_lines() }; std::vector planned; std::vector ends(lines.size()); - auto const facing_out = [&](station::line const &Line, standing_signal const &Signal, double const Distance) { - auto const way{station::point_on(Line, Distance).way}; - return glm::dot(travel_of(Signal.yaw), glm::dvec2{way.x, way.z}); - }; // the lines of the kilometrage the lines out run along, at a junction another one than the line of the station - std::vector numbers(lines.size(), described); - for (std::size_t index = 0; index < lines.size(); ++index) - { - auto const out{station::point_on(lines[index], std::min(2000.0, lines[index].length() * 0.5)).point}; - double km{0.0}; - if (false == kilometrage::km_at(described, out, km)) - if (auto const found{kilometrage::line_at(out)}; false == found.empty()) - numbers[index] = found; - } - auto const km_on = [&](std::size_t const Index, double const Distance) { - double km{0.0}; - if (kilometrage::km_at(numbers[Index], station::point_on(lines[Index], Distance).point, km)) - return km; - auto const chainage{station::project(layout, station::point_on(lines[Index], 0.0).point).x}; - km_of_open(chainage < middle ? chainage - Distance : chainage + Distance, km); - return km; - }; - // the trains of the line run towards the higher km - auto const rising = [&](std::size_t const Index) { - auto const at{station::point_on(lines[Index], 100.0)}; - if (auto const growth{kilometrage::growth_at(numbers[Index], at.point, {at.way.x, at.way.z})}; growth != 0) - return (growth > 0) != lines[Index].inbound; - return (km_on(Index, 1000.0) > km_on(Index, 0.0)) != lines[Index].inbound; - }; - // the braking distance at the highest speed out along the line, at 160 km/h while its tracks have none, unless one is set by hand - auto const braking = [&](std::size_t const Index) { - if (m_record >= 0 && m_records[m_record].braking > 0) - return static_cast(m_records[m_record].braking); - auto speed{0.0}; - auto const &line{lines[Index]}; - for (std::size_t k = 0; k < line.points.size(); ++k) - if (auto const set{line.tracks[line.owners[k]]->VelocityGet()}; line.distances[k] >= 0.0 && set <= kFastest) - speed = std::max(speed, set); - return braking_distance(speed > 0.0 ? speed : 160.0); - }; + std::vector numbers; + for (auto const &line : lines) + numbers.push_back(km_line(line)); + auto const km_on = [&](std::size_t const Index, double const Distance) { return km_along(lines[Index], numbers[Index], Distance); }; + auto const rising = [&](std::size_t const Index) { return km_rising(lines[Index], numbers[Index]); }; + auto const braking = [&](std::size_t const Index) { return line_braking(lines[Index]); }; for (std::size_t index = 0; index < lines.size(); ++index) ends[index].signal = next_entry(lines[index], standing, ends[index].distance); + // the standing signals taken away: of a boundary drawn away, of the other block left along a line + std::vector gone(standing.size(), 0); for (std::size_t index = 0; index < lines.size(); ++index) { + if (false == processed[index]) + continue; auto const &line{lines[index]}; auto const &end{ends[index]}; auto const &entry{entries[line.entry]}; - auto const least{braking(index)}; + auto const least{braking(index) * (aspects[index] == 4 ? 0.5 : 1.0)}; if (automatic[index] == 1) { - // the block signals, Ie-4 § 51: the sections no shorter than the braking distance and no longer than twice it, the line to the - // next station parted evenly, towards the end of the tracks a little over the braking distance each - std::vector places; - if (line.station || end.signal >= 0) + // the boundary drawn, on the line drawn on and across on its other track + std::optional boundary; + if (static_cast(index) == edited) + boundary = drawn; + else if (edited >= 0) + if (double gap{0.0}; true) + if (auto const across{station::distance_on(line, station::point_on(lines[edited], drawn).point, gap)}; gap < 12.0) + boundary = across; + if (false == along[index].empty() || boundary.has_value()) { - // as many on both tracks of the line - auto const reach{line.partner >= 0 ? (end.distance + ends[line.partner].distance) * 0.5 : end.distance}; - auto const sections{std::max(1.0, std::floor(reach / (line.partner >= 0 ? std::max(least, braking(line.partner)) : least)))}; - for (auto k{1.0}; k < sections; k += 1.0) - places.push_back(end.distance * k / sections); + // the block signals standing kept as they stand, the boundary drawn added or the ones standing at it taken away + for (auto const k : along[index]) + { + double gap{0.0}; + auto const distance{station::distance_on(line, standing[k].position, gap)}; + if (boundary.has_value() && Edit->remove && std::abs(distance - *boundary) < kBoundaryReach) + { + gone[k] = 1; + continue; + } + planned_signal signal; + signal.name = standing[k].name; + signal.line = index; + signal.distance = distance; + signal.match = static_cast(k); + signal.kept = true; + planned.push_back(std::move(signal)); + } + if (boundary.has_value() && false == Edit->remove) + { + planned_signal signal; + signal.name = station::block_number(km_on(index, *boundary), line.number, rising(index)); + signal.plate = signal.name; + signal.line = index; + signal.distance = *boundary; + planned.push_back(std::move(signal)); + } } else - for (auto distance{least * kBlockShare}; distance <= end.distance - 100.0; distance += least * kBlockShare) - places.push_back(distance); - for (auto const distance : places) { - planned_signal signal; - signal.kind = station::signal::role::block; - signal.name = station::block_number(km_on(index, distance), line.number, rising(index)); - signal.plate = signal.name; - signal.line = index; - signal.distance = distance; - planned.push_back(std::move(signal)); + // the block signals, Ie-4 § 51: the sections no shorter than the braking distance, half of it with four aspects, and no + // longer than twice it, the line to the next station parted evenly, towards the end of the tracks a little over the + // least each + std::vector places; + if (line.station || end.signal >= 0) + { + // as many on both tracks of the line + auto const reach{line.partner >= 0 ? (end.distance + ends[line.partner].distance) * 0.5 : end.distance}; + auto const other{line.partner >= 0 ? braking(line.partner) * (aspects[line.partner] == 4 ? 0.5 : 1.0) : least}; + auto const sections{std::max(1.0, std::floor(reach / std::max(least, other)))}; + for (auto k{1.0}; k < sections; k += 1.0) + places.push_back(end.distance * k / sections); + } + else + for (auto distance{least * kBlockShare}; distance <= end.distance - 100.0; distance += least * kBlockShare) + places.push_back(distance); + for (auto const distance : places) + { + planned_signal signal; + signal.name = station::block_number(km_on(index, distance), line.number, rising(index)); + signal.plate = signal.name; + signal.line = index; + signal.distance = distance; + planned.push_back(std::move(signal)); + } } } else if (automatic[index] == 0) { // the warning disc of the entry signal, the braking distance before it - auto const distance{std::min(least, end.distance - 100.0)}; + auto const distance{std::min(braking(index), end.distance - 100.0)}; if (distance >= 400.0) { planned_signal signal; @@ -2490,19 +2838,46 @@ void srk_tool::set_lines() planned.push_back(std::move(signal)); } } + // the signals of the other block left along the line: the warning discs of a line of the automatic block, the block signals of + // one of the semi-automatic one + if (Edit != nullptr || automatic[index] == 2) + continue; + for (std::size_t k = 0; k < standing.size(); ++k) + { + if (automatic[index] == 1 ? standing[k].kind != "warning" : std::find(along[index].begin(), along[index].end(), k) == along[index].end()) + continue; + // not where the line shares its track with one of the other block + auto shared{false}; + for (std::size_t other = 0; other < lines.size() && false == shared; ++other) + { + if ((automatic[other] == 0) == (automatic[index] == 0)) + continue; + double gap{0.0}; + auto const distance{station::distance_on(lines[other], standing[k].position, gap)}; + shared = gap < 4.5 && distance > 50.0 && distance < lines[other].length() - 1.0; + } + double gap{0.0}; + auto const distance{station::distance_on(line, standing[k].position, gap)}; + if (false == shared && gap < 4.5 && distance > 50.0 && distance < line.length() - 1.0) + gone[k] = 1; + } } // the standing ones by their places, then by their names - std::vector used(standing.size(), 0); + std::vector used(gone); + for (auto const &signal : planned) + if (signal.match >= 0) + used[signal.match] = 1; for (auto const index : entry_standing) used[index] = 1; for (auto const &end : ends) if (end.signal >= 0) used[end.signal] = 1; std::vector> candidates; - std::vector standing_at(standing.size(), 0.0); for (std::size_t i = 0; i < planned.size(); ++i) { auto const &signal{planned[i]}; + if (signal.match >= 0) + continue; auto const &line{lines[signal.line]}; auto const kind{signal.kind == station::signal::role::block ? "block" : "warning"}; auto const reach{signal.kind == station::signal::role::block ? 300.0 : 400.0}; @@ -2538,9 +2913,12 @@ void srk_tool::set_lines() for (std::size_t k = 0; k < standing.size(); ++k) if (0 == used[k]) taken.insert(ToLower(standing[k].name)); + for (auto const &signal : planned) + if (signal.kept) + taken.insert(ToLower(signal.name)); for (auto &signal : planned) { - if (signal.kind != station::signal::role::block) + if (signal.kind != station::signal::role::block || signal.kept) continue; auto const base{signal.name}; for (char letter = 'a'; taken.count(ToLower(signal.name)) > 0 && letter <= 'z'; ++letter) @@ -2563,7 +2941,7 @@ void srk_tool::set_lines() for (std::size_t index = 0; index < lines.size(); ++index) { auto const &line{lines[index]}; - if (automatic[index] != 1) + if (automatic[index] != 1 || false == processed[index]) continue; struct stop { @@ -2689,6 +3067,8 @@ void srk_tool::set_lines() // the sections named, each track by its middle for (std::size_t index = 0; index < lines.size(); ++index) { + if (false == processed[index]) + continue; std::vector parts; for (auto *track : lines[index].tracks) { @@ -2716,6 +3096,8 @@ void srk_tool::set_lines() signal_changes changes; std::size_t blocks{0}; std::size_t discs{0}; + std::size_t swapped{0}; + std::size_t renamed{0}; for (auto const &signal : planned) { auto const block{signal.kind == station::signal::role::block}; @@ -2725,6 +3107,16 @@ void srk_tool::set_lines() auto const relink{block && false == found.linked.empty() && ToLower(found.linked) != ToLower(signal.linked)}; if (ToLower(found.name) != ToLower(signal.name) || relink) changes.renamed.push_back({found.instance, signal.name, signal.plate, relink ? signal.linked : std::string{}}); + if (ToLower(found.name) != ToLower(signal.name)) + ++renamed; + // of the aspects of its line + auto const head{std::find_if(heads.begin(), heads.end(), [&](signal_head const &Head) { return Head.file == found.file; })}; + if (block && head != heads.end() && head->aspects != 0 && head->aspects != aspects[signal.line]) + if (auto const other{block_template(heads, found.file, aspects[signal.line])}; false == other.empty()) + { + changes.files.emplace_back(found.instance, other); + ++swapped; + } continue; } auto const &line{lines[signal.line]}; @@ -2737,6 +3129,7 @@ void srk_tool::set_lines() order.facing = inbound == approach.outward ? -1 : 1; order.side = order.facing; order.kind = block ? "block" : "warning"; + order.file = block ? block_template(heads, {}, aspects[signal.line]) : std::string{}; order.name = signal.name; order.plate = signal.plate; order.linked = signal.linked; @@ -2745,19 +3138,22 @@ void srk_tool::set_lines() } for (auto const &[index, linked] : relinked) changes.renamed.push_back({standing[index].instance, standing[index].name, std::string{}, linked}); + for (std::size_t k = 0; k < standing.size(); ++k) + if (gone[k]) + changes.removed.push_back(standing[k].instance); + auto const removed{changes.removed.size()}; // the block signals switched by the sections they guard include_info control; std::string missing; auto const controlled{editor_includes::load(kControl, control, missing)}; if (controlled) { - auto const standing_controls{m_templates.standing_templates([](std::string const &File) { return File == kControl; })}; for (auto const &found : sections) { if (found.guard.empty()) continue; - auto const already{std::find_if(standing_controls.begin(), standing_controls.end(), [&](standing_template const &Control) { return false == Control.values.empty() && ToLower(Control.values[0]) == found.name; })}; - if (already != standing_controls.end()) + auto const already{std::find_if(controls.begin(), controls.end(), [&](standing_template const &Control) { return false == Control.values.empty() && ToLower(Control.values[0]) == found.name; })}; + if (already != controls.end()) { if (already->values.size() > 5 && ToLower(already->values[5]) != ToLower(found.guard)) { @@ -2775,14 +3171,32 @@ void srk_tool::set_lines() item.values["#6"] = found.guard; changes.others.push_back(std::move(item)); } + // those of the sections gone from the lines set taken away with them + std::unordered_set named; + for (auto const &found : sections) + named.insert(found.name); + for (auto const &standing_control : controls) + { + if (standing_control.values.empty() || named.count(ToLower(standing_control.values[0])) > 0) + continue; + for (std::size_t index = 0; index < lines.size(); ++index) + { + double gap{0.0}; + auto const distance{station::distance_on(lines[index], standing_control.location, gap)}; + if (processed[index] && automatic[index] != 2 && gap < 4.5 && distance > 0.0 && distance < lines[index].length()) + { + changes.removed.push_back(standing_control.instance); + break; + } + } + } } - if (changes.placed.empty() && changes.renamed.empty() && changes.others.empty() && changes.values.empty() && before.empty() && created.empty()) + if (changes.placed.empty() && changes.renamed.empty() && changes.others.empty() && changes.values.empty() && changes.files.empty() && changes.removed.empty() && before.empty() && created.empty()) { m_status = STR_C("The lines are set already"); return; } std::string error; - auto const renamed{changes.renamed.size()}; m_signals.signal_batch(changes, std::move(before), std::move(created), error); if (false == error.empty()) { @@ -2791,6 +3205,11 @@ void srk_tool::set_lines() } m_status = format(STR_P("%zu block signal placed", "%zu block signals placed", blocks), blocks) + ", " + format(STR_P("%zu warning disc", "%zu warning discs", discs), discs) + ", " + format(STR_P("%zu renamed", "%zu renamed", renamed), renamed) + ", " + format(STR_P("%zu block section", "%zu block sections", sections.size()), sections.size()); + if (removed > 0) + m_status += ", " + format(STR_P("%zu signal taken away", "%zu signals taken away", removed), removed); + if (swapped > 0) + m_status += ", " + format(STR_P("%zu given the template of the aspects of its line", "%zu given the templates of the aspects of their lines", swapped), swapped); + m_status += ". " + STR("Ctrl+Z takes it back"); if (false == controlled && std::any_of(sections.begin(), sections.end(), [](section const &Found) { return false == Found.guard.empty(); })) m_status += "\n" + format(STR_C("%s isn't in the scenery folder: the block signals won't change their lights"), kControl); } @@ -3019,13 +3438,20 @@ void srk_tool::check_heads(std::vector const &Standing) for (std::size_t k = 0; k < check.routes.size(); ++k) { auto const &route{check.routes[k]}; - if (route.next >= 0 && route.warning < 0 && route.length < braking - 1.0) + if (route.next < 0 || route.warning >= 0) + continue; + if (template_of != nullptr && template_of->aspects == 4) + { + if (route.length < braking * 0.5 - 1.0) + check.problems.push_back(format(STR_C("The block section to %s is %.0f m long, shorter than half the braking distance of %.0f m, of the 4-aspect block"), check.nexts[k].c_str(), route.length, braking)); + } + else if (route.length < braking - 1.0) check.problems.push_back(format(STR_C("The block section to %s is %.0f m long, shorter than the braking distance of %.0f m, Ie-4 § 8 ust. 1"), check.nexts[k].c_str(), route.length, braking)); } if (template_of != nullptr) { check.fits = shows(*template_of, need); - auto const best{best_head(heads, signal.kind, template_of->mount, template_of->name, need)}; + auto const best{best_head(heads, signal.kind, template_of->mount, template_of->name, template_of->aspects, need)}; if (best >= 0) { check.fit = heads[best].file; @@ -3919,7 +4345,9 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri } if (Command == "srkblock") { - // srkblock entry= kind=sbl|semi|auto: the block of the line out beyond the entry signal, the lines listed + // srkblock entry= kind=sbl|sbl4|semi|auto: the block of the line out beyond the entry signal, the lines listed + if (m_history.history_revision() != m_history_revision) + refresh(); if (false == m_layout.has_value()) { WriteLog("SELFTEST srkblock: no station open"); @@ -3933,14 +4361,49 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri if (kind->second == "auto") record_of_open().blocks.erase(entry->second); else - record_of_open().blocks[entry->second] = kind->second == "sbl"; + record_of_open().blocks[entry->second] = kind->second == "sbl4" ? block_kind::four : kind->second == "sbl" ? block_kind::three : block_kind::semi; m_changed = true; } survey_signals(); if (false == m_lines_problem.empty()) WriteLog("SELFTEST srkblock: " + m_lines_problem); for (auto const &row : m_lines) - WriteLog(format("SELFTEST srkblock %s track %d %s to %s %.0f m blocks %zu: %s why %d leftovers %zu", row.entry.c_str(), row.number, row.inbound ? "in" : "out", row.next.empty() ? "-" : row.next.c_str(), row.length, row.blocks, row.automatic ? "sbl" : "semi", static_cast(row.why), row.leftovers)); + WriteLog(format("SELFTEST srkblock %s track %d %s to %s %.0f m blocks %zu: %s why %d own %d leftovers %zu legacy %d stops%s", row.entry.c_str(), row.number, row.inbound ? "in" : "out", row.next.empty() ? "-" : row.next.c_str(), row.length, row.blocks, + row.block == block_kind::four ? "sbl4" : row.block == block_kind::three ? "sbl" : "semi", static_cast(row.why), static_cast(row.own), row.leftovers, row.legacy ? 1 : 0, [&] { + std::string text; + for (auto const &[distance, name] : row.stops) + text += format(" %s@%.0f", name.c_str(), distance); + return text; + }().c_str())); + return editor_selftest_result::handled; + } + if (Command == "srkboundary") + { + // srkboundary entry= at= [nudge=]: a click while drawing the block boundaries, on the line beyond the entry signal at the + // distance out, nudge metres aside + if (m_history.history_revision() != m_history_revision) + refresh(); + auto const values{selftest_values(Words)}; + auto const entry{values.count("entry") > 0 ? values.at("entry") : std::string{}}; + auto const at{values.count("at") > 0 ? std::atof(values.at("at").c_str()) : 0.0}; + auto const nudge{values.count("nudge") > 0 ? std::atof(values.at("nudge").c_str()) : 0.0}; + auto const found{std::find_if(m_lines.begin(), m_lines.end(), [&](line_row const &Row) { return Row.entry == entry; })}; + if (false == m_layout.has_value() || found == m_lines.end()) + { + WriteLog("SELFTEST srkboundary: no line beyond " + entry); + return editor_selftest_result::handled; + } + auto const place{station::point_on(m_line_ways[found - m_lines.begin()], at)}; + m_drawing = true; + bool valid{false}; + auto const label{boundary_hover(place.point + glm::dvec3{-place.way.z, 0.0, place.way.x} * nudge, valid)}; + WriteLog(format("SELFTEST srkboundary %s at %.0f: %s%s", entry.c_str(), at, valid ? "" : "(refused) ", label.c_str())); + if (valid) + { + boundary_click(); + WriteLog("SELFTEST srkboundary: " + m_status); + } + m_drawing = false; return editor_selftest_result::handled; } if (Command == "srkstrip") diff --git a/application/editormodesrk.h b/application/editormodesrk.h index fae49384..73810cf6 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -37,6 +37,12 @@ class srk_tool final : public editor_tool void forget(scene::basic_node const *Node) override; editor_selftest_result selftest(std::string const &Command, std::istringstream &Words) override; void set_open(bool const Open) { m_open = Open; } + // the block boundaries drawn by hand on the lines out of the open station + bool drawing() const { return m_open && m_drawing && m_layout.has_value(); } + // what a click at the point of a track does while the block boundaries are drawn, for the label at the mouse; Valid: it does + // something + std::string boundary_hover(glm::dvec3 const &Point, bool &Valid); + void boundary_click(); // the stations described go to the files of their layers, as "//$s" lines void store(); @@ -46,6 +52,13 @@ class srk_tool final : public editor_tool editor_history &m_history; editor_templates &m_templates; editor_signals &m_signals; + // the block of a line out of the station + enum class block_kind + { + semi, // semi-automatic: a warning disc before the entry signal + three, // the automatic one of three aspects: the block sections no shorter than the braking distance + four // of four aspects, the warning two signals ahead: the sections no shorter than half of it + }; // where the block of a line out of the station comes from enum class block_reason { @@ -66,7 +79,7 @@ class srk_tool final : public editor_tool std::string km; // of the axis of the station along the line, empty while unknown bool reversed{false}; // the km rise to the left of the schematic, the switches numbered from there int braking{0}; // m, the braking distance of the line set by hand, 0: after its speed - std::map blocks; // of the lines by their entry signals: the automatic block on it, as set by hand + std::map blocks; // of the lines by their entry signals, as set by hand }; // the schematic: the chainage across, the offsets upwards, exaggerated struct view @@ -180,8 +193,14 @@ class srk_tool final : public editor_tool std::string next; // the entry signal of the next station facing out along it, shown; empty for none bool station{false}; // ends at the next station rather than at the end of the tracks std::size_t blocks{0}; // the block signals standing along it - bool automatic{false}; // of the automatic block + std::vector> stops; // the same, out along it, shown + block_kind block{block_kind::semi}; block_reason why{block_reason::single}; // the block it has, so + block_kind own{block_kind::semi}; // the block it has by itself, unless set by hand + block_reason own_why{block_reason::single}; + // block signals stand along it, none switched by block sections of the editor: it keeps the block of the scenery as it stands + bool legacy{false}; + double braking{0.0}; // m, the braking distance on it // the signals of the other block standing along it: warning discs on a line of the automatic block, block signals on one of the // semi-automatic one, the first of them std::size_t leftovers{0}; @@ -189,7 +208,28 @@ class srk_tool final : public editor_tool glm::dvec2 leftover_travel{0.0, 1.0}; }; std::vector m_lines; + std::vector m_line_ways; // of the lines listed std::string m_lines_problem; // why the station has no lines listed + bool m_drawing{false}; // the block boundaries drawn by hand + // a block boundary under the mouse while they are drawn: block signals across the line there, or the ones standing taken away + struct boundary + { + bool valid{false}; + bool remove{false}; + std::string entry; // of the line under the mouse, its entry signal shown + glm::dvec3 point{0.0}; // on its axis + std::vector places; // of the signals, on the axes of the tracks + std::vector ways; // the trains run there + std::vector names; + }; + boundary m_boundary; + // a block boundary drawn on a line of the automatic block of the open station + struct block_edit + { + std::string entry; // of the line, its entry signal shown + glm::dvec3 point{0.0}; // on its axis + bool remove{false}; // the block signals standing there taken away, else placed + }; std::string m_status; void load(); @@ -233,13 +273,22 @@ class srk_tool final : public editor_tool int length_of(TTrack const *Track) const; // the signals of the lines out of the station: on a double-track line the block signals to the next station and the block // sections between them, on a single-track one a warning disc before the entry signal; one step of the history - void set_lines(); + void set_lines(block_edit const *Edit = nullptr); // the lines out of the open station from its entry signals standing, Entries: the signals of the plan they begin at, Owners: // the standing ones; false with the reason while it has none bool lines_of_open(std::vector const &Standing, std::vector &Lines, std::vector &Entries, std::vector &Owners, std::string &Problem) const; - // the line has the automatic block: as set by hand for its entry signal, else where block signals stand along it, else on a - // double-track line - bool automatic_block(station::line const &Line, std::string const &Entry, std::vector const &Standing, block_reason &Why, std::size_t &Blocks) const; + // the block of the line: as set by hand for its entry signal, else the automatic one where block signals stand along it, of four + // aspects where one of them has, else on a double-track line, of the aspects of the block signal chosen in the Signals tool, else + // the semi-automatic one; Own: the block it has by itself + block_kind block_of(station::line const &Line, std::string const &Entry, std::vector const &Standing, std::vector const &Heads, block_reason &Why, block_kind &Own, block_reason &Own_why, std::size_t &Blocks) const; + // the line of the kilometrage a line out of the open station runs along: of the station, at a junction another one + std::string km_line(station::line const &Line) const; + // the km out along the line, Number: of the line of its kilometrage + double km_along(station::line const &Line, std::string const &Number, double const Distance) const; + // the trains of the line run towards the higher km + bool km_rising(station::line const &Line, std::string const &Number) const; + // m, the braking distance at the highest speed out along the line, at 160 km/h while its tracks have none, unless set by hand + double line_braking(station::line const &Line) const; // the lines out of the open station, for the panel void survey_lines(std::vector const &Standing); void render_lines(); diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 3850906e..9b349d59 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -458,6 +458,16 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) } return intent; } + if (m_track_tab == track_tab::srk && m_srk.drawing()) + { + bool valid{false}; + intent.track = m_hover.track; + intent.path = m_hover.path; + intent.position = m_hover.track != nullptr ? m_hover.point : ground; + intent.action = m_srk.boundary_hover(intent.position, valid); + intent.what = kind::srk_boundary; + return intent; + } if (m_track_tab == track_tab::srk && m_hover.track != nullptr) { intent.track = m_hover.track; @@ -810,6 +820,7 @@ void editor_mode::track_press(track_intent const &Intent) if (Intent.track != nullptr) m_km.pick(*Intent.track, Intent.position); return; + case kind::srk_boundary: m_srk.boundary_click(); return; case kind::fouling: if (Intent.path == 1) m_fouling.fix(Intent.index); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 37919193..6b5d7ab3 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11110,9 +11110,6 @@ msgstr "linia dwutorowa" msgid "a single-track line" msgstr "linia jednotorowa" -msgid "automatic (SBL)" -msgstr "samoczynna (SBL)" - msgid "semi-automatic" msgstr "półsamoczynna" @@ -11176,18 +11173,12 @@ msgstr "%s, auto" msgid "Auto: %s, %s" msgstr "Auto: %s, %s" -msgid "Automatic block (SBL)" -msgstr "Blokada samoczynna (SBL)" - msgid "Placed on a double-track line only: on a single track the block signals would face both ways, the editor leaves those standing as they are" msgstr "Edytor stawia ją tylko na linii dwutorowej: na jednym torze semafory SBL musiałyby stać w obu kierunkach, takie, które już stoją, zostawia bez zmian" msgid "Semi-automatic block" msgstr "Blokada półsamoczynna" -msgid "The block of the line, both tracks of a double-track one alike. Automatic (SBL): block signals with block sections, no warning disc. Semi-automatic: a warning disc ToA the braking distance before the entry signal. By itself: the automatic one where block signals stand along the line or it's double-track, else the semi-automatic one" -msgstr "Blokada liniowa szlaku, na dwutorowym taka sama na obu torach. Samoczynna (SBL): semafory SBL z odstępami blokowymi, bez tarczy ostrzegawczej. Półsamoczynna: tarcza ostrzegawcza ToA w odległości drogi hamowania przed semaforem wjazdowym. Auto: samoczynna, gdy na szlaku stoją semafory SBL albo linia jest dwutorowa, inaczej półsamoczynna" - msgid "its SHP resonator belongs %.0f m before it" msgstr "rezonator SHP powinien stać %.0f m przed nim" @@ -11251,17 +11242,107 @@ msgstr[0] "%zu ma tory kończące się przed miejscem rezonatora" msgstr[1] "%zu mają tory kończące się przed miejscem rezonatora" msgstr[2] "%zu ma tory kończące się przed miejscem rezonatora" -msgid "%zu warning disc stands along it, of the semi-automatic block: delete it in the Signals tool" -msgid_plural "%zu warning discs stand along it, of the semi-automatic block: delete them in the Signals tool" -msgstr[0] "Stoi na nim %zu tarcza ostrzegawcza, z blokady półsamoczynnej: usuń ją w narzędziu Sygnalizatory (H)" -msgstr[1] "Stoją na nim %zu tarcze ostrzegawcze, z blokady półsamoczynnej: usuń je w narzędziu Sygnalizatory (H)" -msgstr[2] "Stoi na nim %zu tarcz ostrzegawczych, z blokady półsamoczynnej: usuń je w narzędziu Sygnalizatory (H)" - -msgid "%zu block signal stands along it, of the automatic block: delete it in the Signals tool" -msgid_plural "%zu block signals stand along it, of the automatic block: delete them in the Signals tool" -msgstr[0] "Stoi na nim %zu semafor SBL, z blokady samoczynnej: usuń go w narzędziu Sygnalizatory (H)" -msgstr[1] "Stoją na nim %zu semafory SBL, z blokady samoczynnej: usuń je w narzędziu Sygnalizatory (H)" -msgstr[2] "Stoi na nim %zu semaforów SBL, z blokady samoczynnej: usuń je w narzędziu Sygnalizatory (H)" - msgid "LMB: fly to the first of them" msgstr "LPM: leć do pierwszego z nich" + +msgid "SBL, 3 aspects" +msgstr "SBL trzystawna" + +msgid "SBL, 4 aspects" +msgstr "SBL czterostawna" + +msgid "Automatic block, 3 aspects (SBL)" +msgstr "Samoczynna blokada liniowa trzystawna (SBL)" + +msgid "Automatic block, 4 aspects (SBL)" +msgstr "Samoczynna blokada liniowa czterostawna (SBL)" + +msgid "A block signal tells of the next one, Ie-1: red, orange for the next at stop, green. The block sections no shorter than the braking distance" +msgstr "Semafor SBL zapowiada następny, Ie-1: czerwone, pomarańczowe gdy następny wskazuje Stój, zielone. Odstępy nie krótsze niż droga hamowania" + +msgid "A block signal tells of the next two, Ie-1: red, orange for the next at stop, green flashing for the one after it at stop, green. The block sections no shorter than half the braking distance, two of them no shorter than it" +msgstr "Semafor SBL zapowiada dwa następne, Ie-1: czerwone, pomarańczowe gdy następny wskazuje Stój, zielone migające gdy Stój wskazuje ten za nim, zielone. Odstępy nie krótsze niż połowa drogi hamowania, dwa razem nie krótsze niż cała" + +msgid "No block section of the editor switches its block signals: it keeps the block of the scenery as it stands" +msgstr "Jego semaforów SBL nie przełącza żaden odstęp edytora: zostaje blokada scenerii, tak jak stoi" + +msgid "of the scenery" +msgstr "ze scenerii" + +msgid "Block signals stand along the line, none of them switched by a block section of the editor: the line keeps the block of the scenery as it stands" +msgstr "Na szlaku stoją semafory SBL, ale żadnego nie przełącza odstęp edytora: szlak zostaje z blokadą scenerii, tak jak stoi" + +msgid "The block of the line, both tracks of a double-track one alike. Automatic (SBL): block signals with block sections, no warning disc, of 3 or 4 aspects, the templates of the block signals standing swapped for those of the aspects. Semi-automatic: a warning disc ToA the braking distance before the entry signal. By itself: the automatic one where block signals stand along the line, of their aspects, or it's double-track, of the aspects of the block signal chosen in the Signals tool, else the semi-automatic one" +msgstr "Blokada szlaku, na obu torach dwutorowego taka sama. Samoczynna (SBL): semafory SBL z odstępami, bez tarczy ostrzegawczej, trzy- albo czterostawna; szablony stojących semaforów SBL zostają podmienione na te o właściwej liczbie stawności. Półsamoczynna: tarcza ostrzegawcza ToA w drodze hamowania przed semaforem wjazdowym. Auto: samoczynna, gdy na szlaku stoją semafory SBL (stawność jak u nich) albo szlak jest dwutorowy (stawność jak u semafora SBL wybranego w narzędziu Sygnalizatory), inaczej półsamoczynna" + +msgid "Draw the block boundaries" +msgstr "Rysuj granice odstępów" + +msgid "LMB on a track of a line of the automatic block, beyond the entry signal: block signals across both tracks there, numbered after the hectometre ahead of them, the tracks cut 15 m beyond them and the new block sections named; LMB on a block boundary standing takes its signals away, the sections either side joined. A line whose block signals stand keeps them: Set the line signals places them evenly only on a line without any. One step of undo each" +msgstr "LPM na torze szlaku z SBL, za semaforem wjazdowym: semafory SBL na obu torach w tym miejscu, numerowane po hektometrze przed nimi, tory przecięte 15 m za nimi i nazwane nowe odstępy. LPM na istniejącej granicy zabiera jej semafory, a odstępy po obu stronach łączą się w jeden. Szlak, na którym stoją już semafory SBL, je zachowuje: Ustaw sygnalizatory szlakowe rozstawia je równo tylko na szlaku bez żadnego. Każde kliknięcie to jeden krok cofania" + +msgid "LMB on a track of a line beyond the entry signal: a block boundary there; on one standing: taken away" +msgstr "LPM na torze szlaku za semaforem wjazdowym: granica odstępu w tym miejscu; na istniejącej granicy: usunięcie" + +msgid "No block section of the editor switches the block signals beyond %s: the line keeps them as they stand" +msgstr "Semaforów SBL za %s nie przełącza żaden odstęp edytora: szlak zostaje, tak jak stoi" + +msgid "The line beyond %s has the semi-automatic block: choose the automatic one for it in the table of the lines" +msgstr "Szlak za %s ma blokadę półsamoczynną: wybierz dla niego SBL w tabeli szlaków" + +msgid "The line beyond %s is single-track: the editor draws the block boundaries of double-track lines only" +msgstr "Szlak za %s jest jednotorowy: edytor rysuje granice odstępów tylko na szlakach dwutorowych" + +msgid "A block boundary stands %.0f m or more from the entry signals" +msgstr "Granica odstępu musi stać co najmniej %.0f m od semaforów wjazdowych" + +msgid "Click: take the block signals %s away, the block sections either side joined: %.2f km" +msgstr "Klik: usuń semafory SBL %s, odstępy po obu stronach połączone: %.2f km" + +msgid "Click: block signals %s here: the block sections %.2f km and %.2f km" +msgstr "Klik: semafory SBL %s tutaj, odstępy %.2f km i %.2f km" + +msgid "shorter than half the braking distance, %.0f m, of the 4-aspect block" +msgstr "krótszy niż połowa drogi hamowania (%.0f m), a to SBL czterostawna" + +msgid "shorter than the braking distance, %.0f m" +msgstr "krótszy niż droga hamowania, %.0f m" + +msgid "The line drawn on isn't there any more" +msgstr "Szlaku, na którym rysujesz, już nie ma" + +msgid "The line beyond %s has no automatic block the editor sets: choose one for it in the table of the lines" +msgstr "Szlak za %s nie ma SBL, którą ustawia edytor: wybierz ją w tabeli szlaków" + +msgid "The block section to %s is %.0f m long, shorter than half the braking distance of %.0f m, of the 4-aspect block" +msgstr "Odstęp do %s ma %.0f m, mniej niż połowa drogi hamowania %.0f m, a to SBL czterostawna" + +msgid "Drawing the block boundaries: LMB on a track of a line out of the station, beyond its entry signal" +msgstr "Rysowanie granic odstępów: LPM na torze szlaku wychodzącego ze stacji, za semaforem wjazdowym" + +msgid "%zu warning disc stands along it, of the semi-automatic block: Set the line signals takes it away" +msgid_plural "%zu warning discs stand along it, of the semi-automatic block: Set the line signals takes them away" +msgstr[0] "Stoi na nim %zu tarcza ostrzegawcza z blokady półsamoczynnej: Ustaw sygnalizatory szlakowe ją usunie" +msgstr[1] "Stoją na nim %zu tarcze ostrzegawcze z blokady półsamoczynnej: Ustaw sygnalizatory szlakowe je usunie" +msgstr[2] "Stoi na nim %zu tarcz ostrzegawczych z blokady półsamoczynnej: Ustaw sygnalizatory szlakowe je usunie" + +msgid "%zu block signal stands along it, of the automatic block: Set the line signals takes it away" +msgid_plural "%zu block signals stand along it, of the automatic block: Set the line signals takes them away" +msgstr[0] "Stoi na nim %zu semafor SBL z blokady samoczynnej: Ustaw sygnalizatory szlakowe go usunie" +msgstr[1] "Stoją na nim %zu semafory SBL z blokady samoczynnej: Ustaw sygnalizatory szlakowe je usunie" +msgstr[2] "Stoi na nim %zu semaforów SBL z blokady samoczynnej: Ustaw sygnalizatory szlakowe je usunie" + +msgid "%zu signal taken away" +msgid_plural "%zu signals taken away" +msgstr[0] "usunięto %zu sygnalizator" +msgstr[1] "usunięto %zu sygnalizatory" +msgstr[2] "usunięto %zu sygnalizatorów" + +msgid "%zu given the template of the aspects of its line" +msgid_plural "%zu given the templates of the aspects of their lines" +msgstr[0] "%zu dostał szablon o stawności swojego szlaku" +msgstr[1] "%zu dostały szablony o stawności swoich szlaków" +msgstr[2] "%zu dostało szablony o stawności swoich szlaków" + +msgid "Ctrl+Z takes it back" +msgstr "Ctrl+Z cofa" From c7fecc011e616995b1c74953bd90f36b675fb59c Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 19:33:57 +0200 Subject: [PATCH 072/145] test(editor): selftests draw the block boundaries, srkboundary clicking at a block signal standing, and check the block sections of the scenery; the monkey opens the SRK tool too --- application/editormodeselftestmonkey.cpp | 2 +- application/editormodesrk.cpp | 57 ++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/application/editormodeselftestmonkey.cpp b/application/editormodeselftestmonkey.cpp index c949f68e..255e9147 100644 --- a/application/editormodeselftestmonkey.cpp +++ b/application/editormodeselftestmonkey.cpp @@ -82,7 +82,7 @@ editor_mode::selftest_result editor_mode::selftest_monkey(std::string const &com lines.push_back(std::string{"button "} + Button + " release"); lines.emplace_back("wait 2"); }; - char const *const tools[]{"esc", "l", "g", "c", "t", "n", "u", "h", "b", "p", "v", "j", "k", "o", "q", "w", "e"}; + char const *const tools[]{"esc", "l", "g", "c", "t", "n", "u", "h", "i", "b", "p", "v", "j", "k", "o", "q", "w", "e"}; char const *const modifiers[]{"", "ctrl", "shift", "alt"}; for (int step = 0; step < steps; ++step) { diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index e844a5f4..7cc5e399 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -4379,13 +4379,13 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri } if (Command == "srkboundary") { - // srkboundary entry= at= [nudge=]: a click while drawing the block boundaries, on the line beyond the entry signal at the - // distance out, nudge metres aside + // srkboundary entry= at=|stop= [nudge=]: a click while drawing the block boundaries, on the line beyond the entry + // signal at the distance out or at a block signal standing along it, nudge metres aside if (m_history.history_revision() != m_history_revision) refresh(); auto const values{selftest_values(Words)}; auto const entry{values.count("entry") > 0 ? values.at("entry") : std::string{}}; - auto const at{values.count("at") > 0 ? std::atof(values.at("at").c_str()) : 0.0}; + auto at{values.count("at") > 0 ? std::atof(values.at("at").c_str()) : 0.0}; auto const nudge{values.count("nudge") > 0 ? std::atof(values.at("nudge").c_str()) : 0.0}; auto const found{std::find_if(m_lines.begin(), m_lines.end(), [&](line_row const &Row) { return Row.entry == entry; })}; if (false == m_layout.has_value() || found == m_lines.end()) @@ -4393,6 +4393,8 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri WriteLog("SELFTEST srkboundary: no line beyond " + entry); return editor_selftest_result::handled; } + if (values.count("stop") > 0 && false == found->stops.empty()) + at = found->stops[static_cast(std::abs(std::atoi(values.at("stop").c_str()))) % found->stops.size()].first; auto const place{station::point_on(m_line_ways[found - m_lines.begin()], at)}; m_drawing = true; bool valid{false}; @@ -4406,6 +4408,55 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri m_drawing = false; return editor_selftest_result::handled; } + if (Command == "srkdraw") + { + // srkdraw on|off: the block boundaries drawn by hand, or not + std::string word; + Words >> word; + m_drawing = word != "off"; + WriteLog(std::string{"SELFTEST srkdraw "} + (m_drawing ? "on" : "off")); + return editor_selftest_result::handled; + } + if (Command == "srkcheck") + { + // srkcheck: the block sections of the scenery checked: each switched by a block signal standing, none twice, no two signals of + // one name + std::vector standing; + m_signals.standing_signals(standing, true); + std::unordered_map names; + for (auto const &signal : standing) + ++names[ToLower(signal.name)]; + std::size_t bad{0}; + for (auto const &[name, count] : names) + if (count > 1) + { + ++bad; + WriteLog(format("SELFTEST srkcheck: %d signals named %s", count, name.c_str())); + } + std::unordered_map sections; + auto const controls{m_templates.standing_templates([](std::string const &File) { return File == kControl; })}; + for (auto const &control : controls) + { + if (control.values.size() < 6) + { + ++bad; + WriteLog("SELFTEST srkcheck: a block section without its signal"); + continue; + } + if (++sections[ToLower(control.values[0])] > 1) + { + ++bad; + WriteLog("SELFTEST srkcheck: block section " + control.values[0] + " twice"); + } + if (names.count(ToLower(control.values[5])) == 0) + { + ++bad; + WriteLog("SELFTEST srkcheck: block section " + control.values[0] + " switched by " + control.values[5] + ", which doesn't stand"); + } + } + WriteLog(format("SELFTEST srkcheck: %zu signals, %zu block sections, %zu bad", standing.size(), controls.size(), bad)); + return editor_selftest_result::handled; + } if (Command == "srkstrip") { // srkstrip shown=0: the schematic hidden, the 3d view whole for a screenshot From d090e93c812078fb5590a2754e2908fb641b6610 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 19:51:55 +0200 Subject: [PATCH 073/145] fix(editor): a block boundary drawn places its signal across only where the other track runs near, the hover and the click count the block signals facing the trains alike, an SHP resonator at a joint finds the piece of a track cut for another one, and a batch of signals missing its template changes nothing but the tracks cut, Ctrl+Z taking them back; the km of each line kept for the hover, the shorter block section named in its warning, the block column wider --- application/editormodesignal.cpp | 7 +++++ application/editormodesrk.cpp | 52 ++++++++++++++++++++------------ application/editormodesrk.h | 2 ++ lang/editor_pl.po | 15 +++++---- 4 files changed, 51 insertions(+), 25 deletions(-) diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 2261dd47..7b875f04 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -980,6 +980,13 @@ std::size_t editor_mode::signal_batch(signal_changes const &Changes, std::vector items.push_back(std::move(placing)); reads.emplace_back(spot, variant->read.empty() ? std::string{} : editor_includes::substitute(variant->read, {order.name})); } + if (false == Error.empty()) + { + // nothing changed but the tracks cut for them, a step of undo + if (false == States.empty() || false == Created.empty()) + push_track_snapshot(std::move(States), std::move(Created)); + return 0; + } // the names passed round among the signals go through ones of their own first, or a read event renamed would be renamed again items.insert(items.end(), Changes.others.begin(), Changes.others.end()); EditorSnapshot step; diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 7cc5e399..34661098 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -668,6 +668,17 @@ std::unordered_set guarded(std::vector const &Co return result; } +// a block signal standing by the track of the line facing its trains, Distance: how far out along it +bool block_along(station::line const &Line, standing_signal const &Signal, double &Distance) +{ + double gap{0.0}; + Distance = station::distance_on(Line, Signal.position, gap); + if (Signal.kind != "block" || gap > 4.5 || Distance < 50.0 || Distance > Line.length() - 1.0) + return false; + auto const way{station::point_on(Line, Distance).way}; + return (Line.inbound ? -1.0 : 1.0) * glm::dot(travel_of(Signal.yaw), glm::dvec2{way.x, way.z}) >= 0.87; +} + } // namespace srk_tool::shp_state srk_tool::head_check::shp_of() const @@ -1335,9 +1346,9 @@ void srk_tool::render_lines() auto changed{false}; if (ImGui::BeginTable("##srklinelist", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn(STR_C("Line"), ImGuiTableColumnFlags_WidthStretch, 0.27f); - ImGui::TableSetupColumn(STR_C("To"), ImGuiTableColumnFlags_WidthStretch, 0.33f); - ImGui::TableSetupColumn(STR_C("Block"), ImGuiTableColumnFlags_WidthStretch, 0.40f); + ImGui::TableSetupColumn(STR_C("Line"), ImGuiTableColumnFlags_WidthStretch, 0.26f); + ImGui::TableSetupColumn(STR_C("To"), ImGuiTableColumnFlags_WidthStretch, 0.29f); + ImGui::TableSetupColumn(STR_C("Block"), ImGuiTableColumnFlags_WidthStretch, 0.45f); ImGui::TableHeadersRow(); for (std::size_t index = 0; index < m_lines.size() && false == changed; ++index) { @@ -1481,7 +1492,7 @@ std::string srk_tool::boundary_hover(glm::dvec3 const &Point, bool &Valid) m_boundary.point = station::point_on(line, at).point; // the other track across, the block signals standing within reach on both std::vector> tracks{{static_cast(best), at}}; - if (row.partner >= 0 && row.partner < static_cast(m_line_ways.size())) + if (row.partner >= 0 && row.partner < static_cast(std::min(m_lines.size(), m_line_ways.size())) && false == m_lines[row.partner].legacy && m_lines[row.partner].block != block_kind::semi) if (double gap{0.0}; true) if (auto const across{station::distance_on(m_line_ways[row.partner], m_boundary.point, gap)}; gap < 12.0) tracks.emplace_back(static_cast(row.partner), across); @@ -1517,10 +1528,9 @@ std::string srk_tool::boundary_hover(glm::dvec3 const &Point, bool &Valid) continue; } auto const place{station::point_on(way, distance)}; - auto const number{km_line(way)}; m_boundary.places.push_back(place.point); m_boundary.ways.push_back(place.way); - m_boundary.names.push_back(station::block_number(km_along(way, number, distance), way.number, km_rising(way, number))); + m_boundary.names.push_back(station::block_number(km_along(way, other.km_number, distance), way.number, other.rising)); } std::string names; for (auto const &name : m_boundary.names) @@ -1532,7 +1542,7 @@ std::string srk_tool::boundary_hover(glm::dvec3 const &Point, bool &Valid) auto const least{row.block == block_kind::four ? row.braking * 0.5 : row.braking}; auto label{format(STR_C("Click: block signals %s here: the block sections %.2f km and %.2f km"), names.c_str(), (at - previous) / 1000.0, (next - at) / 1000.0)}; if (std::min(at - previous, next - at) < least) - label += "\n" + format(row.block == block_kind::four ? STR_C("shorter than half the braking distance, %.0f m, of the 4-aspect block") : STR_C("shorter than the braking distance, %.0f m"), least); + label += "\n" + format(row.block == block_kind::four ? STR_C("The block section of %.2f km is shorter than half the braking distance, %.0f m, of the 4-aspect block") : STR_C("The block section of %.2f km is shorter than the braking distance, %.0f m"), std::min(at - previous, next - at) / 1000.0, least); return label; } @@ -2408,9 +2418,10 @@ void srk_tool::set_signals() } } std::string error; + auto const touched{false == before.empty() || false == created.empty()}; auto const count{m_signals.signal_batch({placed, renamed, {}, {}, {}}, std::move(before), std::move(created), error)}; if (false == error.empty()) - m_status = format(STR_C("The signals couldn't be placed: %s"), error.c_str()); + m_status = format(STR_C("The signals couldn't be placed: %s"), error.c_str()) + (touched ? ". " + STR("Ctrl+Z takes back the tracks cut") : std::string{}); else m_status = format(STR_P("%zu signal placed", "%zu signals placed", count), count) + ", " + format(STR_P("%zu renamed", "%zu renamed", renamed.size()), renamed.size()); if (false == unfit.empty()) @@ -2590,12 +2601,13 @@ void srk_tool::survey_lines(std::vector const &Standing) row.travel = travel_of(Standing[owners[line.entry]].yaw); row.block = block_of(line, row.entry, Standing, heads, row.why, row.own, row.own_why, row.blocks); row.braking = line_braking(line); + row.km_number = km_line(line); + row.rising = km_rising(line, row.km_number); auto switched{false}; for (auto const &signal : Standing) { - double gap{0.0}; - auto const distance{station::distance_on(line, signal.position, gap)}; - if (signal.kind != "block" || gap > 4.5 || distance < 50.0 || distance > line.length() - 1.0) + double distance{0.0}; + if (false == block_along(line, signal, distance)) continue; row.stops.emplace_back(distance, signal_shown(signal.name, stem)); switched = switched || guards.count(ToLower(signal.name)) > 0; @@ -2666,11 +2678,8 @@ void srk_tool::set_lines(block_edit const *Edit) auto switched{false}; for (std::size_t k = 0; k < standing.size(); ++k) { - if (standing[k].kind != "block") - continue; - double gap{0.0}; - auto const distance{station::distance_on(line, standing[k].position, gap)}; - if (gap > 4.5 || distance < 50.0 || distance > line.length() - 1.0 || (line.inbound ? -1.0 : 1.0) * facing_out(line, standing[k], distance) < 0.87) + double distance{0.0}; + if (false == block_along(line, standing[k], distance)) continue; along[index].push_back(k); switched = switched || guards.count(ToLower(standing[k].name)) > 0; @@ -2702,7 +2711,10 @@ void srk_tool::set_lines(block_edit const *Edit) } processed[edited] = 1; if (auto const partner{lines[edited].partner}; partner >= 0 && automatic[partner] == 1) - processed[partner] = 1; + { + station::distance_on(lines[partner], station::point_on(lines[edited], drawn).point, gap); + processed[partner] = gap < 12.0 ? 1 : 0; + } } auto const described{line_of_open()}; double known{0.0}; @@ -3197,10 +3209,11 @@ void srk_tool::set_lines(block_edit const *Edit) return; } std::string error; + auto const touched{false == before.empty() || false == created.empty()}; m_signals.signal_batch(changes, std::move(before), std::move(created), error); if (false == error.empty()) { - m_status = format(STR_C("The signals couldn't be placed: %s"), error.c_str()); + m_status = format(STR_C("The signals couldn't be placed: %s"), error.c_str()) + (touched ? ". " + STR("Ctrl+Z takes back the tracks cut") : std::string{}); return; } m_status = format(STR_P("%zu block signal placed", "%zu block signals placed", blocks), blocks) + ", " + format(STR_P("%zu warning disc", "%zu warning discs", discs), discs) + ", " + @@ -3224,6 +3237,7 @@ void srk_tool::survey_signals() m_heads.clear(); m_heads_known = false; m_lines.clear(); + m_line_ways.clear(); m_lines_problem = STR("Looking for the signal templates..."); if (false == m_measured) measure({}); @@ -3642,7 +3656,7 @@ void srk_tool::set_shp() signal_changes changes; for (auto const &entry : places) { - auto *track{entry.track != nullptr ? entry.track : piece_at(entry.cut, entry.inside)}; + auto *track{entry.track != nullptr ? piece_at(entry.track, entry.point) : piece_at(entry.cut, entry.inside)}; // the trains run into the track at the point: at the start of its path running towards its end if (glm::distance(track->m_paths.front().points[entry.facing > 0 ? segment_data::point::start : segment_data::point::end], entry.point) > 1.0) { diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 73810cf6..89399483 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -201,6 +201,8 @@ class srk_tool final : public editor_tool // block signals stand along it, none switched by block sections of the editor: it keeps the block of the scenery as it stands bool legacy{false}; double braking{0.0}; // m, the braking distance on it + std::string km_number; // the line of the kilometrage it runs along + bool rising{true}; // its trains run towards the higher km // the signals of the other block standing along it: warning discs on a line of the automatic block, block signals on one of the // semi-automatic one, the first of them std::size_t leftovers{0}; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 6b5d7ab3..5993edaa 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11246,10 +11246,10 @@ msgid "LMB: fly to the first of them" msgstr "LPM: leć do pierwszego z nich" msgid "SBL, 3 aspects" -msgstr "SBL trzystawna" +msgstr "SBL 3-stawna" msgid "SBL, 4 aspects" -msgstr "SBL czterostawna" +msgstr "SBL 4-stawna" msgid "Automatic block, 3 aspects (SBL)" msgstr "Samoczynna blokada liniowa trzystawna (SBL)" @@ -11302,11 +11302,14 @@ msgstr "Klik: usuń semafory SBL %s, odstępy po obu stronach połączone: %.2f msgid "Click: block signals %s here: the block sections %.2f km and %.2f km" msgstr "Klik: semafory SBL %s tutaj, odstępy %.2f km i %.2f km" -msgid "shorter than half the braking distance, %.0f m, of the 4-aspect block" -msgstr "krótszy niż połowa drogi hamowania (%.0f m), a to SBL czterostawna" +msgid "The block section of %.2f km is shorter than half the braking distance, %.0f m, of the 4-aspect block" +msgstr "Odstęp %.2f km jest krótszy niż połowa drogi hamowania (%.0f m) przy SBL czterostawnej" -msgid "shorter than the braking distance, %.0f m" -msgstr "krótszy niż droga hamowania, %.0f m" +msgid "The block section of %.2f km is shorter than the braking distance, %.0f m" +msgstr "Odstęp %.2f km jest krótszy niż droga hamowania (%.0f m)" + +msgid "Ctrl+Z takes back the tracks cut" +msgstr "Ctrl+Z cofa przecięcia torów" msgid "The line drawn on isn't there any more" msgstr "Szlaku, na którym rysujesz, już nie ma" From 4c55951d8a63ed4611c5f73403bad12d0064d9f6 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 20:47:52 +0200 Subject: [PATCH 074/145] feat(editor): the automatic block both ways or mixed, and the lines out on the schematic A block boundary of a line both ways has, on each track, the block signal of its trains 15 m before the joint and the one of the wrong-track trains 15 m beyond it, named after its twin with N, both on the outside of the line, the templates leaning the other way picked for the N ones, as on the lines of PKP PLK. The mixed block keeps the automatic block for the trains of the track and the semi-automatic one for the wrong-track ones: no N signals, a warning disc the braking distance before the entry signal of the wrong-track trains. A choice under the table of the lines sets either for the double-track lines of the station, kept in the record of the station, and by itself a line takes the mixed block where a warning disc stands for its wrong-track trains and no N signal does. Set the line signals places the N signals or the warning disc and takes the other away, a block boundary drawn places the N signals of a line both ways, and sbl_odstep.inc switches both block signals of a section with its 7th parameter. The name shown at the mouse while drawing takes the letter the block signal will get where its number stands already. The station schematic shows the lines out to the next stations straightened: their block sections in the colours of their names with their lengths, the joints, the block signals and the warning discs with their km on the outside of the line, the axis in km; Lines fits them in, a section under the mouse tells its name, length and km, and a click on it flies there in the 3d view. --- application/editormodesignal.cpp | 2 +- application/editormodesrk.cpp | 643 +++++++++++++++++++++++++------ application/editormodesrk.h | 50 ++- application/editortool.h | 1 + lang/editor_pl.po | 43 ++- 5 files changed, 616 insertions(+), 123 deletions(-) diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 7b875f04..0082a5a0 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -938,7 +938,7 @@ bool editor_mode::signal_heads(std::vector &Heads, bool const Wait) if (item.lamps.empty()) continue; auto const kind{kind_index(item.kind)}; - Heads.push_back({item.file, item.name, item.kind, item.mount, item.lamps, item.speeds, kind >= 0 && m_signal.chosen[kind] == index, item.aspects}); + Heads.push_back({item.file, item.name, item.kind, item.mount, item.lamps, item.speeds, kind >= 0 && m_signal.chosen[kind] == index, item.aspects, item.lean}); } return true; } diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 34661098..93730e7f 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -62,6 +62,7 @@ double constexpr kShpWindow{100.0}; // m, either way of its place, a resonator s double constexpr kResonatorDrop{0.2}; // m, of its origin below the axis double constexpr kBoundaryReach{30.0}; // m, either way of a block signal, a click while drawing the boundaries takes it away double constexpr kBoundaryEnd{50.0}; // m, of a block boundary drawn from the entry signals at the ends of the line +double constexpr kWrongTrack{30.0}; // m, a standing wrong-track block signal counts for the one of a boundary within it of its place // m, of a train at the speed, as Ie-4 § 8 takes it: the least block section of the automatic block, a warning disc that far before // its signal @@ -630,25 +631,28 @@ int resonator_at(std::vector const &Way, double const W // the template of a block signal of the aspects wanted, Like: the one standing, empty for the one chosen in the Signals tool, taken // where it has them, else the one of their mount with them, its counterpart by the name first, then of its lights; not the first of -// a line, whose events tell a station signal; empty for none -std::string block_template(std::vector const &Heads, std::string const &Like, int const Aspects) +// a line, whose events tell a station signal; Wrong: of a wrong-track one, standing on the left, its head leaning right where one +// does, a head leaning so only for it; empty for none +std::string block_template(std::vector const &Heads, std::string const &Like, int const Aspects, bool const Wrong = false) { + auto const leans = [&](signal_head const &Head) { return (Head.lean == "right") == Wrong; }; + auto const any_leans{std::any_of(Heads.begin(), Heads.end(), [&](signal_head const &Head) { return Head.kind == "block" && Head.aspects == Aspects && leans(Head); })}; signal_head const *like{nullptr}; for (auto const &head : Heads) if (head.kind == "block" && (Like.empty() ? head.chosen : head.file == Like)) like = &head; - if (like != nullptr && like->aspects == Aspects) + if (like != nullptr && like->aspects == Aspects && (leans(*like) || false == any_leans)) return like->file; auto counterpart{like != nullptr ? like->name : std::string{}}; if (auto const digit{counterpart.find_first_of("34")}; digit != std::string::npos) counterpart[digit] = counterpart[digit] == '3' ? '4' : '3'; signal_head const *best{nullptr}; - auto rank{std::numeric_limits::max()}; + auto rank{std::make_pair(std::numeric_limits::max(), std::size_t{0})}; for (auto const &head : Heads) { - if (head.kind != "block" || head.aspects != Aspects || ToLower(head.name).find("pierwszy") != std::string::npos) + if (head.kind != "block" || head.aspects != Aspects || ToLower(head.name).find("pierwszy") != std::string::npos || (any_leans && false == leans(head))) continue; - auto const score{like == nullptr ? 0 : (head.mount != like->mount ? 4 : 0) + (head.name != counterpart ? 2 : 0) + (head.lamps != like->lamps ? 1 : 0)}; + auto const score{std::make_pair(like == nullptr ? 0 : (head.mount != like->mount ? 4 : 0) + (head.name != counterpart ? 2 : 0) + (head.lamps != like->lamps ? 1 : 0), head.file.size())}; if (score < rank) { rank = score; @@ -663,20 +667,49 @@ std::unordered_set guarded(std::vector const &Co { std::unordered_set result; for (auto const &control : Controls) - if (control.values.size() > 5) - result.insert(ToLower(control.values[5])); + for (std::size_t index = 5; index < control.values.size() && index < 7; ++index) + result.insert(ToLower(control.values[index])); return result; } -// a block signal standing by the track of the line facing its trains, Distance: how far out along it -bool block_along(station::line const &Line, standing_signal const &Signal, double &Distance) +// a block signal standing by the track of the line facing its trains, or Wrong: its wrong-track ones, Distance: how far out along it +bool faces_along(station::line const &Line, standing_signal const &Signal, double &Distance, bool const Wrong = false) { double gap{0.0}; Distance = station::distance_on(Line, Signal.position, gap); - if (Signal.kind != "block" || gap > 4.5 || Distance < 50.0 || Distance > Line.length() - 1.0) + if (gap > 4.5 || Distance < 50.0 || Distance > Line.length() - 1.0) return false; auto const way{station::point_on(Line, Distance).way}; - return (Line.inbound ? -1.0 : 1.0) * glm::dot(travel_of(Signal.yaw), glm::dvec2{way.x, way.z}) >= 0.87; + return (Line.inbound != Wrong ? -1.0 : 1.0) * glm::dot(travel_of(Signal.yaw), glm::dvec2{way.x, way.z}) >= 0.87; +} + +bool block_along(station::line const &Line, standing_signal const &Signal, double &Distance, bool const Wrong = false) +{ + auto const along{faces_along(Line, Signal, Distance, Wrong)}; + return Signal.kind == "block" && along; +} + +// a warning disc by the line facing its wrong-track trains, on no other line out of the station: of the semi-automatic block they +// run on, the automatic one of the track the other way +bool wrong_disc(std::vector const &Lines, std::size_t const Index, standing_signal const &Signal) +{ + double distance{0.0}; + if (Signal.kind != "warning" || false == faces_along(Lines[Index], Signal, distance, true)) + return false; + for (std::size_t other = 0; other < Lines.size(); ++other) + { + double gap{0.0}; + auto const at{station::distance_on(Lines[other], Signal.position, gap)}; + if (other != Index && gap < 4.5 && at > 50.0 && at < Lines[other].length() - 1.0) + return false; + } + return true; +} + +// the way the trains of the line run out along it, 1 out, -1 in +double out_of(station::line const &Line) +{ + return Line.inbound ? -1.0 : 1.0; } } // namespace @@ -730,6 +763,15 @@ void srk_tool::load() } from = comma + 1; } + else if (word == "ways" && words >> word) + for (std::size_t from = 0; from < word.size();) + { + auto const comma{std::min(word.find(',', from), word.size())}; + auto const item{word.substr(from, comma - from)}; + if (auto const equals{item.find('=')}; equals != std::string::npos && equals > 0) + entry.ways[item.substr(0, equals)] = item.substr(equals + 1) == "2"; + from = comma + 1; + } else if (word == "anchor") words >> entry.anchor; else if (word == "name") @@ -771,7 +813,11 @@ void srk_tool::store() for (auto const &[signal, kind] : entry.blocks) if (false == signal.empty() && signal.find_first_of(" ,=\t") == std::string::npos) blocks += (blocks.empty() ? " blocks " : ",") + signal + (kind == block_kind::four ? "=sbl4" : kind == block_kind::three ? "=sbl" : "=semi"); - lines.push_back(format("station %s line %s km %s from %s anchor %s%s%s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), word_of(entry.km).c_str(), entry.reversed ? "right" : "left", anchor_of(entry).c_str(), braking.c_str(), blocks.c_str(), entry.name.c_str())); + std::string ways; + for (auto const &[signal, both] : entry.ways) + if (false == signal.empty() && signal.find_first_of(" ,=\t") == std::string::npos) + ways += (ways.empty() ? " ways " : ",") + signal + (both ? "=2" : "=1"); + lines.push_back(format("station %s line %s km %s from %s anchor %s%s%s%s name %s", word_of(entry.code).c_str(), word_of(entry.line).c_str(), word_of(entry.km).c_str(), entry.reversed ? "right" : "left", anchor_of(entry).c_str(), braking.c_str(), blocks.c_str(), ways.c_str(), entry.name.c_str())); } scene::Layers.mark(layer, kMark, std::move(lines)); } @@ -1004,24 +1050,29 @@ void srk_tool::draw_overlay() const drawlist->AddText(ImVec2(screen.x - size.x * 0.5f, screen.y - size.y - 2.0f), kUseful, text.c_str()); } } - // the block boundary under the mouse while they are drawn: a bar across each track and a post where its signal goes + // the block boundary under the mouse while they are drawn: a bar across each track at the joint and a post where each signal goes if (drawing() && m_boundary.valid) - for (std::size_t k = 0; k < m_boundary.places.size(); ++k) + { + auto const colour{m_boundary.remove ? IM_COL32(255, 90, 80, 255) : IM_COL32(110, 230, 120, 255)}; + for (auto const &[point, way] : m_boundary.joints) { - auto const colour{m_boundary.remove ? IM_COL32(255, 90, 80, 255) : IM_COL32(110, 230, 120, 255)}; - auto const &way{m_boundary.ways[k]}; glm::dvec3 const across{-way.z, 0.0, way.x}; - auto const base{m_boundary.places[k] + lift}; - projection.line(drawlist, base - across * 2.0, base + across * 2.0, colour, 4.0f); - projection.line(drawlist, base + across * 2.4, base + across * 2.4 + glm::dvec3{0.0, 6.0, 0.0}, colour, 3.0f); + projection.line(drawlist, point + lift - across * 2.0, point + lift + across * 2.0, colour, 4.0f); + } + for (auto const &post : m_boundary.posts) + { + auto const base{post.point + lift + post.side * 2.4}; + projection.line(drawlist, post.point + lift, base, colour, 2.0f); + projection.line(drawlist, base, base + glm::dvec3{0.0, 6.0, 0.0}, colour, 3.0f); ImVec2 screen; - if (k >= m_boundary.names.size() || false == projection.project(base + across * 2.4 + glm::dvec3{0.0, 6.5, 0.0}, screen)) + if (false == projection.project(base + glm::dvec3{0.0, 6.5, 0.0}, screen)) continue; - auto const &text{m_boundary.names[k]}; + auto const &text{post.name}; auto const size{ImGui::CalcTextSize(text.c_str())}; drawlist->AddRectFilled(ImVec2(screen.x - size.x * 0.5f - 4.0f, screen.y - size.y - 4.0f), ImVec2(screen.x + size.x * 0.5f + 4.0f, screen.y), colour, 3.0f); drawlist->AddText(ImVec2(screen.x - size.x * 0.5f, screen.y - size.y - 2.0f), IM_COL32(0, 0, 0, 255), text.c_str()); } + } for (auto const &mark : m_signal_marks) { ImVec2 screen; @@ -1343,6 +1394,14 @@ void srk_tool::render_lines() } m_changed = true; }; + auto const set_ways = [&](line_row const &Row, std::optional const Both) { + auto &entry{record_of_open()}; + if (Both.has_value()) + entry.ways[Row.entry] = *Both; + else + entry.ways.erase(Row.entry); + m_changed = true; + }; auto changed{false}; if (ImGui::BeginTable("##srklinelist", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { @@ -1379,6 +1438,8 @@ void srk_tool::render_lines() ImGui::TextUnformatted(STR_C("A single-track line")); ImGui::TextUnformatted(format(STR_P("%zu block signal stands along it", "%zu block signals stand along it", row.blocks), row.blocks).c_str()); ImGui::TextUnformatted(format(STR_C("Block: %s, %s"), kind(row.block), reason(row.why)).c_str()); + if (row.number > 0 && row.block != block_kind::semi) + ImGui::TextUnformatted(row.both ? STR_C("Wrong-track running: SBL both ways, the block signals with N") : STR_C("Wrong-track running: the mixed block, a warning disc before the entry signal")); if (row.legacy) ImGui::TextColored(kDoubtful, "%s", STR_C("No block section of the editor switches its block signals: it keeps the block of the scenery as it stands")); if (row.leftovers > 0) @@ -1441,13 +1502,42 @@ void srk_tool::render_lines() } ImGui::EndTable(); } + // the wrong-track trains on the automatic block too, or on the semi-automatic one: the mixed block, the double-track lines of the + // automatic block of the station alike + std::vector two_track; + for (auto const &row : m_lines) + if (row.number > 0 && row.block != block_kind::semi && false == row.legacy) + two_track.push_back(&row); + if (false == two_track.empty()) + { + auto const all = [&](bool const Both) { return std::all_of(two_track.begin(), two_track.end(), [&](line_row const *Row) { return Row->both == Both; }); }; + int ways{all(true) ? 2 : all(false) ? 1 : 0}; + ImGui::TextDisabled("%s", STR_C("Wrong-track running")); + ImGui::BeginDisabled(changed); + if (segmented("##srkways", ways, + {{2, STR_C("SBL both ways"), "At each block boundary the block signals of both tracks either way: of the trains of the track 15 m " + "before the joint, for the wrong-track ones 15 m beyond it, with N (523N), on the outside of the line, as " + "on the lines of PKP PLK"}, + {1, STR_C("Mixed"), "The automatic block for the trains of the track, the semi-automatic one for the wrong-track ones: no N " + "signals, a warning disc ToB the braking distance before the entry signal of the wrong-track trains, on the " + "outside of the line. Set the line signals places it and takes the N signals away"}})) + { + for (auto const *row : two_track) + set_ways(*row, (ways == 2) == row->own_both ? std::nullopt : std::optional{ways == 2}); + changed = true; + } + ImGui::EndDisabled(); + if (ways == 0) + ImGui::TextDisabled("%s", STR_C("Each line as it stands: a choice sets all of them")); + } if (changed) survey_signals(); ImGui::Checkbox(STR_C("Draw the block boundaries"), &m_drawing); - item_tooltip("LMB on a track of a line of the automatic block, beyond the entry signal: block signals across both tracks there, " - "numbered after the hectometre ahead of them, the tracks cut 15 m beyond them and the new block sections named; LMB on a " - "block boundary standing takes its signals away, the sections either side joined. A line whose block signals stand keeps " - "them: Set the line signals places them evenly only on a line without any. One step of undo each"); + item_tooltip("LMB on a track of a line of the automatic block, beyond the entry signal: a block boundary there, the tracks cut " + "across the line, block signals 15 m before the joint, numbered after the hectometre ahead of them, on a line both ways " + "the wrong-track ones 15 m beyond it, with N, and the new block sections named; LMB on a block boundary standing takes its " + "signals away, the sections either side joined. A line whose block signals stand keeps them: Set the line signals places " + "them evenly only on a line without any. One step of undo each"); if (m_drawing) { ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); @@ -1512,29 +1602,49 @@ std::string srk_tool::boundary_hover(glm::dvec3 const &Point, bool &Valid) else next = std::min(next, stop.first); } + // the joint across each track, the block signal of its trains before it, the wrong-track one beyond it, both on the outside + auto const post_at = [&](station::line const &Way, double const Distance, std::string const &Name) { + auto const on{station::point_on(Way, Distance)}; + glm::dvec3 const travel{on.way * out_of(Way)}; + m_boundary.posts.push_back({on.point, glm::dvec3{-travel.z, 0.0, travel.x}, Name}); + }; for (auto const &[index, distance] : tracks) { auto const &other{m_lines[index]}; auto const &way{m_line_ways[index]}; + auto const drop{station::signal_drop * out_of(way)}; + auto joint{distance}; if (m_boundary.remove) { auto const stop{near_stop(other, distance)}; if (stop == other.stops.end()) continue; - auto const place{station::point_on(way, stop->first)}; - m_boundary.places.push_back(place.point); - m_boundary.ways.push_back(place.way); - m_boundary.names.push_back(stop->second); - continue; + joint = stop->first; + post_at(way, joint - drop, stop->second); + auto const wrong{std::min_element(other.wrong.begin(), other.wrong.end(), [&](auto const &A, auto const &B) { return std::abs(A.first - joint - drop) < std::abs(B.first - joint - drop); })}; + if (wrong != other.wrong.end() && std::abs(wrong->first - joint - drop) < kWrongTrack) + post_at(way, wrong->first, wrong->second); } - auto const place{station::point_on(way, distance)}; - m_boundary.places.push_back(place.point); - m_boundary.ways.push_back(place.way); - m_boundary.names.push_back(station::block_number(km_along(way, other.km_number, distance), way.number, other.rising)); + else + { + // a number standing already gets a letter, as Set the line signals gives it + auto const free = [&](std::string const &Base) { + auto name{Base}; + for (char letter = 'a'; m_signal_names.count(ToLower(name)) > 0 && letter <= 'z'; ++letter) + name = Base + letter; + return name; + }; + auto const name{free(station::block_number(km_along(way, other.km_number, joint - drop), way.number, other.rising))}; + post_at(way, joint - drop, signal_shown(name, {})); + if (other.both) + post_at(way, joint + drop, signal_shown(free(name + "N"), {})); + } + auto const on{station::point_on(way, joint)}; + m_boundary.joints.emplace_back(on.point, on.way); } std::string names; - for (auto const &name : m_boundary.names) - names += (names.empty() ? "" : ", ") + name; + for (auto const &post : m_boundary.posts) + names += (names.empty() ? "" : ", ") + post.name; m_boundary.valid = true; Valid = true; if (m_boundary.remove) @@ -2529,6 +2639,31 @@ srk_tool::block_kind srk_tool::block_of(station::line const &Line, std::string c return Own; } +bool srk_tool::both_ways(std::vector const &Lines, std::size_t const Index, std::string const &Entry, std::vector const &Standing, bool &Hand, bool &Own) const +{ + auto const &Line{Lines[Index]}; + auto along{false}; + auto wrong{false}; + auto mixed{false}; + for (auto const &signal : Standing) + { + double distance{0.0}; + along = along || block_along(Line, signal, distance); + wrong = wrong || block_along(Line, signal, distance, true); + mixed = mixed || wrong_disc(Lines, Index, signal); + } + // block signals for the wrong-track trains, or none of either but a warning disc for them: the mixed block + Own = Line.number > 0 && (wrong || (false == along && false == mixed)); + Hand = false; + if (m_record >= 0) + if (auto const set{m_records[m_record].ways.find(Entry)}; set != m_records[m_record].ways.end()) + { + Hand = true; + return Line.number > 0 && set->second; + } + return Own; +} + std::string srk_tool::km_line(station::line const &Line) const { auto const described{line_of_open()}; @@ -2603,16 +2738,58 @@ void srk_tool::survey_lines(std::vector const &Standing) row.braking = line_braking(line); row.km_number = km_line(line); row.rising = km_rising(line, row.km_number); + row.both = both_ways(lines, static_cast(&line - lines.data()), row.entry, Standing, row.both_hand, row.own_both); auto switched{false}; for (auto const &signal : Standing) { double distance{0.0}; + if (block_along(line, signal, distance, true)) + { + row.wrong.emplace_back(distance, signal_shown(signal.name, stem)); + row.posts.push_back({distance, signal_shown(signal.name, stem), true, km_along(line, row.km_number, distance)}); + } if (false == block_along(line, signal, distance)) continue; - row.stops.emplace_back(distance, signal_shown(signal.name, stem)); + row.stops.emplace_back(distance + station::signal_drop * out_of(line), signal_shown(signal.name, stem)); + row.posts.push_back({distance, signal_shown(signal.name, stem), false, km_along(line, row.km_number, distance)}); switched = switched || guards.count(ToLower(signal.name)) > 0; } + // the warning discs: of the semi-automatic block, of the wrong-track trains of the mixed one + for (auto const &signal : Standing) + { + double distance{0.0}; + auto const mixed{row.block != block_kind::semi && wrong_disc(lines, static_cast(&line - lines.data()), signal)}; + if (signal.kind == "warning" && (mixed || (row.block == block_kind::semi && faces_along(line, signal, distance)))) + { + faces_along(line, signal, distance, mixed); + row.posts.push_back({distance, signal_shown(signal.name, stem), mixed, km_along(line, row.km_number, distance)}); + } + } std::sort(row.stops.begin(), row.stops.end()); + std::sort(row.wrong.begin(), row.wrong.end()); + // the schematic: straight out of the entry signal along the chainage, the track circuits along it in the colours of their names + auto const entry_at{station::project(*m_layout, station::point_on(line, 0.0).point)}; + row.chainage = entry_at.x; + row.offset = entry_at.y; + row.outward = station::project(*m_layout, station::point_on(line, std::min(50.0, line.length())).point).x >= entry_at.x ? 1.0 : -1.0; + double probe{0.0}; + row.km_known = kilometrage::km_at(row.km_number, station::point_on(line, 0.0).point, probe) || km_of_open(row.chainage, probe); + for (std::size_t k = 0; k < line.points.size() && k < line.distances.size() && k < line.owners.size(); ++k) + { + auto const *track{line.owners[k] < line.tracks.size() ? line.tracks[line.owners[k]] : nullptr}; + auto const name{track == nullptr || track->Isolated.empty() ? std::string{} : track->Isolated.front()->asName}; + if (row.sections.empty() || row.sections.back().name != name) + row.sections.push_back({line.distances[k], line.distances[k], name}); + row.sections.back().to = line.distances[k]; + if (k > 0 && row.sections.size() > 1 && row.sections.back().from == line.distances[k]) + row.sections[row.sections.size() - 2].to = line.distances[k]; + } + row.sections.erase(std::remove_if(row.sections.begin(), row.sections.end(), [](line_row::run const &Run) { return Run.name.empty() || Run.to - std::max(0.0, Run.from) < 1.0; }), row.sections.end()); + for (auto &run : row.sections) + { + run.km_from = km_along(line, row.km_number, run.from); + run.km_to = km_along(line, row.km_number, run.to); + } row.legacy = false == row.stops.empty() && false == switched; auto const next{next_entry(line, Standing, row.length)}; row.next = next >= 0 ? signal_shown(Standing[next].name, stem) : std::string{}; @@ -2630,6 +2807,8 @@ void srk_tool::survey_lines(std::vector const &Standing) { if (signal.kind != (automatic ? "warning" : "block") || false == on_line(m_line_ways[index], signal.position)) continue; + if (automatic && false == row.both && wrong_disc(m_line_ways, index, signal)) + continue; auto shared{false}; for (std::size_t other = 0; other < m_lines.size() && false == shared; ++other) shared = (m_lines[other].block != block_kind::semi) != automatic && on_line(m_line_ways[other], signal.position); @@ -2665,7 +2844,9 @@ void srk_tool::set_lines(block_edit const *Edit) // 0: the semi-automatic one std::vector automatic(lines.size(), 0); std::vector aspects(lines.size(), 3); + std::vector both(lines.size(), 0); // the automatic block both ways std::vector> along(lines.size()); // the block signals standing along each line facing its trains + std::vector> wrong(lines.size()); // and facing its wrong-track trains for (std::size_t index = 0; index < lines.size(); ++index) { auto const &line{lines[index]}; @@ -2675,13 +2856,19 @@ void srk_tool::set_lines(block_edit const *Edit) std::size_t blocks{0}; auto const kind{block_of(line, entries[line.entry].name, standing, heads, why, own, own_why, blocks)}; aspects[index] = kind == block_kind::four ? 4 : 3; + auto hand{false}; + auto own_both{false}; + both[index] = both_ways(lines, index, entries[line.entry].name, standing, hand, own_both) ? 1 : 0; auto switched{false}; for (std::size_t k = 0; k < standing.size(); ++k) { double distance{0.0}; - if (false == block_along(line, standing[k], distance)) + if (block_along(line, standing[k], distance, true)) + wrong[index].push_back(k); + else if (block_along(line, standing[k], distance)) + along[index].push_back(k); + else continue; - along[index].push_back(k); switched = switched || guards.count(ToLower(standing[k].name)) > 0; } if (kind != block_kind::semi) @@ -2739,6 +2926,8 @@ void srk_tool::set_lines(block_edit const *Edit) std::string linked{"none"}; int match{-1}; // of the standing signals bool kept{false}; // standing as it is, its name and its plate kept + bool wrong{false}; // facing the wrong-track trains + int twin{-1}; // the block signal of the other way at its boundary, of the planned ones }; struct far_end { @@ -2766,9 +2955,56 @@ void srk_tool::set_lines(block_edit const *Edit) auto const &end{ends[index]}; auto const &entry{entries[line.entry]}; auto const least{braking(index) * (aspects[index] == 4 ? 0.5 : 1.0)}; + // the block signal of a boundary facing its trains before the joint, the wrong-track one beyond it + auto const drop{station::signal_drop * out_of(line)}; + auto const plan = [&](double const Distance, std::string const &Name, int const Match, bool const Wrong) { + planned_signal signal; + signal.name = Name; + signal.plate = Match >= 0 ? std::string{} : Name; + signal.line = index; + signal.distance = Distance; + signal.match = Match; + signal.kept = Match >= 0; + signal.wrong = Wrong; + planned.push_back(std::move(signal)); + return static_cast(planned.size() - 1); + }; + std::unordered_set paired; + auto const nearest_wrong = [&](double const Wanted, double &At) { + int found{-1}; + auto nearest{kWrongTrack}; + for (auto const k : wrong[index]) + { + double distance{0.0}; + block_along(line, standing[k], distance, true); + if (0 == gone[k] && 0 == paired.count(k) && std::abs(distance - Wanted) < nearest) + { + nearest = std::abs(distance - Wanted); + found = static_cast(k); + At = distance; + } + } + return found; + }; + // the wrong-track one: the one standing beyond the joint, else a new one there; none of one way only + auto const add_wrong = [&](int const Normal) { + auto at{planned[Normal].distance + 2.0 * drop}; + auto const match{nearest_wrong(at, at)}; + if (match >= 0) + paired.insert(static_cast(match)); + if (false == both[index]) + { + if (match >= 0) + gone[match] = 1; + return; + } + auto const twin{plan(at, match >= 0 ? standing[match].name : planned[Normal].name + "N", match, true)}; + planned[twin].twin = Normal; + planned[Normal].twin = twin; + }; if (automatic[index] == 1) { - // the boundary drawn, on the line drawn on and across on its other track + // the boundary drawn, the joint on the line drawn on and across on its other track std::optional boundary; if (static_cast(index) == edited) boundary = drawn; @@ -2781,30 +3017,20 @@ void srk_tool::set_lines(block_edit const *Edit) // the block signals standing kept as they stand, the boundary drawn added or the ones standing at it taken away for (auto const k : along[index]) { - double gap{0.0}; - auto const distance{station::distance_on(line, standing[k].position, gap)}; - if (boundary.has_value() && Edit->remove && std::abs(distance - *boundary) < kBoundaryReach) + double distance{0.0}; + block_along(line, standing[k], distance); + if (boundary.has_value() && Edit->remove && std::abs(distance + drop - *boundary) < kBoundaryReach) { gone[k] = 1; + auto at{distance + 2.0 * drop}; + if (auto const twin{nearest_wrong(at, at)}; twin >= 0) + gone[twin] = 1; continue; } - planned_signal signal; - signal.name = standing[k].name; - signal.line = index; - signal.distance = distance; - signal.match = static_cast(k); - signal.kept = true; - planned.push_back(std::move(signal)); + add_wrong(plan(distance, standing[k].name, static_cast(k), false)); } if (boundary.has_value() && false == Edit->remove) - { - planned_signal signal; - signal.name = station::block_number(km_on(index, *boundary), line.number, rising(index)); - signal.plate = signal.name; - signal.line = index; - signal.distance = *boundary; - planned.push_back(std::move(signal)); - } + add_wrong(plan(*boundary - drop, station::block_number(km_on(index, *boundary - drop), line.number, rising(index)), -1, false)); } else { @@ -2814,9 +3040,10 @@ void srk_tool::set_lines(block_edit const *Edit) std::vector places; if (line.station || end.signal >= 0) { - // as many on both tracks of the line - auto const reach{line.partner >= 0 ? (end.distance + ends[line.partner].distance) * 0.5 : end.distance}; - auto const other{line.partner >= 0 ? braking(line.partner) * (aspects[line.partner] == 4 ? 0.5 : 1.0) : least}; + // as many on both tracks of the line, where both have the automatic block + auto const paired{line.partner >= 0 && line.partner < static_cast(lines.size()) && automatic[line.partner] == 1}; + auto const reach{paired ? (end.distance + ends[line.partner].distance) * 0.5 : end.distance}; + auto const other{paired ? braking(line.partner) * (aspects[line.partner] == 4 ? 0.5 : 1.0) : least}; auto const sections{std::max(1.0, std::floor(reach / std::max(least, other)))}; for (auto k{1.0}; k < sections; k += 1.0) places.push_back(end.distance * k / sections); @@ -2825,19 +3052,18 @@ void srk_tool::set_lines(block_edit const *Edit) for (auto distance{least * kBlockShare}; distance <= end.distance - 100.0; distance += least * kBlockShare) places.push_back(distance); for (auto const distance : places) - { - planned_signal signal; - signal.name = station::block_number(km_on(index, distance), line.number, rising(index)); - signal.plate = signal.name; - signal.line = index; - signal.distance = distance; - planned.push_back(std::move(signal)); - } + add_wrong(plan(distance, station::block_number(km_on(index, distance), line.number, rising(index)), -1, false)); } + // the wrong-track ones of no boundary taken away + for (auto const k : wrong[index]) + if (0 == paired.count(k)) + gone[k] = 1; } - else if (automatic[index] == 0) + // the warning disc of the entry signal, the braking distance before it: of the semi-automatic block, or of the mixed one where + // it's the entry signal of the wrong-track trains, on the outside of the line facing them + auto const mixed{automatic[index] == 1 && line.number > 0 && false == line.inbound && false == both[index] && Edit == nullptr}; + if (automatic[index] == 0 || mixed) { - // the warning disc of the entry signal, the braking distance before it auto const distance{std::min(braking(index), end.distance - 100.0)}; if (distance >= 400.0) { @@ -2847,6 +3073,7 @@ void srk_tool::set_lines(block_edit const *Edit) signal.plate = "to" + ToLower(entry.name); signal.line = index; signal.distance = distance; + signal.wrong = mixed; planned.push_back(std::move(signal)); } } @@ -2858,6 +3085,9 @@ void srk_tool::set_lines(block_edit const *Edit) { if (automatic[index] == 1 ? standing[k].kind != "warning" : std::find(along[index].begin(), along[index].end(), k) == along[index].end()) continue; + // the one of the wrong-track trains of the mixed block kept + if (automatic[index] == 1 && false == both[index] && wrong_disc(lines, index, standing[k])) + continue; // not where the line shares its track with one of the other block auto shared{false}; for (std::size_t other = 0; other < lines.size() && false == shared; ++other) @@ -2900,7 +3130,7 @@ void srk_tool::set_lines(block_edit const *Edit) double gap{0.0}; auto const distance{station::distance_on(line, standing[k].position, gap)}; auto const facing{facing_out(line, standing[k], distance)}; - if (gap < 4.5 && std::abs(distance - signal.distance) < reach && (line.inbound ? -facing : facing) > 0.87) + if (gap < 4.5 && std::abs(distance - signal.distance) < reach && (line.inbound != signal.wrong ? -facing : facing) > 0.87) candidates.emplace_back(std::abs(distance - signal.distance) + gap * 10.0, i, k); } } @@ -2928,16 +3158,19 @@ void srk_tool::set_lines(block_edit const *Edit) for (auto const &signal : planned) if (signal.kept) taken.insert(ToLower(signal.name)); - for (auto &signal : planned) - { - if (signal.kind != station::signal::role::block || signal.kept) - continue; - auto const base{signal.name}; - for (char letter = 'a'; taken.count(ToLower(signal.name)) > 0 && letter <= 'z'; ++letter) - signal.name = base + letter; - taken.insert(ToLower(signal.name)); - signal.plate = signal.name; - } + // the wrong-track ones after their twins: 523N beside 523 + for (auto const wrong_track : {false, true}) + for (auto &signal : planned) + { + if (signal.kind != station::signal::role::block || signal.kept || signal.wrong != wrong_track) + continue; + auto const base{wrong_track && signal.twin >= 0 ? planned[signal.twin].name + "N" : signal.name}; + signal.name = base; + for (char letter = 'a'; taken.count(ToLower(signal.name)) > 0 && letter <= 'z'; ++letter) + signal.name = base + letter; + taken.insert(ToLower(signal.name)); + signal.plate = signal.name; + } // the block sections of each track of a double-track line, between the signals in the order the trains pass them struct section { @@ -2946,6 +3179,7 @@ void srk_tool::set_lines(block_edit const *Edit) double from{0.0}; double to{0.0}; std::string guard; // the block signal at its start, empty for a station one + std::string wrong_guard; // the one at its end facing the wrong-track trains, empty for none glm::dvec3 start{0.0}; }; std::vector
sections; @@ -2964,7 +3198,7 @@ void srk_tool::set_lines(block_edit const *Edit) }; std::vector order{{0.0, standing[entry_standing[line.entry]].name, -1, static_cast(entry_standing[line.entry])}}; for (std::size_t i = 0; i < planned.size(); ++i) - if (planned[i].line == index) + if (planned[i].line == index && false == planned[i].wrong) order.push_back({planned[i].distance, planned[i].name, static_cast(i), planned[i].match}); if (ends[index].signal >= 0) order.push_back({ends[index].distance, standing[ends[index].signal].name, -1, ends[index].signal}); @@ -2977,6 +3211,8 @@ void srk_tool::set_lines(block_edit const *Edit) return Stop.distance + (line.inbound ? -station::signal_drop : station::signal_drop); return Stop.distance + (Stop.distance > 0.0 ? station::signal_drop : -station::signal_drop); }; + // the wrong-track one of a stop, the wrong-track trains pass them the other way + auto const twin_of = [&](std::size_t const At) { return At < order.size() && order[At].signal >= 0 ? planned[order[At].signal].twin : -1; }; for (std::size_t i = 0; i < order.size(); ++i) { auto const &here{order[i]}; @@ -2986,6 +3222,20 @@ void srk_tool::set_lines(block_edit const *Edit) planned[here.signal].linked = linked; else if (block_before && here.standing >= 0 && false == standing[here.standing].linked.empty() && ToLower(standing[here.standing].linked) != ToLower(linked)) relinked.emplace_back(here.standing, linked); + auto const wrong_before{twin_of(i + 1)}; + auto const wrong_linked{wrong_before >= 0 ? planned[wrong_before].name : std::string{"none"}}; + if (auto const twin{twin_of(i)}; twin >= 0) + planned[twin].linked = wrong_linked; + else if (i == 0 && here.signal < 0 && here.standing >= 0 && false == standing[here.standing].linked.empty()) + { + // the station signal the wrong-track trains come to: after the last wrong-track block signal, after none once it's gone + auto const &had{standing[here.standing].linked}; + auto dropped{false}; + for (std::size_t k = 0; k < standing.size() && false == dropped; ++k) + dropped = gone[k] && ToLower(standing[k].name) == ToLower(had); + if (ToLower(had) != ToLower(wrong_linked) && (wrong_before >= 0 || dropped)) + relinked.emplace_back(here.standing, wrong_linked); + } if (i == 0) continue; auto const &before{order[i - 1]}; @@ -2999,6 +3249,8 @@ void srk_tool::set_lines(block_edit const *Edit) found.from = std::min(joint_of(before), joint_of(here)); found.to = std::max(joint_of(before), joint_of(here)); found.guard = before.signal >= 0 ? before.name : std::string{}; + if (auto const twin{twin_of(i)}; twin >= 0) + found.wrong_guard = planned[twin].name; found.start = station::point_on(line, before.distance).point; sections.push_back(std::move(found)); } @@ -3123,8 +3375,8 @@ void srk_tool::set_lines(block_edit const *Edit) ++renamed; // of the aspects of its line auto const head{std::find_if(heads.begin(), heads.end(), [&](signal_head const &Head) { return Head.file == found.file; })}; - if (block && head != heads.end() && head->aspects != 0 && head->aspects != aspects[signal.line]) - if (auto const other{block_template(heads, found.file, aspects[signal.line])}; false == other.empty()) + if (block && head != heads.end() && head->aspects != 0 && (head->aspects != aspects[signal.line] || (head->lean == "right") != signal.wrong)) + if (auto const other{block_template(heads, found.file, aspects[signal.line], signal.wrong)}; false == other.empty() && other != found.file) { changes.files.emplace_back(found.instance, other); ++swapped; @@ -3132,16 +3384,17 @@ void srk_tool::set_lines(block_edit const *Edit) continue; } auto const &line{lines[signal.line]}; - auto const inbound{line.inbound}; + // its trains run in, the wrong-track ones of an outbound track too; a wrong-track one stands on the outside, on their left + auto const inbound{line.inbound != signal.wrong}; auto const at{station::point_on(line, signal.distance)}; auto const approach{station::point_on(line, signal.distance + (inbound ? 1.0 : -1.0))}; signal_order order; order.track = piece_at(approach.track, approach.point); order.point = at.point; order.facing = inbound == approach.outward ? -1 : 1; - order.side = order.facing; + order.side = signal.wrong ? -order.facing : order.facing; order.kind = block ? "block" : "warning"; - order.file = block ? block_template(heads, {}, aspects[signal.line]) : std::string{}; + order.file = block ? block_template(heads, {}, aspects[signal.line], signal.wrong) : std::string{}; order.name = signal.name; order.plate = signal.plate; order.linked = signal.linked; @@ -3154,7 +3407,7 @@ void srk_tool::set_lines(block_edit const *Edit) if (gone[k]) changes.removed.push_back(standing[k].instance); auto const removed{changes.removed.size()}; - // the block signals switched by the sections they guard + // the block signals switched by the sections they guard, the wrong-track one the 7th, or the one guard twice include_info control; std::string missing; auto const controlled{editor_includes::load(kControl, control, missing)}; @@ -3162,17 +3415,28 @@ void srk_tool::set_lines(block_edit const *Edit) { for (auto const &found : sections) { - if (found.guard.empty()) - continue; auto const already{std::find_if(controls.begin(), controls.end(), [&](standing_template const &Control) { return false == Control.values.empty() && ToLower(Control.values[0]) == found.name; })}; + if (found.guard.empty() && found.wrong_guard.empty()) + { + // guarded by station signals only, once its wrong-track signal is gone + if (already != controls.end()) + changes.removed.push_back(already->instance); + continue; + } + std::array const guards_of{found.guard.empty() ? found.wrong_guard : found.guard, found.wrong_guard.empty() ? found.guard : found.wrong_guard}; if (already != controls.end()) { - if (already->values.size() > 5 && ToLower(already->values[5]) != ToLower(found.guard)) - { - auto values{already->values}; - values[5] = found.guard; + auto values{already->values}; + values.resize(std::max(values.size(), 7)); + auto differ{false}; + for (std::size_t k = 0; k < guards_of.size(); ++k) + if (ToLower(values[5 + k]) != ToLower(guards_of[k])) + { + values[5 + k] = guards_of[k]; + differ = true; + } + if (differ) changes.values.emplace_back(already->instance, std::move(values)); - } continue; } template_item item; @@ -3180,7 +3444,8 @@ void srk_tool::set_lines(block_edit const *Edit) item.location = found.start; item.described = true; item.values["name"] = found.name; - item.values["#6"] = found.guard; + item.values["#6"] = guards_of[0]; + item.values["#7"] = guards_of[1]; changes.others.push_back(std::move(item)); } // those of the sections gone from the lines set taken away with them @@ -3230,8 +3495,11 @@ void srk_tool::set_lines(block_edit const *Edit) void srk_tool::survey_signals() { m_signal_marks.clear(); + m_signal_names.clear(); std::vector standing; m_signals_known = m_layout.has_value() && m_signals.standing_signals(standing, false); + for (auto const &signal : standing) + m_signal_names.insert(ToLower(signal.name)); if (false == m_signals_known) { m_heads.clear(); @@ -3797,6 +4065,12 @@ void srk_tool::fit_view(float const Width, float const Height) from = std::min(from, mark.at.x); to = std::max(to, mark.at.x); } + if (m_fit_lines) + for (auto const &row : m_lines) + { + from = std::min(from, row.chainage + row.outward * row.length); + to = std::max(to, row.chainage + row.outward * row.length); + } auto const span{std::max(50.0, to - from)}; view.from = from - span * 0.04 - 30.0; view.to = to + span * 0.04 + 30.0; @@ -3872,6 +4146,21 @@ void srk_tool::render_canvas(float const Height) } else if (false == hovered && false == ImGui::IsItemActive()) m_hover = -1; + // a line out of the station under the mouse, how far out along it + auto const line_x = [&](line_row const &Row, double const Distance) { return x_of(Row.chainage + Row.outward * Distance); }; + int line_hover{-1}; + double line_at{0.0}; + if (hovered && false == m_dragging && m_hover < 0) + for (std::size_t index = 0; index < m_lines.size() && index < m_line_ways.size() && line_hover < 0; ++index) + { + auto const &row{m_lines[index]}; + auto const distance{(view.from + (io.MousePos.x - corner.x) * across - row.chainage) * row.outward}; + if (std::abs(io.MousePos.y - y_of(row.offset)) < 6.0f && distance >= 0.0 && distance <= row.length) + { + line_hover = static_cast(index); + line_at = distance; + } + } m_length_canvas = m_hover >= 0 && false == m_dragging ? length_of(layout.paths[m_hover].track) : -1; if (ImGui::IsItemDeactivated()) { @@ -3882,6 +4171,12 @@ void srk_tool::render_canvas(float const Height) m_picked = path.track; m_view.focus_track(*path.track, path.index, path.points[path.points.size() / 2]); } + else if (false == m_dragging && line_hover >= 0) + { + auto const on{station::point_on(m_line_ways[line_hover], line_at)}; + auto const out{m_lines[line_hover].inbound ? -1.0 : 1.0}; + m_view.fly_to(on.point + glm::dvec3{0.0, 4.0, 0.0}, glm::normalize(glm::dvec3{on.way.x * out, -0.3, on.way.z * out}), 30.0); + } m_dragging = false; } @@ -3896,12 +4191,17 @@ void srk_tool::render_canvas(float const Height) for (auto const candidate : {10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 2000.0, 5000.0}) if ((step = candidate) >= metres) break; - auto const origin{layout.kilometrage ? layout.km * 1000.0 : 0.0}; - for (auto mark{std::ceil((view.from + origin) / step) * step}; mark <= view.to + origin; mark += step) + // the km of the station where it has them, else the chainage + double km_from{0.0}; + double km_to{0.0}; + auto const kilometrage{km_of_open(view.from, km_from) && km_of_open(view.to, km_to) && std::abs(km_to - km_from) > 1e-6}; + auto const low{kilometrage ? std::min(km_from, km_to) * 1000.0 : view.from}; + auto const high{kilometrage ? std::max(km_from, km_to) * 1000.0 : view.to}; + for (auto mark{std::ceil(low / step) * step}; mark <= high; mark += step) { - auto const x{x_of(mark - origin)}; + auto const x{x_of(kilometrage ? view.from + (mark / 1000.0 - km_from) / (km_to - km_from) * (view.to - view.from) : mark)}; draw->AddLine(ImVec2(x, corner.y), ImVec2(x, corner_end.y), IM_COL32(255, 255, 255, 16)); - auto const label{layout.kilometrage ? format("%.3f", mark / 1000.0) : format("%.0f m", mark)}; + auto const label{kilometrage ? format("km %.3f", mark / 1000.0) : format("%.0f m", mark)}; draw->AddText(ImVec2(x + 3.0f, corner.y + 2.0f), IM_COL32(150, 155, 165, 255), label.c_str()); } } @@ -3921,6 +4221,29 @@ void srk_tool::render_canvas(float const Height) auto const at{screen_of(tick)}; draw->AddLine(ImVec2(at.x, at.y - 5.0f), ImVec2(at.x, at.y + 5.0f), IM_COL32(255, 255, 255, 220), 2.0f); } + // the lines out straightened beyond the entry signals: the block sections in the colours of their names, the joints between + // them, the block signals each a flag pointing the way of its trains + for (std::size_t index = 0; index < m_lines.size(); ++index) + { + auto const &row{m_lines[index]}; + auto const y{y_of(row.offset)}; + draw->AddLine(ImVec2(line_x(row, 0.0), y), ImVec2(line_x(row, row.length), y), kApproach, 2.0f); + for (std::size_t k = 0; k < row.sections.size(); ++k) + { + auto const &run{row.sections[k]}; + auto const lit{static_cast(index) == line_hover && line_at >= run.from && line_at <= run.to}; + draw->AddLine(ImVec2(line_x(row, std::max(0.0, run.from)), y), ImVec2(line_x(row, run.to), y), lit ? kHovered : circuit_colour(run.name), lit ? 4.0f : 2.5f); + for (auto const end : {run.from, run.to}) + if (end > 1.0 && end < row.length - 1.0) + draw->AddLine(ImVec2(line_x(row, end), y - 5.0f), ImVec2(line_x(row, end), y + 5.0f), kJoint, 2.0f); + } + for (auto const &post : row.posts) + { + auto const x{line_x(row, post.distance)}; + auto const way{static_cast(row.outward * (row.inbound ? -1.0 : 1.0) * (post.wrong ? -1.0 : 1.0))}; + draw->AddTriangleFilled(ImVec2(x - 4.0f * way, y - 4.0f), ImVec2(x - 4.0f * way, y + 4.0f), ImVec2(x + 4.0f * way, y), kSignal); + } + } for (auto const &path : layout.paths) if (path.track == selected) draw_path(path, overlay_color::selected, 4.0f); @@ -3999,6 +4322,27 @@ void srk_tool::render_canvas(float const Height) draw->AddTriangleFilled(ImVec2(at.x - 4.0f * way, at.y - 4.0f), ImVec2(at.x - 4.0f * way, at.y + 4.0f), ImVec2(at.x + 4.0f * way, at.y), colour); label(mark.unfit ? mark.text + " !" : mark.text, colour, {ImVec2(at.x, at.y - height - 5.0f), ImVec2(at.x, at.y + 5.0f)}); } + // the block signals of the lines with their km, the lengths of the block sections, on the outside of the line: over the upper + // track of a double-track one, under the lower + for (auto const &row : m_lines) + { + auto const y{y_of(row.offset)}; + auto const up{row.partner < 0 || row.partner >= static_cast(m_lines.size()) || row.offset >= m_lines[row.partner].offset}; + auto const outside = [&](float const Tall, float const Level) { return up ? y - 5.0f - (Tall + 3.0f) * Level + 3.0f : y + 5.0f + (Tall + 3.0f) * (Level - 1.0f); }; + auto const tall{height * (row.km_known ? 2.0f : 1.0f)}; + for (auto const wrong_track : {false, true}) + for (auto const &post : row.posts) + if (post.wrong == wrong_track) + { + auto const x{line_x(row, post.distance)}; + label(row.km_known ? post.name + "\n" + format("%.3f", post.km) : post.name, kSignal, {ImVec2(x, outside(tall, 1.0f)), ImVec2(x, outside(tall, 2.0f))}); + } + for (auto const &run : row.sections) + { + auto const x{line_x(row, (std::max(0.0, run.from) + run.to) * 0.5)}; + label(format("%.2f km", (run.to - std::max(0.0, run.from)) / 1000.0), circuit_colour(run.name), {ImVec2(x, outside(height, 1.0f)), ImVec2(x, outside(height, 2.0f))}); + } + } std::vector order; for (std::size_t index = 0; index < m_circuits.size(); ++index) if (m_circuits[index].labelled) @@ -4030,6 +4374,25 @@ void srk_tool::render_canvas(float const Height) ImGui::TextDisabled("%s", STR_C("LMB: select it")); ImGui::EndTooltip(); } + else if (line_hover >= 0) + { + auto const &row{m_lines[line_hover]}; + ImGui::BeginTooltip(); + ImGui::TextUnformatted((format(STR_C("The line out beyond the entry signal %s"), row.entry.c_str()) + ", " + (row.number == 0 ? std::string{STR_C("single track")} : format(STR_C("track %d"), row.number))).c_str()); + auto const run{std::find_if(row.sections.begin(), row.sections.end(), [&](line_row::run const &Run) { return line_at >= Run.from && line_at <= Run.to; })}; + if (run != row.sections.end()) + { + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(circuit_colour(run->name)), STR_C("Block section %s, %.2f km"), run->name.c_str(), (run->to - std::max(0.0, run->from)) / 1000.0); + if (row.km_known) + ImGui::TextDisabled(STR_C("km %.3f to %.3f"), std::min(run->km_from, run->km_to), std::max(run->km_from, run->km_to)); + } + else + ImGui::TextDisabled("%s", STR_C("No track circuit here")); + if (row.km_known && line_hover < static_cast(m_line_ways.size())) + ImGui::TextDisabled("km %.3f", km_along(m_line_ways[line_hover], row.km_number, line_at)); + ImGui::TextDisabled("%s", STR_C("LMB: show it in the 3d view")); + ImGui::EndTooltip(); + } } void srk_tool::render_windows() @@ -4053,9 +4416,21 @@ void srk_tool::render_windows() if (false == ImGui::IsWindowDocked() && (position.y + size.y > io.DisplaySize.y + 1.0f || position.x + 80.0f > io.DisplaySize.x || position.x + size.x < 80.0f)) ImGui::SetWindowPos(ImVec2(std::clamp(position.x, 0.0f, std::max(0.0f, io.DisplaySize.x - size.x)), std::max(0.0f, std::min(position.y, io.DisplaySize.y - size.y)))); if (ImGui::Button(STR_C("Fit"))) + { + m_fit_lines = false; m_view_state.exaggeration = 0.0f; + } item_tooltip("Shows the whole station"); ImGui::SameLine(); + ImGui::BeginDisabled(m_lines.empty()); + if (ImGui::Button(STR_C("Lines"))) + { + m_fit_lines = true; + m_view_state.exaggeration = 0.0f; + } + ImGui::EndDisabled(); + item_tooltip("Shows the station with its lines out to the next stations: their block sections, the block signals with their km"); + ImGui::SameLine(); ImGui::SetNextItemWidth(140.0f); ImGui::SliderFloat("##srkexaggeration", &m_view_state.exaggeration, 1.0f, 500.0f, STR_C("offsets x%.0f"), ImGuiSliderFlags_Logarithmic); item_tooltip("How much the offsets of the tracks are stretched against the lengths, also Ctrl+wheel over the plot"); @@ -4359,7 +4734,8 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri } if (Command == "srkblock") { - // srkblock entry= kind=sbl|sbl4|semi|auto: the block of the line out beyond the entry signal, the lines listed + // srkblock entry= [kind=sbl|sbl4|semi|auto] [ways=1|2|auto]: the block of the line out beyond the entry signal, both ways or + // not with the other track of the line, the lines listed if (m_history.history_revision() != m_history_revision) refresh(); if (false == m_layout.has_value()) @@ -4378,17 +4754,31 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri record_of_open().blocks[entry->second] = kind->second == "sbl4" ? block_kind::four : kind->second == "sbl" ? block_kind::three : block_kind::semi; m_changed = true; } + if (auto const ways{values.find("ways")}; entry != values.end() && ways != values.end()) + { + auto const row{std::find_if(m_lines.begin(), m_lines.end(), [&](line_row const &Row) { return Row.entry == entry->second; })}; + std::vector entries{entry->second}; + if (row != m_lines.end() && row->partner >= 0 && row->partner < static_cast(m_lines.size())) + entries.push_back(m_lines[row->partner].entry); + for (auto const &name : entries) + if (ways->second == "auto") + record_of_open().ways.erase(name); + else + record_of_open().ways[name] = ways->second == "2"; + m_changed = true; + } survey_signals(); if (false == m_lines_problem.empty()) WriteLog("SELFTEST srkblock: " + m_lines_problem); + auto const listed = [](std::vector> const &Stops) { + std::string text; + for (auto const &[distance, name] : Stops) + text += format(" %s@%.0f", name.c_str(), distance); + return text; + }; for (auto const &row : m_lines) - WriteLog(format("SELFTEST srkblock %s track %d %s to %s %.0f m blocks %zu: %s why %d own %d leftovers %zu legacy %d stops%s", row.entry.c_str(), row.number, row.inbound ? "in" : "out", row.next.empty() ? "-" : row.next.c_str(), row.length, row.blocks, - row.block == block_kind::four ? "sbl4" : row.block == block_kind::three ? "sbl" : "semi", static_cast(row.why), static_cast(row.own), row.leftovers, row.legacy ? 1 : 0, [&] { - std::string text; - for (auto const &[distance, name] : row.stops) - text += format(" %s@%.0f", name.c_str(), distance); - return text; - }().c_str())); + WriteLog(format("SELFTEST srkblock %s track %d %s to %s %.0f m blocks %zu: %s why %d own %d ways %d%s leftovers %zu legacy %d stops%s wrong%s", row.entry.c_str(), row.number, row.inbound ? "in" : "out", row.next.empty() ? "-" : row.next.c_str(), row.length, row.blocks, + row.block == block_kind::four ? "sbl4" : row.block == block_kind::three ? "sbl" : "semi", static_cast(row.why), static_cast(row.own), row.both ? 2 : 1, row.both_hand ? " hand" : "", row.leftovers, row.legacy ? 1 : 0, listed(row.stops).c_str(), listed(row.wrong).c_str())); return editor_selftest_result::handled; } if (Command == "srkboundary") @@ -4422,6 +4812,28 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri m_drawing = false; return editor_selftest_result::handled; } + if (Command == "srkfit") + { + // srkfit lines|station|from= to=: the schematic fitting the lines out, the station, or the chainage between + std::string word; + std::string mode; + std::map range; + while (Words >> word) + if (auto const equals{word.find('=')}; equals != std::string::npos) + range[word.substr(0, equals)] = word.substr(equals + 1); + else + mode = word; + m_fit_lines = mode == "lines"; + m_view_state.exaggeration = 0.0f; + if (range.count("from") > 0 && range.count("to") > 0 && m_layout.has_value()) + { + fit_view(1000.0f, 400.0f); + m_view_state.from = std::atof(range.at("from").c_str()); + m_view_state.to = std::atof(range.at("to").c_str()); + } + WriteLog("SELFTEST srkfit " + mode); + return editor_selftest_result::handled; + } if (Command == "srkdraw") { // srkdraw on|off: the block boundaries drawn by hand, or not @@ -4462,11 +4874,12 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri ++bad; WriteLog("SELFTEST srkcheck: block section " + control.values[0] + " twice"); } - if (names.count(ToLower(control.values[5])) == 0) - { - ++bad; - WriteLog("SELFTEST srkcheck: block section " + control.values[0] + " switched by " + control.values[5] + ", which doesn't stand"); - } + for (std::size_t index = 5; index < control.values.size() && index < 7; ++index) + if (names.count(ToLower(control.values[index])) == 0) + { + ++bad; + WriteLog("SELFTEST srkcheck: block section " + control.values[0] + " switched by " + control.values[index] + ", which doesn't stand"); + } } WriteLog(format("SELFTEST srkcheck: %zu signals, %zu block sections, %zu bad", standing.size(), controls.size(), bad)); return editor_selftest_result::handled; diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 89399483..8329cf73 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -80,6 +80,7 @@ class srk_tool final : public editor_tool bool reversed{false}; // the km rise to the left of the schematic, the switches numbered from there int braking{0}; // m, the braking distance of the line set by hand, 0: after its speed std::map blocks; // of the lines by their entry signals, as set by hand + std::map ways; // the automatic block of the lines by their entry signals both ways, as set by hand }; // the schematic: the chainage across, the offsets upwards, exaggerated struct view @@ -101,6 +102,7 @@ class srk_tool final : public editor_tool std::size_t m_history_revision{0}; TTrack const *m_picked{nullptr}; // the track selected when the station was looked for view m_view_state; + bool m_fit_lines{false}; // the schematic fits the lines out to the next stations too int m_hover{-1}; // path of the layout under the mouse in the schematic bool m_dragging{false}; std::vector> m_found; // the stations of the scenery: a switch and the count of them @@ -193,16 +195,45 @@ class srk_tool final : public editor_tool std::string next; // the entry signal of the next station facing out along it, shown; empty for none bool station{false}; // ends at the next station rather than at the end of the tracks std::size_t blocks{0}; // the block signals standing along it - std::vector> stops; // the same, out along it, shown + // the block boundaries of the same facing its trains, the joints they turn them to stop at, out along it, with the signal shown + std::vector> stops; + std::vector> wrong; // the block signals facing its wrong-track trains, out along it, shown block_kind block{block_kind::semi}; block_reason why{block_reason::single}; // the block it has, so block_kind own{block_kind::semi}; // the block it has by itself, unless set by hand block_reason own_why{block_reason::single}; + // the automatic block both ways: at each block boundary a block signal of each track either way, the wrong-track one with N + bool both{false}; + bool both_hand{false}; // so set by hand + bool own_both{false}; // so by itself // block signals stand along it, none switched by block sections of the editor: it keeps the block of the scenery as it stands bool legacy{false}; double braking{0.0}; // m, the braking distance on it std::string km_number; // the line of the kilometrage it runs along bool rising{true}; // its trains run towards the higher km + bool km_known{false}; // the km out along it are known + // in the schematic, straightened: the chainage of the entry signal, 1 or -1 the way out along the chainage, the offset of the + // track there + double chainage{0.0}; + double outward{1.0}; + double offset{0.0}; + struct run + { + double from{0.0}; // out along the line + double to{0.0}; + std::string name; + double km_from{0.0}; + double km_to{0.0}; + }; + std::vector sections; // the track circuits along it, out from the station + struct post + { + double distance{0.0}; // out along the line + std::string name; // shown + bool wrong{false}; // facing the wrong-track trains + double km{0.0}; + }; + std::vector posts; // the block signals along it // the signals of the other block standing along it: warning discs on a line of the automatic block, block signals on one of the // semi-automatic one, the first of them std::size_t leftovers{0}; @@ -211,6 +242,7 @@ class srk_tool final : public editor_tool }; std::vector m_lines; std::vector m_line_ways; // of the lines listed + std::unordered_set m_signal_names; // of the signals of the scenery, lowercase: a block signal drawn on one's number takes a letter std::string m_lines_problem; // why the station has no lines listed bool m_drawing{false}; // the block boundaries drawn by hand // a block boundary under the mouse while they are drawn: block signals across the line there, or the ones standing taken away @@ -219,10 +251,15 @@ class srk_tool final : public editor_tool bool valid{false}; bool remove{false}; std::string entry; // of the line under the mouse, its entry signal shown - glm::dvec3 point{0.0}; // on its axis - std::vector places; // of the signals, on the axes of the tracks - std::vector ways; // the trains run there - std::vector names; + glm::dvec3 point{0.0}; // the joint, on its axis + std::vector> joints; // of each track, on its axis, the way out along it + struct post + { + glm::dvec3 point{0.0}; // on the axis + glm::dvec3 side{1.0, 0.0, 0.0}; // the signal stands off the axis so + std::string name; + }; + std::vector posts; // of the block signals }; boundary m_boundary; // a block boundary drawn on a line of the automatic block of the open station @@ -283,6 +320,9 @@ class srk_tool final : public editor_tool // aspects where one of them has, else on a double-track line, of the aspects of the block signal chosen in the Signals tool, else // the semi-automatic one; Own: the block it has by itself block_kind block_of(station::line const &Line, std::string const &Entry, std::vector const &Standing, std::vector const &Heads, block_reason &Why, block_kind &Own, block_reason &Own_why, std::size_t &Blocks) const; + // the automatic block of the double-track line both ways: as set by hand for its entry signal, else where block signals stand + // along it, when some of them face its wrong-track trains, else on a new line; Own: so by itself + bool both_ways(std::vector const &Lines, std::size_t Index, std::string const &Entry, std::vector const &Standing, bool &Hand, bool &Own) const; // the line of the kilometrage a line out of the open station runs along: of the station, at a junction another one std::string km_line(station::line const &Line) const; // the km out along the line, Number: of the line of its kilometrage diff --git a/application/editortool.h b/application/editortool.h index 69114202..165b45bb 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -117,6 +117,7 @@ struct signal_head std::vector speeds; // of the signal itself, km/h: -1 the highest allowed, 20 the substitute signal bool chosen{false}; // the Signals tool places it for its kind int aspects{0}; // of a block signal: 3, or 4 with the warning two signals ahead + std::string lean; // its head leans towards the track: left, standing on the right of it, right, or none }; // one step of the history of the signalling: signals placed, standing ones renamed, other templates placed with them, the values // of standing ones replaced diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 5993edaa..2019360c 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11278,8 +11278,47 @@ msgstr "Blokada szlaku, na obu torach dwutorowego taka sama. Samoczynna (SBL): s msgid "Draw the block boundaries" msgstr "Rysuj granice odstępów" -msgid "LMB on a track of a line of the automatic block, beyond the entry signal: block signals across both tracks there, numbered after the hectometre ahead of them, the tracks cut 15 m beyond them and the new block sections named; LMB on a block boundary standing takes its signals away, the sections either side joined. A line whose block signals stand keeps them: Set the line signals places them evenly only on a line without any. One step of undo each" -msgstr "LPM na torze szlaku z SBL, za semaforem wjazdowym: semafory SBL na obu torach w tym miejscu, numerowane po hektometrze przed nimi, tory przecięte 15 m za nimi i nazwane nowe odstępy. LPM na istniejącej granicy zabiera jej semafory, a odstępy po obu stronach łączą się w jeden. Szlak, na którym stoją już semafory SBL, je zachowuje: Ustaw sygnalizatory szlakowe rozstawia je równo tylko na szlaku bez żadnego. Każde kliknięcie to jeden krok cofania" +msgid "LMB on a track of a line of the automatic block, beyond the entry signal: a block boundary there, the tracks cut across the line, block signals 15 m before the joint, numbered after the hectometre ahead of them, on a line both ways the wrong-track ones 15 m beyond it, with N, and the new block sections named; LMB on a block boundary standing takes its signals away, the sections either side joined. A line whose block signals stand keeps them: Set the line signals places them evenly only on a line without any. One step of undo each" +msgstr "LPM na torze szlaku z SBL, za semaforem wjazdowym: granica odstępu w tym miejscu, tory przecięte w poprzek szlaku, semafory SBL 15 m przed złączem, numerowane po hektometrze przed nimi, na szlaku w obu kierunkach także semafory dla jazdy po torze lewym 15 m za złączem, z N, i nazwane nowe odstępy. LPM na istniejącej granicy zabiera jej semafory, a odstępy po obu stronach łączą się w jeden. Szlak, na którym stoją już semafory SBL, je zachowuje: Ustaw sygnalizatory szlakowe rozstawia je równo tylko na szlaku bez żadnego. Każde kliknięcie to jeden krok cofania" + +msgid "Wrong-track running" +msgstr "Jazda po torze lewym" + +msgid "SBL both ways" +msgstr "SBL dwukierunkowa" + +msgid "Mixed" +msgstr "Mieszana" + +msgid "At each block boundary the block signals of both tracks either way: of the trains of the track 15 m before the joint, for the wrong-track ones 15 m beyond it, with N (523N), on the outside of the line, as on the lines of PKP PLK" +msgstr "Na każdej granicy odstępu semafory SBL obu torów w obu kierunkach: dla jazdy po torze właściwym 15 m przed złączem, dla jazdy po torze lewym 15 m za nim, z N (523N), po zewnętrznej stronie szlaku, jak na liniach PKP PLK" + +msgid "The automatic block for the trains of the track, the semi-automatic one for the wrong-track ones: no N signals, a warning disc ToB the braking distance before the entry signal of the wrong-track trains, on the outside of the line. Set the line signals places it and takes the N signals away" +msgstr "SBL dla jazdy po torze właściwym, blokada półsamoczynna dla jazdy po torze lewym: bez semaforów N, tarcza ostrzegawcza ToB w drodze hamowania przed semaforem wjazdowym z toru lewego, po zewnętrznej stronie szlaku. Ustaw sygnalizatory szlakowe ją stawia i zabiera semafory N" + +msgid "Each line as it stands: a choice sets all of them" +msgstr "Każdy szlak tak, jak stoi: wybór ustawia wszystkie" + +msgid "Wrong-track running: SBL both ways, the block signals with N" +msgstr "Jazda po torze lewym: SBL dwukierunkowa, semafory z N" + +msgid "Wrong-track running: the mixed block, a warning disc before the entry signal" +msgstr "Jazda po torze lewym: blokada mieszana, tarcza ostrzegawcza przed semaforem wjazdowym" + +msgid "Shows the station with its lines out to the next stations: their block sections, the block signals with their km" +msgstr "Pokazuje stację ze szlakami do sąsiednich stacji: ich odstępy i semafory SBL z kilometrażem" + +msgid "Block section %s, %.2f km" +msgstr "Odstęp %s, %.2f km" + +msgid "km %.3f to %.3f" +msgstr "km %.3f do %.3f" + +msgid "No track circuit here" +msgstr "Tu nie ma odcinka izolowanego" + +msgid "LMB: show it in the 3d view" +msgstr "LPM: pokaż w widoku 3d" msgid "LMB on a track of a line beyond the entry signal: a block boundary there; on one standing: taken away" msgstr "LPM na torze szlaku za semaforem wjazdowym: granica odstępu w tym miejscu; na istniejącej granicy: usunięcie" From c024d7c262563bfa5283f421aa664796b41ebc18 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 21:32:34 +0200 Subject: [PATCH 075/145] feat(editor): the signals and the signs by a track go with it as the Plan lays it anew, square to the line, not along it A curve laid anew, its radius changed or made compound, or a straight moved: the paths of the chain are cut again where their joints stood, set square onto the new line, each keeping its stretch of the ground, so their events, their track circuits and the cuts of the block sections stay where they were instead of being handed on along the line; the parts between the switches alike, the old even sharing kept where the joints don't follow one another along the new line. The objects by a track taken for a change go with it by themselves: the signals and the warning discs it or its run reads, the SHP resonators by their events, and the controls of the block sections, the hectometre posts, the W17 markers and the other signs by the track nearest to them, within 12 m. They move square to the new axis keeping their offset and their turn, an object moved by hand meanwhile staying where it was put. Such a binding isn't kept in the scenery nor shown among the bound objects of the infra tool, which can still bind the object for good, and the undo and the redo take the objects back and forth with the tracks. The status of the Plan tells how many objects moved with the tracks. A path taking an object over the other way round keeps it on its side, turned as it was. --- application/editormode.h | 17 +++ application/editormodedigitize.cpp | 4 +- application/editormodeinfra.cpp | 208 ++++++++++++++++++++++++++-- application/editormoderoute.cpp | 65 +++++++-- application/editormodestraights.cpp | 8 +- editor/editorAlignment.cpp | 23 +++ editor/editorAlignment.hpp | 7 + editor/editorInfra.hpp | 3 + editor/editorTrack.cpp | 20 ++- editor/editorTrack.hpp | 5 +- lang/editor_pl.po | 6 + 11 files changed, 336 insertions(+), 30 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 161ee849..842cd07c 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -2372,6 +2372,23 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::vector m_infra_buffer; // states of the objects before the change in progress std::vector> m_drive_buffer; bool m_infra_suspended{false}; + // the point objects which go with a path: the signals and the warning discs it reads, the SHP resonators, and the controls of the + // block sections, the signs and the posts by the path nearest to them + struct point_object + { + scene::instance_handle include{0}; + glm::dvec3 location{0.0}; + TTrack *reader{nullptr}; + TTrack *nearest{nullptr}; // of one no path reads + infra::category group{infra::category::other}; + }; + std::vector m_point_objects; + std::size_t m_point_objects_revision{static_cast(-1)}; + std::size_t m_point_objects_count{0}; + void infra_adopt(TTrack &Track); + int infra_bound_count() const; // by the infra tool, not the ones a change of their track took along + std::set> m_infra_carried; // the objects their tracks moved since the tool cleared it + std::string infra_carried_note(); // a line of the status telling how many, empty for none; clears them void infra_load(); void infra_store(); void infra_forget(scene::basic_node const *Node); diff --git a/application/editormodedigitize.cpp b/application/editormodedigitize.cpp index 99ea6d26..75e76f81 100644 --- a/application/editormodedigitize.cpp +++ b/application/editormodedigitize.cpp @@ -389,9 +389,9 @@ std::string editor_mode::digitize_scenery(bool const Write) m_infra.scope = 3; infra_recognize(); report.found = static_cast(m_infra.candidates.size()); - auto const before{m_bindings.size()}; + auto const before{infra_bound_count()}; infra_bind_chosen(); - report.bound = static_cast(m_bindings.size() - before); + report.bound = infra_bound_count() - before; m_infra.scope = scope; } auto text{format(STR_C("Switches: %d, kept already %d, of the catalogue %d, measured %d, parts of slips %d, odd %d"), report.switches, report.known, report.designed, report.measured, report.slips, report.odd)}; diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index 1fef9e8c..abc9f30a 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -49,6 +49,11 @@ using geometry::plan_distance; using geometry::plan_of; std::string const kBindingMark{"//$b"}; +// the templates of the signalling which go with a path without a signal template of their own: the SHP resonator, read by its +// event, and the control of a block section, by the path nearest to it +std::string const kResonatorFile{"rezonator.inc"}; +std::string const kControlFile{"sbl_odstep.inc"}; +double constexpr kAdoptReach{12.0}; // m, of a point object from the axis of its path template bool contains(std::vector const &Items, Type_ const &Item) @@ -803,7 +808,7 @@ void editor_mode::infra_store() { std::vector points; std::optional yaw; - if (binding.anchors.empty() || false == object_points(binding, points, yaw)) + if (binding.adopted || binding.anchors.empty() || false == object_points(binding, points, yaw)) continue; auto layer{scene::Layers.resolve(binding.anchors.front().at.track->layer())}; if (false == scene::Layers.valid(layer) || false == scene::Layers.accepts(layer)) @@ -870,6 +875,18 @@ infra::binding *editor_mode::infra_find(infra::binding const &Binding) return nullptr; } +std::string editor_mode::infra_carried_note() +{ + auto const count{m_infra_carried.size()}; + m_infra_carried.clear(); + return count > 0 ? format(STR_P("\n%zu object by the tracks moved sideways with them", "\n%zu objects by the tracks moved sideways with them", count), count) : std::string{}; +} + +int editor_mode::infra_bound_count() const +{ + return static_cast(std::count_if(m_bindings.begin(), m_bindings.end(), [](infra::binding const &Binding) { return false == Binding.adopted; })); +} + void editor_mode::infra_recognize() { auto &state{m_infra}; @@ -977,7 +994,8 @@ void editor_mode::infra_recognize() Binding.label = object_label(Binding); candidate.offset = Binding.anchors.front().offset; candidate.reason = Reason; - candidate.bound = (infra_find(Binding) != nullptr); + auto const *existing{infra_find(Binding)}; + candidate.bound = (existing != nullptr && false == existing->adopted); candidate.chosen = (false == candidate.bound && Binding.group != infra::category::other && Binding.group != infra::category::environment); candidate.binding = std::move(Binding); state.read += candidate.reader != nullptr ? 1 : 0; @@ -1153,6 +1171,133 @@ void editor_mode::infra_buffer(infra::binding const &Binding, bool const Refresh m_infra_buffer.push_back(infra_state_of(Binding)); } +void editor_mode::infra_adopt(TTrack &Track) +{ + auto const revision{history_revision()}; + if (revision != m_point_objects_revision || scene::Layers.instance_count() != m_point_objects_count) + { + m_point_objects_revision = revision; + m_point_objects_count = scene::Layers.instance_count(); + m_point_objects.clear(); + std::unordered_map readers; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved) + for (int list = 0; list < 6; ++list) + for (auto const &event : editor_track::events(*track, list)) + if (false == event.first.empty()) + readers.emplace(ToLower(event.first), track); + static auto const rules{infra::default_rules()}; + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + include_place place; + if (false == include_place_of(handle, place) || place.values.empty() || false == scene::Layers.removable(handle)) + continue; + point_object object; + object.include = handle; + object.location = include_location(place); + auto const file{ToLower(place.file)}; + auto const *signal{signal_template_of(place.file)}; + std::string read; + if (signal != nullptr) + { + object.group = infra::category::signal; + read = signal->read.empty() ? std::string{} : editor_includes::substitute(signal->read, place.values); + } + else if (file == kResonatorFile) + { + object.group = infra::category::equipment; + read = place.values.front() + "_shp"; + } + else if (file == kControlFile) + object.group = infra::category::logic; + else + { + auto const named{infra::category_named(place.category)}; + object.group = named.has_value() ? *named : infra::classify(place.file + ' ' + place.description, rules); + if (object.group != infra::category::signal && object.group != infra::category::sign) + continue; + } + if (false == read.empty()) + if (auto const reader{readers.find(ToLower(read))}; reader != readers.end()) + object.reader = reader->second; + m_point_objects.push_back(std::move(object)); + } + // the path nearest to each one no path reads, the objects by cells of the plan + auto constexpr cell{200.0}; + auto const key = [&](double const Value) { return static_cast(std::floor(Value / cell)); }; + std::map, std::vector> cells; + for (std::size_t i = 0; i < m_point_objects.size(); ++i) + if (m_point_objects[i].reader == nullptr) + cells[{key(m_point_objects[i].location.x), key(m_point_objects[i].location.z)}].push_back(i); + std::vector nearest(m_point_objects.size(), kAdoptReach); + for (auto *track : simulation::Paths.sequence()) + { + if (cells.empty() || track == nullptr || track->m_editorremoved || false == editor_track::is_supported(*track)) + continue; + auto const area{area_of(*track)}; + auto const reach{area.radius + kAdoptReach}; + for (auto x = key(area.centre.x - reach); x <= key(area.centre.x + reach); ++x) + for (auto z = key(area.centre.z - reach); z <= key(area.centre.z + reach); ++z) + { + auto const found{cells.find({x, z})}; + if (found == cells.end()) + continue; + for (auto const i : found->second) + { + auto &object{m_point_objects[i]}; + if (plan_distance(object.location, area.centre) > area.radius + nearest[i]) + continue; + infra::station station; + bool interior; + auto const distance{infra::project(*track, object.location, station, interior)}; + if (distance < nearest[i] && beside(station, object.location, interior)) + { + nearest[i] = distance; + object.nearest = track; + } + } + } + } + } + auto const area{area_of(Track)}; + for (auto const &object : m_point_objects) + { + if (plan_distance(object.location, area.centre) > area.radius + kAdoptReach) + continue; + infra::binding binding; + binding.type = infra::kind::include; + binding.include = object.include; + if (infra_find(binding) != nullptr) + continue; + // of the line of the track: read by it or by a path of its run, else nearest to it; not one of the track alongside + if (object.reader != nullptr && object.reader != &Track) + { + auto const spans{editor_track::run_route(*object.reader, 250.0).spans}; + if (std::none_of(spans.begin(), spans.end(), [&](auto const &Span) { return Span.track == &Track; })) + continue; + } + if (object.reader == nullptr && object.nearest != &Track) + continue; + infra::station station; + bool interior; + if (infra::project(Track, object.location, station, interior) > kAdoptReach || false == beside(station, object.location, interior)) + continue; + std::vector points; + std::optional yaw; + if (false == object_points(binding, points, yaw) || points.empty()) + continue; + binding.group = object.group; + binding.anchors.push_back(infra::make_anchor(station, points.front())); + binding.turns = yaw.has_value() && object.group != infra::category::logic; + if (binding.turns) + binding.yaw = turn_of(binding, *yaw); + binding.label = object_label(binding); + binding.adopted = true; + binding.measured = points.front(); + m_bindings.push_back(std::move(binding)); + } +} + void editor_mode::track_captured(TTrack const &Track) { sweeps_captured(Track); @@ -1160,6 +1305,8 @@ void editor_mode::track_captured(TTrack const &Track) if (m_infra_suspended || false == m_infra.follow) return; infra_load(); + // the signals and the like of the track follow it, staying where they stand along it + infra_adopt(const_cast(Track)); if (m_bindings.empty()) return; Live.refresh(); @@ -1188,6 +1335,8 @@ void editor_mode::track_captured(TTrack const &Track) if (binding.turns && yaw.has_value() && binding.anchors.front().at.track == &Track) binding.yaw = turn_of(binding, *yaw); } + if (binding.adopted && false == points.empty()) + binding.measured = points.front(); infra_buffer(binding, true); } } @@ -1223,6 +1372,7 @@ void editor_mode::tracks_committed(std::vector const &Tracks) // only a path running where the old one did, not the one alongside return nearest_beside(*everywhere, Foot, 1.0, Station); }; + std::vector dropped; for (auto &binding : m_bindings) { auto const affected{std::any_of(binding.anchors.begin(), binding.anchors.end(), [&](infra::anchor const &Anchor) { @@ -1230,6 +1380,14 @@ void editor_mode::tracks_committed(std::vector const &Tracks) })}; if (false == affected || false == alive(binding)) continue; + std::vector standing; + std::optional facing; + if (binding.adopted && (false == object_points(binding, standing, facing) || standing.empty() || glm::distance(standing.front(), binding.measured) > 0.01)) + { + // moved by another tool while its track was being changed: it stays where that put it, bound again with the next change + dropped.push_back(binding.include); + continue; + } auto moved{binding}; auto lost{false}; for (auto &anchor : moved.anchors) @@ -1256,23 +1414,53 @@ void editor_mode::tracks_committed(std::vector const &Tracks) } auto const offset{anchor.offset}; auto const height{anchor.height}; + // a path taking the object over may run the other way: the object stays on its side, turned as it was + auto const before{plan_of(anchor.end - anchor.start)}; anchor = infra::make_anchor(station, anchor.foot); - anchor.offset = offset; + auto const reversed{glm::length(before) > 1e-6 && glm::dot(before, infra::frame_at(anchor.at).forward) < 0.0}; + anchor.offset = reversed ? -offset : offset; anchor.height = height; + if (reversed && &anchor == &moved.anchors.front()) + moved.yaw = clamp_circular(moved.yaw + 180.0); } if (lost) { - binding.lost = true; + if (binding.adopted) + dropped.push_back(binding.include); + else + binding.lost = true; continue; } + if (binding.adopted) + { + // where nothing moved, the directive stays as it is written + auto const there{infra::place(moved.anchors.front())}; + auto const turned{facing.has_value() && moved.turns ? std::abs(clamp_circular(infra::heading(infra::frame_at(moved.anchors.front().at).forward) + moved.yaw - *facing + 180.0) - 180.0) : 0.0}; + if (glm::distance(there, standing.front()) < 0.005 && turned < 0.01) + { + moved.lost = false; + binding = std::move(moved); + continue; + } + } infra_buffer(binding, false); moved.lost = false; binding = std::move(moved); infra_move(binding); + m_infra_carried.emplace(binding.node, binding.include); + if (binding.adopted) + { + std::vector points; + std::optional yaw; + if (object_points(binding, points, yaw) && false == points.empty()) + binding.measured = points.front(); + } for (auto &entry : m_infra_buffer) if (entry.saved.same(binding)) entry.moved = true; } + if (false == dropped.empty()) + m_bindings.erase(std::remove_if(m_bindings.begin(), m_bindings.end(), [&](infra::binding const &Binding) { return Binding.adopted && contains(dropped, Binding.include); }), m_bindings.end()); } void editor_mode::track_retired(TTrack &Track) @@ -1360,6 +1548,9 @@ void editor_mode::infra_attach(EditorSnapshot &Snapshot) auto const involved = [&](infra::object_state const &State) { if (State.moved) return true; + // one the editor bound for the change which didn't move: its own changes, if any, are taken back by the tool which made them + if (State.saved.adopted) + return false; for (auto const &anchor : State.saved.anchors) { if (contains(Snapshot.created, anchor.at.track) || contains(Snapshot.removed, anchor.at.track)) @@ -1394,7 +1585,7 @@ void editor_mode::render_infra_body() "models and includes turn with the path and stay upright. The move is a part of the undo step of the change.")); ImGui::SameLine(); ImGui::Checkbox(STR_C("Show in 3D"), &state.show); - ImGui::Text(STR_C("Objects bound in the scenery: %d"), static_cast(m_bindings.size())); + ImGui::Text(STR_C("Objects bound in the scenery: %d"), infra_bound_count()); if (false == m_bindings_unresolved.empty()) { ImGui::SameLine(); @@ -1612,9 +1803,9 @@ void editor_mode::render_infra_bound() std::unordered_set const tracks(scope.begin(), scope.end()); std::vector along; for (std::size_t i = 0; i < m_bindings.size(); ++i) - if (std::any_of(m_bindings[i].anchors.begin(), m_bindings[i].anchors.end(), [&](infra::anchor const &Anchor) { return tracks.count(Anchor.at.track) > 0; })) + if (false == m_bindings[i].adopted && std::any_of(m_bindings[i].anchors.begin(), m_bindings[i].anchors.end(), [&](infra::anchor const &Anchor) { return tracks.count(Anchor.at.track) > 0; })) along.push_back(i); - ImGui::Text(STR_C("%d of %d"), static_cast(along.size()), static_cast(m_bindings.size())); + ImGui::Text(STR_C("%d of %d"), static_cast(along.size()), infra_bound_count()); if (false == along.empty()) { ImGui::SameLine(); @@ -1678,7 +1869,8 @@ void editor_mode::draw_infra_overlay() const drawlist->AddCircleFilled(screen, Size, Colour); }; for (auto const &binding : m_bindings) - draw(binding, binding.lost ? overlay_color::invalid : IM_COL32(90, 230, 110, 220), 3.5f); + if (false == binding.adopted) + 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; diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index 187b55b5..8508b939 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -590,6 +590,7 @@ void editor_mode::route_lay() { auto &route{m_route}; route.status.clear(); + m_infra_carried.clear(); if (false == route.result.valid || route.chain.tracks.empty()) return; editor_track::chain chain; @@ -760,9 +761,10 @@ void editor_mode::route_lay() std::vector relaid; if (chain.switches.empty()) { - auto pieces{alignment::pieces(route.result, route.design, chain.tracks.size())}; + std::vector intervals; + auto pieces{alignment::pieces_at(route.result, route.design, chain.tracks.size(), editor_track::chain_joints(chain), intervals)}; editor_track::keep_heights(chain, pieces); - relaid = editor_track::relay(chain, pieces, &to); + relaid = editor_track::relay(chain, pieces, &to, &intervals); } else if (false == route_relay_through_switches(chain, states, relaid, to)) { @@ -793,6 +795,7 @@ void editor_mode::route_lay() route.status = added > 0 ? format(STR_P("Re-laid %zu path, added %zu", "Re-laid %zu paths, added %zu", chain.tracks.size()), chain.tracks.size(), added) : format(STR_P("Re-laid %zu path", "Re-laid %zu paths", chain.tracks.size()), chain.tracks.size()); if (startextension > 0.0 || endextension > 0.0) route.status += format(STR_C(", the curve took over %.2f / %.2f m of the adjoining straights"), startextension, endextension); + route.status += infra_carried_note(); // the crossovers taken out before which fit the lines now if (false == route.relaying && false == route.crossovers_out.empty()) { @@ -844,14 +847,14 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, } auto const oldlength{before}; std::vector placements; - std::vector breaks; + std::vector ends; // of the switches along the new line for (std::size_t k = 0; k < Chain.switches.size(); ++k) { auto const &passage{Chain.switches[k]}; auto const &main{passage.track->m_paths.front()}; auto const length{bezier{main}.plan_length()}; auto const from{starts[k] + length * 0.5 < oldlength * 0.5 ? starts[k] : result.length - (oldlength - starts[k])}; - if (from < 0.0 || from + length > result.length || (false == breaks.empty() && from < breaks.back() - 0.01)) + if (from < 0.0 || from + length > result.length || (false == ends.empty() && from < ends.back() - 0.01)) { route.error = format(STR_C("The switch %s doesn't fit on the designed line"), name(passage.track).c_str()); return false; @@ -874,22 +877,50 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, return false; } placements.push_back({passage.track, from, length, entry, angle}); - breaks.push_back(from); - breaks.push_back(from + length); + ends.push_back(from); + ends.push_back(from + length); } - // the regular parts between the switches, one interval each, the switches taking the ones between + // the regular parts between the switches, cut at the joints of their paths set square onto the new line where they follow one + // another along it, else one interval each; the switches taking the ones between auto const parts{editor_track::chain_parts(Chain)}; + std::vector breaks; std::vector counts; - std::vector partof; + std::vector> owners; // of each interval: the part, -1 for none, and its path, -1 for any std::size_t part{0}; for (std::size_t k = 0; k <= Chain.switches.size(); ++k) { auto const first{k == 0 ? std::size_t{0} : Chain.switches[k - 1].before}; auto const last{k == Chain.switches.size() ? Chain.tracks.size() : Chain.switches[k].before}; - counts.push_back(last - first); - partof.push_back(last > first ? static_cast(part++) : -1); + if (last > first && part < parts.size()) + { + auto const inner{alignment::breaks_at(result, editor_track::chain_joints(parts[part]), k == 0 ? 0.0 : ends[2 * k - 1], k == Chain.switches.size() ? result.length : ends[2 * k])}; + if (last - first > 1 && inner.size() + 1 == last - first) + for (std::size_t j = 0; j < last - first; ++j) + { + if (j > 0) + breaks.push_back(inner[j - 1]); + counts.push_back(1); + owners.emplace_back(static_cast(part), static_cast(j)); + } + else + { + counts.push_back(last - first); + owners.emplace_back(static_cast(part), -1); + } + ++part; + } + else + { + counts.push_back(last - first); + owners.emplace_back(-1, -1); + } if (k < Chain.switches.size()) + { + breaks.push_back(ends[2 * k]); + breaks.push_back(ends[2 * k + 1]); counts.push_back(1); + owners.emplace_back(-1, -1); + } } if (part != parts.size()) { @@ -899,9 +930,15 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, std::vector intervals; auto const pieces{alignment::pieces(result, route.design, 0, breaks, counts, &intervals)}; std::vector> groups(parts.size()); - for (std::size_t i = 0; i < pieces.size(); ++i) - if (intervals[i] % 2 == 0 && partof[intervals[i] / 2] >= 0) - groups[partof[intervals[i] / 2]].push_back(pieces[i]); + std::vector> paths(parts.size()); // of each piece of a part, its path, where its joints cut the line + for (std::size_t i = 0; i < pieces.size() && i < intervals.size(); ++i) + if (intervals[i] < owners.size() && owners[intervals[i]].first >= 0) + { + auto const &[owner, path]{owners[intervals[i]]}; + groups[owner].push_back(pieces[i]); + if (path >= 0) + paths[owner].push_back(static_cast(path)); + } for (std::size_t i = 0; i < parts.size(); ++i) if (groups[i].size() < parts[i].tracks.size()) { @@ -938,7 +975,7 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, auto group{groups[i]}; editor_track::keep_heights(parts[i], group); TTrack *last{nullptr}; - auto const added{editor_track::relay(parts[i], group, &last)}; + auto const added{editor_track::relay(parts[i], group, &last, paths[i].size() == group.size() ? &paths[i] : nullptr)}; if (last != nullptr && parts[i].tracks.back() == Chain.tracks.back()) Last = last; Created.insert(Created.end(), added.begin(), added.end()); diff --git a/application/editormodestraights.cpp b/application/editormodestraights.cpp index 239ced1e..d65d302c 100644 --- a/application/editormodestraights.cpp +++ b/application/editormodestraights.cpp @@ -786,6 +786,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co { auto &state{m_straights}; state.status.clear(); + m_infra_carried.clear(); struct span { TTrack *track; @@ -973,9 +974,10 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co state.status = refused(m_route.result.errors.empty() ? STR_C("The curve can't be fitted") : m_route.result.errors.front()); return; } - auto pieces{alignment::pieces(m_route.result, m_route.design, m_route.chain.tracks.size())}; + std::vector intervals; + auto pieces{alignment::pieces_at(m_route.result, m_route.design, m_route.chain.tracks.size(), editor_track::chain_joints(m_route.chain), intervals)}; editor_track::keep_heights(m_route.chain, pieces); - auto created{editor_track::relay(m_route.chain, pieces, &m_route.to)}; + auto created{editor_track::relay(m_route.chain, pieces, &m_route.to, &intervals)}; editor_track::commit(tracks); for (auto const &entry : splitstates) { @@ -996,7 +998,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co std::string error; editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error, true); straight_refresh(); - state.status = STR_C("Done: click a curve to adjust it; Ctrl+Z takes it back"); + state.status = STR_C("Done: click a curve to adjust it; Ctrl+Z takes it back") + infra_carried_note(); } double editor_mode::straight_tool_radius(editor_track::straight const &Line) const diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index c8a88a32..b0b501c1 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -2128,6 +2128,29 @@ trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector< return best; } +std::vector breaks_at(result const &Result, std::vector const &Points, double const From, double const To) +{ + double constexpr apart{0.2}; + std::vector breaks; + auto previous{From}; + for (auto const &at : project(Result, Points)) + { + if (at.chainage < previous + apart || at.chainage > To - apart) + return {}; + breaks.push_back(previous = at.chainage); + } + return breaks; +} + +std::vector pieces_at(result const &Result, design const &Design, std::size_t const Count, std::vector const &Joints, std::vector &Intervals) +{ + Intervals.clear(); + auto const breaks{breaks_at(Result, Joints, 0.0, Result.length)}; + if (breaks.empty() || breaks.size() != Joints.size()) + return pieces(Result, Design, Count); + return pieces(Result, Design, Count, breaks, std::vector(breaks.size() + 1, 1), &Intervals); +} + std::size_t minimum_pieces(result const &Result, design const &Design) { std::size_t count{0}; diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index a96c00b8..c91801af 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -217,6 +217,13 @@ std::size_t minimum_pieces(result const &Result, design const &Design); // one more than the breaks; Intervals receives the number of the interval of each piece std::vector pieces(result const &Result, design const &Design, std::size_t const Count, std::vector const &Breaks = {}, std::vector const &Counts = {}, std::vector *Intervals = nullptr); +// the points of the plan set square onto the line, the chainages of them in their order between From and To; empty where they +// don't follow one another along it +std::vector breaks_at(result const &Result, std::vector const &Points, double const From, double const To); +// the pieces of the line ending at the joints of the old paths set square onto it, so that each old path keeps its stretch of the +// ground: Intervals receives the old path of each piece, or stays empty where the joints don't follow one another along the line +// and the pieces are cut as by pieces() alone +std::vector pieces_at(result const &Result, design const &Design, std::size_t const Count, std::vector const &Joints, std::vector &Intervals); struct join_end { diff --git a/editor/editorInfra.hpp b/editor/editorInfra.hpp index a2f34c32..8bd7029d 100644 --- a/editor/editorInfra.hpp +++ b/editor/editorInfra.hpp @@ -117,6 +117,9 @@ struct binding bool turns{false}; // the object has a rotation which follows the path std::string label; bool lost{false}; // none of the paths around could take the object over + // bound by the editor itself when its track was taken for a change, as a signal its path reads: not kept in the scenery + bool adopted{false}; + glm::dvec3 measured{0.0}; // where an adopted object stood when its track was taken bool same(binding const &Other) const { diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 853ed3c7..edd60a4d 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -3346,18 +3346,22 @@ void editor_track::move_straight(straight const &Line, glm::dvec3 const &Start, } } -std::vector editor_track::relay(chain const &Chain, std::vector const &Pieces, TTrack **Last) +std::vector editor_track::relay(chain const &Chain, std::vector const &Pieces, TTrack **Last, std::vector const *Intervals) { std::vector created; auto const count{Chain.tracks.size()}; if (count == 0 || Pieces.size() < count) return created; + // each path of the chain keeps the stretch of the ground it had, where the pieces were cut at its joints: its events and its + // track circuit stay where they were, not handed on along the line + auto const along{Intervals != nullptr && Intervals->size() == Pieces.size() && Intervals->front() == 0 && Intervals->back() + 1 == count && + std::adjacent_find(Intervals->begin(), Intervals->end(), [](std::size_t const A, std::size_t const B) { return B < A || B > A + 1; }) == Intervals->end()}; std::vector tracks; std::size_t previous{count}; for (std::size_t i = 0; i < Pieces.size(); ++i) { - auto const index{i * count / Pieces.size()}; + auto const index{along ? (*Intervals)[i] : i * count / Pieces.size()}; TTrack *track; bool forward; if (index != previous) @@ -3382,6 +3386,18 @@ std::vector editor_track::relay(chain const &Chain, std::vector editor_track::chain_joints(chain const &Chain) +{ + std::vector joints; + for (std::size_t i = 0; i + 1 < Chain.tracks.size() && i < Chain.forward.size(); ++i) + { + auto const &path{Chain.tracks[i]->m_paths.front()}; + auto const &end{path.points[Chain.forward[i] ? segment_data::point::end : segment_data::point::start]}; + joints.emplace_back(end.x, end.z); + } + return joints; +} + TTrack *editor_track::clone(TTrack const &Template, std::string const &Name) { std::ostringstream text; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index acaf502a..b42a1dd0 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -506,7 +506,10 @@ class editor_track 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, TTrack **Last = nullptr); + // Intervals: the path of the chain each piece takes the place of, else they're handed out in turn + static std::vector relay(chain const &Chain, std::vector const &Pieces, TTrack **Last = nullptr, std::vector const *Intervals = nullptr); + // the joints between the paths of the chain in its order, in the plan + static std::vector chain_joints(chain const &Chain); static void retire(TTrack &Track); // a retired path back in the scenery, found by its name again; it's committed by the caller static void revive(TTrack &Track); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 2019360c..9abad58e 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -9098,6 +9098,12 @@ msgstr[0] "Ułożono na nowo %zu tor" msgstr[1] "Ułożono na nowo %zu tory" msgstr[2] "Ułożono na nowo %zu torów" +msgid "\n%zu object by the tracks moved sideways with them" +msgid_plural "\n%zu objects by the tracks moved sideways with them" +msgstr[0] "\n%zu obiekt przy torach przesunął się z nimi w bok" +msgstr[1] "\n%zu obiekty przy torach przesunęły się z nimi w bok" +msgstr[2] "\n%zu obiektów przy torach przesunęło się z nimi w bok" + msgid ", the curve took over %.2f / %.2f m of the adjoining straights" msgstr ", łuk zajął %.2f / %.2f m przyległych prostych" From de61cf05dbc6df2479aa938f0fd6537316e6a04b Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 21:53:41 +0200 Subject: [PATCH 076/145] test(editor): the orthophoto selftest takes the height of the layer, for sceneries far above zero --- application/editormodeselftestterrain.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/application/editormodeselftestterrain.cpp b/application/editormodeselftestterrain.cpp index 95244a8c..7cdbf4b0 100644 --- a/application/editormodeselftestterrain.cpp +++ b/application/editormodeselftestterrain.cpp @@ -61,9 +61,12 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co } int fit{1}; int inscene{0}; + float height{0.0f}; words >> config.north >> config.east >> config.radius >> fit >> inscene; config.drape = fit != 0; config.in_scene = inscene != 0; + if (words >> height) + config.height = height; m_orthophoto.settings(config); m_orthophoto.enabled(true); WriteLog(format("SELFTEST orthophoto at %.0f %.0f, radius %d, %s%s", config.north, config.east, config.radius, config.drape ? "fitted" : "flat", config.in_scene ? ", in the scene" : "")); From 2d9be3ae10e7948c573133278a8163d23c518130 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 22:40:01 +0200 Subject: [PATCH 077/145] feat(editor): a deleted track leaves its signal reads and its track circuit to the track laid in its place, and the SRK check lists the signals no track reads and the broken lines, with a Repair A deleted track whose read events or track circuit no other track took says so by the cursor, naming the signals and the circuits, and the editor keeps them for the track laid in its place, within a metre of its axis. The read of each signal goes to the piece by the signal, on the list its facing there gives, so a track laid the other way round keeps the trains reading it; the other events go to the piece at its middle, the circuit to a track lying there for the most part, a new track taking it instead of the one of the track it was made from. It happens in the step which lays the track, so Ctrl+Z takes it back and laying it again gets them again; a track which took a circuit over and is removed by another step leaves it to that step, and the events of a signal taken away stay off the tracks. The SRK panel of a station lists what it and its lines need: signals no track reads, lines broken off where the tracks go on beyond a gap, tracks of a line of the automatic block in no block section, block sections whose tracks are gone. A row shows the place; Repair has the track by each signal read it again, as moving it does, and sets the line signals again where tracks are in no block section. A warning after an operation shows by the cursor in amber, a refusal in red as before. srkcheck counts these too, srkfix repairs, join takes the height of the ends for sceneries far above zero. --- application/editormode.cpp | 8 + application/editormode.h | 19 ++ application/editormodeselftestjoin.cpp | 15 +- application/editormodesignal.cpp | 406 +++++++++++++++++++++++-- application/editormodesrk.cpp | 172 ++++++++++- application/editormodesrk.h | 16 + application/editormodetrack.cpp | 8 + application/editortool.h | 3 + application/editoruilayer.cpp | 31 +- application/editoruilayer.h | 4 + lang/editor_pl.po | 63 ++++ 11 files changed, 704 insertions(+), 41 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 171d726e..35742e4d 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1333,6 +1333,13 @@ bool editor_mode::update() { Global.editor_tracks = (current_work_area() == work_area::tracks); selftest_step(); + // after the tool has told its own status + if (false == m_orphan_note.empty()) + { + ui()->set_notice(m_orphan_note); + WriteLog("Editor: " + m_orphan_note); + m_orphan_note.clear(); + } if (m_vehicle.take_leave()) { Application.pop_mode(); @@ -6202,6 +6209,7 @@ void editor_mode::exit() g_redo.clear(); m_history.clear(); + m_orphans.clear(); m_fill_last.clear(); m_orthophoto.cancel_pending(); m_orthophoto.detach_scene(); // the layer is an editor aid, keep it out of the other modes diff --git a/application/editormode.h b/application/editormode.h index 842cd07c..d450a717 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1905,6 +1905,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool standing_signals(std::vector &Signals, bool const Wait) override; std::size_t signal_batch(signal_changes const &Changes, std::vector> States, std::vector Created, std::string &Error) override; bool signal_heads(std::vector &Heads, bool const Wait) override; + std::size_t signal_reattach(std::vector const &Instances) override; struct signal_template { @@ -1978,6 +1979,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri signal_template const *signal_template_of(std::string const &File) const; bool signal_spot_at(TTrack &Track, int const Path, glm::dvec3 const &Point, glm::dvec3 const &Cursor, int const Facing, int const Side, signal_template const &Template, signal_spot &Spot) const; void signal_refresh(bool const Force = false); + // the signals standing, read by the tracks as they are now, the templates as found so far + std::vector signal_standing_now() const; int signal_hit() const; void signal_press(track_intent const &Intent); void signal_release(); @@ -2001,6 +2004,22 @@ class editor_mode : public application_mode, private editor_track::observer, pri void draw_signal_overlay() const; std::string signal_intent_label(signal_template const &Template, signal_spot const &Spot) const; std::string signal_variant(signal_template const &Template, signal_spot const &Spot) const; + // what a deleted track carried and no other one took, its read events and track circuits, for a track laid in its place + struct orphan_record + { + TTrack *track{nullptr}; // retired; the record waits while it stays so + std::vector> events; // the name, the list it was on + std::vector circuits; + // the tracks given the circuits: while one stays the circuits don't wait, once a step other than an undo removes one they + // are its own + std::vector given; + bool fresh{false}; // made by the step being taken + }; + std::vector m_orphans; + std::string m_orphan_note; // shown by the cursor after the step which made it + void orphans_register(std::vector const &Removed, std::vector const &Kept); + // Created: made by the step, their circuits the ones of the tracks they were made from; Changed: the others it changed + void orphans_take_over(std::vector const &Created, std::vector const &Changed); // a model laid along the track: bent to follow it, or repeated along it struct sweep_tool diff --git a/application/editormodeselftestjoin.cpp b/application/editormodeselftestjoin.cpp index 892e301b..1695da02 100644 --- a/application/editormodeselftestjoin.cpp +++ b/application/editormodeselftestjoin.cpp @@ -55,6 +55,17 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma glm::dvec3 a{0.0}; glm::dvec3 b{0.0}; words >> a.x >> a.z >> b.x >> b.z; + double radius{0.0}; + int variant{0}; + int transitions{0}; + double height{0.0}; + if (command == "join") + { + words >> radius >> variant >> transitions; + // of the ends, for sceneries far above zero + if (words >> height) + a.y = b.y = height; + } auto const from{editor_track::find_free_end(nullptr, 1, a, 3.0, {})}; extend_tool tool; if (from.track == nullptr || false == free_end(*from.track, from.point.path, from.point.kind == editor_track::point_kind::end, tool)) @@ -67,10 +78,6 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma double length{0.0}; if (command == "join") { - double radius{0.0}; - int variant{0}; - int transitions{0}; - words >> radius >> variant >> transitions; auto const to{editor_track::find_free_end(nullptr, 1, b, 3.0, {from.track})}; if (to.track == nullptr) { diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 0082a5a0..c27b8a77 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "utilities/Globals.h" #include "utilities/utilities.h" +#include "world/Event.h" #include "world/Track.h" #include "imgui/imgui.h" @@ -48,6 +49,9 @@ float const kDragStart{6.0f}; double const kDragTravel{1.0}; double const kRefreshPeriod{2.0}; double const kSignalReach{10.0}; +// a track laid in the place of a deleted one runs within it of its axis +double const kOrphanReach{1.0}; +std::size_t const kOrphanRecords{64}; char const *const kKinds[] = {"main", "block", "warning", "shunt", "stop", "substitute", "repeater"}; char const *const kKindLabels[] = {"Semaphore", "Automatic block", "Warning signal", "Shunting signal", "Stop signal", "Substitute signal", "Repeater"}; @@ -222,6 +226,54 @@ void draw_signal_icon(ImDrawList *Draw, ImVec2 const Origin, ImVec2 const Size, Draw->AddCircleFilled(ImVec2(centre, headtop + 2.0f + lamp * 1.1f + i * lamp * 2.2f), lamp, lamp_colour(Lamps[i])); } + +// the point of an axis of the track nearest in the plan to the one given: how far, on which path, where on it +double plan_gap(TTrack const &Track, glm::dvec3 const &Point, int &Path, double &Parameter) +{ + auto best{std::numeric_limits::max()}; + for (int path = 0; path < static_cast(Track.m_paths.size()); ++path) + { + auto const parameter{nearest_on(Track, path, Point)}; + auto const distance{geometry::plan_distance(bezier{Track.m_paths[path]}.point(parameter), Point)}; + if (distance < best) + { + best = distance; + Path = path; + Parameter = parameter; + } + } + return best; +} + +bool plan_overlaps(TTrack const &A, TTrack const &B, double const Margin) +{ + auto const box = [](TTrack const &Track, glm::dvec2 &Low, glm::dvec2 &High) { + Low = glm::dvec2{std::numeric_limits::max()}; + High = -Low; + for (auto const &path : Track.m_paths) + { + bezier const curve{path}; + for (auto const &point : {curve.p0, curve.p1, curve.p2, curve.p3}) + { + Low = glm::min(Low, plan_of(point)); + High = glm::max(High, plan_of(point)); + } + } + }; + glm::dvec2 low_a, high_a, low_b, high_b; + box(A, low_a, high_a); + box(B, low_b, high_b); + return low_a.x - Margin <= high_b.x && low_b.x - Margin <= high_a.x && low_a.y - Margin <= high_b.y && low_b.y - Margin <= high_a.y; +} + +std::string listed(std::vector const &Names) +{ + std::string text; + for (auto const &name : Names) + text += (text.empty() ? "" : ", ") + name; + return text; +} + } // namespace bool editor_mode::signal_scan(bool const Wait) @@ -369,38 +421,7 @@ void editor_mode::signal_refresh(bool const Force) tool.standing_history = m_history.size(); tool.standing_redo = g_redo.size(); tool.standing_time = now; - tool.standing.clear(); - if (scene::Layers.empty()) - return; - auto const found{standing_templates([this](std::string const &File) { return signal_template_of(File) != nullptr; })}; - std::unordered_map> readers; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved) - continue; - for (int list = 0; list < 6; ++list) - for (auto const &event : editor_track::events(*track, list)) - if (false == event.first.empty()) - readers.emplace(ToLower(event.first), std::make_pair(track, list)); - } - for (auto const &include : found) - { - auto const *item{signal_template_of(include.file)}; - signal_standing standing; - standing.include = include; - standing.kind = kind_index(item->kind); - standing.name = include.values.empty() ? std::string{} : include.values.front(); - if (false == item->read.empty()) - { - standing.read = editor_includes::substitute(item->read, include.values); - if (auto const reader{readers.find(ToLower(standing.read))}; reader != readers.end()) - { - standing.track = reader->second.first; - standing.list = reader->second.second; - } - } - tool.standing.push_back(std::move(standing)); - } + tool.standing = signal_standing_now(); if (tool.selected != 0 && std::none_of(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })) tool.selected = 0; } @@ -513,6 +534,45 @@ void editor_mode::signal_move_update() tool.moved_valid = track != nullptr && signal_drop_at(*found, *track, path, held, 0, tool.moved); } +std::vector editor_mode::signal_standing_now() const +{ + std::vector standing; + if (scene::Layers.empty() || m_signal.templates.empty()) + return standing; + auto const found{standing_templates([this](std::string const &File) { return signal_template_of(File) != nullptr; })}; + std::unordered_map> readers; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved) + continue; + for (int list = 0; list < 6; ++list) + for (auto const &event : editor_track::events(*track, list)) + if (false == event.first.empty()) + readers.emplace(ToLower(event.first), std::make_pair(track, list)); + } + for (auto const &include : found) + { + auto const *item{signal_template_of(include.file)}; + if (item == nullptr) + continue; + signal_standing signal; + signal.include = include; + signal.kind = kind_index(item->kind); + signal.name = include.values.empty() ? std::string{} : include.values.front(); + if (false == item->read.empty()) + { + signal.read = editor_includes::substitute(item->read, include.values); + if (auto const reader{readers.find(ToLower(signal.read))}; reader != readers.end()) + { + signal.track = reader->second.first; + signal.list = reader->second.second; + } + } + standing.push_back(std::move(signal)); + } + return standing; +} + editor_mode::signal_standing const *editor_mode::signal_standing_of(scene::instance_handle const Instance) const { auto const &standing{m_signal.standing}; @@ -841,6 +901,287 @@ void editor_mode::signal_detach_events(std::string const &Read, std::vector const &Removed, std::vector const &Kept) +{ + auto const carries = [](TTrack &Track) { + for (int list = 0; list < 6; ++list) + for (auto const &event : editor_track::events(Track, list)) + if (false == event.first.empty()) + return true; + return false == Track.Isolated.empty(); + }; + // a record of a track removed again, after an undo brought it back, is the step's to make anew + m_orphans.erase(std::remove_if(m_orphans.begin(), m_orphans.end(), [&](orphan_record const &Record) { return std::find(Removed.begin(), Removed.end(), Record.track) != Removed.end(); }), m_orphans.end()); + // a track which took the circuits over, removed by the step, leaves them to what the step made + for (auto &record : m_orphans) + if (std::any_of(record.given.begin(), record.given.end(), [&](TTrack const *Track) { return std::find(Removed.begin(), Removed.end(), Track) != Removed.end(); })) + { + record.circuits.clear(); + record.given.clear(); + } + std::vector gone; + for (auto *track : Removed) + if (track != nullptr && track->m_editorremoved && false == track->m_paths.empty() && carries(*track)) + gone.push_back(track); + if (gone.empty()) + return; + // what the step moved onto the tracks left isn't lost + std::unordered_set carried; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved) + for (int list = 0; list < 6; ++list) + for (auto const &event : editor_track::events(*track, list)) + carried.insert(ToLower(event.first)); + std::unordered_set circuits; + for (auto *track : Kept) + if (track != nullptr && false == track->m_editorremoved) + for (auto const *isolated : track->Isolated) + if (isolated != nullptr) + circuits.insert(ToLower(isolated->asName)); + for (auto *track : gone) + { + orphan_record record; + record.track = track; + record.fresh = true; + for (int list = 0; list < 6; ++list) + for (auto const &event : editor_track::events(*track, list)) + { + auto const name{ToLower(event.first)}; + if (false == name.empty() && carried.count(name) == 0 && std::none_of(record.events.begin(), record.events.end(), [&](auto const &Event) { return Event.first == name; })) + record.events.emplace_back(name, list); + } + for (auto const *isolated : track->Isolated) + if (isolated != nullptr && circuits.count(ToLower(isolated->asName)) == 0) + record.circuits.push_back(isolated->asName); + if (record.events.empty() && record.circuits.empty()) + continue; + m_orphans.push_back(std::move(record)); + } + if (m_orphans.size() > kOrphanRecords) + m_orphans.erase(m_orphans.begin(), m_orphans.begin() + static_cast(m_orphans.size() - kOrphanRecords)); +} + +void editor_mode::orphans_take_over(std::vector const &Created, std::vector const &Changed) +{ + // a record waits while its track stays deleted, so the step laying another one in its place, undone and laid again, gets it again + if (std::none_of(m_orphans.begin(), m_orphans.end(), [](orphan_record const &Record) { return Record.track->m_editorremoved; })) + return; + std::vector candidates; + for (auto const *tracks : {&Created, &Changed}) + for (auto *track : *tracks) + if (track != nullptr && false == track->m_editorremoved && false == track->m_paths.empty() && track->Dynamics.empty() && editor_track::is_supported(*track) && + std::find(candidates.begin(), candidates.end(), track) == candidates.end()) + candidates.push_back(track); + if (candidates.empty() && std::none_of(m_orphans.begin(), m_orphans.end(), [](orphan_record const &Record) { return Record.fresh; })) + return; + std::unordered_set carried; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved) + for (int list = 0; list < 6; ++list) + for (auto const &event : editor_track::events(*track, list)) + carried.insert(ToLower(event.first)); + // not the list of the Signals tool, nor its templates once found: the step taken may hold one of them; looked for once an event + // waits + std::vector signals; + std::unordered_map readers; + auto known{false}; + auto const reader_of = [&](std::string const &Name) -> signal_standing const * { + if (false == known) + { + known = true; + if (m_signal.templates.empty()) + signal_scan(false); + signals = signal_standing_now(); + for (auto const &standing : signals) + if (false == standing.read.empty()) + readers.emplace(ToLower(standing.read), &standing); + } + auto const found{readers.find(Name)}; + return found != readers.end() ? found->second : nullptr; + }; + std::vector taken; + std::vector left; + std::size_t taken_events{0}; + std::size_t left_events{0}; + std::vector taken_circuits; + std::vector left_circuits; + std::size_t deleted{0}; + for (auto &record : m_orphans) + { + if (false == record.track->m_editorremoved) + { + record.fresh = false; + continue; + } + if (candidates.empty() && false == record.fresh) + continue; + auto const &old{*record.track}; + deleted += record.fresh ? 1 : 0; + for (auto const &[name, list] : record.events) + { + if (carried.count(name) > 0) + continue; + auto const *standing{reader_of(name)}; + auto got{false}; + // an event no longer there, of a signal taken away, stays off the tracks + if (false == candidates.empty() && simulation::Events.FindEvent(name) != nullptr) + { + // the place on the old axis: by the signal reading it, else the middle of the track + int old_path{0}; + double old_parameter{0.5}; + auto const by_signal{standing != nullptr && plan_gap(old, standing->include.location, old_path, old_parameter) <= kSignalReach}; + if (false == by_signal) + { + old_path = 0; + old_parameter = 0.5; + } + auto const anchor{bezier{old.m_paths[old_path]}.point(old_parameter)}; + auto const old_forward{forward_at(old, old_path, old_parameter)}; + TTrack *best{nullptr}; + int best_path{0}; + double best_parameter{0.0}; + auto nearest{kOrphanReach}; + for (auto *track : candidates) + { + int path{0}; + double parameter{0.0}; + auto const gap{plan_gap(*track, anchor, path, parameter)}; + if (gap <= nearest && std::abs(glm::dot(forward_at(*track, path, parameter), old_forward)) > 0.9) + { + nearest = gap; + best = track; + best_path = path; + best_parameter = parameter; + } + } + if (best != nullptr) + { + // laid the other way round, the trains running towards its end read it at its start + auto const reversed{glm::dot(forward_at(*best, best_path, best_parameter), old_forward) < 0.0}; + auto target{reversed && list % 3 != 0 ? list + (list % 3 == 1 ? 1 : -1) : list}; + if (by_signal) + { + auto const &location{standing->include.location}; + auto const axis{bezier{best->m_paths[best_path]}.point(nearest_on(*best, best_path, location))}; + auto const side{glm::dot(plan_of(location) - plan_of(axis), right_of(travel_of(standing->include.yaw))) >= 0.0 ? 1 : -1}; + signal_spot spot; + if (signal_drop_at(*standing, *best, best_path, location, side, spot)) + target = spot.list + (list >= 3 ? 3 : 0); + } + editor_track::events(*best, target).emplace_back(name, nullptr); + std::string missing; + editor_track::bind_events(*best, missing); + best->mark_dirty(); + carried.insert(name); + got = true; + } + } + if (got) + { + if (standing != nullptr) + taken.push_back(standing->name); + else + ++taken_events; + } + else if (record.fresh) + { + if (standing != nullptr) + left.push_back(standing->name); + else + ++left_events; + } + } + if (false == record.circuits.empty() && std::all_of(record.given.begin(), record.given.end(), [](TTrack const *Track) { return Track->m_editorremoved; })) + { + auto got{false}; + // a track lying in the place of the old one for the most part: a new one, in the circuit of the track it was made from, + // or one without a circuit + for (auto *track : candidates) + { + auto const made{std::find(Created.begin(), Created.end(), track) != Created.end()}; + if ((false == made && false == track->Isolated.empty()) || false == plan_overlaps(*track, old, 2.0)) + continue; + auto const same{track->Isolated.size() == record.circuits.size() && std::equal(record.circuits.begin(), record.circuits.end(), track->Isolated.begin(), [](std::string const &Name, TIsolated const *Isolated) { return Isolated != nullptr && ToLower(Isolated->asName) == ToLower(Name); })}; + if (same) + continue; + std::size_t on{0}; + std::size_t all{0}; + for (auto const &path : track->m_paths) + { + bezier const curve{path}; + for (int k = 0; k <= 32; ++k) + { + int old_path{0}; + double old_parameter{0.0}; + ++all; + if (plan_gap(old, curve.point(k / 32.0), old_path, old_parameter) <= kOrphanReach) + ++on; + } + } + if (all == 0 || on * 2 < all) + continue; + editor_track::isolated(*track, record.circuits); + if (std::find(record.given.begin(), record.given.end(), track) == record.given.end()) + record.given.push_back(track); + got = true; + } + if (got) + taken_circuits.insert(taken_circuits.end(), record.circuits.begin(), record.circuits.end()); + else if (record.fresh) + left_circuits.insert(left_circuits.end(), record.circuits.begin(), record.circuits.end()); + } + record.fresh = false; + } + auto const parts = [](std::vector const &Signals, std::size_t const Events, std::vector const &Circuits) { + std::vector part; + if (false == Signals.empty()) + part.push_back(format(STR_P("the read of signal %s", "the reads of signals %s", static_cast(Signals.size())), listed(Signals).c_str())); + if (Events > 0) + part.push_back(format(STR_P("%zu event", "%zu events", static_cast(Events)), Events)); + if (false == Circuits.empty()) + part.push_back(format(STR_P("the track circuit %s", "the track circuits %s", static_cast(Circuits.size())), listed(Circuits).c_str())); + return listed(part); + }; + std::string note; + if (auto const got{parts(taken, taken_events, taken_circuits)}; false == got.empty()) + note = format(STR_C("Taken over from the deleted track: %s"), got.c_str()); + if (auto const lost{parts(left, left_events, left_circuits)}; false == lost.empty()) + note += (note.empty() ? "" : "\n") + + format(STR_P("The deleted track carried %s: a track laid in its place takes it over, Ctrl+Z brings it back", "The deleted tracks carried %s: tracks laid in their place take it over, Ctrl+Z brings them back", static_cast(std::max(deleted, 1))), lost.c_str()); + if (false == note.empty()) + m_orphan_note += (m_orphan_note.empty() ? "" : "\n") + note; +} + +std::size_t editor_mode::signal_reattach(std::vector const &Instances) +{ + signal_refresh(true); + std::vector> states; + std::size_t attached{0}; + for (auto const instance : Instances) + { + auto const *standing{signal_standing_of(instance)}; + if (standing == nullptr || standing->read.empty() || standing->track != nullptr || simulation::Events.FindEvent(ToLower(standing->read)) == nullptr) + continue; + signal_spot spot; + if (false == signal_standing_spot(*standing, spot) || spot.track == nullptr || false == spot.track->Dynamics.empty()) + continue; + if (std::none_of(states.begin(), states.end(), [&](auto const &State) { return State.first == spot.track; })) + states.emplace_back(spot.track, editor_track::capture(*spot.track)); + auto &events{editor_track::events(*spot.track, spot.list)}; + if (std::none_of(events.begin(), events.end(), [&](auto const &Event) { return ToLower(Event.first) == ToLower(standing->read); })) + events.emplace_back(ToLower(standing->read), nullptr); + std::string missing; + editor_track::bind_events(*spot.track, missing); + spot.track->mark_dirty(); + ++attached; + } + if (states.empty()) + return 0; + push_track_snapshot(std::move(states)); + signal_refresh(true); + return attached; +} + bool editor_mode::signal_delete(scene::instance_handle const Instance) { if (false == scene::Layers.tracked(Instance) || false == scene::Layers.removable(Instance)) @@ -922,6 +1263,7 @@ bool editor_mode::standing_signals(std::vector &Signals, bool c signal.file = standing.include.file; signal.track = standing.track; signal.facing = standing.track == nullptr ? 0 : standing.list % 3 == 2 ? 1 : standing.list % 3 == 1 ? -1 : 0; + signal.read = standing.read; } return true; } diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 93730e7f..1db9f353 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -50,6 +50,7 @@ double constexpr kLabelRange{700.0}; // m, of the names shown there, farther one double constexpr kRailHead{0.18}; // m, above the axis of a path double constexpr kLineLimit{30000.0}; // m, of a line followed out to the next station +double constexpr kGapReach{1500.0}; // m, the tracks going on beyond the end of a line found within it double constexpr kBlockShare{1.1}; // of the braking distance, the block sections towards the end of the tracks double constexpr kPredicted{305.0}; // m, out from the next station to its entry signal, while it has none // the events of a block section, switching the block signal guarding it @@ -1151,6 +1152,7 @@ void srk_tool::render_options() metric_tiles({{format("%zu", layout.switches.size()), STR_C("switches"), kSwitch}, {format("%zu", tracks.size() - layout.switches.size()), STR_C("tracks"), kPlain}, {format("%.0f m", layout.to - layout.from), STR_C("length"), ImGui::GetColorU32(ImGuiCol_Text)}}); + render_problems(); if (begin_fields("##srkstation")) { auto const text = [&](char const *Label, char const *Id, std::string const &Value, char const *Hint, auto const &Set) { @@ -2784,6 +2786,10 @@ void srk_tool::survey_lines(std::vector const &Standing) if (k > 0 && row.sections.size() > 1 && row.sections.back().from == line.distances[k]) row.sections[row.sections.size() - 2].to = line.distances[k]; } + std::vector> bare; + for (std::size_t k = 1; k + 1 < row.sections.size(); ++k) + if (row.sections[k].name.empty() && row.sections[k].from >= 0.0 && row.sections[k].to - row.sections[k].from >= 1.0) + bare.emplace_back(row.sections[k].from, row.sections[k].to); row.sections.erase(std::remove_if(row.sections.begin(), row.sections.end(), [](line_row::run const &Run) { return Run.name.empty() || Run.to - std::max(0.0, Run.from) < 1.0; }), row.sections.end()); for (auto &run : row.sections) { @@ -2791,6 +2797,8 @@ void srk_tool::survey_lines(std::vector const &Standing) run.km_to = km_along(line, row.km_number, run.to); } row.legacy = false == row.stops.empty() && false == switched; + if (false == row.legacy && row.block != block_kind::semi) + row.bare = std::move(bare); auto const next{next_entry(line, Standing, row.length)}; row.next = next >= 0 ? signal_shown(Standing[next].name, stem) : std::string{}; m_lines.push_back(std::move(row)); @@ -2820,6 +2828,125 @@ void srk_tool::survey_lines(std::vector const &Standing) } } +void srk_tool::survey_problems(std::vector const &Standing) +{ + m_problems.clear(); + if (false == m_layout.has_value()) + return; + auto const stem{stem_of_open()}; + auto const plan = [](glm::dvec3 const &Point) { return glm::dvec2{Point.x, Point.z}; }; + auto const by_open = [&](glm::dvec3 const &Point) { + if (gap_to(*m_layout, Point) <= 8.0) + return true; + return std::any_of(m_line_ways.begin(), m_line_ways.end(), [&](station::line const &Line) { + double gap{0.0}; + auto const distance{station::distance_on(Line, Point, gap)}; + return gap <= 8.0 && distance > -50.0 && distance < Line.length() + 1.0; + }); + }; + for (auto const &signal : Standing) + if (false == signal.read.empty() && signal.track == nullptr && by_open(signal.position)) + m_problems.push_back({format(STR_C("Signal %s: no track reads it"), signal_shown(signal.name, stem).c_str()), signal.position, signal.instance}); + for (std::size_t index = 0; index < m_lines.size() && index < m_line_ways.size(); ++index) + { + auto const &row{m_lines[index]}; + auto const &line{m_line_ways[index]}; + if (false == row.station && line.points.size() > 1 && line.length() < kLineLimit - 50.0) + { + // the tracks go on beyond a gap: one missing, deleted maybe + auto const end{line.points.back()}; + auto const step{plan(line.points.back() - line.points[line.points.size() - 2])}; + if (glm::length(step) > 1e-9) + { + auto const way{glm::normalize(step)}; + std::vector own(line.tracks.begin(), line.tracks.end()); + for (auto const &target : editor_track::free_ends(nullptr, 1, end, kGapReach, own)) + { + auto const ahead{plan(target.position - end)}; + auto const along{glm::dot(ahead, way)}; + auto const direction{plan(target.direction)}; + if (along < 5.0 || along < glm::length(ahead) * 0.8 || glm::length(direction) < 1e-9 || std::abs(glm::dot(glm::normalize(direction), way)) < 0.7) + continue; + m_problems.push_back({format(STR_C("The line out of %s breaks off %.0f m beyond it, the tracks go on %.0f m farther"), row.entry.c_str(), line.length(), glm::length(ahead)), end}); + break; + } + } + } + for (auto const &bare : row.bare) + m_problems.push_back({format(STR_C("The line out of %s: %.0f m of tracks %.0f m beyond the signal are in no block section"), row.entry.c_str(), bare.second - bare.first, bare.first), station::point_on(line, (bare.first + bare.second) * 0.5).point, 0, true}); + } + // the block sections their controls switch, the tracks gone + std::unordered_set circuits; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved) + for (auto const *isolated : track->Isolated) + if (isolated != nullptr) + circuits.insert(ToLower(isolated->asName)); + for (auto const &control : m_templates.standing_templates([](std::string const &File) { return File == kControl; })) + if (false == control.values.empty() && circuits.count(ToLower(control.values[0])) == 0 && by_open(control.location)) + m_problems.push_back({format(STR_C("Block section %s has no track"), control.values[0].c_str()), control.location}); +} + +void srk_tool::render_problems() +{ + if (m_problems.empty()) + return; + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, kWrong); + auto const open{ImGui::CollapsingHeader(format("%s (%zu)###srkproblems", STR_C("To repair"), m_problems.size()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)}; + ImGui::PopStyleColor(); + item_tooltip("What the station and its lines need yet: signals no track reads, lines broken off where a track is missing, tracks of a line of the automatic block in no block section, block sections whose tracks are gone. Click a row to see the place"); + if (false == open) + return; + for (std::size_t index = 0; index < m_problems.size(); ++index) + { + auto const &problem{m_problems[index]}; + ImGui::PushStyleColor(ImGuiCol_Text, problem.signal != 0 || problem.lines ? kDoubtful : kWrong); + if (ImGui::Selectable((problem.text + "##srkproblem" + std::to_string(index)).c_str())) + m_view.fly_to(problem.at + glm::dvec3{0.0, 4.0, 0.0}, glm::normalize(glm::dvec3{0.3, -0.5, 0.8}), 60.0); + ImGui::PopStyleColor(); + } + auto const fixable{static_cast(std::count_if(m_problems.begin(), m_problems.end(), [](problem const &Problem) { return Problem.signal != 0 || Problem.lines; }))}; + if (fixable == 0) + { + ImGui::TextDisabled("%s", STR_C("A track missing is laid in the Track tool: laid in the place of a deleted one it takes over its events and its track circuit")); + return; + } + if (main_button(format("%s (%zu)###srkfix", STR_C("Repair"), fixable).c_str(), ImVec2(-1.0f, 0.0f))) + fix_problems(); + item_tooltip("The signals no track reads are read by the track they stand by again, as moving them does, the lines with tracks in no block section get their line signals set again. A track missing is laid in the Track tool. A step of undo each"); +} + +void srk_tool::fix_problems() +{ + std::vector signals; + auto lines{false}; + for (auto const &problem : m_problems) + { + if (problem.signal != 0) + signals.push_back(problem.signal); + lines = lines || problem.lines; + } + std::vector done; + if (false == signals.empty()) + { + auto const read{m_signals.signal_reattach(signals)}; + done.push_back(format(STR_P("%zu of %zu signal read again", "%zu of %zu signals read again", static_cast(signals.size())), read, signals.size())); + if (read < signals.size()) + done.back() += STR(": the rest stand farther than 10 m from a track, by vehicles or their events are gone"); + } + auto const status{done.empty() ? std::string{} : done.front()}; + if (lines) + { + m_status.clear(); + set_lines(); + if (false == status.empty()) + m_status = status + (m_status.empty() ? "" : "\n" + m_status); + } + else + m_status = status; +} + void srk_tool::set_lines(block_edit const *Edit) { m_status.clear(); @@ -3506,6 +3633,7 @@ void srk_tool::survey_signals() m_heads_known = false; m_lines.clear(); m_line_ways.clear(); + m_problems.clear(); m_lines_problem = STR("Looking for the signal templates..."); if (false == m_measured) measure({}); @@ -3513,6 +3641,7 @@ void srk_tool::survey_signals() } check_heads(standing); survey_lines(standing); + survey_problems(standing); auto const stem{stem_of_open()}; std::vector stops; for (auto const &signal : standing) @@ -4881,7 +5010,48 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri WriteLog("SELFTEST srkcheck: block section " + control.values[0] + " switched by " + control.values[index] + ", which doesn't stand"); } } - WriteLog(format("SELFTEST srkcheck: %zu signals, %zu block sections, %zu bad", standing.size(), controls.size(), bad)); + std::size_t unread{0}; + for (auto const &signal : standing) + if (false == signal.read.empty() && signal.track == nullptr) + { + ++unread; + WriteLog("SELFTEST srkcheck: signal " + signal.name + " read by no track, its event " + signal.read); + } + std::unordered_set circuits; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved) + for (auto const *isolated : track->Isolated) + if (isolated != nullptr) + circuits.insert(ToLower(isolated->asName)); + std::size_t bare{0}; + for (auto const &control : controls) + if (false == control.values.empty() && circuits.count(ToLower(control.values[0])) == 0) + { + ++bare; + WriteLog("SELFTEST srkcheck: block section " + control.values[0] + " without a track"); + } + bad += unread + bare; + if (m_layout.has_value()) + { + survey_signals(); + for (auto const &problem : m_problems) + WriteLog(format("SELFTEST srkcheck: open station: %s at %.1f %.1f%s%s", problem.text.c_str(), problem.at.x, problem.at.z, problem.signal != 0 ? " (signal)" : "", problem.lines ? " (lines)" : "")); + } + WriteLog(format("SELFTEST srkcheck: %zu signals, %zu block sections, %zu unread, %zu without a track, %zu problems of the open station, %zu bad", standing.size(), controls.size(), unread, bare, m_problems.size(), bad)); + return editor_selftest_result::handled; + } + if (Command == "srkfix") + { + // srkfix: the Repair of the check of the open station + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkfix: no station open"); + return editor_selftest_result::handled; + } + survey_signals(); + auto const before{m_problems.size()}; + fix_problems(); + WriteLog(format("SELFTEST srkfix: %zu problems before, status %s", before, m_status.c_str())); return editor_selftest_result::handled; } if (Command == "srkstrip") diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 8329cf73..742075fd 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -226,6 +226,7 @@ class srk_tool final : public editor_tool double km_to{0.0}; }; std::vector sections; // the track circuits along it, out from the station + std::vector> bare; // out along it, tracks between its block sections in none, of the automatic block struct post { double distance{0.0}; // out along the line @@ -244,6 +245,16 @@ class srk_tool final : public editor_tool std::vector m_line_ways; // of the lines listed std::unordered_set m_signal_names; // of the signals of the scenery, lowercase: a block signal drawn on one's number takes a letter std::string m_lines_problem; // why the station has no lines listed + // what the check of the open station finds wrong: signals no track reads, lines broken off, tracks between the block sections of + // a line in none, block sections without a track + struct problem + { + std::string text; + glm::dvec3 at{0.0}; + scene::instance_handle signal{0}; // Repair has a track read it again + bool lines{false}; // Repair sets the line signals again + }; + std::vector m_problems; bool m_drawing{false}; // the block boundaries drawn by hand // a block boundary under the mouse while they are drawn: block signals across the line there, or the ones standing taken away struct boundary @@ -334,6 +345,11 @@ class srk_tool final : public editor_tool // the lines out of the open station, for the panel void survey_lines(std::vector const &Standing); void render_lines(); + void survey_problems(std::vector const &Standing); + void render_problems(); + // the signals no track reads read by the tracks they stand by, the lines with tracks in no block section set again; a step of + // the history each + void fix_problems(); // the SHP track resonators placed where the signals checked need them and have none: 200 m before an entry signal, a block // signal and a warning disc, at an exit signal onto a line of the automatic block, the track cut where none of its joints lies // within 5 m, its event put on the track the trains run into there; one step of the history diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 7deed462..7e7a5447 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -277,6 +277,14 @@ void editor_mode::push_track_snapshot(std::vector changed; + for (auto const &entry : States) + changed.push_back(entry.first); + auto kept{Created}; + kept.insert(kept.end(), changed.begin(), changed.end()); + orphans_register(Removed, kept); + orphans_take_over(Created, changed); trim_history(); auto *track = false == States.empty() ? States.front().first : false == Created.empty() ? Created.front() : Removed.front(); EditorSnapshot snap; diff --git a/application/editortool.h b/application/editortool.h index 165b45bb..12213703 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -105,6 +105,7 @@ struct standing_signal std::string file; // of its template TTrack *track{nullptr}; // the trains read it on, nullptr while none does int facing{0}; // the trains it faces run 1: towards the end of the path of the track, -1: towards its start, 0 unknown + std::string read; // its event the trains read, empty for a template without one }; // a signal template as the signalling picks one: the lights it has and the speeds its events show struct signal_head @@ -216,6 +217,8 @@ class editor_signals virtual std::size_t signal_batch(signal_changes const &Changes, std::vector> States, std::vector Created, std::string &Error) = 0; // the signal templates with lights, false while they are still being looked for virtual bool signal_heads(std::vector &Heads, bool const Wait) = 0; + // the signals given which no track reads, read again by the track they stand by: one step of the history. returns: how many + virtual std::size_t signal_reattach(std::vector const &Instances) = 0; protected: ~editor_signals() = default; diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index 10dbe775..0a054eb6 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -1027,28 +1027,51 @@ void editor_ui::set_status(std::string const &Status, bool const Error) { m_layerspanel.status = Status; m_layerspanel.status_error = Error; + if (ImGui::GetCurrentContext() != nullptr) + m_statustime = ImGui::GetTime(); if (Error && false == Status.empty()) { m_refusal = Status; m_refusaltime = ImGui::GetTime(); + m_refusalnotice = false; } } +void editor_ui::set_notice(std::string const &Notice) +{ + if (Notice.empty() || ImGui::GetCurrentContext() == nullptr) + return; + auto &status{m_layerspanel.status}; + // the status of the operation it follows, told a moment ago, stays above it + if (ImGui::GetTime() - m_statustime < 1.0 && false == status.empty()) + status += "\n" + Notice; + else + { + status = Notice; + m_layerspanel.status_error = false; + } + m_statustime = ImGui::GetTime(); + m_refusal = Notice; + m_refusaltime = ImGui::GetTime(); + m_refusalnotice = true; +} + void editor_ui::render_refusal() { auto const age{ImGui::GetTime() - m_refusaltime}; - if (m_refusal.empty() || age > 4.0) + auto const shown{m_refusalnotice ? 7.0 : 4.0}; + if (m_refusal.empty() || age > shown) return; auto const &io{ImGui::GetIO()}; - auto const alpha{static_cast(std::clamp(4.0 - age, 0.0, 1.0))}; + auto const alpha{static_cast(std::clamp(shown - age, 0.0, 1.0))}; auto const wrap{std::round(ImGui::GetFontSize() * 30.0f)}; auto const size{ImGui::CalcTextSize(m_refusal.c_str(), nullptr, false, wrap)}; ImVec2 at{io.MousePos.x + 20.0f, io.MousePos.y - 24.0f - size.y}; at.x = std::clamp(at.x, 10.0f, std::max(10.0f, io.DisplaySize.x - size.x - 10.0f)); at.y = std::clamp(at.y, 10.0f, std::max(10.0f, io.DisplaySize.y - size.y - 10.0f)); auto *draw{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())}; - draw->AddRectFilled(ImVec2(at.x - 6.0f, at.y - 4.0f), ImVec2(at.x + size.x + 6.0f, at.y + size.y + 4.0f), IM_COL32(40, 0, 0, static_cast(210 * alpha)), 4.0f); - draw->AddText(ImGui::GetFont(), ImGui::GetFontSize(), at, IM_COL32(255, 150, 130, static_cast(255 * alpha)), m_refusal.c_str(), nullptr, wrap); + draw->AddRectFilled(ImVec2(at.x - 6.0f, at.y - 4.0f), ImVec2(at.x + size.x + 6.0f, at.y + size.y + 4.0f), m_refusalnotice ? IM_COL32(45, 32, 0, static_cast(215 * alpha)) : IM_COL32(40, 0, 0, static_cast(210 * alpha)), 4.0f); + draw->AddText(ImGui::GetFont(), ImGui::GetFontSize(), at, m_refusalnotice ? IM_COL32(255, 205, 110, static_cast(255 * alpha)) : IM_COL32(255, 150, 130, static_cast(255 * alpha)), m_refusal.c_str(), nullptr, wrap); } void editor_ui::render_rotation_controls() diff --git a/application/editoruilayer.h b/application/editoruilayer.h index 9fe7ee28..cfb9de43 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -162,6 +162,8 @@ class editor_ui : public ui_layer static void register_settings(); // shows outcome of an operation in the layers window void set_status(std::string const &Status, bool const Error = false); + // a warning after an operation done: added to its status, shown by the cursor for a while + void set_notice(std::string const &Notice); private: // methods @@ -211,6 +213,8 @@ class editor_ui : public ui_layer bool m_layoutreset{false}; // the default layout is to be built again on the next frame std::string m_refusal; double m_refusaltime{-100.0}; + bool m_refusalnotice{false}; // a warning rather than a refusal + double m_statustime{-100.0}; bool m_tooloptionsopen{true}; bool m_inspectoropen{true}; bool m_sceneopen{true}; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 9abad58e..4bf74539 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11394,3 +11394,66 @@ msgstr[2] "%zu dostało szablony o stawności swoich szlaków" msgid "Ctrl+Z takes it back" msgstr "Ctrl+Z cofa" + +msgid "the read of signal %s" +msgid_plural "the reads of signals %s" +msgstr[0] "odczyt semafora %s" +msgstr[1] "odczyty semaforów %s" +msgstr[2] "odczyty semaforów %s" + +msgid "%zu event" +msgid_plural "%zu events" +msgstr[0] "%zu zdarzenie" +msgstr[1] "%zu zdarzenia" +msgstr[2] "%zu zdarzeń" + +msgid "the track circuit %s" +msgid_plural "the track circuits %s" +msgstr[0] "odcinek izolowany %s" +msgstr[1] "odcinki izolowane %s" +msgstr[2] "odcinki izolowane %s" + +msgid "Taken over from the deleted track: %s" +msgstr "Przejęte po usuniętym torze: %s" + +msgid "The deleted track carried %s: a track laid in its place takes it over, Ctrl+Z brings it back" +msgid_plural "The deleted tracks carried %s: tracks laid in their place take it over, Ctrl+Z brings them back" +msgstr[0] "Usunięty tor niósł %s: tor ułożony w tym miejscu przejmie to sam, Ctrl+Z cofa" +msgstr[1] "Usunięte tory niosły %s: tory ułożone w tym miejscu przejmą to same, Ctrl+Z cofa" +msgstr[2] "Usunięte tory niosły %s: tory ułożone w tym miejscu przejmą to same, Ctrl+Z cofa" + +msgid "Signal %s: no track reads it" +msgstr "Semafor %s: żaden tor go nie odczytuje" + +msgid "The line out of %s breaks off %.0f m beyond it, the tracks go on %.0f m farther" +msgstr "Szlak za semaforem %s urywa się po %.0f m, tory biegną dalej %.0f m za przerwą" + +msgid "The line out of %s: %.0f m of tracks %.0f m beyond the signal are in no block section" +msgstr "Szlak za semaforem %s: %.0f m torów %.0f m za semaforem nie należy do żadnego odstępu" + +msgid "Block section %s has no track" +msgstr "Odstęp %s nie ma już toru" + +msgid "To repair" +msgstr "Do naprawy" + +msgid "What the station and its lines need yet: signals no track reads, lines broken off where a track is missing, tracks of a line of the automatic block in no block section, block sections whose tracks are gone. Click a row to see the place" +msgstr "Czego stacji i jej szlakom jeszcze brakuje: semafory, których żaden tor nie odczytuje, szlaki urwane tam, gdzie brakuje toru, tory szlaku z SBL poza odstępami, odstępy, których tory zniknęły. Kliknij wiersz, żeby zobaczyć miejsce" + +msgid "A track missing is laid in the Track tool: laid in the place of a deleted one it takes over its events and its track circuit" +msgstr "Brakujący tor układa się narzędziem Tor: ułożony w miejscu usuniętego przejmuje jego zdarzenia i odcinek izolowany" + +msgid "Repair" +msgstr "Napraw" + +msgid "The signals no track reads are read by the track they stand by again, as moving them does, the lines with tracks in no block section get their line signals set again. A track missing is laid in the Track tool. A step of undo each" +msgstr "Semafory, których żaden tor nie odczytuje, znów odczyta tor, przy którym stoją, tak jak po ich przesunięciu; szlakom z torami poza odstępami sygnalizacja zostanie ustawiona jeszcze raz. Brakujący tor układa się narzędziem Tor. Każda poprawka to jeden krok cofania" + +msgid "%zu of %zu signal read again" +msgid_plural "%zu of %zu signals read again" +msgstr[0] "Odczyt przywrócony: %zu z %zu semafora" +msgstr[1] "Odczyt przywrócony: %zu z %zu semaforów" +msgstr[2] "Odczyt przywrócony: %zu z %zu semaforów" + +msgid ": the rest stand farther than 10 m from a track, by vehicles or their events are gone" +msgstr ": pozostałe stoją dalej niż 10 m od toru, przy torze z pojazdami albo ich zdarzenia już nie istnieją" From eab3dca70084a1132cee90bb4611feb0a40282d5 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 12:26:07 +0200 Subject: [PATCH 078/145] feat(editor): the Signals tool goes into the signalling one: its templates under Signals in the SRK panel, the ones the station's signals are placed with too; a click on a tile or Place by a track arms placing, Esc or a click on the armed tile stops it, a click on a track else opening its station; a signal picked in 3D or in Signal heads shows its card at the top of the panel, its template, to the other side, turn around; H no longer a tool --- application/editormode.cpp | 16 ++--- application/editormode.h | 6 +- application/editormodeselftestmonkey.cpp | 2 +- application/editormodeselftestsignal.cpp | 6 +- application/editormodesignal.cpp | 77 +++++++++++++++++------- application/editormodesrk.cpp | 15 +++-- application/editormodesrk.h | 2 +- application/editormodetrack.cpp | 32 +++++----- application/editormodetrackux.cpp | 70 ++++++++++----------- application/editormodeviewport.cpp | 5 +- application/editortool.h | 8 ++- lang/editor_pl.po | 48 +++++++++------ 12 files changed, 168 insertions(+), 119 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 35742e4d..16a22c92 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1502,7 +1502,7 @@ bool editor_mode::update() draw_switch_slide(); if (m_track_tab == track_tab::turntable) draw_turntable_overlay(); - if (m_track_tab == track_tab::signals) + if (m_track_tab == track_tab::srk) { signal_refresh(); signal_move_update(); @@ -5232,7 +5232,7 @@ void editor_mode::render_first_steps() trial::turnout}, {"Signals and posts", nullptr, - {{"H", "The signals tool: the type of the signal in the panel, from the templates of the scenery"}, + {{"I", "The signalling tool: click a type of signal in its Signals, from the templates of the scenery"}, {"LMB", "A click by a track places the signal on that side"}, {"LMB + move", "Press by the track and drag along it: the trains running that way see the signal"}, {"LMB + move", "Drag a standing signal: it moves along its track"}, @@ -5382,11 +5382,11 @@ void editor_mode::render_first_steps() } case trial::signals: { - ImGui::BeginDisabled(ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::signals); - if (ImGui::Button(STR_C("The signals tool (H)"))) + ImGui::BeginDisabled(ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::srk); + if (ImGui::Button(STR_C("The signalling tool (I)"))) { choose_work_area(work_area::tracks); - show_track_tab(track_tab::signals); + show_track_tab(track_tab::srk); } ImGui::EndDisabled(); break; @@ -5657,7 +5657,7 @@ void editor_mode::render_gizmo_options() 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 && + 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::srk && m_track_tab != track_tab::lineside) return; int op{m_gizmo_enabled ? static_cast(m_gizmo_op) : -1}; @@ -5911,7 +5911,7 @@ void editor_mode::render_include_gizmo(glm::mat4 const &View, glm::mat4 const &P // in the signal tool a press on the mast drags the signal along its track: the gizmo, whose up axis runs along the // mast, gives way over a signal and while one is dragged - auto const yield{ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::signals && false == ImGuizmo::IsUsing() && (m_signal.moving || m_signal.placing || signal_hit() >= 0)}; + auto const yield{ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::srk && false == ImGuizmo::IsUsing() && (m_signal.moving || m_signal.placing || signal_hit() >= 0)}; ImGuizmo::Enable(false == yield); ImGuizmo::Manipulate(glm::value_ptr(View), glm::value_ptr(Projection), static_cast(operation), mode, glm::value_ptr(matrix), nullptr, snap); ImGuizmo::Enable(true); @@ -6505,6 +6505,8 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in cancel_track_tools(); m_straights.tool = 0; } + else if (m_track_tab == track_tab::srk && m_signal.armed) + m_signal.armed = false; else if (m_track_tab == track_tab::path && m_track_set.size() >= 2) select_tracks({}); else if (m_track_tab != track_tab::path) diff --git a/application/editormode.h b/application/editormode.h index d450a717..80cc6388 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1557,7 +1557,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri editor_icons::icon picture; int family; // 0 builds the track, 1 equips it, 2 checks it }; - static std::array const &track_modes(); + static std::array const &track_modes(); bool track_plan_tab() const { return m_track_tab == track_tab::straights || m_track_tab == track_tab::route; } track_tab plan_tab_for(TTrack const *Track) const; // the objects along the track: a tab for each tool @@ -1906,6 +1906,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::size_t signal_batch(signal_changes const &Changes, std::vector> States, std::vector Created, std::string &Error) override; bool signal_heads(std::vector &Heads, bool const Wait) override; std::size_t signal_reattach(std::vector const &Instances) override; + void select_signal(scene::instance_handle const Instance) override; + void render_signal_templates() override; struct signal_template { @@ -1950,6 +1952,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri int revision{-1}; int kind{0}; std::array chosen{-1, -1, -1, -1, -1, -1, -1}; + bool armed{false}; // a click by a track places the chosen template char filter[64]{}; char name[64]{"A"}; char plate[64]{}; @@ -1975,6 +1978,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri }; signal_tool m_signal; bool signal_scan(bool Wait = true); + signal_template const *signal_current() const; signal_template const *signal_armed() const; signal_template const *signal_template_of(std::string const &File) const; bool signal_spot_at(TTrack &Track, int const Path, glm::dvec3 const &Point, glm::dvec3 const &Cursor, int const Facing, int const Side, signal_template const &Template, signal_spot &Spot) const; diff --git a/application/editormodeselftestmonkey.cpp b/application/editormodeselftestmonkey.cpp index 255e9147..e01cf08c 100644 --- a/application/editormodeselftestmonkey.cpp +++ b/application/editormodeselftestmonkey.cpp @@ -82,7 +82,7 @@ editor_mode::selftest_result editor_mode::selftest_monkey(std::string const &com lines.push_back(std::string{"button "} + Button + " release"); lines.emplace_back("wait 2"); }; - char const *const tools[]{"esc", "l", "g", "c", "t", "n", "u", "h", "i", "b", "p", "v", "j", "k", "o", "q", "w", "e"}; + char const *const tools[]{"esc", "l", "g", "c", "t", "n", "u", "i", "b", "p", "v", "j", "k", "o", "q", "w", "e"}; char const *const modifiers[]{"", "ctrl", "shift", "alt"}; for (int step = 0; step < steps; ++step) { diff --git a/application/editormodeselftestsignal.cpp b/application/editormodeselftestsignal.cpp index aab2d651..53dbf476 100644 --- a/application/editormodeselftestsignal.cpp +++ b/application/editormodeselftestsignal.cpp @@ -53,7 +53,7 @@ editor_mode::selftest_result editor_mode::selftest_signal(std::string const &com if (command == "signal" || command == "signalmove" || command == "signaldelete" || command == "signalrename" || command == "signalprobe" || command == "signalflip" || command == "signalturn" || command == "signalselect" || command == "signalswap") { auto values{selftest_values(words)}; - show_track_tab(track_tab::signals); + show_track_tab(track_tab::srk); signal_scan(); signal_refresh(true); auto &tool{m_signal}; @@ -96,12 +96,12 @@ editor_mode::selftest_result editor_mode::selftest_signal(std::string const &com auto const side{values["side"] == "left" ? -1 : 1}; auto const facing{values["facing"] == "along" ? 1 : values["facing"] == "against" ? -1 : 0}; signal_spot spot; - if (false == spot_on(*signal_armed(), facing, side, spot)) + if (false == spot_on(*signal_current(), facing, side, spot)) { WriteLog("SELFTEST signal: no such track " + values["track"]); return selftest_result::handled; } - auto const placed{signal_place(spot, *signal_armed())}; + auto const placed{signal_place(spot, *signal_current())}; WriteLog(format("SELFTEST signal %s: %s", placed ? "placed" : "failed", tool.status.c_str())); } else if (command == "signalmove") diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index c27b8a77..740b1549 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -350,7 +350,7 @@ bool editor_mode::signal_scan(bool const Wait) return true; } -editor_mode::signal_template const *editor_mode::signal_armed() const +editor_mode::signal_template const *editor_mode::signal_current() const { auto const &tool{m_signal}; if (tool.kind < 0 || tool.kind >= kKindCount) @@ -359,6 +359,11 @@ editor_mode::signal_template const *editor_mode::signal_armed() const return index >= 0 && index < static_cast(tool.templates.size()) ? &tool.templates[index] : nullptr; } +editor_mode::signal_template const *editor_mode::signal_armed() const +{ + return m_signal.armed ? signal_current() : nullptr; +} + editor_mode::signal_template const *editor_mode::signal_template_of(std::string const &File) const { auto const file{ToLower(File)}; @@ -980,7 +985,7 @@ void editor_mode::orphans_take_over(std::vector const &Created, std::v for (int list = 0; list < 6; ++list) for (auto const &event : editor_track::events(*track, list)) carried.insert(ToLower(event.first)); - // not the list of the Signals tool, nor its templates once found: the step taken may hold one of them; looked for once an event + // not the signals standing as last listed, nor the templates once found: the step taken may hold one of them; looked for once an event // waits std::vector signals; std::unordered_map readers; @@ -1504,7 +1509,7 @@ void editor_mode::render_signal_palette() ImGui::SetNextItemWidth(-1.0f); ImGui::InputTextWithHint("##signalfilter", STR_C("filter: name, w24, sbl..."), tool.filter, sizeof(tool.filter)); auto const filter{ToLower(tool.filter)}; - auto const *armed{signal_armed()}; + auto const *current{signal_current()}; std::vector> groups; std::vector keys; for (int i = 0; i < static_cast(tool.templates.size()); ++i) @@ -1538,20 +1543,24 @@ void editor_mode::render_signal_palette() { auto const &group{groups[g]}; auto const &first{tool.templates[group.front()]}; - auto const chosen{armed != nullptr && group_of(armed->mount, armed->lamps) == keys[g] && armed->kind == first.kind}; + auto const chosen{current != nullptr && group_of(current->mount, current->lamps) == keys[g] && current->kind == first.kind}; if (g % columns != 0) ImGui::SameLine(); ImGui::PushID(g); auto const origin{ImGui::GetCursorScreenPos()}; if (ImGui::InvisibleButton("##tile", tile)) { - auto pick{group.front()}; - if (armed != nullptr && chosen) - pick = tool.chosen[tool.kind]; - tool.chosen[tool.kind] = pick; + if (chosen && tool.armed) + tool.armed = false; + else + { + tool.chosen[tool.kind] = chosen ? tool.chosen[tool.kind] : group.front(); + tool.armed = true; + } } auto const hovered{ImGui::IsItemHovered()}; - draw->AddRectFilled(origin, ImVec2(origin.x + tile.x, origin.y + tile.y), chosen ? IM_COL32(60, 90, 140, 255) : hovered ? IM_COL32(60, 60, 60, 255) : IM_COL32(40, 40, 40, 255), 4.0f); + auto const armed{chosen && tool.armed}; + draw->AddRectFilled(origin, ImVec2(origin.x + tile.x, origin.y + tile.y), armed ? IM_COL32(45, 110, 60, 255) : chosen ? IM_COL32(60, 90, 140, 255) : hovered ? IM_COL32(60, 60, 60, 255) : IM_COL32(40, 40, 40, 255), 4.0f); draw_signal_icon(draw, origin, ImVec2(tile.x, tile.y - 14.0f), first.mount, first.lamps, chosen ? overlay_color::selected : IM_COL32(120, 120, 120, 255)); auto const caption{first.lamps.empty() ? first.name : first.lamps}; auto const captionsize{ImGui::CalcTextSize(caption.c_str())}; @@ -1564,24 +1573,25 @@ void editor_mode::render_signal_palette() ImGui::TextUnformatted(first.description.empty() ? first.name.c_str() : first.description.c_str()); ImGui::TextDisabled(STR_C("%zu variants, e.g. %s"), group.size(), first.file.c_str()); ImGui::TextDisabled("%s", first.read.empty() ? STR_C("The train doesn't read it") : STR_C("The train reads it by an event of the path")); + ImGui::TextDisabled("%s", armed ? STR_C("Click: stop placing") : STR_C("Click: place it by a track, the signalling of the station places its signals of this kind with it too")); ImGui::EndTooltip(); } ImGui::PopID(); } ImGui::EndChild(); - if (armed == nullptr) + if (current == nullptr) return; - auto const key{group_of(armed->mount, armed->lamps)}; + auto const key{group_of(current->mount, current->lamps)}; ImGui::SetNextItemWidth(-1.0f); - if (ImGui::BeginCombo("##signalvariant", armed->name.c_str())) + if (ImGui::BeginCombo("##signalvariant", current->name.c_str())) { for (int i = 0; i < static_cast(tool.templates.size()); ++i) { auto const &item{tool.templates[i]}; - if (item.kind != armed->kind || group_of(item.mount, item.lamps) != key) + if (item.kind != current->kind || group_of(item.mount, item.lamps) != key) continue; auto const label{item.name + (item.lean == "left" ? std::string{" \xE2\x86\x96"} : item.lean == "right" ? std::string{" \xE2\x86\x97"} : std::string{}) + "##" + item.file}; - if (ImGui::Selectable(label.c_str(), &item == armed)) + if (ImGui::Selectable(label.c_str(), &item == current)) tool.chosen[tool.kind] = i; if (ImGui::IsItemHovered() && false == item.description.empty()) ImGui::SetTooltip("%s\n%s", item.description.c_str(), item.file.c_str()); @@ -1590,20 +1600,27 @@ void editor_mode::render_signal_palette() } if (ImGui::IsItemHovered()) { - auto const read{armed->read.empty() ? std::string{armed->kind == "warning" || armed->kind == "repeater" ? STR_C("The train doesn't read it: it repeats what the signal ahead of it shows") : STR_C("The train doesn't read it")} - : format(STR_C("The train reads it by %s on the path it stands by"), editor_includes::substitute(armed->read, {tool.name[0] != '\0' ? std::string{tool.name} : std::string{"?"}}).c_str())}; + auto const read{current->read.empty() ? std::string{current->kind == "warning" || current->kind == "repeater" ? STR_C("The train doesn't read it: it repeats what the signal ahead of it shows") : STR_C("The train doesn't read it")} + : format(STR_C("The train reads it by %s on the path it stands by"), editor_includes::substitute(current->read, {tool.name[0] != '\0' ? std::string{tool.name} : std::string{"?"}}).c_str())}; ImGui::SetTooltip("%s\n%s", STR_C("Variant of the chosen type. A head leaning to one side is swapped for its counterpart leaning towards the track on the other side"), read.c_str()); } - if (false == armed->description.empty()) - ImGui::TextWrapped("%s", armed->description.c_str()); + if (false == current->description.empty()) + ImGui::TextWrapped("%s", current->description.c_str()); + if (tool.armed) + { + if (ImGui::Button(STR_C("Stop placing (Esc)"), ImVec2(-1.0f, 0.0f))) + tool.armed = false; + } + else if (main_button(STR_C("Place by a track"), ImVec2(-1.0f, 0.0f))) + tool.armed = true; + item_tooltip("A click by a track places the chosen signal on that side, a press dragged along it turns it to the trains running that way. The signalling of the station places its signals of each kind with the template chosen here"); } void editor_mode::render_signal_selected() { - auto const *found{signal_standing_of(m_signal.selected)}; + auto const *found{m_signal.selected == m_instance ? signal_standing_of(m_signal.selected) : nullptr}; if (found == nullptr) return; - ImGui::Separator(); ImGui::TextUnformatted(format(STR_C("Selected: %s (%s)"), found->name.c_str(), found->include.file.c_str()).c_str()); item_tooltip("Name, plate and the linked signal: in the node properties. Del removes it with its event"); if (found->read.empty()) @@ -1640,6 +1657,22 @@ void editor_mode::render_signal_selected() if (ImGui::Button(STR_C("Turn around"), ImVec2(width, 0.0f))) signal_turn(instance); item_tooltip("Turns the signal to the trains of the other direction, they read it by the other event of the path"); + ImGui::Separator(); +} + +void editor_mode::select_signal(scene::instance_handle const Instance) +{ + signal_refresh(true); + signal_select(Instance); +} + +void editor_mode::render_signal_templates() +{ + ImGui::Spacing(); + if (ImGui::CollapsingHeader(format("%s###srksignals", STR_C("Signals")).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + render_signal_ui(); + else + m_signal.armed = false; } void editor_mode::render_signal_ui() @@ -1669,8 +1702,7 @@ void editor_mode::render_signal_ui() tool.kind = kinds.front().value; choice_chips("signalkinds", tool.kind, kinds); render_signal_palette(); - auto const *armed{signal_armed()}; - ImGui::Separator(); + auto const *armed{signal_current()}; if (begin_fields("##signalnames", 0.3f)) { field_row("Name"); @@ -1746,7 +1778,6 @@ void editor_mode::render_signal_ui() ImGui::PopID(); if (false == tool.status.empty()) ImGui::TextWrapped("%s", tool.status.c_str()); - render_signal_selected(); } void editor_mode::draw_signal_overlay() const diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 1db9f353..23a261ec 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -630,7 +630,7 @@ int resonator_at(std::vector const &Way, double const W return best; } -// the template of a block signal of the aspects wanted, Like: the one standing, empty for the one chosen in the Signals tool, taken +// the template of a block signal of the aspects wanted, Like: the one standing, empty for the one chosen in Signals, taken // where it has them, else the one of their mount with them, its counterpart by the name first, then of its lights; not the first of // a line, whose events tell a station signal; Wrong: of a wrong-track one, standing on the left, its head leaning right where one // does, a head leaning so only for it; empty for none @@ -1135,12 +1135,11 @@ void srk_tool::render_options() if (false == m_layout.has_value()) { ImGui::TextDisabled("%s", STR_C("The signalling of a station: its schematic, the numbers of the switches and the tracks, the track circuits and the signals")); - ImGui::Spacing(); - gesture_rows({{"LMB on a switch", "opens its station"}}); if (false == m_status.empty()) ImGui::TextColored(kDoubtful, "%s", m_status.c_str()); ImGui::Spacing(); render_found(); + m_signals.render_signal_templates(); return; } auto const &layout{*m_layout}; @@ -1253,7 +1252,7 @@ void srk_tool::render_options() "each line, A, B... from the start of the km, Z, Y... from the end, the circuit from 15 m beyond it to the station bpd_ita; " "an exit signal 15 m before the joint at the end of each station track a train is sent out of, F1, G2 towards the start, R1, " "P2 towards the end; a shunting signal Tm1, Tm2... at the end of a shorter track. A signal standing by the place of one gets " - "its name, none is doubled. The kinds of signals chosen in the Signals tool. One step of undo"); + "its name, none is doubled. Each kind with its template chosen in Signals below. One step of undo"); step_heading(4, STR_C("Lines")); render_lines(); if (ImGui::Button(STR_C("Set the line signals"), ImVec2(-1.0f, 0.0f))) @@ -1303,6 +1302,7 @@ void srk_tool::render_options() } if (false == m_status.empty()) ImGui::TextColored(kDoubtful, "%s", m_status.c_str()); + m_signals.render_signal_templates(); ImGui::Spacing(); m_length_hover = -1; if (ImGui::CollapsingHeader(format("%s (%zu)###srklengths", STR_C("Track lengths"), m_lengths.size()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) @@ -1499,7 +1499,7 @@ void srk_tool::render_lines() "warning disc, of 3 or 4 aspects, the templates of the block signals standing swapped for those of the aspects. " "Semi-automatic: a warning disc ToA the braking distance before the entry signal. By itself: the automatic one where " "block signals stand along the line, of their aspects, or it's double-track, of the aspects of the block signal " - "chosen in the Signals tool, else the semi-automatic one"); + "chosen in Signals below, else the semi-automatic one"); ImGui::PopID(); } ImGui::EndTable(); @@ -1818,7 +1818,10 @@ void srk_tool::render_heads() ImGui::TableNextRow(); ImGui::TableNextColumn(); if (ImGui::Selectable((head.name + "##srkhead" + std::to_string(index)).c_str(), static_cast(index) == m_head_hover, ImGuiSelectableFlags_SpanAllColumns)) + { m_view.fly_to(head.position + glm::dvec3{0.0, 4.0, 0.0}, glm::normalize(glm::dvec3{head.travel.x, -0.3, head.travel.y}), 30.0); + m_signals.select_signal(head.instance); + } if (ImGui::IsItemHovered()) { m_head_hover = static_cast(index); @@ -2541,7 +2544,7 @@ void srk_tool::set_signals() std::string names; for (auto const &name : unfit) names += (names.empty() ? "" : ", ") + name; - m_status += "\n" + format(STR_C("No template has the lights %s need, they got the one chosen in the Signals tool: see Signal heads"), names.c_str()); + m_status += "\n" + format(STR_C("No template has the lights %s need, they got the one chosen in Signals: see Signal heads"), names.c_str()); } } diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 742075fd..3c603b82 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -328,7 +328,7 @@ class srk_tool final : public editor_tool // the standing ones; false with the reason while it has none bool lines_of_open(std::vector const &Standing, std::vector &Lines, std::vector &Entries, std::vector &Owners, std::string &Problem) const; // the block of the line: as set by hand for its entry signal, else the automatic one where block signals stand along it, of four - // aspects where one of them has, else on a double-track line, of the aspects of the block signal chosen in the Signals tool, else + // aspects where one of them has, else on a double-track line, of the aspects of the block signal chosen in Signals, else // the semi-automatic one; Own: the block it has by itself block_kind block_of(station::line const &Line, std::string const &Entry, std::vector const &Standing, std::vector const &Heads, block_reason &Why, block_kind &Own, block_reason &Own_why, std::size_t &Blocks) const; // the automatic block of the double-track line both ways: as set by hand for its entry signal, else where block signals stand diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 7e7a5447..383aeac3 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -453,17 +453,16 @@ 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", editor_icons::icon::select, 0}, {"Lay track", "L", track_tab::lay, "Lays new track through the clicked points, on the ground or from a free end of a track", editor_icons::icon::lay_track, 0}, {"Plan", "G", track_tab::straights, "The plan of the line laid: LMB on a straight drags its ends or the middle, on a curve its vertex and radius. C works too", editor_icons::icon::curve, 0}, {"Switch", "T", track_tab::turnout, "Puts switches into the selected straight or curve, edits the geometry of the selected switch", editor_icons::icon::turnout, 0}, {"Parallel", "N", track_tab::parallel, "A second track beside a track: LMB on it, then the side and the spacing", editor_icons::icon::parallel, 0}, {"Turntable", "U", track_tab::turntable, "Places a turntable, leads tracks out of it and fits the tracks around to it", editor_icons::icon::turntable, 1}, - {"Signals", "H", track_tab::signals, "Signals by the track, from the templates of the scenery, with the event the train reads them by", editor_icons::icon::signal, 1}, - {"Signalling", "I", track_tab::srk, "The signalling of a station: LMB on a switch lays its station out as a schematic", editor_icons::icon::station, 1}, + {"Signalling", "I", track_tab::srk, "The signals and the signalling of the stations: LMB on a switch lays its station out as a schematic, the signals placed by hand from the templates in the panel", editor_icons::icon::signal, 1}, {"Kilometrage", "X", track_tab::kilometrage, "The km of the railway lines: LMB on a track puts a point of the km there, a jump of the km too", editor_icons::icon::kilometrage, 1}, {"Objects", "B", track_tab::lineside, "Along the track: hectometre posts, fouling point markers, trainsets", editor_icons::icon::objects, 1}, {"Profile", "P", track_tab::profile, "Vertical profile (grade line) along the line", editor_icons::icon::profile, 2}, @@ -492,6 +491,7 @@ void editor_mode::show_track_tab(track_tab const Wanted) m_lay.status.clear(); m_lay.active = Tab == track_tab::lay; m_switch.armed = -1; + m_signal.armed = false; m_track_tab = Tab; if ((Tab == track_tab::lay || Tab == track_tab::straights || Tab == track_tab::route) && false == m_norms_asked && EditorSettings.norms() == editorSettings::track_norms::ask && std::getenv("EU07_EDITOR_SELFTEST") == nullptr) { @@ -525,7 +525,7 @@ void editor_mode::show_track_tab(track_tab const Wanted) scan_turntables(false); turntable_read(track); break; - case track_tab::signals: + case track_tab::srk: signal_refresh(true); break; default: @@ -608,7 +608,6 @@ std::string editor_mode::track_mode_name() const case track_tab::lineside: return STR_C("Objects along the track"); case track_tab::infra: return STR_C("Infrastructure"); case track_tab::parallel: return STR_C("Parallel track"); - case track_tab::signals: return STR_C("Signal"); case track_tab::srk: return STR_C("Signalling"); case track_tab::kilometrage: return STR_C("Kilometrage"); case track_tab::turntable: return STR_C("Turntable"); @@ -707,8 +706,10 @@ void editor_mode::render_track_tool_options() ImGui::TextDisabled("%s", STR_C("What the check finds is listed in its window")); break; case track_tab::lineside: render_lineside_ui(); break; - case track_tab::signals: render_signal_ui(); break; - case track_tab::srk: m_srk.render_options(); break; + case track_tab::srk: + render_signal_selected(); + m_srk.render_options(); + break; case track_tab::kilometrage: m_km.render_options(); break; case track_tab::turntable: render_turntable_ui(); break; default: @@ -1711,16 +1712,17 @@ std::vector editor_mode::track_key_hints(bool const All) hints = {{"LMB on a track", "the line the objects go along"}}; else if (m_track_window_open && m_track_tab == track_tab::parallel) hints = {{"LMB on a track", "the new track goes beside it"}, {"Click on a side in the panel", "the side of the new track"}}; - else if (m_track_window_open && m_track_tab == track_tab::signals && m_signal.moving) + else if (m_track_window_open && m_track_tab == track_tab::srk && m_signal.moving) hints = {{"Drag along", "moves the signal along the track"}, {"Drag over the track", "to the other side"}, {"Release", "done"}, {"Esc", "cancel"}}; - else if (m_track_window_open && m_track_tab == track_tab::signals && m_signal.placing) + else if (m_track_window_open && m_track_tab == track_tab::srk && m_signal.placing) hints = {{"Drag along", "the trains it faces run that way"}, {"Release", "done"}, {"Esc", "cancel"}}; - else if (m_track_window_open && m_track_tab == track_tab::signals && signal_armed() == nullptr) - hints = {{"List in the panel", "the type of the signal"}, {"Drag a signal", "moves it along the track"}, {"LMB on a signal", "selects it"}}; - else if (m_track_window_open && m_track_tab == track_tab::signals) + else if (m_track_window_open && m_track_tab == track_tab::srk && false == m_srk.drawing()) { - hints = {{"LMB by a track", "places the signal on that side"}, {"Press+drag along", "the trains it faces run that way"}, {"Drag a signal", "moves it along the track"}, {"LMB on a signal", "selects it"}}; - if (m_signal.selected != 0) + if (signal_armed() != nullptr) + hints = {{"LMB by a track", "places the signal on that side"}, {"Press+drag along", "the trains it faces run that way"}, {"Esc", "stops placing"}}; + else + hints = {{"LMB on a switch", "opens its station"}, {"List in the panel", "the type of the signal"}, {"Drag a signal", "moves it along the track"}, {"LMB on a signal", "selects it"}}; + if (m_signal.selected != 0 && m_signal.selected == m_instance) hints.push_back({"Del", "removes the selected signal with its event"}); } else if (m_track_window_open && m_track_tab == track_tab::turntable && m_turntable.placing) diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 9b349d59..f45e585b 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -424,40 +424,6 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) } if (m_track_tab == track_tab::turntable) return turntable_intent(ground, shift); - if (m_track_tab == track_tab::signals) - { - if (auto const hit{signal_hit()}; hit >= 0) - { - auto const &standing{m_signal.standing[hit]}; - intent.index = hit; - intent.position = standing.include.location; - intent.action = format(STR_C("Click: select the signal %s, drag: move it along the track"), standing.name.c_str()); - intent.what = kind::signal_move; - return intent; - } - auto const *armed{signal_armed()}; - if (m_hover.track != nullptr && armed != nullptr) - { - signal_spot spot; - if (signal_spot_at(*m_hover.track, m_hover.path, m_hover.point, cursor_level(m_hover.point.y), 0, 0, *armed, spot)) - { - intent.track = m_hover.track; - intent.path = m_hover.path; - intent.position = spot.position; - intent.action = signal_intent_label(*armed, spot); - intent.what = kind::signal_place; - return intent; - } - } - if (m_hover.track != nullptr) - { - intent.track = m_hover.track; - intent.path = m_hover.path; - intent.position = m_hover.point; - return make(kind::select, "Choose the type of the signal in the Tracks window first"); - } - return intent; - } if (m_track_tab == track_tab::srk && m_srk.drawing()) { bool valid{false}; @@ -468,12 +434,38 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) intent.what = kind::srk_boundary; return intent; } - if (m_track_tab == track_tab::srk && m_hover.track != nullptr) + if (m_track_tab == track_tab::srk) { - intent.track = m_hover.track; - intent.path = m_hover.path; - intent.position = m_hover.point; - return make(kind::select, m_hover.track->eType != tt_Normal ? "Click: the station of this switch" : "Click: the station of the switch nearest along this track"); + if (auto const hit{signal_hit()}; hit >= 0) + { + auto const &standing{m_signal.standing[hit]}; + intent.index = hit; + intent.position = standing.include.location; + intent.action = format(STR_C("Click: select the signal %s, drag: move it along the track"), standing.name.c_str()); + intent.what = kind::signal_move; + return intent; + } + if (auto const *armed{signal_armed()}; armed != nullptr) + { + signal_spot spot; + if (m_hover.track != nullptr && signal_spot_at(*m_hover.track, m_hover.path, m_hover.point, cursor_level(m_hover.point.y), 0, 0, *armed, spot)) + { + intent.track = m_hover.track; + intent.path = m_hover.path; + intent.position = spot.position; + intent.action = signal_intent_label(*armed, spot); + intent.what = kind::signal_place; + } + return intent; + } + if (m_hover.track != nullptr) + { + intent.track = m_hover.track; + intent.path = m_hover.path; + intent.position = m_hover.point; + return make(kind::select, m_hover.track->eType != tt_Normal ? "Click: the station of this switch" : "Click: the station of the switch nearest along this track"); + } + return intent; } if (m_track_tab == track_tab::kilometrage && m_hover.track != nullptr) { diff --git a/application/editormodeviewport.cpp b/application/editormodeviewport.cpp index 3f20b92c..a709576c 100644 --- a/application/editormodeviewport.cpp +++ b/application/editormodeviewport.cpp @@ -177,17 +177,16 @@ void editor_mode::work_area_tools(std::vector &Buttons) add(track_tab::straights, false); add(track_tab::turnout, false); add(track_tab::parallel, false); - auto const advanced{m_track_tab == track_tab::turntable || m_track_tab == track_tab::signals || m_track_tab == track_tab::srk || m_track_tab == track_tab::kilometrage || m_track_tab == track_tab::lineside || m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || + auto const advanced{m_track_tab == track_tab::turntable || m_track_tab == track_tab::srk || m_track_tab == track_tab::kilometrage || m_track_tab == track_tab::lineside || m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || m_track_tab == track_tab::joints || m_gauge.is_open()}; auto const more{EditorSettings.track_tools_more()}; - Buttons.push_back({icon::more, more ? STR_C("Less") : STR_C("More"), nullptr, STR_C("Turntable, signals, signalling, kilometrage, objects, trainset, profile, speed, joints, gauge"), more, [more]() { + Buttons.push_back({icon::more, more ? STR_C("Less") : STR_C("More"), nullptr, STR_C("Turntable, signalling and signals, kilometrage, objects, trainset, profile, speed, joints, gauge"), more, [more]() { EditorSettings.track_tools_more(false == more); EditorSettings.save(); }, true}); if (false == more && false == advanced) break; add(track_tab::turntable, true); - add(track_tab::signals, false); add(track_tab::srk, false); add(track_tab::kilometrage, false); add(track_tab::lineside, false); diff --git a/application/editortool.h b/application/editortool.h index 12213703..7fd1255f 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -36,7 +36,7 @@ enum class editor_selftest_result enum class editor_first_step { tracks, lay, laid, wheel, join, turnout, undo, save, drive, look, top, select, shape, profile, profile_point, profile_apply, terrain, nmt, plan, plan_band, plan_apply, signal, posts, count }; -enum class editor_track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals, parallel, srk, kilometrage }; +enum class editor_track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, parallel, srk, kilometrage }; struct track_hover { @@ -116,7 +116,7 @@ struct signal_head std::string mount; std::string lamps; // its chambers from the top: z green, c red, p orange, b white, n blue, x blind std::vector speeds; // of the signal itself, km/h: -1 the highest allowed, 20 the substitute signal - bool chosen{false}; // the Signals tool places it for its kind + bool chosen{false}; // chosen for its kind among the templates of the signals, placed by hand and with the signalling int aspects{0}; // of a block signal: 3, or 4 with the warning two signals ahead std::string lean; // its head leans towards the track: left, standing on the right of it, right, or none }; @@ -219,6 +219,10 @@ class editor_signals virtual bool signal_heads(std::vector &Heads, bool const Wait) = 0; // the signals given which no track reads, read again by the track they stand by: one step of the history. returns: how many virtual std::size_t signal_reattach(std::vector const &Instances) = 0; + // the signal picked, its card at the top of the panel + virtual void select_signal(scene::instance_handle const Instance) = 0; + // the templates of the signals: the one of each kind placed by hand and with the signalling of a station + virtual void render_signal_templates() = 0; protected: ~editor_signals() = default; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 4bf74539..7d4ff6b7 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -3570,8 +3570,8 @@ msgstr "krawędź peronu od osi toru" msgid "Signals" msgstr "Sygnalizatory" -msgid "Signals by the track, from the templates of the scenery, with the event the train reads them by" -msgstr "Sygnalizatory przy torze, z szablonów scenerii, z eventem, którym pociąg je odczytuje" +msgid "Click: stop placing" +msgstr "Klik: koniec stawiania" msgid "moves the signal along the track" msgstr "przesuwa sygnalizator wzdłuż toru" @@ -3600,8 +3600,20 @@ msgstr "usuwa zaznaczony sygnalizator razem z jego eventem" msgid "Click: select the signal %s, drag: move it along the track" msgstr "LPM: zaznacz sygnalizator %s, przeciągnij: przesuń go wzdłuż toru" -msgid "Choose the type of the signal in the Tracks window first" -msgstr "Najpierw wybierz typ sygnalizatora w oknie Tory" +msgid "Click: place it by a track, the signalling of the station places its signals of this kind with it too" +msgstr "Klik: stawiaj go przy torze; SRK stacji stawia nim też swoje sygnalizatory tego rodzaju" + +msgid "Stop placing (Esc)" +msgstr "Zakończ stawianie (Esc)" + +msgid "Place by a track" +msgstr "Stawiaj przy torze" + +msgid "stops placing" +msgstr "kończy stawianie" + +msgid "A click by a track places the chosen signal on that side, a press dragged along it turns it to the trains running that way. The signalling of the station places its signals of each kind with the template chosen here" +msgstr "Klik przy torze stawia wybrany sygnalizator po tej stronie, wciśnięcie przeciągnięte wzdłuż toru obraca go do pociągów jadących w tę stronę. SRK stacji stawia swoje sygnalizatory każdego rodzaju szablonem wybranym tutaj" msgid "Semaphore" msgstr "Semafor" @@ -5981,8 +5993,8 @@ msgstr "Rozjazd krzyżowy pojedynczy (Rkp) albo podwójny (Rkpd) na skrzyżowani msgid "Crossover, double, scissors, ladder" msgstr "Przejście rozjazdowe, podwójne przejście rozjazdowe, nożyce, ulica zwrotnicowa" -msgid "Turntable, signals, signalling, kilometrage, objects, trainset, profile, speed, joints, gauge" -msgstr "Obrotnica, sygnalizatory, SRK, kilometraż, obiekty, skład, profil, prędkość, styki, skrajnia" +msgid "Turntable, signalling and signals, kilometrage, objects, trainset, profile, speed, joints, gauge" +msgstr "Obrotnica, SRK i sygnalizatory, kilometraż, obiekty, skład, profil, prędkość, styki, skrajnia" msgid "No switch drive models" msgstr "Brak modeli napędów" @@ -7499,8 +7511,8 @@ msgstr "B: słupki hektometrowe całej linii naraz" msgid "Signals and posts" msgstr "Sygnalizatory i słupki" -msgid "The signals tool: the type of the signal in the panel, from the templates of the scenery" -msgstr "Narzędzie Sygnalizatory: typ sygnalizatora w panelu, z szablonów scenerii" +msgid "The signalling tool: click a type of signal in its Signals, from the templates of the scenery" +msgstr "Narzędzie SRK: kliknij typ sygnalizatora w sekcji Sygnalizatory, z szablonów scenerii" msgid "A click by a track places the signal on that side" msgstr "Klik przy torze stawia sygnalizator po tej stronie" @@ -7520,8 +7532,8 @@ msgstr "Postaw sygnalizator" msgid "Place the hectometre posts of a line" msgstr "Postaw słupki hektometrowe linii" -msgid "The signals tool (H)" -msgstr "Narzędzie Sygnalizatory (H)" +msgid "The signalling tool (I)" +msgstr "Narzędzie SRK (I)" msgid "\nPaths off the route brought to its height:" msgstr "\nTory obok trasy doprowadzone do jej wysokości:" @@ -10281,8 +10293,8 @@ msgstr "Wyłączone: tor kładziony od innego toru albo dołączany do niego jes msgid "Signalling" msgstr "SRK" -msgid "The signalling of a station: LMB on a switch lays its station out as a schematic" -msgstr "Sterowanie ruchem na stacji: LPM na rozjazd rozkłada jego stację w schemat" +msgid "The signals and the signalling of the stations: LMB on a switch lays its station out as a schematic, the signals placed by hand from the templates in the panel" +msgstr "Sygnalizatory i sterowanie ruchem na stacjach: LPM na rozjazd rozkłada jego stację w schemat, sygnalizatory stawia się ręcznie z szablonów w panelu" msgid "Click: the station of this switch" msgstr "Klik: stacja tego rozjazdu" @@ -10464,8 +10476,8 @@ msgstr "Tor %s ma teraz numer %s" msgid "Set the signals" msgstr "Ustaw semafory" -msgid "Places the signals of the station, as on the lines of PKP PLK, and names them after its code: an entry signal 300 m out on each line, A, B... from the start of the km, Z, Y... from the end, the circuit from 15 m beyond it to the station bpd_ita; an exit signal 15 m before the joint at the end of each station track a train is sent out of, F1, G2 towards the start, R1, P2 towards the end; a shunting signal Tm1, Tm2... at the end of a shorter track. A signal standing by the place of one gets its name, none is doubled. The kinds of signals chosen in the Signals tool. One step of undo" -msgstr "Stawia semafory stacji jak na liniach PKP PLK i nazywa je od jej skrótu: wjazdowy 300 m przed stacją na każdej linii, A, B... od początku km, Z, Y... od końca, a odcinek od 15 m za nim do stacji to bpd_ita; wyjazdowy 15 m przed stykiem na końcu każdego toru, z którego wyprawia się pociągi, F1, G2 w stronę początku, R1, P2 w stronę końca; tarcza manewrowa Tm1, Tm2... na końcu krótszego toru. Semafor, który już stoi w miejscu któregoś z nich, dostaje tylko jego nazwę, nic się nie dubluje. Modele semaforów te wybrane w narzędziu Sygnalizatory. Jeden krok cofania" +msgid "Places the signals of the station, as on the lines of PKP PLK, and names them after its code: an entry signal 300 m out on each line, A, B... from the start of the km, Z, Y... from the end, the circuit from 15 m beyond it to the station bpd_ita; an exit signal 15 m before the joint at the end of each station track a train is sent out of, F1, G2 towards the start, R1, P2 towards the end; a shunting signal Tm1, Tm2... at the end of a shorter track. A signal standing by the place of one gets its name, none is doubled. Each kind with its template chosen in Signals below. One step of undo" +msgstr "Stawia semafory stacji jak na liniach PKP PLK i nazywa je od jej skrótu: wjazdowy 300 m przed stacją na każdej linii, A, B... od początku km, Z, Y... od końca, a odcinek od 15 m za nim do stacji to bpd_ita; wyjazdowy 15 m przed stykiem na końcu każdego toru, z którego wyprawia się pociągi, F1, G2 w stronę początku, R1, P2 w stronę końca; tarcza manewrowa Tm1, Tm2... na końcu krótszego toru. Semafor, który już stoi w miejscu któregoś z nich, dostaje tylko jego nazwę, nic się nie dubluje. Każdy rodzaj szablonem wybranym niżej w Sygnalizatorach. Jeden krok cofania" msgid "Give the station its code first: the signals are named after it" msgstr "Najpierw wpisz skrót stacji: od niego biorą się nazwy semaforów" @@ -11038,8 +11050,8 @@ msgstr "Daje każdemu zaznaczonemu semaforowi szablon tego samego rodzaju i moco msgid "The lights go by the routes of the trains: 40 km/h through the switches 1:9, 60 through 1:12 and 1:14, 100 through 1:18.5, by their radii, for these a lower orange light, with an orange strip for 60, a green one for 100, Ie-1; an upper orange one where the next signal can show stop; the white one of the substitute signal. A signal telling of the next one standing nearer than the braking distance slows the trains with it at stop, Ie-4 § 8 ust. 5" msgstr "Światła wynikają z przebiegów pociągów: 40 km/h przez rozjazdy 1:9, 60 przez 1:12 i 1:14, 100 przez 1:18,5, liczone z ich promieni; dla nich dolne światło pomarańczowe, z pomarańczowym pasem dla 60 i zielonym dla 100, Ie-1; górne pomarańczowe, gdy następny semafor może pokazać stój; białe dla sygnału zastępczego. Semafor zapowiadający następny, który stoi bliżej niż droga hamowania, zmniejsza prędkość pociągów, gdy tamten pokazuje stój, Ie-4 § 8 ust. 5" -msgid "No template has the lights %s need, they got the one chosen in the Signals tool: see Signal heads" -msgstr "Żaden szablon nie ma świateł potrzebnych dla %s, dostały ten wybrany w narzędziu Sygnalizatory: zobacz Komory semaforów" +msgid "No template has the lights %s need, they got the one chosen in Signals: see Signal heads" +msgstr "Żaden szablon nie ma świateł potrzebnych dla %s, dostały ten wybrany w Sygnalizatorach: zobacz Komory semaforów" msgid "No main signal within %.0f m ahead of it to tell of" msgstr "Na %.0f m przed nią nie ma semafora do zapowiadania" @@ -11278,8 +11290,8 @@ msgstr "ze scenerii" msgid "Block signals stand along the line, none of them switched by a block section of the editor: the line keeps the block of the scenery as it stands" msgstr "Na szlaku stoją semafory SBL, ale żadnego nie przełącza odstęp edytora: szlak zostaje z blokadą scenerii, tak jak stoi" -msgid "The block of the line, both tracks of a double-track one alike. Automatic (SBL): block signals with block sections, no warning disc, of 3 or 4 aspects, the templates of the block signals standing swapped for those of the aspects. Semi-automatic: a warning disc ToA the braking distance before the entry signal. By itself: the automatic one where block signals stand along the line, of their aspects, or it's double-track, of the aspects of the block signal chosen in the Signals tool, else the semi-automatic one" -msgstr "Blokada szlaku, na obu torach dwutorowego taka sama. Samoczynna (SBL): semafory SBL z odstępami, bez tarczy ostrzegawczej, trzy- albo czterostawna; szablony stojących semaforów SBL zostają podmienione na te o właściwej liczbie stawności. Półsamoczynna: tarcza ostrzegawcza ToA w drodze hamowania przed semaforem wjazdowym. Auto: samoczynna, gdy na szlaku stoją semafory SBL (stawność jak u nich) albo szlak jest dwutorowy (stawność jak u semafora SBL wybranego w narzędziu Sygnalizatory), inaczej półsamoczynna" +msgid "The block of the line, both tracks of a double-track one alike. Automatic (SBL): block signals with block sections, no warning disc, of 3 or 4 aspects, the templates of the block signals standing swapped for those of the aspects. Semi-automatic: a warning disc ToA the braking distance before the entry signal. By itself: the automatic one where block signals stand along the line, of their aspects, or it's double-track, of the aspects of the block signal chosen in Signals below, else the semi-automatic one" +msgstr "Blokada szlaku, na obu torach dwutorowego taka sama. Samoczynna (SBL): semafory SBL z odstępami, bez tarczy ostrzegawczej, trzy- albo czterostawna; szablony stojących semaforów SBL zostają podmienione na te o właściwej liczbie stawności. Półsamoczynna: tarcza ostrzegawcza ToA w drodze hamowania przed semaforem wjazdowym. Auto: samoczynna, gdy na szlaku stoją semafory SBL (stawność jak u nich) albo szlak jest dwutorowy (stawność jak u semafora SBL wybranego niżej w Sygnalizatorach), inaczej półsamoczynna" msgid "Draw the block boundaries" msgstr "Rysuj granice odstępów" From d14b4534ee5583f5a68444b2d91eb20a17deebe6 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 12:51:58 +0200 Subject: [PATCH 079/145] feat(editor): the station schematic generalized: each station track on a row of its own, the stretches between the switches shortened, the connections slanted between the rows, the signals and the useful lengths on the drawn tracks --- application/editormodesrk.cpp | 180 +++++++++++--- application/editormodesrk.h | 5 + editor/editorStation.cpp | 429 ++++++++++++++++++++++++++++++++++ editor/editorStation.hpp | 24 ++ 4 files changed, 602 insertions(+), 36 deletions(-) diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 23a261ec..251182ac 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -834,6 +834,7 @@ void srk_tool::open(TTrack &Track) return; } m_layout = std::move(found); + m_plot = station::generalize(*m_layout); m_hover = -1; m_view_state.exaggeration = 0.0f; m_history_revision = m_history.history_revision(); @@ -3656,6 +3657,18 @@ void srk_tool::survey_signals() auto const ahead{station::project(*m_layout, signal.position + glm::dvec3{travel.x, 0.0, travel.y} * 5.0)}; auto const unfit{std::any_of(m_heads.begin(), m_heads.end(), [&](head_check const &Head) { return Head.instance == signal.instance && (false == Head.fits || false == Head.problems.empty()); })}; m_signal_marks.push_back({at, ahead.x > at.x, signal_shown(signal.name, stem), signal.position + glm::dvec3{0.0, signal.kind == "shunt" ? 3.0 : 7.0, 0.0}, unfit}); + // in the plot on the track it stands by, to its side + auto &mark{m_signal_marks.back()}; + mark.drawn = m_plot.at(at); + auto nearest{6.0}; + for (std::size_t path = 0; path < m_layout->paths.size(); ++path) + for (std::size_t k = 0; k < m_layout->paths[path].schematic.size(); ++k) + if (auto const &point{m_layout->paths[path].schematic[k]}; glm::distance(point, at) < nearest) + { + nearest = glm::distance(point, at); + mark.drawn = m_plot.paths[path][k]; + mark.side = at.y > point.y ? 1.0f : -1.0f; + } if (signal.kind != "main" && signal.kind != "shunt") continue; station::signal stop; @@ -4095,6 +4108,7 @@ void srk_tool::survey() m_circuit_of.assign(m_layout.has_value() ? m_layout->paths.size() : 0, -1); m_circuits.clear(); m_ticks.clear(); + m_ticks_drawn.clear(); m_lengths.clear(); m_measured = false; m_numbered = false; @@ -4143,6 +4157,7 @@ void srk_tool::survey() nearest = distance; on = layout.paths[path].schematic[k]; mark.world = layout.paths[path].points[k]; + mark.drawn = m_plot.paths[path][k]; } mark.at = on; auto const entry{entry_circuit(mark.text, stem)}; @@ -4161,6 +4176,7 @@ void srk_tool::survey() glm::dvec3 point; glm::dvec3 way; int circuit; + glm::dvec2 drawn; }; std::vector ends; for (std::size_t index = 0; index < layout.paths.size(); ++index) @@ -4169,8 +4185,8 @@ void srk_tool::survey() if (points.size() < 2) continue; auto const circuit{m_circuit_of[index]}; - ends.push_back({points.front(), points[1] - points.front(), circuit}); - ends.push_back({points.back(), points.back() - points[points.size() - 2], circuit}); + ends.push_back({points.front(), points[1] - points.front(), circuit, m_plot.paths[index].front()}); + ends.push_back({points.back(), points.back() - points[points.size() - 2], circuit, m_plot.paths[index].back()}); } m_joints.clear(); for (std::size_t i = 0; i < ends.size(); ++i) @@ -4179,6 +4195,7 @@ void srk_tool::survey() std::none_of(m_joints.begin(), m_joints.end(), [&](joint_mark const &Joint) { return glm::distance(Joint.at, ends[i].point) < 0.05; })) { m_ticks.push_back(station::project(layout, ends[i].point)); + m_ticks_drawn.push_back(ends[i].drawn); glm::dvec2 const way{glm::normalize(glm::dvec2{ends[i].way.x, ends[i].way.z} + glm::dvec2{1e-12, 0.0})}; m_joints.push_back({ends[i].point, glm::dvec3{-way.y, 0.0, way.x}}); } @@ -4203,12 +4220,25 @@ void srk_tool::fit_view(float const Width, float const Height) from = std::min(from, row.chainage + row.outward * row.length); to = std::max(to, row.chainage + row.outward * row.length); } + from = m_plot.x(from); + to = m_plot.x(to); auto const span{std::max(50.0, to - from)}; view.from = from - span * 0.04 - 30.0; view.to = to + span * 0.04 + 30.0; - view.centre = (layout.low + layout.high) * 0.5; + auto low{std::numeric_limits::max()}; + auto high{std::numeric_limits::lowest()}; + for (std::size_t index = 0; index < layout.paths.size() && index < m_plot.paths.size(); ++index) + if (false == layout.paths[index].approach) + for (auto const &point : m_plot.paths[index]) + { + low = std::min(low, point.y); + high = std::max(high, point.y); + } + if (low > high) + low = high = 0.0; + view.centre = (low + high) * 0.5; auto const across{(view.to - view.from) / std::max(1.0f, Width)}; - auto const vertical{(layout.high - layout.low + 20.0) / std::max(1.0f, Height * 0.75f)}; + auto const vertical{(high - low + 2.0 * station::row_height) / std::max(1.0f, Height * 0.85f)}; view.exaggeration = static_cast(std::clamp(across / vertical, 1.0, 500.0)); } @@ -4245,9 +4275,16 @@ void srk_tool::render_canvas(float const Height) view.to -= io.MouseDelta.x * across; view.centre += io.MouseDelta.y * vertical; } - auto const x_of = [&](double const Chainage) { return corner.x + static_cast((Chainage - view.from) / across); }; - auto const y_of = [&](double const Offset) { return corner.y + size.y * 0.5f - static_cast((Offset - view.centre) / vertical); }; - auto const screen_of = [&](glm::dvec2 const &Point) { return ImVec2(x_of(Point.x), y_of(Point.y)); }; + // the plot draws the generalized schematic: its x shortens the chainage between the switches, its height puts the tracks on rows + auto const plot_x = [&](double const X) { return corner.x + static_cast((X - view.from) / across); }; + auto const plot_y = [&](double const Height) { return corner.y + size.y * 0.5f - static_cast((Height - view.centre) / vertical); }; + auto const x_of = [&](double const Chainage) { return plot_x(m_plot.x(Chainage)); }; + auto const screen_of = [&](glm::dvec2 const &Point) { return ImVec2(plot_x(Point.x), plot_y(Point.y)); }; + auto const chainage_at = [&](float const Mouse) { return m_plot.chainage(view.from + (Mouse - corner.x) * across); }; + auto const drawn_of = [&](std::size_t const Index) -> std::vector const & { + static std::vector const none; + return Index < m_plot.paths.size() ? m_plot.paths[Index] : none; + }; // the path under the mouse if (hovered && false == m_dragging) @@ -4257,7 +4294,7 @@ void srk_tool::render_canvas(float const Height) auto const &mouse{io.MousePos}; for (int index = 0; index < static_cast(layout.paths.size()); ++index) { - auto const &points{layout.paths[index].schematic}; + auto const &points{drawn_of(index)}; for (std::size_t i = 1; i < points.size(); ++i) { auto const a{screen_of(points[i - 1])}; @@ -4286,8 +4323,8 @@ void srk_tool::render_canvas(float const Height) for (std::size_t index = 0; index < m_lines.size() && index < m_line_ways.size() && line_hover < 0; ++index) { auto const &row{m_lines[index]}; - auto const distance{(view.from + (io.MousePos.x - corner.x) * across - row.chainage) * row.outward}; - if (std::abs(io.MousePos.y - y_of(row.offset)) < 6.0f && distance >= 0.0 && distance <= row.length) + auto const distance{(chainage_at(io.MousePos.x) - row.chainage) * row.outward}; + if (std::abs(io.MousePos.y - plot_y(m_plot.at({row.chainage, row.offset}).y)) < 6.0f && distance >= 0.0 && distance <= row.length) { line_hover = static_cast(index); line_at = distance; @@ -4318,37 +4355,44 @@ void srk_tool::render_canvas(float const Height) draw->PushClipRect(corner, corner_end, true); // the km, or the chainage, along the top { - auto const metres{across * 90.0}; + auto const first{chainage_at(corner.x)}; + auto const last{chainage_at(corner_end.x)}; + auto const metres{(last - first) / size.x * 90.0}; double step{10.0}; for (auto const candidate : {10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 2000.0, 5000.0}) if ((step = candidate) >= metres) break; - // the km of the station where it has them, else the chainage + // the km of the station where it has them, else the chainage; the shortened stretches skip the marks too near double km_from{0.0}; double km_to{0.0}; - auto const kilometrage{km_of_open(view.from, km_from) && km_of_open(view.to, km_to) && std::abs(km_to - km_from) > 1e-6}; - auto const low{kilometrage ? std::min(km_from, km_to) * 1000.0 : view.from}; - auto const high{kilometrage ? std::max(km_from, km_to) * 1000.0 : view.to}; + auto const kilometrage{km_of_open(first, km_from) && km_of_open(last, km_to) && std::abs(km_to - km_from) > 1e-6}; + auto const low{kilometrage ? std::min(km_from, km_to) * 1000.0 : first}; + auto const high{kilometrage ? std::max(km_from, km_to) * 1000.0 : last}; + auto shown{std::numeric_limits::lowest()}; for (auto mark{std::ceil(low / step) * step}; mark <= high; mark += step) { - auto const x{x_of(kilometrage ? view.from + (mark / 1000.0 - km_from) / (km_to - km_from) * (view.to - view.from) : mark)}; - draw->AddLine(ImVec2(x, corner.y), ImVec2(x, corner_end.y), IM_COL32(255, 255, 255, 16)); + auto const x{x_of(kilometrage ? first + (mark / 1000.0 - km_from) / (km_to - km_from) * (last - first) : mark)}; auto const label{kilometrage ? format("km %.3f", mark / 1000.0) : format("%.0f m", mark)}; + if (std::abs(x - shown) < ImGui::CalcTextSize(label.c_str()).x + 12.0f) + continue; + shown = x; + draw->AddLine(ImVec2(x, corner.y), ImVec2(x, corner_end.y), IM_COL32(255, 255, 255, 16)); draw->AddText(ImVec2(x + 3.0f, corner.y + 2.0f), IM_COL32(150, 155, 165, 255), label.c_str()); } } auto const *selected{m_selection.selected_track()}; - auto const draw_path = [&](station::path const &Path, ImU32 const Colour, float const Width) { - for (std::size_t i = 1; i < Path.schematic.size(); ++i) - draw->AddLine(screen_of(Path.schematic[i - 1]), screen_of(Path.schematic[i]), Colour, Width); + auto const draw_path = [&](std::size_t const Index, ImU32 const Colour, float const Width) { + auto const &points{drawn_of(Index)}; + for (std::size_t i = 1; i < points.size(); ++i) + draw->AddLine(screen_of(points[i - 1]), screen_of(points[i]), Colour, Width); }; - for (auto const &path : layout.paths) - if (path.approach) - draw_path(path, kApproach, 2.0f); + for (std::size_t index = 0; index < layout.paths.size(); ++index) + if (layout.paths[index].approach) + draw_path(index, kApproach, 2.0f); for (std::size_t index = 0; index < layout.paths.size(); ++index) if (auto const &path{layout.paths[index]}; false == path.approach || m_circuit_of[index] >= 0) - draw_path(path, m_circuit_of[index] >= 0 ? m_circuits[m_circuit_of[index]].colour : is_switch(*path.track) ? kSwitch : kPlain, 2.5f); - for (auto const &tick : m_ticks) + draw_path(index, m_circuit_of[index] >= 0 ? m_circuits[m_circuit_of[index]].colour : is_switch(*path.track) ? kSwitch : kPlain, 2.5f); + for (auto const &tick : m_ticks_drawn) { auto const at{screen_of(tick)}; draw->AddLine(ImVec2(at.x, at.y - 5.0f), ImVec2(at.x, at.y + 5.0f), IM_COL32(255, 255, 255, 220), 2.0f); @@ -4358,7 +4402,7 @@ void srk_tool::render_canvas(float const Height) for (std::size_t index = 0; index < m_lines.size(); ++index) { auto const &row{m_lines[index]}; - auto const y{y_of(row.offset)}; + auto const y{plot_y(m_plot.at({row.chainage, row.offset}).y)}; draw->AddLine(ImVec2(line_x(row, 0.0), y), ImVec2(line_x(row, row.length), y), kApproach, 2.0f); for (std::size_t k = 0; k < row.sections.size(); ++k) { @@ -4376,11 +4420,11 @@ void srk_tool::render_canvas(float const Height) draw->AddTriangleFilled(ImVec2(x - 4.0f * way, y - 4.0f), ImVec2(x - 4.0f * way, y + 4.0f), ImVec2(x + 4.0f * way, y), kSignal); } } - for (auto const &path : layout.paths) - if (path.track == selected) - draw_path(path, overlay_color::selected, 4.0f); + for (std::size_t index = 0; index < layout.paths.size(); ++index) + if (layout.paths[index].track == selected) + draw_path(index, overlay_color::selected, 4.0f); if (m_hover >= 0) - draw_path(layout.paths[m_hover], kHovered, 4.0f); + draw_path(static_cast(m_hover), kHovered, 4.0f); // the useful lengths of the station track shown: of the trains to the right over it, to the left under it, an arrowhead where // they stop std::vector> useful; @@ -4396,7 +4440,7 @@ void srk_tool::render_canvas(float const Height) std::vector line; for (auto const &point : stretch_of(track, part.rear.along, part.head.along)) { - auto at{screen_of(station::project(layout, point))}; + auto at{screen_of(m_plot.at(station::project(layout, point)))}; at.y += shift; line.push_back(at); } @@ -4433,7 +4477,10 @@ void srk_tool::render_canvas(float const Height) for (std::size_t index = 0; index < layout.switches.size(); ++index) { auto const *track{layout.switches[index]}; - auto const at{screen_of(station::project(layout, track->m_paths.front().points[segment_data::point::start]))}; + auto at{screen_of(m_plot.at(station::project(layout, track->m_paths.front().points[segment_data::point::start])))}; + for (std::size_t path = 0; path < layout.paths.size(); ++path) + if (layout.paths[path].track == track && layout.paths[path].index == 0 && false == drawn_of(path).empty()) + at = screen_of(drawn_of(path).front()); draw->AddCircleFilled(at, 3.5f, kSwitch); auto text{track->name()}; if (false == stem.empty() && text.size() > stem.size() && text.compare(0, stem.size(), stem) == 0) @@ -4448,7 +4495,8 @@ void srk_tool::render_canvas(float const Height) // the signals: a flag pointing the way of the trains they face for (auto const &mark : m_signal_marks) { - auto const at{screen_of(mark.at)}; + auto at{screen_of(mark.drawn)}; + at.y -= mark.side * 6.0f; auto const way{mark.ahead ? 1.0f : -1.0f}; auto const colour{mark.unfit ? kUnfit : kSignal}; draw->AddTriangleFilled(ImVec2(at.x - 4.0f * way, at.y - 4.0f), ImVec2(at.x - 4.0f * way, at.y + 4.0f), ImVec2(at.x + 4.0f * way, at.y), colour); @@ -4458,7 +4506,7 @@ void srk_tool::render_canvas(float const Height) // track of a double-track one, under the lower for (auto const &row : m_lines) { - auto const y{y_of(row.offset)}; + auto const y{plot_y(m_plot.at({row.chainage, row.offset}).y)}; auto const up{row.partner < 0 || row.partner >= static_cast(m_lines.size()) || row.offset >= m_lines[row.partner].offset}; auto const outside = [&](float const Tall, float const Level) { return up ? y - 5.0f - (Tall + 3.0f) * Level + 3.0f : y + 5.0f + (Tall + 3.0f) * (Level - 1.0f); }; auto const tall{height * (row.km_known ? 2.0f : 1.0f)}; @@ -4483,14 +4531,14 @@ void srk_tool::render_canvas(float const Height) for (auto const index : order) { auto const &mark{m_circuits[index]}; - auto const at{screen_of(mark.at)}; + auto const at{screen_of(mark.drawn)}; label(mark.text, mark.colour, {ImVec2(at.x, at.y + 4.0f), ImVec2(at.x, at.y - height - 4.0f)}); } draw->PopClipRect(); if (hovered && m_hover >= 0 && false == m_dragging) { auto const &path{layout.paths[m_hover]}; - auto const along{view.from + (io.MousePos.x - corner.x) * across}; + auto const along{chainage_at(io.MousePos.x)}; ImGui::BeginTooltip(); ImGui::TextUnformatted(path.track->name().empty() ? STR_C("(no name)") : path.track->name().c_str()); ImGui::TextDisabled("%s, %.1f m", is_switch(*path.track) ? STR_C("switch") : path.approach ? STR_C("line out of the station") : STR_C("track"), path.length); @@ -5057,6 +5105,66 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri WriteLog(format("SELFTEST srkfix: %zu problems before, status %s", before, m_status.c_str())); return editor_selftest_result::handled; } + if (Command == "srkdrawing") + { + // srkdrawing [track=]: the schematic as the plot draws it: the rows of the tracks, the shortened chainage, the points + // on the rows, on slants, the corners; the points of the paths of the circuit, offset > row + if (false == m_layout.has_value()) + { + WriteLog("SELFTEST srkdrawing: no station open"); + return editor_selftest_result::handled; + } + auto const &layout{*m_layout}; + std::map rows; + for (auto const &run : m_plot.runs) + if (false == run.paths.empty()) + rows[run.row] += format(" %s@%.1f", layout.paths[run.paths.front()].track->name().c_str(), run.offset); + std::string list; + for (auto const &[row, names] : rows) + list += format(" |%d:", row) + names; + WriteLog(format("SELFTEST srkdrawing tracks %zu, rows %zu, spacing %.2f", m_plot.runs.size(), rows.size(), m_plot.spacing)); + WriteLog("SELFTEST srkdrawing rows" + list); + WriteLog(format("SELFTEST srkdrawing switches over %.0f..%.0f m drawn %.0f..%.0f, back %.1f..%.1f", layout.from, layout.to, m_plot.x(layout.from), m_plot.x(layout.to), m_plot.chainage(m_plot.x(layout.from)), m_plot.chainage(m_plot.x(layout.to)))); + std::size_t on_rows{0}; + std::size_t between{0}; + std::size_t bends{0}; + std::size_t backwards{0}; + for (std::size_t index = 0; index < layout.paths.size(); ++index) + { + if (layout.paths[index].approach) + continue; + auto const &points{m_plot.paths[index]}; + for (auto const &point : points) + { + auto const rows{point.y / station::row_height}; + if (std::abs(rows - std::round(rows)) < 1e-6) + ++on_rows; + else + ++between; + } + for (std::size_t k = 2; k < points.size(); ++k) + { + auto const a{points[k - 1] - points[k - 2]}; + auto const b{points[k] - points[k - 1]}; + if (std::abs(a.x) > 1e-6 && std::abs(b.x) > 1e-6 && (std::abs(a.y) > 1e-6 || std::abs(b.y) > 1e-6) && std::abs(a.y / a.x - b.y / b.x) > 1e-3) + ++bends; + } + for (std::size_t k = 1; k < points.size(); ++k) + if ((points[k].x - points[k - 1].x) * (layout.paths[index].schematic[k].x - layout.paths[index].schematic[k - 1].x) < 0.0) + ++backwards; + } + WriteLog(format("SELFTEST srkdrawing points on rows %zu, between %zu, bends %zu, x backwards %zu", on_rows, between, bends, backwards)); + if (auto const values{selftest_values(Words)}; values.count("track")) + for (std::size_t index = 0; index < layout.paths.size(); ++index) + if (auto const &path{layout.paths[index]}; false == path.track->Isolated.empty() && path.track->Isolated.front()->asName == values.at("track")) + { + std::string list; + for (std::size_t k = 0; k < path.schematic.size(); k += 4) + list += format(" %.0f/%.2f>%.2f", path.schematic[k].x, path.schematic[k].y, m_plot.paths[index][k].y / station::row_height); + WriteLog(format("SELFTEST srkdrawing %s/%d:", path.track->name().c_str(), path.index) + list); + } + return editor_selftest_result::handled; + } if (Command == "srkstrip") { // srkstrip shown=0: the schematic hidden, the 3d view whole for a screenshot diff --git a/application/editormodesrk.h b/application/editormodesrk.h index 3c603b82..1134c12a 100644 --- a/application/editormodesrk.h +++ b/application/editormodesrk.h @@ -96,6 +96,7 @@ class srk_tool final : public editor_tool bool m_changed{false}; std::vector m_records; std::optional m_layout; + station::drawing m_plot; // the schematic of the layout as the plot draws it int m_record{-1}; // of the open station, -1 while it has none TTrack *m_anchor{nullptr}; // the switch the open station was found from bool m_reversed{false}; // the switches numbered from the right end of the schematic @@ -114,10 +115,12 @@ class srk_tool final : public editor_tool ImU32 colour{0}; double length{0.0}; bool labelled{true}; // the circuits of the switches go without, named after them + glm::dvec2 drawn{0.0}; // in the plot }; std::vector m_circuit_of; // of each path of the layout, an index into m_circuits, -1 for none std::vector m_circuits; std::vector m_ticks; // the joints between the circuits, in the schematic + std::vector m_ticks_drawn; // the same in the plot struct joint_mark { glm::dvec3 at{0.0}; @@ -131,6 +134,8 @@ class srk_tool final : public editor_tool std::string text; glm::dvec3 head{0.0}; // above its lamps, in the scenery bool unfit{false}; // its lights don't show what its routes need + glm::dvec2 drawn{0.0}; // on its track in the plot + float side{0.0f}; // 1: it stands to the left of the track in the plot, -1: to the right }; std::vector m_signal_marks; bool m_signals_known{false}; // the signals standing surveyed, once their templates are found diff --git a/editor/editorStation.cpp b/editor/editorStation.cpp index 85b1f640..a252b91a 100644 --- a/editor/editorStation.cpp +++ b/editor/editorStation.cpp @@ -641,6 +641,435 @@ double km_of(layout const &Layout, double const Chainage) return Layout.km + Chainage / 1000.0; } +namespace +{ + +// a track rising more than this to the chainage runs across the rows +double constexpr kAcross{0.04}; +// m, two tracks this far apart lie side by side +double constexpr kSideBySide{1.0}; +// m of the chainage, points of two tracks compared side by side +double constexpr kAbreast{3.0}; +// m, a plain stretch between the switches keeps this much of its length in the plot, the rest shortened to kKept of it +double constexpr kWhole{60.0}; +double constexpr kKept{0.3}; +// m, how far a slant is followed over the tracks to the row it ends on +double constexpr kSlantSearch{400.0}; +// m, the plot puts a point of the schematic on a path no farther from it +double constexpr kNearPath{15.0}; + +double shortened(double const Length) +{ + return Length <= kWhole ? Length : kWhole + (Length - kWhole) * kKept; +} + +double lengthened(double const Length) +{ + return Length <= kWhole ? Length : kWhole + (Length - kWhole) / kKept; +} + +// rows, for the offset gone off a row: at most three rows further +double rows_off(double const Steps) +{ + auto const size{std::abs(Steps)}; + return std::copysign(size <= 1.0 ? size : 1.0 + 2.0 * (1.0 - std::exp((1.0 - size) * 0.5)), Steps); +} + +} // namespace + +double drawing::x(double const Chainage) const +{ + if (warp.empty()) + return Chainage; + if (Chainage <= warp.front().x) + return warp.front().y - shortened(warp.front().x - Chainage); + if (Chainage >= warp.back().x) + return warp.back().y + shortened(Chainage - warp.back().x); + auto const next{std::upper_bound(warp.begin(), warp.end(), Chainage, [](double const Value, glm::dvec2 const &Point) { return Value < Point.x; })}; + auto const &a{*(next - 1)}; + auto const &b{*next}; + return a.y + (b.y - a.y) * (Chainage - a.x) / std::max(1e-9, b.x - a.x); +} + +double drawing::chainage(double const X) const +{ + if (warp.empty()) + return X; + if (X <= warp.front().y) + return warp.front().x - lengthened(warp.front().y - X); + if (X >= warp.back().y) + return warp.back().x + lengthened(X - warp.back().y); + auto const next{std::upper_bound(warp.begin(), warp.end(), X, [](double const Value, glm::dvec2 const &Point) { return Value < Point.y; })}; + auto const &a{*(next - 1)}; + auto const &b{*next}; + return a.x + (b.x - a.x) * (X - a.y) / std::max(1e-9, b.y - a.y); +} + +glm::dvec2 drawing::at(glm::dvec2 const &Schematic) const +{ + auto best{kNearPath * kNearPath}; + glm::dvec2 result{x(Schematic.x), Schematic.y / spacing * row_height}; + if (false == runs.empty()) + { + auto const [low, high]{std::minmax_element(runs.begin(), runs.end(), [](run const &A, run const &B) { return A.row < B.row; })}; + result.y = std::clamp(result.y, (low->row - 3.0) * row_height, (high->row + 3.0) * row_height); + } + for (std::size_t p = 0; p < schematic.size() && p < paths.size(); ++p) + { + auto const &points{schematic[p]}; + if (points.empty() || std::max(points.front().x, points.back().x) + 2.0 * kNearPath < Schematic.x || std::min(points.front().x, points.back().x) - 2.0 * kNearPath > Schematic.x) + continue; + for (std::size_t i = 1; i < points.size(); ++i) + { + auto const &a{points[i - 1]}; + auto const along{points[i] - a}; + auto const length2{glm::dot(along, along)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(Schematic - a, along) / length2, 0.0, 1.0) : 0.0}; + auto const gap{a + along * t - Schematic}; + if (auto const distance2{glm::dot(gap, gap)}; distance2 < best) + { + best = distance2; + result = paths[p][i - 1] + (paths[p][i] - paths[p][i - 1]) * t; + } + } + } + return result; +} + +drawing generalize(layout const &Layout) +{ + drawing result; + auto const &paths{Layout.paths}; + auto const count{paths.size()}; + for (auto const &path : paths) + result.schematic.push_back(path.schematic); + auto const plain = [&](std::size_t const Path) { return paths[Path].track == nullptr || false == node(*paths[Path].track); }; + auto const usable = [&](std::size_t const Path) { return paths[Path].points.size() >= 2 && paths[Path].points.size() == paths[Path].schematic.size(); }; + auto const end_point = [&](std::size_t const Path, bool const Atend) { return Atend ? paths[Path].points.back() : paths[Path].points.front(); }; + // the ends of the paths which meet: of each path, at its start and at its end + std::vector>, 2>> met(count); + for (std::size_t p = 0; p < count; ++p) + for (std::size_t q = p + 1; q < count && usable(p); ++q) + for (auto const pend : {false, true}) + for (auto const qend : {false, true}) + if (usable(q) && plan_distance(end_point(p, pend), end_point(q, qend)) < 0.05) + { + met[p][pend].emplace_back(q, qend); + met[q][qend].emplace_back(p, pend); + } + // the points running along the axis rather than across it + std::vector> along(count); + for (std::size_t p = 0; p < count; ++p) + { + auto const &points{paths[p].schematic}; + for (std::size_t i = 0; i < points.size(); ++i) + { + auto const &a{points[i > 0 ? i - 1 : i]}; + auto const &b{points[i + 1 < points.size() ? i + 1 : i]}; + along[p].push_back(std::abs(b.x - a.x) > 1e-3 && std::abs(b.y - a.y) < std::abs(b.x - a.x) * kAcross); + } + } + // the station tracks: plain paths joined end to end where nothing else meets them, each on a row of its own + std::vector run_of(count, -1); + for (std::size_t p = 0; p < count; ++p) + { + if (run_of[p] >= 0 || false == plain(p) || false == usable(p)) + continue; + auto const index{static_cast(result.runs.size())}; + result.runs.emplace_back(); + run_of[p] = index; + std::vector stack{p}; + while (false == stack.empty()) + { + auto const current{stack.back()}; + stack.pop_back(); + result.runs[index].paths.push_back(current); + for (auto const atend : {false, true}) + { + if (met[current][atend].size() != 1) + continue; + auto const [other, otherend]{met[current][atend].front()}; + if (plain(other) && run_of[other] < 0 && met[other][otherend].size() == 1) + { + run_of[other] = index; + stack.push_back(other); + } + } + } + } + // of each run the points along the axis, by the chainage: the middle offset of them, which other runs lie beside it on the right + auto &runs{result.runs}; + std::vector> samples(runs.size()); + std::vector level(runs.size(), 0); + for (std::size_t r = 0; r < runs.size(); ++r) + { + for (auto const p : runs[r].paths) + for (std::size_t i = 0; i < paths[p].schematic.size(); ++i) + if (along[p][i]) + samples[r].push_back(paths[p].schematic[i]); + if (samples[r].empty()) + continue; + level[r] = 1; + std::sort(samples[r].begin(), samples[r].end(), [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x < B.x; }); + std::vector offsets; + for (auto const &sample : samples[r]) + offsets.push_back(sample.y); + std::nth_element(offsets.begin(), offsets.begin() + offsets.size() / 2, offsets.end()); + runs[r].offset = offsets[offsets.size() / 2]; + } + std::vector> below(runs.size()); + std::vector gaps; + for (std::size_t a = 0; a < runs.size(); ++a) + for (std::size_t b = a + 1; b < runs.size(); ++b) + { + if (0 == level[a] || 0 == level[b] || samples[a].back().x + kAbreast < samples[b].front().x || samples[b].back().x + kAbreast < samples[a].front().x) + continue; + auto votes{0}; + for (auto const &sample : samples[a]) + { + auto const found{std::lower_bound(samples[b].begin(), samples[b].end(), sample.x - kAbreast, [](glm::dvec2 const &Sample, double const Value) { return Sample.x < Value; })}; + if (found == samples[b].end() || found->x > sample.x + kAbreast || std::abs(sample.y - found->y) <= kSideBySide) + continue; + votes += sample.y > found->y ? 1 : -1; + // of the tracks next to each other + if (auto const gap{std::abs(sample.y - found->y)}; gap > 3.0 && gap < 8.0) + gaps.push_back(gap); + } + if (votes > 0) + below[a].push_back(b); + else if (votes < 0) + below[b].push_back(a); + } + if (false == gaps.empty()) + { + std::nth_element(gaps.begin(), gaps.begin() + gaps.size() / 2, gaps.end()); + result.spacing = std::clamp(gaps[gaps.size() / 2], 3.5, 12.0); + } + // the rows: by the offsets, then pushed apart away from the axis where a track lies beside another on the same row or beyond + for (std::size_t r = 0; r < runs.size(); ++r) + runs[r].row = static_cast(std::lround(runs[r].offset / result.spacing)); + for (std::size_t pass = 0; pass < runs.size() + 1; ++pass) + { + auto changed{false}; + for (std::size_t a = 0; a < runs.size(); ++a) + for (auto const b : below[a]) + if (runs[a].row <= runs[b].row) + { + if (runs[a].offset + runs[b].offset >= 0.0) + runs[a].row = runs[b].row + 1; + else + runs[b].row = runs[a].row - 1; + changed = true; + } + if (false == changed) + break; + } + // the main track through the station on the middle row + if (false == Layout.main.empty()) + for (std::size_t p = 0; p < count; ++p) + if (run_of[p] >= 0 && level[run_of[p]] && paths[p].track == Layout.main.front()) + { + auto const shift{runs[run_of[p]].row}; + for (auto &run : runs) + run.row -= shift; + break; + } + // a group of tracks far aside, as a yard, closer: no more than one row left empty before it + { + std::vector used; + for (std::size_t r = 0; r < runs.size(); ++r) + if (level[r]) + used.push_back(runs[r].row); + used.push_back(0); + std::sort(used.begin(), used.end()); + used.erase(std::unique(used.begin(), used.end()), used.end()); + std::map closer{{0, 0}}; + auto const middle{static_cast(std::find(used.begin(), used.end(), 0) - used.begin())}; + for (auto i{middle + 1}; i < used.size(); ++i) + closer[used[i]] = closer[used[i - 1]] + std::min(used[i] - used[i - 1], 2); + for (auto i{middle}; i > 0; --i) + closer[used[i - 1]] = closer[used[i]] - std::min(used[i] - used[i - 1], 2); + for (std::size_t r = 0; r < runs.size(); ++r) + if (level[r]) + runs[r].row = closer[runs[r].row]; + } + // the breakpoints at the ends of the switches, the stretches between them shortened + std::vector stops; + for (std::size_t p = 0; p < count; ++p) + if (false == paths[p].approach && false == plain(p) && false == paths[p].schematic.empty()) + { + stops.push_back(paths[p].schematic.front().x); + stops.push_back(paths[p].schematic.back().x); + } + if (stops.empty()) + stops = {Layout.from, Layout.to}; + std::sort(stops.begin(), stops.end()); + for (auto const stop : stops) + if (result.warp.empty()) + result.warp.emplace_back(stop, stop); + else if (stop - result.warp.back().x > 1.0) + result.warp.emplace_back(stop, result.warp.back().y + shortened(stop - result.warp.back().x)); + // the row of each point: the row of its track where it runs along it; a switch going straight on takes the row of the track it + // joins, its curved path keeps it at its ends only + auto constexpr none{std::numeric_limits::min()}; + std::vector> rows(count); + for (std::size_t p = 0; p < count; ++p) + { + rows[p].assign(paths[p].schematic.size(), none); + if (run_of[p] >= 0 && level[run_of[p]]) + for (std::size_t i = 0; i < rows[p].size(); ++i) + if (along[p][i]) + rows[p][i] = runs[run_of[p]].row; + } + for (int pass = 0; pass < 32; ++pass) + { + auto changed{false}; + for (std::size_t p = 0; p < count; ++p) + { + if (plain(p) || false == usable(p)) + continue; + auto const joined = [&](bool const Atend) { + for (auto const &[other, otherend] : met[p][Atend]) + if (auto const value{otherend ? rows[other].back() : rows[other].front()}; value != none) + return value; + return none; + }; + auto &row{rows[p]}; + auto const straight{std::all_of(along[p].begin(), along[p].end(), [](char const Along) { return Along != 0; })}; + auto const first{along[p].front() ? joined(false) : none}; + auto const last{along[p].back() ? joined(true) : none}; + std::vector wanted(row.size(), none); + if (straight && first != none && (last == none || last == first)) + std::fill(wanted.begin(), wanted.end(), first); + else if (straight && last != none && first == none) + std::fill(wanted.begin(), wanted.end(), last); + else + { + wanted.front() = first; + wanted.back() = last; + } + if (wanted != row) + { + row = std::move(wanted); + changed = true; + } + } + if (false == changed) + break; + } + auto const height = [&](int const Row) { return static_cast(Row) * row_height; }; + // the way along the path from its end, the chord of its first metres + auto const leaving = [&](std::size_t const Path, bool const Atend) { + auto const &points{paths[Path].points}; + auto const reach{std::min(5, points.size() - 1)}; + auto const way{Atend ? points[points.size() - 1 - reach] - points.back() : points[reach] - points.front()}; + return glm::normalize(glm::dvec2{way.x, way.z} + glm::dvec2{1e-12, 0.0}); + }; + struct on_row + { + glm::dvec2 drawn{0.0}; + double offset{0.0}; // in the schematic + }; + // the first point on a row along the tracks on from the end of the path, away from it: at a switch along the path going on the + // straightest, so a slant over a ladder of switches runs to its far end + auto const outward = [&](std::size_t const From, bool const Atend, on_row &Found) { + auto current{From}; + auto exit{Atend}; + auto walked{0.0}; + std::unordered_set seen{From}; + while (walked <= kSlantSearch) + { + auto const away{-leaving(current, exit)}; + auto best{0.0}; + auto next{count}; + auto entered{false}; + for (auto const &[other, otherend] : met[current][exit]) + if (seen.count(other) == 0) + if (auto const straightness{glm::dot(leaving(other, otherend), away)}; straightness > best) + { + best = straightness; + next = other; + entered = otherend; + } + if (next == count) + return false; + seen.insert(next); + auto const &row{rows[next]}; + auto const &schematic{paths[next].schematic}; + auto const size{row.size()}; + for (std::size_t k = 0; k < size; ++k) + { + auto const index{entered ? size - 1 - k : k}; + if (k > 0) + walked += glm::distance(schematic[index], schematic[entered ? index + 1 : index - 1]); + if (walked > kSlantSearch) + return false; + if (row[index] != none) + { + Found = {{result.x(schematic[index].x), height(row[index])}, schematic[index].y}; + return true; + } + } + current = next; + exit = false == entered; + } + return false; + }; + // the points in the plot: on their rows, between them on a straight slant from the last row to the next; far from any row a few + // rows beyond the outer ones at most + auto lowest{0}; + auto highest{0}; + for (std::size_t r = 0; r < runs.size(); ++r) + if (level[r]) + { + lowest = std::min(lowest, runs[r].row); + highest = std::max(highest, runs[r].row); + } + auto const loose = [&](double const Offset) { return std::clamp(Offset / result.spacing, lowest - 3.0, highest + 3.0) * row_height; }; + result.paths.resize(count); + for (std::size_t p = 0; p < count; ++p) + { + auto const &points{paths[p].schematic}; + auto const &row{rows[p]}; + auto &drawn{result.paths[p]}; + for (std::size_t i = 0; i < points.size(); ++i) + drawn.emplace_back(result.x(points[i].x), row[i] != none ? height(row[i]) : loose(points[i].y)); + if (false == usable(p)) + continue; + for (std::size_t i = 0; i < points.size();) + { + if (row[i] != none) + { + ++i; + continue; + } + auto end{i}; + while (end + 1 < points.size() && row[end + 1] == none) + ++end; + on_row before{i > 0 ? on_row{drawn[i - 1], points[i - 1].y} : on_row{}}; + on_row after{end + 1 < points.size() ? on_row{drawn[end + 1], points[end + 1].y} : on_row{}}; + auto const from{i > 0 || outward(p, false, before)}; + auto const to{end + 1 < points.size() || outward(p, true, after)}; + for (auto k{i}; k <= end; ++k) + { + if (from && to && std::abs(after.drawn.x - before.drawn.x) > 0.5) + { + auto const t{std::clamp((drawn[k].x - before.drawn.x) / (after.drawn.x - before.drawn.x), 0.0, 1.0)}; + drawn[k].y = before.drawn.y + (after.drawn.y - before.drawn.y) * t; + } + // off a row towards no other, a line out leaving the station aside + else if (from || to) + { + auto const &known{from ? before : after}; + drawn[k].y = known.drawn.y + row_height * rows_off((points[k].y - known.offset) / result.spacing); + } + } + i = end + 1; + } + } + return result; +} + std::vector stations(options const &Options) { std::vector result; diff --git a/editor/editorStation.hpp b/editor/editorStation.hpp index eb97b20e..4596424a 100644 --- a/editor/editorStation.hpp +++ b/editor/editorStation.hpp @@ -83,6 +83,30 @@ struct circuit double length{0.0}; }; +// the schematic generalized for the plot: each station track on a row of its own, level, the rows evenly apart in the order the +// tracks lie side by side, a path between them on a straight slant; the plain stretches between the switches shortened +struct drawing +{ + struct run + { + std::vector paths; // of the layout, joined end to end without a switch between + double offset{0.0}; // the middle one of its stretches along the axis + int row{0}; + }; + std::vector runs; + double spacing{4.75}; // m, of the tracks of the station + std::vector warp; // the chainage and the x of the plot at the breakpoints, by the chainage + std::vector> paths; // of each path of the layout, its points in the plot: the x and the height + std::vector> schematic; // the same in the schematic + double x(double const Chainage) const; + double chainage(double const X) const; + // the point of the schematic in the plot: on the path nearest to it, unless none runs near + glm::dvec2 at(glm::dvec2 const &Schematic) const; +}; +// m of the plot between two rows +double constexpr row_height{5.0}; +drawing generalize(layout const &Layout); + // the station of the switch, or of the plain track near one std::optional find(TTrack &From, options const &Options = {}); // the chainage along the axis and the offset to the left of it, as the map shows it From fe3dd758ea436d15e32cf877f06429f7e4edb327 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 13:07:14 +0200 Subject: [PATCH 080/145] feat(editor): the signalling, the hectometre posts and the terrain go to layers of their own: _srk.scm, _hektometry.scm, _teren.scm, made by the first of them and included by the scenario file, the active layer left as it is; the station descriptions of the signalling there too --- application/editormode.cpp | 15 ++++++++--- application/editormode.h | 5 ++-- application/editormodelineside.cpp | 43 +++++++++++++++++++++++++++--- application/editormodesignal.cpp | 4 +-- application/editormodesrk.cpp | 4 ++- application/editortool.h | 5 +++- lang/editor_pl.po | 3 +++ scene/scenelayers.cpp | 7 +++-- scene/scenelayers.h | 8 +++--- scene/scenelayerssave.cpp | 12 +++++++-- 10 files changed, 85 insertions(+), 21 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 16a22c92..8d16bbee 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -2986,8 +2986,9 @@ bool editor_mode::save() } first_step_done(first_step::save); - // the heightmap terrain is kept in files of its own, the scenery only needs the directive which loads it + // the heightmap terrain is kept in files of its own, the scenery only needs the directive which loads it, in a layer of its own std::vector rootstatements; + std::map> statements; if (m_streamer.active()) { std::string error; @@ -2997,7 +2998,13 @@ bool editor_mode::save() return false; } if (false == scene::Layers.terrain_directive()) - rootstatements.emplace_back("heightmap_terrain " + m_streamer.name() + " endheightmap_terrain"); + { + auto const directive{"heightmap_terrain " + m_streamer.name() + " endheightmap_terrain"}; + if (auto const layer{output_layer("teren")}; layer != null_handle) + statements[layer].push_back(directive); + else + rootstatements.push_back(directive); + } } if (m_profile.changed) @@ -3016,14 +3023,14 @@ bool editor_mode::save() m_orthophoto_mark = map; scene::Layers.mark(scene::layer_handle{1}, "//$o", {map}); } - auto const terrainadded{false == rootstatements.empty()}; + auto const terrainadded{false == rootstatements.empty() || false == statements.empty()}; auto const controls{turntable_controls(rootstatements)}; auto const noses{nose_controls(rootstatements)}; std::vector trailing; auto const placed{m_vehicle.trainset_lines(trailing)}; if (placed == 0) starter_trainset(trailing); - auto const result = scene::Layers.save(rootstatements, trailing); + auto const result = scene::Layers.save(rootstatements, trailing, statements); if (result.success && controls) m_turntable_included = true; if (result.success && noses) diff --git a/application/editormode.h b/application/editormode.h index 80cc6388..130df383 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1896,8 +1896,9 @@ class editor_mode : public application_mode, private editor_track::observer, pri void apply_switch_drive_choice(TTrack &Switch); void switch_drive_captured(TTrack const &Track); void switch_drives_committed(std::vector const &Tracks); - // places them in the active layer, as one step of the undo. returns: how many were placed - std::size_t place_templates(std::vector const &Items, std::string &Error) override; + scene::layer_handle output_layer(std::string const &Part) override; + // places them in the active layer or the given one, as one step of the undo. returns: how many were placed + std::size_t place_templates(std::vector const &Items, std::string &Error, scene::layer_handle Layer = scene::layer_handle{}) override; // moves them, as one step of the undo std::size_t move_templates(std::vector> const &Moves) override; // takes them out, as one step of the undo diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp index fdfc030e..b22a19b3 100644 --- a/application/editormodelineside.cpp +++ b/application/editormodelineside.cpp @@ -243,7 +243,41 @@ std::vector editor_mode::standing_templates(std: return result; } -std::size_t editor_mode::place_templates(std::vector const &Items, std::string &Error) +scene::layer_handle editor_mode::output_layer(std::string const &Part) +{ + if (scene::Layers.empty()) + return null_handle; + auto stem{scene::Layers.layer(scene::layer_handle{1}).name}; + if (auto const slash{stem.find_last_of("/\\")}; slash != std::string::npos) + stem.erase(0, slash + 1); + if (auto const dot{stem.rfind('.')}; dot != std::string::npos) + stem.erase(dot); + stem.erase(0, stem.find_first_not_of('$')); + if (stem.empty()) + stem = "scenery"; + std::string reason; + // a file of that name the scenery doesn't include, or one which can't take more, gives way to the next number + for (int attempt = 1; attempt <= 9; ++attempt) + { + auto const name{ToLower(stem + "_" + Part + (attempt > 1 ? std::to_string(attempt) : std::string{}) + ".scm")}; + auto layer{null_handle}; + for (std::size_t index = 1; index <= scene::Layers.size() && layer == null_handle; ++index) + if (auto const &existing{scene::Layers.layer(static_cast(index))}; false == existing.dead && false == existing.removed && existing.name == name) + layer = scene::Layers.resolve(static_cast(index)); + if (layer == null_handle) + layer = scene::Layers.create(name, scene::layer_handle{1}, reason, false); + if (layer != null_handle && scene::Layers.accepts(layer, &reason)) + { + scene::Layers.visible(layer, true); + scene::Layers.locked(layer, false); + return layer; + } + } + WriteLog("Editor: no layer for " + Part + ", the active one takes it: " + reason, logtype::generic); + return null_handle; +} + +std::size_t editor_mode::place_templates(std::vector const &Items, std::string &Error, scene::layer_handle const Layer) { if (Items.empty()) return 0; @@ -252,7 +286,7 @@ std::size_t editor_mode::place_templates(std::vector const &Items 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())}; + auto const layer{scene::Layers.resolve(Layer != null_handle ? Layer : 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); @@ -569,8 +603,11 @@ void hekto_tool::hekto_place() items.push_back(std::move(item)); } std::string error; - auto const placed{m_templates.place_templates(items, error)}; + auto const layer{items.empty() ? null_handle : m_templates.output_layer("hektometry")}; + auto const placed{m_templates.place_templates(items, error, layer)}; tool.status = placed > 0 ? format(STR_P("%zu hectometre post placed, Ctrl+Z takes it back", "%zu hectometre posts placed, Ctrl+Z takes them back", static_cast(placed)), placed) : error; + if (placed > 0 && layer != null_handle) + tool.status += format(STR_C(" (layer \"%s\")"), scene::Layers.layer(layer).name.c_str()); if (placed > 0) m_guide.first_step_done(editor_first_step::posts); WriteLog("Editor: " + tool.status, logtype::generic); diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp index 740b1549..86180806 100644 --- a/application/editormodesignal.cpp +++ b/application/editormodesignal.cpp @@ -844,7 +844,7 @@ bool editor_mode::signal_place(signal_spot const &Spot, signal_template const &T if (Template.linked) item.values["#7"] = tool.linked[0] != '\0' ? std::string{tool.linked} : std::string{"none"}; std::string error; - if (place_templates({item}, error) == 0) + if (place_templates({item}, error, output_layer("srk")) == 0) { tool.status = refused(STR_C("The signal couldn't be placed: ") + error); return false; @@ -1452,7 +1452,7 @@ std::size_t editor_mode::signal_batch(signal_changes const &Changes, std::vector std::vector instances; if (Error.empty() && false == items.empty()) { - if (place_templates(items, Error) == 0) + if (place_templates(items, Error, output_layer("srk")) == 0) reads.clear(); else { diff --git a/application/editormodesrk.cpp b/application/editormodesrk.cpp index 251182ac..da290173 100644 --- a/application/editormodesrk.cpp +++ b/application/editormodesrk.cpp @@ -879,7 +879,9 @@ srk_tool::record &srk_tool::record_of_open() auto *first{m_layout->switches.front()}; entry.anchor = first->name(); entry.track = first; - entry.layer = alignment_store::layer_of(*first); + entry.layer = m_templates.output_layer("srk"); + if (entry.layer == null_handle) + entry.layer = alignment_store::layer_of(*first); m_records.push_back(std::move(entry)); m_record = static_cast(m_records.size()) - 1; m_anchor = first; diff --git a/application/editortool.h b/application/editortool.h index 7fd1255f..17af2764 100644 --- a/application/editortool.h +++ b/application/editortool.h @@ -199,7 +199,10 @@ class editor_templates { public: virtual std::vector standing_templates(std::function const &Accept) const = 0; - virtual std::size_t place_templates(std::vector const &Items, std::string &Error) = 0; + // the layer a tool keeps what it makes in, "_.scm", made when the scenery has none yet. returns: null_handle if there can't be one + virtual scene::layer_handle output_layer(std::string const &Part) = 0; + // in the active layer unless given another + virtual std::size_t place_templates(std::vector const &Items, std::string &Error, scene::layer_handle Layer = scene::layer_handle{}) = 0; virtual std::size_t move_templates(std::vector> const &Moves) = 0; virtual std::size_t remove_templates(std::vector const &Instances) = 0; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 7d4ff6b7..87a4e573 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11469,3 +11469,6 @@ msgstr[2] "Odczyt przywrócony: %zu z %zu semaforów" msgid ": the rest stand farther than 10 m from a track, by vehicles or their events are gone" msgstr ": pozostałe stoją dalej niż 10 m od toru, przy torze z pojazdami albo ich zdarzenia już nie istnieją" + +msgid " (layer \"%s\")" +msgstr " (warstwa \"%s\")" diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index 18274767..a2d158f1 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -558,7 +558,7 @@ void node_layers::stat(layer_handle const Layer) } // creates empty layer, included from specified one. returns: handle to the new layer, or null_handle with the explanation in Reason -layer_handle node_layers::create(std::string Name, layer_handle Parent, std::string &Reason) +layer_handle node_layers::create(std::string Name, layer_handle Parent, std::string &Reason, bool const Activate) { Parent = resolve(Parent); if (false == valid(Parent) || layer(Parent).removed || layer(Parent).binary) @@ -639,7 +639,10 @@ layer_handle node_layers::create(std::string Name, layer_handle Parent, std::str created.context_begin = valid(anchor) ? layer(anchor).context_end : layer(Parent).context_insert(); created.context_end = created.context_begin; - active(layerhandle); + if (Activate) + { + active(layerhandle); + } return layerhandle; } diff --git a/scene/scenelayers.h b/scene/scenelayers.h index 1eb2e4d5..bf342e10 100644 --- a/scene/scenelayers.h +++ b/scene/scenelayers.h @@ -294,8 +294,8 @@ class node_layers void mark(layer_handle Layer, std::string const &Mark, std::vector Lines); // layer management. changes are applied to the scenery files by save() - // creates empty layer, included from specified one. returns: handle to the new layer, or null_handle with the explanation in Reason - layer_handle create(std::string Name, layer_handle Parent, std::string &Reason); + // creates empty layer, included from specified one, made active unless told otherwise. returns: handle to the new layer, or null_handle with the explanation in Reason + layer_handle create(std::string Name, layer_handle Parent, std::string &Reason, bool const Activate = true); bool can_remove(layer_handle Layer, std::string &Reason) const; // marks specified layer and the layers it includes as dropped from the scenery. their files are left on the disk bool remove(layer_handle Layer); @@ -348,8 +348,8 @@ class node_layers // writes changes made in the editor to the scenery files: changed, created and deleted model instances and // memory cells, and layer changes. the files are left intact unless the whole operation can be performed. - // Rootstatements: additional statements to place at the end of the scenario file - save_result save(std::vector const &Rootstatements = {}, std::vector const &Trailingstatements = {}); + // Rootstatements: additional statements to place at the end of the scenario file; Statements: the same for other layers + save_result save(std::vector const &Rootstatements = {}, std::vector const &Trailingstatements = {}, std::map> const &Statements = {}); std::string check_sources() const; private: diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index c869ff7d..7d6a1ecb 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -633,7 +633,7 @@ bool node_layers::compose(save_state &State, layer_handle const Layer, compositi } // writes changes made in the editor to the scenery files -save_result node_layers::save(std::vector const &Rootstatements, std::vector const &Trailingstatements) +save_result node_layers::save(std::vector const &Rootstatements, std::vector const &Trailingstatements, std::map> const &Statements) { save_result result; if (m_layers.empty()) @@ -1455,7 +1455,7 @@ save_result node_layers::save(std::vector const &Rootstatements, st continue; } if (layer(candidate).created || state.patches.count(candidate) != 0 || appended.count(candidate) != 0 || created.count(candidate) != 0 || placed.count(candidate) != 0 || - markedtext.count(candidate) != 0 || (candidate == root && (false == Rootstatements.empty() || false == Trailingstatements.empty()))) + markedtext.count(candidate) != 0 || Statements.count(candidate) != 0 || (candidate == root && (false == Rootstatements.empty() || false == Trailingstatements.empty()))) { outputs.emplace_back(candidate); } @@ -1568,6 +1568,14 @@ save_result node_layers::save(std::vector const &Rootstatements, st added.text += statement + eol; } } + if (auto const lookup{Statements.find(output)}; lookup != Statements.end()) + { + for (auto const &statement : lookup->second) + { + ensure_newline(added.text, eol); + added.text += statement + eol; + } + } if (output == root && false == Trailingstatements.empty()) { composition trailing; From a892532fda446776060d2175224b1cd5c95a48ab Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 13:20:06 +0200 Subject: [PATCH 081/145] feat(editor): the top view draws the tracks whole, without the ballast or the rails alone, in a colour of their own over the imagery and the map: both rails close up, one line further out; a choice by the extent and in the View menu, kept in the settings --- application/editormode.cpp | 68 ++++++++++++++++++++----- application/editormode.h | 2 + application/editormodeselftestscene.cpp | 4 +- application/editormodetrackux.cpp | 32 ++++++++++-- editor/editorSettings.cpp | 9 +++- editor/editorSettings.hpp | 10 ++-- lang/editor_pl.po | 27 ++++++++++ rendering/opengl33renderer.cpp | 10 +++- utilities/Globals.h | 2 +- 9 files changed, 137 insertions(+), 27 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 8d16bbee..9572dfed 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -645,7 +645,7 @@ editor_ui *editor_mode::ui() const bool editor_mode::init() { EditorSettings.load(); - Global.EditorOrthoBallast = EditorSettings.top_ballast(); + Global.EditorOrthoTracks = EditorSettings.top_tracks(); set_norms_free(EditorSettings.norms() == editorSettings::track_norms::free_laying); EditorModelSets.load(); Camera.Init({0, 15, 0}, {glm::radians(-30.0), glm::radians(180.0), 0}, nullptr); @@ -1486,6 +1486,8 @@ bool editor_mode::update() draw_track_labels(); draw_track_legend(); } + else if (Global.EditorOrtho && Global.EditorOrthoTracks > 1) + draw_track_lines(); if (selected_track()) draw_track_overlay(); if (route_active()) @@ -3627,13 +3629,11 @@ 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)) + if (ImGui::BeginMenu(STR_C("Tracks in the top view"))) { - EditorSettings.top_ballast(Global.EditorOrthoBallast); - EditorSettings.save(); + render_top_tracks_choice(false); + ImGui::EndMenu(); } - 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 (ImGui::BeginMenu(STR_C("Track colours"))) { char const *const names[]{"Plain", "By the radius", "By the speed", "By the grade"}; @@ -5656,13 +5656,7 @@ void editor_mode::render_gizmo_options() 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")); + render_top_tracks_choice(true); } 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::srk && m_track_tab != track_tab::lineside) @@ -6999,3 +6993,51 @@ void editor_mode::starter_trainset(std::vector &Trailing) return; } } + +void editor_mode::render_top_tracks_choice(bool const Combo) +{ + static char const *const labels[]{"Whole", "Without ballast", "Rails only"}; + static char const *const tooltips[]{"The tracks as in the 3D view", "The rails and the sleepers, the ground under them shows", "The rails alone, without the sleepers and the timbers of the switches, as on a plan"}; + auto chosen{std::clamp(Global.EditorOrthoTracks, 0, 2)}; + auto const pick = [&](int const Choice) { + Global.EditorOrthoTracks = Choice; + EditorSettings.top_tracks(Choice); + EditorSettings.save(); + }; + if (false == Combo) + { + for (int choice = 0; choice < 3; ++choice) + { + if (ImGui::MenuItem(STR_C(labels[choice]), nullptr, chosen == choice)) + pick(choice); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C(tooltips[choice])); + } + return; + } + ImGui::SetNextItemWidth(130.0f); + if (ImGui::BeginCombo(STR_C("Tracks"), STR_C(labels[chosen]))) + { + for (int choice = 0; choice < 3; ++choice) + { + if (ImGui::Selectable(STR_C(labels[choice]), chosen == choice)) + pick(choice); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C(tooltips[choice])); + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("How the top view draws the tracks")); + if (chosen < 2) + return; + ImGui::SameLine(); + auto colour{ImGui::ColorConvertU32ToFloat4(EditorSettings.top_rails())}; + if (ImGui::ColorEdit3("##toprails", &colour.x, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) + { + EditorSettings.top_rails(ImGui::ColorConvertFloat4ToU32(ImVec4(colour.x, colour.y, colour.z, 1.0f))); + EditorSettings.save(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The colour of the rails, to stand out on the imagery and the map")); +} diff --git a/application/editormode.h b/application/editormode.h index 130df383..b85704eb 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -765,6 +765,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri void render_gizmo(); // gizmo settings, drawn in the toolset window void render_gizmo_options(); + // how the top view draws the tracks: as a combo with the colour of the rails, or as items of a menu + void render_top_tracks_choice(bool const Combo); // the tools of the field of work in the toolbar under the menu void render_toolbar(); // an edit mode of the node bank as the field of work, from the toolbar or its key (1-5) diff --git a/application/editormodeselftestscene.cpp b/application/editormodeselftestscene.cpp index 46c8e2e0..1102c175 100644 --- a/application/editormodeselftestscene.cpp +++ b/application/editormodeselftestscene.cpp @@ -283,8 +283,8 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm if (extent > 0.0f) Global.EditorOrthoExtent = std::clamp(extent, 5.0f, 50000.0f); if (false == ballast.empty()) - Global.EditorOrthoBallast = ballast != "noballast"; - WriteLog(format("SELFTEST ortho extent %.0f ballast %d", Global.EditorOrthoExtent, Global.EditorOrthoBallast ? 1 : 0)); + Global.EditorOrthoTracks = ballast == "rails" ? 2 : ballast == "noballast" ? 1 : 0; + WriteLog(format("SELFTEST ortho extent %.0f tracks %d", Global.EditorOrthoExtent, Global.EditorOrthoTracks)); return selftest_result::handled; } if (command == "mapstate") diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index f45e585b..fc127955 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormodetrackshared.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" +#include "editor/editorSettings.hpp" #include "editor/editorSwitchStore.hpp" #include "utilities/Globals.h" @@ -201,14 +202,18 @@ void editor_mode::update_track_hover() void editor_mode::draw_track_lines() const { auto const metres{ortho_metres_per_pixel()}; - auto const colouring{m_track_colouring}; - if (metres < kLinesFrom && colouring == track_colouring::plain) + auto const colouring{ui()->mode() == nodebank_panel::TRACK ? m_track_colouring : track_colouring::plain}; + // the top view of the rails alone draws them in a colour of their own, as on a plan: both rails where they're apart on the screen + auto const rails{metres > 0.0 && Global.EditorOrthoTracks > 1 && colouring == track_colouring::plain}; + if (metres < kLinesFrom && colouring == track_colouring::plain && false == rails) return; auto const &io{ImGui::GetIO()}; screen_projection const projection; glm::dvec2 const centre{Global.pCamera.Pos.x, Global.pCamera.Pos.z}; auto const reach{Global.EditorOrtho ? 0.5 * metres * std::hypot(static_cast(io.DisplaySize.x), static_cast(io.DisplaySize.y)) : kColouredReach}; - auto const width{colouring == track_colouring::plain ? 1.5f : 3.0f}; + auto const width{rails ? std::max(2.0f, static_cast(0.07 / metres)) : colouring == track_colouring::plain ? 1.5f : 3.0f}; + auto const railcolour{EditorSettings.top_rails()}; + std::vector points; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; for (auto *track : simulation::Paths.sequence()) { @@ -220,7 +225,7 @@ void editor_mode::draw_track_lines() const bezier const curve{path}; if (false == plan_near(curve, centre, reach)) continue; - auto colour{IM_COL32(235, 235, 235, 150)}; + auto colour{rails ? railcolour : IM_COL32(235, 235, 235, 150)}; if (colouring == track_colouring::radius) { auto const radius{::track_mode::piece_plan_radius(path)}; @@ -229,7 +234,24 @@ void editor_mode::draw_track_lines() const else if (colouring == track_colouring::speed) colour = velocity > 0.0 ? band_colour(kSpeedBands, velocity) : kUncoloured; auto const length{curve.plan_length(4)}; - auto const count{std::clamp(static_cast(length / std::max(metres, 0.5) / 8.0), colouring == track_colouring::grade ? std::clamp(static_cast(length / 10.0), 1, 8) : 1, 32)}; + auto const count{std::clamp(static_cast(length / std::max(metres, 0.5) / 8.0), colouring == track_colouring::grade ? std::clamp(static_cast(length / 10.0), 1, 8) : 1, rails ? 128 : 32)}; + if (rails && track->fTrackWidth / metres > 6.0) + { + points.clear(); + for (int k = 0; k <= count; ++k) + points.push_back(curve.point(static_cast(k) / count)); + auto const half{(track->fTrackWidth + 0.07) * 0.5}; + auto const across = [&](std::size_t const K) { + auto const along{points[std::min(K + 1, points.size() - 1)] - points[K > 0 ? K - 1 : 0]}; + auto const normal{glm::dvec3{-along.z, 0.0, along.x}}; + auto const size{glm::length(normal)}; + return size > 1e-9 ? normal * (half / size) : glm::dvec3{0.0}; + }; + for (auto const side : {-1.0, 1.0}) + for (std::size_t k = 1; k < points.size(); ++k) + projection.line(drawlist, points[k - 1] + across(k - 1) * side, points[k] + across(k) * side, colour, width); + continue; + } auto previous{curve.point(0.0)}; for (int k = 1; k <= count; ++k) { diff --git a/editor/editorSettings.cpp b/editor/editorSettings.cpp index b2590272..8c22d39c 100644 --- a/editor/editorSettings.cpp +++ b/editor/editorSettings.cpp @@ -109,7 +109,11 @@ bool editorSettings::load() else if (key == "track_tools_all") m_track_tools_more = (value == "1"); else if (key == "top_ballast") - m_top_ballast = (value == "1"); + m_top_tracks = value == "1" ? 0 : 1; + else if (key == "top_tracks") + m_top_tracks = std::clamp(std::atoi(value.c_str()), 0, 2); + else if (key == "top_rails") + m_top_rails = static_cast(std::strtoul(value.c_str(), nullptr, 16)) | 0xff000000u; else if (key == "track_norms") m_norms = value == "free" ? track_norms::free_laying : value == "plk" ? track_norms::plk : track_norms::ask; else if (key.rfind(origin_prefix, 0) == 0) @@ -158,7 +162,8 @@ 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 << "top_tracks " << m_top_tracks << "\n"; + stream << "top_rails " << std::hex << (m_top_rails & 0xffffffu) << std::dec << "\n"; stream << "track_norms " << (m_norms == track_norms::free_laying ? "free" : m_norms == track_norms::plk ? "plk" : "ask") << "\n"; stream << std::fixed << std::setprecision(3); for (auto const &origin : m_orthophoto_origins) diff --git a/editor/editorSettings.hpp b/editor/editorSettings.hpp index 3adfe782..de7628b8 100644 --- a/editor/editorSettings.hpp +++ b/editor/editorSettings.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -62,8 +63,10 @@ public: 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; } + int top_tracks() const { return m_top_tracks; } + void top_tracks(int const Tracks) { m_top_tracks = Tracks; } + std::uint32_t top_rails() const { return m_top_rails; } + void top_rails(std::uint32_t const Colour) { m_top_rails = Colour; } // how the new tracks are laid: asked as the laying starts, or the choice kept enum class track_norms { @@ -86,7 +89,8 @@ private: bool m_first_steps{true}; bool m_terrain_offer{true}; bool m_track_tools_more{false}; - bool m_top_ballast{true}; + int m_top_tracks{0}; // the railway in the top view: whole, without the ballast, the rails alone + std::uint32_t m_top_rails{0xffd246ff}; // colour of the rails alone, as ImGui keeps it: ABGR track_norms m_norms{track_norms::ask}; }; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 87a4e573..d57b4c01 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11472,3 +11472,30 @@ msgstr ": pozostałe stoją dalej niż 10 m od toru, przy torze z pojazdami albo msgid " (layer \"%s\")" msgstr " (warstwa \"%s\")" + +msgid "Tracks in the top view" +msgstr "Tory w widoku z góry" + +msgid "Whole" +msgstr "Całe" + +msgid "Without ballast" +msgstr "Bez podsypki" + +msgid "Rails only" +msgstr "Same szyny" + +msgid "The tracks as in the 3D view" +msgstr "Tory jak w widoku 3D" + +msgid "The rails and the sleepers, the ground under them shows" +msgstr "Szyny i podkłady, spod nich widać teren" + +msgid "The rails alone, without the sleepers and the timbers of the switches, as on a plan" +msgstr "Same szyny, bez podkładów i podrozjazdnic, jak na planie" + +msgid "How the top view draws the tracks" +msgstr "Jak widok z góry rysuje tory" + +msgid "The colour of the rails, to stand out on the imagery and the map" +msgstr "Kolor szyn, żeby odcinały się od ortofotomapy i mapy" diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 4d699b52..5d89a8bb 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -54,7 +54,14 @@ editor_track_view_range(float const Range, bool const Apply) bool editor_ballast_hidden(TTrack const *Track, bool const Colour) { - return Colour && EditorModeFlag && Global.EditorOrtho && false == Global.EditorOrthoBallast && (Track->iCategoryFlag & 15) == 1; + return Colour && EditorModeFlag && Global.EditorOrtho && Global.EditorOrthoTracks > 0 && (Track->iCategoryFlag & 15) == 1; +} + +// ...or the sleepers and the timbers of the switches too, the rails alone as on a plan +bool +editor_sleepers_hidden() +{ + return EditorModeFlag && Global.EditorOrtho && Global.EditorOrthoTracks > 1; } // the top view draws the scenery as far as the tracks; its models keep their own ranges @@ -4701,6 +4708,7 @@ void opengl33_renderer::Render(scene::basic_cell::path_sequence::const_iterator switch( m_renderpass.draw_mode ) { case rendermode::color: case rendermode::reflections: { + if( editor_sleepers_hidden() ) { break; } for( auto first { First }; first != Last; ++first ) { auto *track = *first; if( false == track->m_visible ) { continue; } diff --git a/utilities/Globals.h b/utilities/Globals.h index c63197aa..36be95b6 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -174,7 +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 + int EditorOrthoTracks{ 0 }; // the railway in the top view of the editor: 0 whole, 1 without the ballast, 2 the rails alone 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 From 5e846018b6f3d60e05c6f297225e0c45dee2ce5c Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 13:35:07 +0200 Subject: [PATCH 082/145] feat(editor): far out the top view shows the base map of OpenStreetMap from geoportal.gov.pl in place of the imagery, tiles as large as the view needs, the ground not drawn under it nor streamed as far as the view; the tracks over it in the colour of the rails, blue by default, and the joints of the pieces left out far out --- application/editormode.cpp | 19 ++- application/editormodetrack.cpp | 4 + application/editormodetrackux.cpp | 2 +- editor/editorOrthophoto.cpp | 201 +++++++++++++++++++++++++++--- editor/editorOrthophoto.hpp | 14 ++- editor/editorSettings.hpp | 2 +- editor/editorTerrainStreamer.cpp | 2 +- lang/editor_pl.po | 3 + rendering/opengl33renderer.cpp | 3 +- utilities/Globals.h | 1 + 10 files changed, 229 insertions(+), 22 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 9572dfed..32af41a2 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1470,6 +1470,14 @@ bool editor_mode::update() draw_terrain_overlay(); // --- geoportal orthophoto: streamed around the camera, drawn beneath the other overlays --- + // far out the top view shows the base map in place of the imagery, and the ground isn't drawn under it + { + auto const &io{ImGui::GetIO()}; + auto const metres{Global.EditorOrtho && io.DisplaySize.y > 0.0f ? 2.0 * static_cast(Global.EditorOrthoExtent) / io.DisplaySize.y : 0.0}; + auto const aspect{io.DisplaySize.y > 0.0f ? static_cast(io.DisplaySize.x / io.DisplaySize.y) : 1.0}; + Global.EditorOrthoMap = m_orthophoto.enabled() && metres >= (m_orthophoto.overview() ? 1.6 : 2.0); + m_orthophoto.overview(Global.EditorOrthoMap ? metres : 0.0, static_cast(Global.EditorOrthoExtent) * std::sqrt(1.0 + aspect * aspect)); + } m_orthophoto.update(Camera.Pos); nmt_update(); draw_orthophoto(); @@ -1486,7 +1494,7 @@ bool editor_mode::update() draw_track_labels(); draw_track_legend(); } - else if (Global.EditorOrtho && Global.EditorOrthoTracks > 1) + else if (Global.EditorOrtho && (Global.EditorOrthoTracks > 1 || Global.EditorOrthoMap)) draw_track_lines(); if (selected_track()) draw_track_overlay(); @@ -2494,6 +2502,15 @@ void editor_mode::draw_orthophoto() ImGuiIO const &io = ImGui::GetIO(); float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; m_orthophoto.draw(editor_mode::projection_matrix(aspect) * view, Camera.Pos, io.DisplaySize.x, io.DisplaySize.y); + if (false == m_orthophoto.overview()) + return; + char const *credit{STR_C("(c) OpenStreetMap contributors, geoportal.gov.pl")}; + auto const size{ImGui::CalcTextSize(credit)}; + auto const corner{editor_ui::view_max()}; + ImVec2 const from{corner.x - size.x - 12.0f, corner.y - size.y - 44.0f}; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + drawlist->AddRectFilled(ImVec2(from.x - 4.0f, from.y - 2.0f), ImVec2(from.x + size.x + 4.0f, from.y + size.y + 2.0f), IM_COL32(255, 255, 255, 190), 3.0f); + drawlist->AddText(from, IM_COL32(40, 40, 40, 255), credit); } void editor_mode::draw_ortho_compass() const diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 383aeac3..8301bd94 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -2201,8 +2201,12 @@ void editor_mode::draw_build_overlay() const // in the 3D view only near the camera, where they can be told apart; from above all of them glm::dvec3 const eye{Global.pCamera.Pos}; auto const in_reach = [&](glm::dvec3 const &Point) { return Global.EditorOrtho || glm::distance(Point, eye) <= kDotsRange; }; + // far out the joints of the pieces would cover the tracks, only the free ends are told + auto const joints{false == Global.EditorOrtho || display.y <= 0.0f || 2.0 * static_cast(Global.EditorOrthoExtent) / display.y <= 1.0}; for (auto const *track : simulation::Paths.sequence()) { + if (false == joints) + break; if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) continue; for (auto const &path : track->m_paths) diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index fc127955..ee669f04 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -204,7 +204,7 @@ void editor_mode::draw_track_lines() const auto const metres{ortho_metres_per_pixel()}; auto const colouring{ui()->mode() == nodebank_panel::TRACK ? m_track_colouring : track_colouring::plain}; // the top view of the rails alone draws them in a colour of their own, as on a plan: both rails where they're apart on the screen - auto const rails{metres > 0.0 && Global.EditorOrthoTracks > 1 && colouring == track_colouring::plain}; + auto const rails{metres > 0.0 && (Global.EditorOrthoTracks > 1 || Global.EditorOrthoMap) && colouring == track_colouring::plain}; if (metres < kLinesFrom && colouring == track_colouring::plain && false == rails) return; auto const &io{ImGui::GetIO()}; diff --git a/editor/editorOrthophoto.cpp b/editor/editorOrthophoto.cpp index f9349777..cfd024d3 100644 --- a/editor/editorOrthophoto.cpp +++ b/editor/editorOrthophoto.cpp @@ -40,6 +40,9 @@ namespace fs = std::filesystem; constexpr int standard_pixels{1024}; // ~25 cm/px for a 256 m tile, matches the standard orthophoto constexpr int hires_pixels{4096}; // ~6 cm/px, the WMS MaxWidth/MaxHeight of the geoportal services constexpr int probe_pixels{64}; // coverage test of the high-resolution product +constexpr int map_pixels{1024}; // a side of a tile of the map, whatever its size in metres +constexpr int map_stamp{32}; // rows under a tile of the map the service writes its credit into, cut off +constexpr char const *map_service{"https://mapy.geoportal.gov.pl/wss/ext/OSM/BaseMap/service"}; constexpr int worker_count{3}; // concurrent requests; the service is slow but shouldn't be hammered constexpr std::size_t upload_budget{16u << 20}; // bytes of texture data uploaded per frame (always at least one tile) constexpr std::size_t vertex_budget{60000}; // ImGui uses 16-bit indices and not every backend supports vertex offsets @@ -336,6 +339,31 @@ bool load_tile(http_client &Client, tile_request const &Request, image &Out, boo return true; } +// the base map of OpenStreetMap from geoportal.gov.pl, in the metres of PUWG 1992 as the imagery. the service writes its credit into +// the corner of every image: the image reaches a little further south and the rows with it are cut off +bool load_map(http_client &Client, int const Level, int const Ix, int const Iy, image &Out, std::string &Error, stop_check const &Stop) +{ + auto const size{static_cast(editor_orthophoto::tile_size) << Level}; + auto const below{size * map_stamp / map_pixels}; + auto const url{std::string{map_service} + "?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=osm&STYLES=&SRS=EPSG:2180&FORMAT=image/png&WIDTH=" + std::to_string(map_pixels) + + "&HEIGHT=" + std::to_string(map_pixels + map_stamp) + "&BBOX=" + std::to_string(Ix * size) + "," + std::to_string(Iy * size - below) + "," + std::to_string((Ix + 1) * size) + "," + + std::to_string((Iy + 1) * size)}; + std::vector data; + auto const path{cache_root() / "osm" / (std::to_string(size) + "_" + std::to_string(Iy) + "_" + std::to_string(Ix) + ".png")}; + if (!fetch(Client, url, path, data, Error, Stop) || !decode(data, Out, Error)) + return false; + if (Out.width != map_pixels || Out.height != map_pixels + map_stamp) + { + Error = "unexpected size of the map " + std::to_string(Out.width) + "x" + std::to_string(Out.height); + std::error_code ec; + fs::remove(path, ec); + return false; + } + Out.height = map_pixels; + Out.rgba.resize(static_cast(map_pixels) * map_pixels * 4); + return true; +} + // the services cover Poland only (EPSG:2180 extent from their capabilities, plus a margin); no point asking about other places bool in_service_area(int Ix, int Iy) { @@ -550,6 +578,9 @@ void editor_orthophoto::enabled(bool State) for (auto &entry : m_tiles) release_tile(entry.second); m_tiles.clear(); + for (auto &entry : m_maps) + release_tile(entry.second); + m_maps.clear(); ++m_generation; // results still in flight are now stale std::lock_guard lock(m_mutex); m_done.clear(); @@ -604,6 +635,8 @@ void editor_orthophoto::cancel_pending() // jobs already picked up by a worker still deliver their result, which is accepted if the tile is still wanted for (auto &entry : m_tiles) entry.second.requested = 0; + for (auto &entry : m_maps) + entry.second.requested = 0; } void editor_orthophoto::clear_cache() @@ -612,6 +645,9 @@ void editor_orthophoto::clear_cache() for (auto &entry : m_tiles) release_tile(entry.second); m_tiles.clear(); + for (auto &entry : m_maps) + release_tile(entry.second); + m_maps.clear(); ++m_generation; { std::lock_guard lock(m_mutex); @@ -628,21 +664,28 @@ void editor_orthophoto::retry_failed() entry.second.failures = 0; entry.second.retry_at = 0.0; } + for (auto &entry : m_maps) + { + entry.second.failures = 0; + entry.second.retry_at = 0.0; + } } editor_orthophoto::statistics editor_orthophoto::stats() const { statistics out; - for (auto const &entry : m_tiles) - { - tile const &t = entry.second; + auto const count = [&](tile const &t) { if (t.level > 0 && !t.empty && t.slot.texture != null_handle) ++out.resident; if (t.requested != 0) ++out.loading; else if (t.failures > 0 && t.level == 0) ++out.failed; - } + }; + for (auto const &entry : m_tiles) + count(entry.second); + for (auto const &entry : m_maps) + count(entry.second); return out; } @@ -706,12 +749,12 @@ void editor_orthophoto::release_tile(tile &Tile) Tile.slot = texture_slot{}; } -void editor_orthophoto::schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha) +void editor_orthophoto::schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha, int const Map) { { std::lock_guard lock(m_mutex); - m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.key == Key; }), m_queue.end()); - m_queue.push_back(job{Key, Level, Alpha, source{m_config.year, m_config.hires}, m_generation.load()}); + m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.key == Key && Job.map == Map; }), m_queue.end()); + m_queue.push_back(job{Key, Map, Level, Alpha, source{m_config.year, m_config.hires}, m_generation.load()}); } Tile.requested = Level; Tile.requested_alpha = Alpha; @@ -728,10 +771,17 @@ std::size_t editor_orthophoto::apply(result &Result, double Now) { if (Result.generation != m_generation.load()) return 0; - auto const lookup = m_tiles.find(Result.key); - if (lookup == m_tiles.end()) + tile *found{nullptr}; + if (Result.map > 0) + { + if (auto const lookup = m_maps.find({Result.map, Result.key}); lookup != m_maps.end()) + found = &lookup->second; + } + else if (auto const lookup = m_tiles.find(Result.key); lookup != m_tiles.end()) + found = &lookup->second; + if (found == nullptr) return 0; // left the area in the meantime; the imagery stays in the disk cache - tile &t = lookup->second; + tile &t = *found; bool const current = (t.requested == Result.level && t.requested_alpha == Result.alpha); if (current) t.requested = 0; @@ -1104,6 +1154,19 @@ void editor_orthophoto::update(glm::dvec3 const &Camera) std::lock_guard lock(m_mutex); m_focus = centre; } + if (m_map_level > 0) + { + update_map(Camera, now); + return; + } + if (false == m_maps.empty()) + { + for (auto &entry : m_maps) + release_tile(entry.second); + m_maps.clear(); + std::lock_guard lock(m_mutex); + m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [](job const &Job) { return Job.map > 0; }), m_queue.end()); + } int const radius = m_config.radius; auto const distance = [&](tile_key const &Key) { return std::max(std::abs(Key.first - centre.first), std::abs(Key.second - centre.second)); }; @@ -1133,7 +1196,7 @@ void editor_orthophoto::update(glm::dvec3 const &Camera) if (evicted) { std::lock_guard lock(m_mutex); - m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return distance(Job.key) > radius; }), m_queue.end()); + m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.map == 0 && distance(Job.key) > radius; }), m_queue.end()); } // tiles which need (different) imagery @@ -1175,6 +1238,59 @@ void editor_orthophoto::update(glm::dvec3 const &Camera) update_scene(); } +void editor_orthophoto::overview(double const Scale, double const Reach) +{ + // a pixel of a tile of the map is as large as one of the screen, or up to twice as small + m_map_level = Scale > 0.0 ? std::clamp(static_cast(std::floor(std::log2(Scale * map_pixels / tile_size))), 2, 9) : 0; + m_map_reach = Reach; +} + +void editor_orthophoto::update_map(glm::dvec3 const &Camera, double const Now) +{ + // the imagery gives way to the map, its tiles come back from the disk cache once the view is near again + if (false == m_tiles.empty()) + { + for (auto &entry : m_tiles) + release_tile(entry.second); + m_tiles.clear(); + std::lock_guard lock(m_mutex); + m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [](job const &Job) { return Job.map == 0; }), m_queue.end()); + } + double const east = m_config.east - Camera.x; + double const north = m_config.north + Camera.z; + auto const in_view = [&](map_key const &Key) { + auto const size{tile_size * static_cast(1 << Key.first)}; + return (Key.second.first + 1) * size > east - m_map_reach && Key.second.first * size < east + m_map_reach && (Key.second.second + 1) * size > north - m_map_reach && + Key.second.second * size < north + m_map_reach; + }; + // the tiles of the other levels stay under the ones of the level shown until these come + auto const size{tile_size * static_cast(1 << m_map_level)}; + auto complete{true}; + for (int iy = static_cast(std::floor((north - m_map_reach) / size)); iy <= static_cast(std::floor((north + m_map_reach) / size)); ++iy) + for (int ix = static_cast(std::floor((east - m_map_reach) / size)); ix <= static_cast(std::floor((east + m_map_reach) / size)); ++ix) + { + map_key const key{m_map_level, {ix, iy}}; + tile &t = m_maps[key]; + if (t.level == 0 && false == t.empty) + complete = false; + if (t.empty || t.level == map_pixels || t.requested == map_pixels || Now < t.retry_at) + continue; + schedule(key.second, t, map_pixels, 255, m_map_level); + } + for (auto it = m_maps.begin(); it != m_maps.end();) + { + if (false == in_view(it->first) || std::abs(it->first.first - m_map_level) > 1 || (complete && it->first.first != m_map_level)) + { + release_tile(it->second); + it = m_maps.erase(it); + } + else + ++it; + } + std::lock_guard lock(m_mutex); + m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.map > 0 && m_maps.count({Job.map, Job.key}) == 0; }), m_queue.end()); +} + void editor_orthophoto::worker() { stbi_set_flip_vertically_on_load_thread(0); // the texture loader flips globally; tiles keep the north edge in the first row @@ -1194,8 +1310,10 @@ void editor_orthophoto::worker() long long bestdistance = LLONG_MAX; for (auto it = m_queue.begin(); it != m_queue.end(); ++it) { - long long const dx = it->key.first - m_focus.first; - long long const dy = it->key.second - m_focus.second; + // a tile of the map is that many tiles of the imagery a side + long long const scale = 1ll << it->map; + long long const dx = it->key.first * scale + scale / 2 - m_focus.first; + long long const dy = it->key.second * scale + scale / 2 - m_focus.second; long long const d = dx * dx + dy * dy; if (d < bestdistance) { @@ -1209,6 +1327,7 @@ void editor_orthophoto::worker() result out; out.key = current.key; + out.map = current.map; out.level = current.level; out.generation = current.generation; out.alpha = current.alpha; @@ -1216,11 +1335,11 @@ void editor_orthophoto::worker() image picture; std::string error; tile_request const request{current.key.first, current.key.second, current.level, current.src.year, current.src.hires}; - out.ok = load_tile(client, request, picture, out.empty, error, stopping); + out.ok = current.map > 0 ? load_map(client, current.map, current.key.first, current.key.second, picture, error, stopping) : load_tile(client, request, picture, out.empty, error, stopping); if (m_stop) return; if (!out.ok) - ErrorLog("Editor orthophoto: tile " + std::to_string(current.key.second) + "_" + std::to_string(current.key.first) + " failed: " + error); + ErrorLog(std::string("Editor orthophoto: ") + (current.map > 0 ? "map " + std::to_string(current.map) + " " : std::string{}) + "tile " + std::to_string(current.key.second) + "_" + std::to_string(current.key.first) + " failed: " + error); if (out.ok && current.alpha < 255) for (std::size_t i = 3; i < picture.rgba.size(); i += 4) picture.rgba[i] = static_cast((picture.rgba[i] * current.alpha + 127u) / 255u); @@ -1235,7 +1354,14 @@ void editor_orthophoto::worker() void editor_orthophoto::draw(glm::mat4 const &ViewProjection, glm::dvec3 const &CameraPos, float ScreenWidth, float ScreenHeight) const { - if (!m_enabled || m_config.in_scene || m_config.opacity <= 0.0f || ScreenWidth <= 0.0f || ScreenHeight <= 0.0f) + if (!m_enabled || ScreenWidth <= 0.0f || ScreenHeight <= 0.0f) + return; + if (m_map_level > 0) + { + draw_map(ViewProjection, CameraPos, ScreenWidth, ScreenHeight); + return; + } + if (m_config.in_scene || m_config.opacity <= 0.0f) return; ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); @@ -1380,3 +1506,46 @@ void editor_orthophoto::draw(glm::mat4 const &ViewProjection, glm::dvec3 const & drawlist->PopTextureID(); } } + +void editor_orthophoto::draw_map(glm::mat4 const &ViewProjection, glm::dvec3 const &CameraPos, float ScreenWidth, float ScreenHeight) const +{ + // the map takes the place of the ground, opaque; the top view sees it straight down, a tile is one quad + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); + auto const toscreen = [&](glm::vec4 const &Clip) { return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * ScreenWidth, (0.5f - Clip.y / Clip.w * 0.5f) * ScreenHeight); }; + float constexpr guard{1.02f}; + for (auto const shown : {false, true}) + for (auto const &entry : m_maps) + { + if ((entry.first.first == m_map_level) != shown) + continue; + tile const &t = entry.second; + if (t.level == 0 || t.empty || t.slot.texture == null_handle) + continue; + std::size_t const textureid = GfxRenderer->Texture(t.slot.texture).get_id(); + if (textureid == 0 || textureid == static_cast(static_cast(-1))) + continue; + auto const size{tile_size * static_cast(1 << entry.first.first)}; + glm::dvec2 const corner{m_config.east - entry.first.second.first * size, (entry.first.second.second + 1) * size - m_config.north}; + auto const at = [&](double U, double V) { return ViewProjection * glm::vec4(glm::vec3(glm::dvec3(corner.x - U * size, CameraPos.y - 1.0, corner.y - V * size) - CameraPos), 1.0f); }; + clip_vertex buffer0[12] = {{at(0.0, 0.0), {0.0f, 0.0f}}, {at(1.0, 0.0), {1.0f, 0.0f}}, {at(1.0, 1.0), {1.0f, 1.0f}}, {at(0.0, 1.0), {0.0f, 1.0f}}}; + clip_vertex buffer1[12]; + int count = clip_polygon(buffer0, 4, buffer1, [&](glm::vec4 const &v) { return v.w * guard - v.x; }); + count = clip_polygon(buffer1, count, buffer0, [&](glm::vec4 const &v) { return v.w * guard + v.x; }); + count = clip_polygon(buffer0, count, buffer1, [&](glm::vec4 const &v) { return v.w * guard - v.y; }); + count = clip_polygon(buffer1, count, buffer0, [&](glm::vec4 const &v) { return v.w * guard + v.y; }); + if (count < 3) + continue; + drawlist->PushTextureID((ImTextureID)(intptr_t)(textureid)); + drawlist->PrimReserve((count - 2) * 3, count); + auto const base = static_cast(drawlist->_VtxCurrentIdx); + for (int k = 1; k + 1 < count; ++k) + { + drawlist->PrimWriteIdx(base); + drawlist->PrimWriteIdx(static_cast(base + k)); + drawlist->PrimWriteIdx(static_cast(base + k + 1)); + } + for (int k = 0; k < count; ++k) + drawlist->PrimWriteVtx(toscreen(buffer0[k].position), ImVec2(buffer0[k].uv.x, buffer0[k].uv.y), IM_COL32_WHITE); + drawlist->PopTextureID(); + } +} diff --git a/editor/editorOrthophoto.hpp b/editor/editorOrthophoto.hpp index 1a808d04..32f5156c 100644 --- a/editor/editorOrthophoto.hpp +++ b/editor/editorOrthophoto.hpp @@ -106,6 +106,10 @@ class editor_orthophoto // true for geometry created by the layer, so ground queries can skip it static bool owns(gfx::geometry_handle const &Geometry); + // a top view of Scale metres a pixel shows the base map of OpenStreetMap in place of the imagery, as far as Reach from the + // camera; 0: the imagery + void overview(double const Scale, double const Reach); + bool overview() const { return m_map_level > 0; } // main thread, once per frame: schedules tiles around the camera and uploads finished ones void update(glm::dvec3 const &Camera); // main thread, inside an ImGui frame: emits the overlay into the background draw list. @@ -135,6 +139,7 @@ class editor_orthophoto struct job { tile_key key; + int map{0}; // level of a tile of the map, 256 m << map a side; 0: of the imagery int level{0}; // requested texture size in pixels std::uint8_t alpha{255}; // baked into the texture, for translucency in the scene source src; @@ -144,6 +149,7 @@ class editor_orthophoto struct result { tile_key key; + int map{0}; int level{0}; std::uint8_t alpha{255}; std::uint32_t generation{0}; @@ -188,7 +194,7 @@ class editor_orthophoto static int level_for(int Distance, bool Hires); tile_key camera_tile(glm::dvec3 const &Camera) const; glm::dvec2 tile_corner(tile_key const &Key) const; // world (x,z) of the north-west corner - void schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha); + void schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha, int const Map = 0); std::uint8_t texture_alpha() const; // alpha the textures should carry with the current settings std::size_t apply(result &Result, double Now); // returns bytes uploaded void release_tile(tile &Tile); @@ -198,6 +204,8 @@ class editor_orthophoto void remove_shape(tile &Tile); void release_geometry(tile &Tile); void update_scene(); + void update_map(glm::dvec3 const &Camera, double const Now); + void draw_map(glm::mat4 const &ViewProjection, glm::dvec3 const &CameraPos, float ScreenWidth, float ScreenHeight) const; void start_workers(); void worker(); @@ -205,6 +213,10 @@ class editor_orthophoto bool m_enabled{false}; std::map m_tiles; // main thread only tile_key m_camera_tile{0, 0}; + using map_key = std::pair; // level, then the place on the grid of the tiles of that level + std::map m_maps; // main thread only + int m_map_level{0}; // of the map shown, 0: the imagery + double m_map_reach{0.0}; // m from the camera the view of the map reaches std::vector m_free_slots; // pool of textures, reused between tiles int m_texture_count{0}; ground_query m_ground; diff --git a/editor/editorSettings.hpp b/editor/editorSettings.hpp index de7628b8..e0e9f001 100644 --- a/editor/editorSettings.hpp +++ b/editor/editorSettings.hpp @@ -90,7 +90,7 @@ private: bool m_terrain_offer{true}; bool m_track_tools_more{false}; int m_top_tracks{0}; // the railway in the top view: whole, without the ballast, the rails alone - std::uint32_t m_top_rails{0xffd246ff}; // colour of the rails alone, as ImGui keeps it: ABGR + std::uint32_t m_top_rails{0xffff6e28}; // colour of the rails alone, as ImGui keeps it: ABGR track_norms m_norms{track_norms::ask}; }; diff --git a/editor/editorTerrainStreamer.cpp b/editor/editorTerrainStreamer.cpp index 91067226..feec0a1a 100644 --- a/editor/editorTerrainStreamer.cpp +++ b/editor/editorTerrainStreamer.cpp @@ -598,7 +598,7 @@ void terrain_streamer::update(glm::dvec3 const &Camera) } // 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 top{Global.EditorOrtho && Global.editor_tracks && false == Global.EditorOrthoMap}; 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; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index d57b4c01..c3d704cf 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11499,3 +11499,6 @@ msgstr "Jak widok z góry rysuje tory" msgid "The colour of the rails, to stand out on the imagery and the map" msgstr "Kolor szyn, żeby odcinały się od ortofotomapy i mapy" + +msgid "(c) OpenStreetMap contributors, geoportal.gov.pl" +msgstr "(c) współtwórcy OpenStreetMap, geoportal.gov.pl" diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 5d89a8bb..3d6a3004 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -2649,7 +2649,8 @@ void opengl33_renderer::Render(section_sequence::iterator First, section_sequenc case rendermode::shadows: case rendermode::pickscenery: { - if (false == section->m_shapes.empty()) + // the top view of the editor far out has the map in place of the ground + if (false == section->m_shapes.empty() && false == (EditorModeFlag && Global.EditorOrtho && Global.EditorOrthoMap)) { // since all shapes of the section share center point we can optimize out a few calls here ::glPushMatrix(); diff --git a/utilities/Globals.h b/utilities/Globals.h index 36be95b6..50c5118a 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -175,6 +175,7 @@ struct global_settings { bool EditorOrtho{ false }; float EditorOrthoExtent{ 150.f }; int EditorOrthoTracks{ 0 }; // the railway in the top view of the editor: 0 whole, 1 without the ballast, 2 the rails alone + bool EditorOrthoMap{ false }; // the top view of the editor far out shows the base map in place of the ground, which isn't drawn then 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 From 7c0b3bc8580074032233f1b9f14b12045d89b4d1 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 15:22:17 +0200 Subject: [PATCH 083/145] feat(editor): all the labels of the tracks in the top view, the straights with their lengths too; one which would cover another or the tracks moved out to the nearest free room, a line leading back to its track --- application/editormodetrackux.cpp | 139 ++++++++++++++++++++++++------ 1 file changed, 112 insertions(+), 27 deletions(-) diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index ee669f04..64fe2d8c 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -96,7 +96,6 @@ double constexpr kBeyond{std::numeric_limits::max()}; ImU32 const kUncoloured{IM_COL32(200, 200, 200, 170)}; double const kColouredReach{1500.0}; // m around the camera in the 3D view double const kLabelsTo{1.0}; // m a pixel of the top view, farther out the labels of the tracks are left out -double const kLabelledLength{5.0}; // m, shorter curves get no label std::vector const kRadiusBands{{300.0, IM_COL32(235, 60, 60, 235), "R < 300 m"}, {600.0, IM_COL32(245, 140, 50, 235), "R 300-600 m"}, {1200.0, IM_COL32(240, 220, 60, 235), "R 600-1200 m"}, {3000.0, IM_COL32(110, 210, 80, 235), "R 1200-3000 m"}, {kBeyond, IM_COL32(70, 190, 235, 235), "R > 3000 m"}}; std::vector const kSpeedBands{{40.5, IM_COL32(235, 60, 60, 235), "<= 40 km/h"}, {60.5, IM_COL32(245, 140, 50, 235), "50-60 km/h"}, {80.5, IM_COL32(240, 220, 60, 235), "70-80 km/h"}, @@ -280,30 +279,25 @@ void editor_mode::draw_track_labels() const auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; auto const low{editor_ui::view_min()}; auto const high{editor_ui::view_max()}; - // a label which would cover another one is left out: the switches go first, then the sharper curves - std::vector> taken; - auto const write = [&](glm::dvec3 const &Point, std::string const &Text, ImU32 const Colour) { - ImVec2 at; - if (false == projection.project(Point, at)) - return; - auto const size{ImGui::CalcTextSize(Text.c_str())}; - ImVec2 const from{at.x - size.x * 0.5f - 4.0f, at.y - size.y - 12.0f}; - ImVec2 const to{at.x + size.x * 0.5f + 4.0f, at.y - 8.0f}; - if (from.x < low.x || from.y < low.y || to.x > high.x || to.y > high.y) - return; - if (std::any_of(taken.begin(), taken.end(), [&](auto const &Rect) { return from.x < Rect.second.x + 6.0f && to.x > Rect.first.x - 6.0f && from.y < Rect.second.y + 3.0f && to.y > Rect.first.y - 3.0f; })) - return; - taken.emplace_back(from, to); - drawlist->AddRectFilled(from, to, IM_COL32(0, 0, 0, 165), 3.0f); - drawlist->AddText(ImVec2(from.x + 4.0f, from.y), Colour, Text.c_str()); - }; - struct curve_label + struct track_label { - double radius; + int rank; + double order; glm::dvec3 point; std::string text; + ImU32 colour; + }; + std::vector labels; + // the screen in cells, the ones the tracks are drawn over marked: a label keeps off them when it can + float constexpr cell{8.0f}; + auto const columns{std::max(1, static_cast((high.x - low.x) / cell) + 1)}; + auto const rows{std::max(1, static_cast((high.y - low.y) / cell) + 1)}; + std::vector covered(static_cast(columns) * rows, 0); + auto const cell_of = [&](float const X, float const Y, int &Column, int &Row) { + Column = static_cast((X - low.x) / cell); + Row = static_cast((Y - low.y) / cell); + return Column >= 0 && Row >= 0 && Column < columns && Row < rows; }; - std::vector curves; for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || false == rail(*track) || track->m_paths.empty()) @@ -311,21 +305,112 @@ void editor_mode::draw_track_labels() const bezier const first{track->m_paths.front()}; if (false == plan_near(first, centre, reach)) continue; + for (auto const &path : track->m_paths) + { + bezier const curve{path}; + auto const count{std::clamp(static_cast(curve.plan_length() / (metres * cell)), 2, 256)}; + ImVec2 previous; + bool known{false}; + for (int k = 0; k <= count; ++k) + { + ImVec2 at; + if (false == projection.project(curve.point(static_cast(k) / count), at)) + { + known = false; + continue; + } + if (known) + { + auto const steps{std::max(1, static_cast(std::max(std::abs(at.x - previous.x), std::abs(at.y - previous.y)) / cell * 2.0f))}; + for (int step = 0; step <= steps; ++step) + { + auto const t{static_cast(step) / steps}; + int column, row; + if (cell_of(previous.x + (at.x - previous.x) * t, previous.y + (at.y - previous.y) * t, column, row)) + covered[static_cast(row) * columns + column] = 1; + } + } + previous = at; + known = true; + } + } if (track->eType == tt_Switch) { auto text{name_of(*track)}; if (auto const record{switch_store::of(*track)}) text += " " + switch_store::label(*record); - write(first.point(0.0), text, IM_COL32(255, 220, 120, 255)); + labels.push_back({0, 0.0, first.point(0.0), text, IM_COL32(255, 220, 120, 255)}); continue; } + auto const length{first.plan_length()}; + if (length < 0.5) + continue; auto const radius{::track_mode::piece_plan_radius(track->m_paths.front())}; - if (radius >= 0.5 && first.plan_length() >= kLabelledLength) - curves.push_back({radius, first.point(0.5), format("R %.0f L %.1f", radius, first.plan_length())}); + if (radius >= 0.5) + labels.push_back({1, radius, first.point(0.5), format("R %.0f L %.1f", radius, length), IM_COL32(235, 235, 235, 240)}); + else + labels.push_back({2, -length, first.point(0.5), format("L %.1f", length), IM_COL32(200, 215, 230, 225)}); + } + std::sort(labels.begin(), labels.end(), [](track_label const &A, track_label const &B) { return A.rank != B.rank ? A.rank < B.rank : A.order < B.order; }); + // each label where it reads best: beside its track if it's free there, else moved out to the nearest room, a line leading back + std::vector> taken; + auto const tracks_under = [&](ImVec2 const &From, ImVec2 const &To) { + int count{0}; + for (auto y = From.y; y <= To.y + cell * 0.5f; y += cell) + for (auto x = From.x; x <= To.x + cell * 0.5f; x += cell) + { + int column, row; + if (cell_of(std::min(x, To.x), std::min(y, To.y), column, row)) + count += covered[static_cast(row) * columns + column]; + } + return count; + }; + float constexpr kRings[]{0.0f, 26.0f, 42.0f, 62.0f, 86.0f, 116.0f, 150.0f}; + int constexpr kDirections{16}; + for (auto const &label : labels) + { + ImVec2 at; + if (false == projection.project(label.point, at) || at.x < low.x || at.y < low.y || at.x > high.x || at.y > high.y) + continue; + auto const size{ImGui::CalcTextSize(label.text.c_str())}; + auto const width{size.x + 8.0f}; + auto const height{size.y + 4.0f}; + ImVec2 const nearest{at.x, at.y - 10.0f - height * 0.5f}; + auto best{std::numeric_limits::max()}; + ImVec2 from, to; + for (auto const ring : kRings) + for (int k = 0; k < (ring > 0.0f ? kDirections : 1); ++k) + { + auto const angle{static_cast(k) * 6.2831853f / kDirections}; + ImVec2 const middle{nearest.x + std::cos(angle) * ring * 1.6f, nearest.y + std::sin(angle) * ring}; + ImVec2 const a{middle.x - width * 0.5f, middle.y - height * 0.5f}; + ImVec2 const b{middle.x + width * 0.5f, middle.y + height * 0.5f}; + if (a.x < low.x || a.y < low.y || b.x > high.x || b.y > high.y) + continue; + if (std::any_of(taken.begin(), taken.end(), [&](auto const &Rect) { return a.x < Rect.second.x + 4.0f && b.x > Rect.first.x - 4.0f && a.y < Rect.second.y + 2.0f && b.y > Rect.first.y - 2.0f; })) + continue; + auto const cost{ring + 5.0f * static_cast(tracks_under(a, b))}; + if (cost < best) + { + best = cost; + from = a; + to = b; + } + } + if (best == std::numeric_limits::max()) + continue; + taken.emplace_back(from, to); + ImVec2 const middle{(from.x + to.x) * 0.5f, (from.y + to.y) * 0.5f}; + if (std::hypot(middle.x - nearest.x, middle.y - nearest.y) > 1.0f) + { + ImVec2 const end{std::clamp(at.x, from.x, to.x), std::clamp(at.y, from.y, to.y)}; + auto const lead{(label.colour & 0x00ffffffu) | (170u << 24)}; + drawlist->AddLine(at, end, lead, 1.0f); + drawlist->AddCircleFilled(at, 2.5f, lead); + } + drawlist->AddRectFilled(from, to, IM_COL32(0, 0, 0, 165), 3.0f); + drawlist->AddText(ImVec2(from.x + 4.0f, from.y + 2.0f), label.colour, label.text.c_str()); } - std::sort(curves.begin(), curves.end(), [](curve_label const &A, curve_label const &B) { return A.radius < B.radius; }); - for (auto const &curve : curves) - write(curve.point, curve.text, IM_COL32(235, 235, 235, 240)); } void editor_mode::draw_track_legend() const From 4d2f95aa4367ae9ad70ae07afde64a95e9836389 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 15:55:58 +0200 Subject: [PATCH 084/145] feat(editor): the tracks laid on the terrain model go onto it as the tracks going onto the terrain do, the chain laid since the laying began fitted again at each click, with the tracks around kept and met; a straight or an arc cut by the fit before made one again, a path cut only where it would part from the line by more than 3 cm; a choice of the heights while laying: fitted, from point to point, level, kept in the settings; the fit taken back with its click --- application/editormode.h | 19 +- application/editormodelay.cpp | 22 ++- application/editormodenmt.cpp | 237 +++++++++++++++++++++---- application/editormodeprofile.cpp | 10 +- application/editormodeselftestjoin.cpp | 69 ++++--- application/editormodetrackbed.cpp | 35 ++-- editor/editorProfile.cpp | 49 +++++ editor/editorProfile.hpp | 3 + editor/editorSettings.cpp | 3 + editor/editorSettings.hpp | 11 ++ lang/editor_pl.po | 24 +++ 11 files changed, 392 insertions(+), 90 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index b85704eb..72df0eef 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -436,8 +436,6 @@ class editor_mode : public application_mode, private editor_track::observer, pri void nmt_follow_tracks(); // the height the ground will have where the terrain is made ahead of the track laid and isn't there yet bool nmt_ahead(double const X, double const Z, double &Height) const; - // a track laid through the points gets its formation from the terrain of the model: it needn't keep to every bump of it - bool nmt_formed(std::vector const &Points) const; // the squares of the model under the rectangle are in, none of them waiting to be asked for again bool nmt_ready(glm::dvec2 const &Min, glm::dvec2 const &Max); // Only: the paths of these tracks are shaped anew, the modifiers of those removed taken away; null: all of them. the number @@ -466,8 +464,18 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::size_t nmt_join_ends(); void nmt_auto_start(); void nmt_auto_step(); - // the grade line of the line of tracks through the track fitted to the terrain model and applied; false if it couldn't be - bool nmt_fit_line(TTrack &Track); + // the grade line of the line of tracks through the track fitted to the terrain model and applied; false if it couldn't be. + // The line runs over the tracks not Fitted, the ones fitted stay as they are and the line meets them. Where the model falls + // away under the line by the abutments the tracks go over on a bridge; Laid: the tracks just laid, the formation fills it and + // they are cut only where they can't follow the line otherwise + bool nmt_fit_line(TTrack &Track, std::function const &Fitted, bool const Laid); + // the tracks laid in one go since the history step From put onto the terrain model as one line, as the tracks go onto the + // terrain: the track ending at End then the one the laying goes on from + bool nmt_fit_laid(std::size_t const From, glm::dvec3 const End); + // the tracks laid one after another on one straight or one arc, cut by the grade lines fitted before, made one again + void nmt_merge_laid(std::unordered_set &Laid); + // the points of the paths of the track about a metre apart, in squares of the plan + static void nmt_sample_paths(TTrack const &Track, std::map, std::vector> &Points); // the tool of the terrain the left mouse button works enum class terrain_tool { @@ -1384,6 +1392,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri glm::dvec3 point{0.0}; }; std::vector chain; + std::size_t history{0}; // the step of the history the chain was begun at }; lay_tool m_lay; bool m_norms_free{false}; @@ -2335,7 +2344,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void profile_check(); // Terrain: the heightmap terrain follows the tracks changed. Bridges: stretches of the route over a bridge or a viaduct, the // paths over them cut off at their ends and set to the bridge environment - void profile_apply(bool const Terrain = false, std::vector const &Bridges = {}); + void profile_apply(bool const Terrain = false, std::vector const &Bridges = {}, double const Fewest = 0.0); // the terrain under the tracks changed by the last Edits steps of the history which changed tracks void profile_follow_terrain(std::size_t const Edits); void profile_fit_view(); diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index dc173cdd..9e4e6bcb 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -66,7 +66,7 @@ void editor_mode::update_build_tools() m_extend.mouse = cursor_terrain(m_extend.point.y); m_extend.snap = m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; if (m_extend.snap.track == nullptr) - m_extend.mouse.y = bed_height(m_extend.mouse, plan_of(m_extend.mouse - m_extend.point), m_extend.track); + m_extend.mouse.y = EditorSettings.heights() == editorSettings::lay_heights::level ? m_extend.point.y : bed_height(m_extend.mouse, plan_of(m_extend.mouse - m_extend.point), m_extend.track); joining(m_extend.snap); } if (m_switch.placing) @@ -634,12 +634,14 @@ void editor_mode::finish_extend() give_own_speed(created); editor_track::commit(created); digitize_drawn(created); + auto const from{m_history.size()}; push_track_snapshot({}, created); first_step_done(first_step::laid); if (target.track != nullptr) first_step_done(first_step::join); m_node = created.back(); ui()->set_node(m_node); + nmt_fit_laid(from, created.back()->m_paths.front().points[segment_data::point::end]); straight_refresh(); } @@ -1122,6 +1124,8 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) give_own_speed(created); editor_track::commit(created); digitize_drawn(created); + if (tool.chain.empty()) + tool.history = m_history.size(); push_track_snapshot({}, created); first_step_done(first_step::laid); if (End.track != nullptr) @@ -1152,11 +1156,13 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) } if (scene::Layers.active() == null_handle && style == nullptr) tool.status += "\n" + STR("The scenery isn't opened for editing: new track won't be saved"); + m_node = created.back(); + ui()->set_node(m_node); + if (nmt_fit_laid(tool.history, created.back()->m_paths.front().points[segment_data::point::end])) + tool.status += "\n" + STR("The grade line of the whole track laid in a row fitted to the terrain model, as the tracks go onto the terrain"); WriteLog("Editor: " + tool.status, logtype::generic); tool.points.clear(); tool.start = {}; - m_node = created.back(); - ui()->set_node(m_node); straight_refresh(); } @@ -1522,6 +1528,16 @@ void editor_mode::render_lay_ui() : "Off: a track laid on from another one, or joined to it, is designed for the speed of that track and takes it.\nOn: it's designed for the speed above and the new tracks get it.\nWhile laying: Ctrl+Shift+wheel"); if (auto const [from, to]{laying_ends()}; false == m_norms_free && (from != nullptr || to != nullptr)) ImGui::TextDisabled("%s", design_speed_of(from, to).c_str()); + ImGui::TextDisabled("%s", STR_C("Height of the track on the ground")); + if (int heights{static_cast(EditorSettings.heights())}; + segmented("layheights", heights, + {{0, STR_C("Fitted"), "The grades fitted to the ground as Tracks onto the terrain fits them: vertical curves of the norms, the bumps of the ground gone past, the earthworks made by the terrain. On the terrain model the whole track laid in a row is fitted again at each click"}, + {1, STR_C("Point to point"), "Each point clicked at the height of the ground, an even grade between them: the grade changes only where you click"}, + {2, STR_C("Flat"), "The track goes on at the height it's laid from, whatever the ground; the first point on the ground"}})) + { + EditorSettings.heights(static_cast(heights)); + EditorSettings.save(); + } if (auto *track{selected_track()}; track != nullptr) { std::string reason; diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index 7760a619..e13e5c2e 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -751,20 +751,6 @@ bool editor_mode::nmt_ahead(double const X, double const Z, double &Height) cons return true; } -bool editor_mode::nmt_formed(std::vector const &Points) const -{ - auto const &state{m_nmt_state}; - if (false == state.shape || false == m_nmt.placed() || false == m_streamer.active()) - return false; - bool foreign{false}; - if (nmt_terrain_offset(Points, foreign).has_value()) - return true; - if (foreign) - return false; - double height; - return std::all_of(Points.begin(), Points.end(), [&](glm::dvec2 const &Point) { return false == m_streamer.height_at(Point.x, Point.y, height) && nmt_ahead(Point.x, Point.y, height); }); -} - editor_mode::nmt_chunk editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key, std::optional const Offset) { auto const low{heightmap::chunk_corner(Key)}; @@ -1675,11 +1661,10 @@ void editor_mode::nmt_auto_start() state.status = STR("Fetching the terrain model under the tracks..."); } -bool editor_mode::nmt_fit_line(TTrack &Track) +bool editor_mode::nmt_fit_line(TTrack &Track, std::function const &Fitted, bool const Laid) { auto &profile{m_profile}; - auto const &done{m_nmt_state.done}; - auto const fitted = [&](TTrack const *Other) { return std::find(done.begin(), done.end(), Other) != done.end(); }; + auto const &fitted{Fitted}; // the line runs over the tracks not fitted yet around the track: the ones fitted stay as they are auto route{editor_track::run_route(Track, profile.run_length)}; auto const at{std::find_if(route.spans.begin(), route.spans.end(), [&](auto const &Span) { return Span.track == &Track; })}; @@ -2076,11 +2061,12 @@ bool editor_mode::nmt_fit_line(TTrack &Track) // spans a few metres apart are one bridge on its piers; what is shorter than that is a culvert under the embankment double constexpr kPier{5.0}; // m double constexpr kCulvert{3.0}; // m + double constexpr kFewest{0.03}; // m, how far a path just laid may part from the line before it's cut std::vector heights(count); for (std::size_t i = 0; i < count; ++i) heights[i] = profile::elevation(fitted_line, sampled[i]); std::vector bridges; - for (auto const &stretch : profile::bridged(sampled, heights, model, kBridged, kAbutment)) + for (auto const &stretch : Laid ? std::vector{} : profile::bridged(sampled, heights, model, kBridged, kAbutment)) if (false == bridges.empty() && stretch.from - bridges.back().to < kPier) { bridges.back().to = stretch.to; @@ -2089,7 +2075,7 @@ bool editor_mode::nmt_fit_line(TTrack &Track) else bridges.push_back(stretch); bridges.erase(std::remove_if(bridges.begin(), bridges.end(), [&](profile::departure const &Bridge) { return Bridge.to - Bridge.from < kCulvert; }), bridges.end()); - profile_apply(false, bridges); + profile_apply(false, bridges, Laid ? kFewest : 0.0); auto const applied{profile.error.empty()}; // where the line parts from the model: under it a gap, a bridge or a viaduct, over it a wall or a structure above the track auto const parted{profile::departures(fitted_line, sampled, model, 2.0 * kModel)}; @@ -2186,6 +2172,200 @@ bool editor_mode::nmt_fit_line(TTrack &Track) return applied; } +void editor_mode::nmt_sample_paths(TTrack const &Track, std::map, std::vector> &Points) +{ + for (auto const &path : Track.m_paths) + { + geometry::bezier const curve{path}; + auto const chord{glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end])}; + auto const count{std::max(1, static_cast(std::ceil(chord * 1.1)))}; + for (int i = 0; i <= count; ++i) + { + auto const t{static_cast(i) / count}; + auto const at{curve.point(t)}; + auto const tangent{curve.first(t)}; + auto const run{glm::length(glm::dvec2{tangent.x, tangent.z})}; + if (run > 1e-9) + Points[{static_cast(std::floor(at.x / kPlanCell)), static_cast(std::floor(at.z / kPlanCell))}].push_back( + {at, glm::dvec2{tangent.x, tangent.z} / run, tangent.y / run, at.y, static_cast(Track.rail_height()), &Track}); + } + } +} + +void editor_mode::nmt_merge_laid(std::unordered_set &Laid) +{ + auto const direction = [](segment_data const &Path) { + auto const control{geometry::plan_of(Path.points[segment_data::point::control1])}; + auto const chord{geometry::plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])}; + auto const along{glm::length(control) > 1e-6 ? control : chord}; + return along / std::max(1e-12, glm::length(along)); + }; + auto const straight = [&](segment_data const &Path) { + if (std::abs(Path.radius) > 1e-3f) + return false; + auto const along{direction(Path)}; + return std::abs(geometry::cross(along, geometry::plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start]))) < 1e-3 + && std::abs(geometry::cross(along, geometry::plan_of(Path.points[segment_data::point::control2]))) < 1e-3; + }; + struct circle + { + glm::dvec2 centre{0.0}; + double radius{0.0}; + int side{0}; + double angle{0.0}; + }; + auto const arc = [&](segment_data const &Path) -> std::optional { + auto const radius{std::abs(static_cast(Path.radius))}; + if (radius < 1e-3 || glm::length(geometry::plan_of(Path.points[segment_data::point::control1])) < 1e-6) + return std::nullopt; + auto const start{geometry::plan_of(Path.points[segment_data::point::start])}; + auto const end{geometry::plan_of(Path.points[segment_data::point::end])}; + auto const along{direction(Path)}; + auto const side{geometry::cross(along, end - start) > 0.0 ? 1 : -1}; + circle result{start + glm::dvec2{-along.y, along.x} * (side * radius), radius, side, 0.0}; + if (std::abs(glm::distance(result.centre, end) - radius) > 0.01) + return std::nullopt; + result.angle = std::abs(geometry::signed_angle(start - result.centre, end - result.centre)); + return result; + }; + auto const alike = [](TTrack const &A, TTrack const &B) { + return editor_track::velocity(A) == editor_track::velocity(B) && A.eEnvironment == B.eEnvironment && A.iCategoryFlag == B.iCategoryFlag && A.iDamageFlag == B.iDamageFlag; + }; + std::vector> states; + std::vector removed; + for (bool merged{true}; merged;) + { + merged = false; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || false == Laid.contains(track) || track->eType != tt_Normal || track->m_paths.size() != 1) + continue; + auto *next{track->trNext}; + if (next == nullptr || next == track || next->m_editorremoved || false == Laid.contains(next) || next->eType != tt_Normal || next->m_paths.size() != 1 || next->trPrev != track || false == alike(*track, *next)) + continue; + auto const &first{track->m_paths.front()}; + auto const &second{next->m_paths.front()}; + if (glm::distance(first.points[segment_data::point::end], second.points[segment_data::point::start]) > 1e-3) + continue; + auto const length{geometry::bezier(first).plan_length() + geometry::bezier(second).plan_length()}; + if (length > geometry::longest_path) + continue; + auto path{first}; + path.pieces.clear(); + if (straight(first) && straight(second) && std::abs(geometry::cross(direction(first), direction(second))) < 1e-5 && glm::dot(direction(first), direction(second)) > 0.0) + { + path.points[segment_data::point::control1] = {}; + path.points[segment_data::point::control2] = {}; + path.radius = 0.f; + } + else if (auto const a{arc(first)}, b{arc(second)}; + a && b && a->side == b->side && std::abs(a->radius - b->radius) < 1e-3 * a->radius && glm::distance(a->centre, b->centre) < 0.01 && a->angle + b->angle <= glm::radians(90.0)) + { + auto const pieces{geometry::arc_pieces(first.points[segment_data::point::start], direction(first), 0.0, a->radius, a->angle + b->angle, a->side)}; + if (pieces.size() != 1) + continue; + path.points[segment_data::point::control1] = pieces.front().points[segment_data::point::control1]; + path.points[segment_data::point::control2] = pieces.front().points[segment_data::point::control2]; + } + else + continue; + path.points[segment_data::point::end] = second.points[segment_data::point::end]; + path.rolls[1] = second.rolls[1]; + if (std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == track; })) + states.emplace_back(track, editor_track::capture(*track)); + editor_track::retire(*next); + Laid.erase(next); + removed.push_back(next); + track->m_paths.front() = path; + editor_track::commit({track}); + merged = true; + } + } + if (false == removed.empty()) + push_track_snapshot(std::move(states), {}, std::move(removed)); +} + +bool editor_mode::nmt_fit_laid(std::size_t const From, glm::dvec3 const End) +{ + auto &state{m_nmt_state}; + if (EditorSettings.heights() != editorSettings::lay_heights::fitted || state.stage != nmt_state::automatic::idle || false == m_nmt.placed() || From >= m_history.size()) + return false; + std::unordered_set laid; + for (auto step{From}; step < m_history.size(); ++step) + for (auto const *track : m_history[step].created) + if (track != nullptr && false == track->m_editorremoved && rail(*track)) + laid.insert(track); + auto const at_end = [&]() -> TTrack * { + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && laid.contains(track) && track->eType == tt_Normal && geometry::plan_distance(track->m_paths.front().points[segment_data::point::end], End) < 0.01) + return track; + return nullptr; + }; + auto const steps{m_history.size()}; + nmt_merge_laid(laid); + auto *last{at_end()}; + if (last == nullptr) + return false; + // the tracks around stay as they are, as lines fitted already: the line keeps level with the ones beside it, the more closely + // the nearer they are, and crosses the ones it crosses at their height + double constexpr kAround{60.0}; // m + glm::dvec2 low{std::numeric_limits::max()}; + glm::dvec2 high{std::numeric_limits::lowest()}; + for (auto const *track : laid) + for (auto const &path : track->m_paths) + for (auto const end : {segment_data::point::start, segment_data::point::end}) + { + low = glm::min(low, geometry::plan_of(path.points[end])); + high = glm::max(high, geometry::plan_of(path.points[end])); + } + low -= kAround; + high += kAround; + std::map, std::vector> around; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || false == rail(*track) || laid.contains(track)) + continue; + auto const &points{track->m_paths.front().points}; + auto const a{geometry::plan_of(points[segment_data::point::start])}; + auto const b{geometry::plan_of(points[segment_data::point::end])}; + if (std::max(a.x, b.x) < low.x || std::min(a.x, b.x) > high.x || std::max(a.y, b.y) < low.y || std::min(a.y, b.y) > high.y) + continue; + nmt_sample_paths(*track, around); + } + // the state of the tracks going onto the terrain lent to the one line, the profile tool left as it was + auto const profile{m_profile}; + auto fitted_lines{std::exchange(state.laid, around)}; + auto paths{std::exchange(state.paths, std::move(around))}; + auto loose{std::exchange(state.loose, {})}; + auto const departures{state.departures}; + auto const applied{nmt_fit_line(*last, [&](TTrack const *Other) { return false == laid.contains(Other); }, true)}; + auto const error{m_profile.error}; + state.laid = std::move(fitted_lines); + state.paths = std::move(paths); + state.loose = std::move(loose); + state.departures = departures; + m_profile = profile; + // taken back with the laying + for (auto step{steps}; step < m_history.size(); ++step) + m_history[step].joined = true; + if (applied) + { + laid.clear(); + for (auto step{From}; step < m_history.size(); ++step) + for (auto const *track : m_history[step].created) + if (track != nullptr && false == track->m_editorremoved) + laid.insert(track); + if (auto *track{at_end()}; track != nullptr) + { + m_node = track; + ui()->set_node(m_node); + } + } + else if (false == error.empty()) + WriteLog("Editor: the track laid not put onto the terrain model - " + error, logtype::generic); + return applied; +} + void editor_mode::nmt_auto_step() { auto &state{m_nmt_state}; @@ -2247,22 +2427,7 @@ void editor_mode::nmt_auto_step() state.paths.clear(); for (auto *track : simulation::Paths.sequence()) if (track != nullptr && rail(*track)) - for (auto const &path : track->m_paths) - { - geometry::bezier const curve{path}; - auto const chord{glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end])}; - auto const count{std::max(1, static_cast(std::ceil(chord * 1.1)))}; - for (int i = 0; i <= count; ++i) - { - auto const t{static_cast(i) / count}; - auto const at{curve.point(t)}; - auto const tangent{curve.first(t)}; - auto const run{glm::length(glm::dvec2{tangent.x, tangent.z})}; - if (run > 1e-9) - state.paths[{static_cast(std::floor(at.x / kPlanCell)), static_cast(std::floor(at.z / kPlanCell))}].push_back( - {at, glm::dvec2{tangent.x, tangent.z} / run, tangent.y / run, at.y, static_cast(track->rail_height()), track}); - } - } + nmt_sample_paths(*track, state.paths); state.stage = nmt_state::automatic::fitting; return; } @@ -2289,7 +2454,7 @@ void editor_mode::nmt_auto_step() auto *track{state.left.front()}; auto const name{track->name()}; auto const known{simulation::Paths.sequence().size()}; - if (nmt_fit_line(*track)) + if (nmt_fit_line(*track, [&](TTrack const *Other) { return fitted.contains(Other); }, false)) ++state.lines; else ++state.refused; diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 0d0add80..31835241 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -1450,7 +1450,7 @@ void editor_mode::profile_frame_view() fly_to(centre - right * (shift.x * pixel) + above * (shift.y * pixel), look, distance, 0.35); } -void editor_mode::profile_apply(bool const Terrain, std::vector const &Bridges) +void editor_mode::profile_apply(bool const Terrain, std::vector const &Bridges, double const Fewest) { auto &state{m_profile}; state.status.clear(); @@ -1475,12 +1475,14 @@ void editor_mode::profile_apply(bool const Terrain, std::vector> states; std::vector created; - auto cuts{profile::breaks(line)}; + std::vector ends; + for (auto const &span : state.route.spans) + ends.insert(ends.end(), {span.from, span.to}); + auto cuts{Fewest > 0.0 ? profile::fewest_breaks(line, ends, Fewest) : profile::breaks(line)}; // a bridge ends at a joint or a break of the line by its abutment rather than leave a sliver of a path there double constexpr kBridgeSnap{1.0}; // m auto joints{cuts}; - for (auto const &span : state.route.spans) - joints.insert(joints.end(), {span.from, span.to}); + joints.insert(joints.end(), ends.begin(), ends.end()); auto bridges{Bridges}; for (auto &bridge : bridges) for (auto *end : {&bridge.from, &bridge.to}) diff --git a/application/editormodeselftestjoin.cpp b/application/editormodeselftestjoin.cpp index 1695da02..93b79898 100644 --- a/application/editormodeselftestjoin.cpp +++ b/application/editormodeselftestjoin.cpp @@ -129,6 +129,14 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma WriteLog(format("SELFTEST %s end: %.3f m from the target", command.c_str(), geometry::plan_distance(previous, b))); return selftest_result::handled; } + if (command == "layheights") + { + std::string which; + words >> which; + EditorSettings.heights(which == "points" ? editorSettings::lay_heights::points : which == "level" ? editorSettings::lay_heights::level : editorSettings::lay_heights::fitted); + WriteLog("SELFTEST layheights: " + which); + return selftest_result::handled; + } if (command == "norms") { std::string which; @@ -237,34 +245,47 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma double hanging{0.0}; double buried{0.0}; int paths{0}; + // the paths laid as they lie now, along the line: a grade line fitted afterwards cut some of them + std::vector laid; for (auto step{before}; step < m_history.size(); ++step) for (auto const *track : m_history[step].created) + if (track != nullptr && false == track->m_editorremoved && std::find(laid.begin(), laid.end(), track) == laid.end()) + laid.push_back(track); + if (false == laid.empty()) + { + auto const origin{laid.front()->m_paths.front().points[segment_data::point::start]}; + auto const along{glm::normalize(geometry::plan_of(laid.back()->m_paths.front().points[segment_data::point::end] - origin) + glm::dvec2{1e-9, 0.0})}; + std::stable_sort(laid.begin(), laid.end(), [&](TTrack const *A, TTrack const *B) { + return glm::dot(geometry::plan_of(A->m_paths.front().points[segment_data::point::start] - origin), along) < glm::dot(geometry::plan_of(B->m_paths.front().points[segment_data::point::start] - origin), along); + }); + } + for (auto const *track : laid) + { + auto const &path{track->m_paths.front()}; + geometry::bezier const curve{path}; + auto const begin{glm::normalize(geometry::plan_of(curve.first(0.0)))}; + auto const &a{path.points[segment_data::point::start]}; + auto const &b{path.points[segment_data::point::end]}; + auto const rise = [](glm::dvec3 const &Tangent) { return Tangent.y / std::max(1e-9, glm::length(geometry::plan_of(Tangent))); }; + WriteLog(format("SELFTEST layclicks path %s: %.2f %.2f -> %.2f %.2f, L %.2f m, R %.1f m, kink %.4f deg, step %.4f m, y %.3f -> %.3f, grade %.2f -> %.2f permille, grade break %.3f permille", track->name().c_str(), a.x, a.z, b.x, b.z, curve.plan_length(), static_cast(path.radius), + first ? 0.0 : glm::degrees(std::abs(geometry::signed_angle(heading, begin))), first ? 0.0 : glm::distance(previous, a), a.y, b.y, 1000.0 * rise(curve.first(0.0)), 1000.0 * rise(curve.first(1.0)), + first ? 0.0 : 1000.0 * std::abs(rise(curve.first(0.0)) - slope))); + slope = rise(curve.first(1.0)); + heading = glm::normalize(geometry::plan_of(curve.first(1.0))); + previous = b; + first = false; + auto const count{std::max(1, static_cast(std::ceil(curve.plan_length() / 2.0)))}; + for (int k = 0; k <= count; ++k) { - auto const &path{track->m_paths.front()}; - geometry::bezier const curve{path}; - auto const begin{glm::normalize(geometry::plan_of(curve.first(0.0)))}; - auto const &a{path.points[segment_data::point::start]}; - auto const &b{path.points[segment_data::point::end]}; - auto const rise = [](glm::dvec3 const &Tangent) { return Tangent.y / std::max(1e-9, glm::length(geometry::plan_of(Tangent))); }; - WriteLog(format("SELFTEST layclicks path %s: %.2f %.2f -> %.2f %.2f, L %.2f m, R %.1f m, kink %.4f deg, step %.4f m, y %.3f -> %.3f, grade %.2f -> %.2f permille, grade break %.3f permille", track->name().c_str(), a.x, a.z, b.x, b.z, curve.plan_length(), static_cast(path.radius), - first ? 0.0 : glm::degrees(std::abs(geometry::signed_angle(heading, begin))), first ? 0.0 : glm::distance(previous, a), a.y, b.y, 1000.0 * rise(curve.first(0.0)), 1000.0 * rise(curve.first(1.0)), - first ? 0.0 : 1000.0 * std::abs(rise(curve.first(0.0)) - slope))); - slope = rise(curve.first(1.0)); - heading = glm::normalize(geometry::plan_of(curve.first(1.0))); - previous = b; - first = false; - auto const count{std::max(1, static_cast(std::ceil(curve.plan_length() / 2.0)))}; - for (int k = 0; k <= count; ++k) - { - auto const t{curve.parameter(curve.plan_length() * k / count)}; - auto const target{bed_heights({curve.point(t) + glm::dvec3{0.0, 50.0, 0.0}}, {geometry::plan_of(curve.first(t))}, track).front()}; - if (std::isnan(target)) - continue; - hanging = std::max(hanging, curve.point(t).y - target); - buried = std::max(buried, target - curve.point(t).y); - } - ++paths; + auto const t{curve.parameter(curve.plan_length() * k / count)}; + auto const target{bed_heights({curve.point(t) + glm::dvec3{0.0, 50.0, 0.0}}, {geometry::plan_of(curve.first(t))}, track).front()}; + if (std::isnan(target)) + continue; + hanging = std::max(hanging, curve.point(t).y - target); + buried = std::max(buried, target - curve.point(t).y); } + ++paths; + } WriteLog(format("SELFTEST layclicks %d paths, ballast foot over the ground up to %.3f m, under it up to %.3f m", paths, hanging, buried)); return selftest_result::handled; } diff --git a/application/editormodetrackbed.cpp b/application/editormodetrackbed.cpp index 64abc0da..a51dc07c 100644 --- a/application/editormodetrackbed.cpp +++ b/application/editormodetrackbed.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormode.h" #include "editor/editorGeometry.hpp" #include "editor/editorGroundFollow.hpp" +#include "editor/editorSettings.hpp" #include "utilities/Globals.h" #include "world/Track.h" @@ -26,9 +27,7 @@ using geometry::plan_distance; using geometry::plan_of; double constexpr kSteepest{1000.0}; // per mille: the grades of the user's own track aren't capped -double constexpr kFormed{0.5}; // m, how closely the line keeps to the ground the formation is made in -double constexpr kBare{0.1}; // m, to the ground left as it is -double constexpr kBareRadius{300.0}; // m, the vertical curves over it +double constexpr kModel{0.15}; // m, how closely the line keeps to the ground, as to the terrain model } // namespace @@ -129,35 +128,35 @@ double editor_mode::bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direct void editor_mode::follow_ground(std::vector &Pieces, double const Height0, double const Grade0, double const Height1, double const Grade1, TTrack const *Style) const { - std::vector points; - for (auto const &piece : Pieces) + // from point to point or level the heights lead from one end to the other, whatever the ground between + if (EditorSettings.heights() != editorSettings::lay_heights::fitted) { - geometry::bezier const curve{piece}; - auto const count{std::max(6, static_cast(std::ceil(curve.plan_length() / 10.0)))}; - for (int i = 0; i <= count; ++i) - points.push_back(plan_of(curve.point(static_cast(i) / count))); + ground_follow::follow(Pieces, {Height0, Grade0, Height1, Grade1}, [](std::vector const &Points, std::vector const &) { return std::vector(Points.size(), std::numeric_limits::quiet_NaN()); }); + return; } - // the ground the formation is made in lets the line keep to the norms of the profile, cutting and filling; the ground left as - // it is the line follows, bending as sharply as it has to - auto const formed{nmt_formed(points)}; + // the line goes over the ground as the tracks go onto the terrain: the vertical curves of the norms, the grades as steep as the + // ground has them, a rise or a hollow longer than a structure followed closely, the bumps and what parts from it gone past ground_follow::grading grading; grading.settings.norms = m_profile.line.norms; grading.settings.norms.grade_max = kSteepest; grading.settings.norms.curve_threshold = 0.0; - if (false == formed) - { - grading.settings.norms.radius_factor = 0.0; - grading.settings.norms.radius_min = kBareRadius; - } grading.settings.speed = design_speed(Style, nullptr); grading.fit.grade_max = kSteepest; - grading.fit.tolerance = formed ? kFormed : kBare; + grading.fit.tolerance = kModel; + grading.fit.follow_length = 60.0; + grading.fit.follow_height = 4.0; + grading.fit.follow_steepest = 100.0; + grading.fit.follow_tolerance = 0.05; + grading.reach = kModel; ground_follow::follow(Pieces, {Height0, Grade0, Height1, Grade1}, [&](std::vector const &Points, std::vector const &Directions) { return bed_heights(Points, Directions, Style); }, grading); } glm::dvec3 editor_mode::lay_bed(glm::dvec3 const &Point) { auto const &tool{m_lay}; + // level: on at the height the laying goes on from, the first point on the ground + if (EditorSettings.heights() == editorSettings::lay_heights::level && false == tool.points.empty()) + return {Point.x, tool.points.back().y, Point.z}; TTrack const *style{tool.start.track != nullptr ? tool.start.track : tool.copy_selected ? selected_track() : nullptr}; return {Point.x, bed_height(Point, tool.points.empty() ? glm::dvec2{0.0} : plan_of(Point - tool.points.back()), style), Point.z}; } diff --git a/editor/editorProfile.cpp b/editor/editorProfile.cpp index 9bbabf9b..4fbff5be 100644 --- a/editor/editorProfile.cpp +++ b/editor/editorProfile.cpp @@ -1203,6 +1203,55 @@ std::vector breaks(line const &Line) return result; } +std::vector fewest_breaks(line const &Line, std::vector Joints, double const Tolerance) +{ + std::sort(Joints.begin(), Joints.end()); + Joints.erase(std::unique(Joints.begin(), Joints.end(), [](double const A, double const B) { return std::abs(A - B) < 1e-6; }), Joints.end()); + auto const all{breaks(Line)}; + auto const inward = [&](double const Chainage, double const Other) { return grade(Line, Chainage + std::min(1e-3, std::abs(Other - Chainage) * 0.5) * (Other > Chainage ? 1.0 : -1.0)); }; + auto const follows = [&](double const From, double const To) { + auto const length{To - From}; + auto const h0{elevation(Line, From)}; + auto const h1{elevation(Line, To)}; + auto const g0{inward(From, To) * length}; + auto const g1{inward(To, From) * length}; + auto const count{std::max(4, static_cast(std::ceil(length)))}; + for (int i = 1; i < count; ++i) + { + auto const u{static_cast(i) / count}; + auto const u2{u * u}; + auto const u3{u2 * u}; + auto const height{(2.0 * u3 - 3.0 * u2 + 1.0) * h0 + (u3 - 2.0 * u2 + u) * g0 + (-2.0 * u3 + 3.0 * u2) * h1 + (u3 - u2) * g1}; + if (std::abs(height - elevation(Line, From + u * length)) > Tolerance) + return false; + } + return true; + }; + std::vector result; + for (std::size_t k = 0; k + 1 < Joints.size(); ++k) + { + std::vector stops; + for (auto const chainage : all) + if (chainage > Joints[k] + 1e-6 && chainage < Joints[k + 1] - 1e-6) + stops.push_back(chainage); + stops.push_back(Joints[k + 1]); + auto from{Joints[k]}; + std::size_t next{0}; + while (next + 1 < stops.size()) + { + auto reach{next}; + for (auto i{next + 1}; i < stops.size() && follows(from, stops[i]); ++i) + reach = i; + if (reach + 1 == stops.size()) + break; + result.push_back(stops[reach]); + from = stops[reach]; + next = reach + 1; + } + } + return result; +} + std::string format_chainage(double const Chainage) { auto const value{std::abs(Chainage)}; diff --git a/editor/editorProfile.hpp b/editor/editorProfile.hpp index 5f1c42f7..9380e0d4 100644 --- a/editor/editorProfile.hpp +++ b/editor/editorProfile.hpp @@ -179,6 +179,9 @@ std::vector bridged(std::vector const &Chainages, std::vector // chainages where the paths have to end so that each of them carries a single grade or a single curve std::vector breaks(line const &Line); +// of the breaks those the paths between Joints can't do without: a path from one joint or break kept to the next follows the line +// within Tolerance with the heights and the grades of its ends +std::vector fewest_breaks(line const &Line, std::vector Joints, double const Tolerance); std::string format_chainage(double const Chainage); diff --git a/editor/editorSettings.cpp b/editor/editorSettings.cpp index 8c22d39c..1fc92150 100644 --- a/editor/editorSettings.cpp +++ b/editor/editorSettings.cpp @@ -116,6 +116,8 @@ bool editorSettings::load() m_top_rails = static_cast(std::strtoul(value.c_str(), nullptr, 16)) | 0xff000000u; else if (key == "track_norms") m_norms = value == "free" ? track_norms::free_laying : value == "plk" ? track_norms::plk : track_norms::ask; + else if (key == "lay_heights") + m_heights = value == "points" ? lay_heights::points : value == "level" ? lay_heights::level : lay_heights::fitted; else if (key.rfind(origin_prefix, 0) == 0) { // northing;easting @@ -165,6 +167,7 @@ bool editorSettings::save() stream << "top_tracks " << m_top_tracks << "\n"; stream << "top_rails " << std::hex << (m_top_rails & 0xffffffu) << std::dec << "\n"; stream << "track_norms " << (m_norms == track_norms::free_laying ? "free" : m_norms == track_norms::plk ? "plk" : "ask") << "\n"; + stream << "lay_heights " << (m_heights == lay_heights::points ? "points" : m_heights == lay_heights::level ? "level" : "fitted") << "\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 e0e9f001..45a964b9 100644 --- a/editor/editorSettings.hpp +++ b/editor/editorSettings.hpp @@ -76,6 +76,16 @@ public: }; track_norms norms() const { return m_norms; } void norms(track_norms Norms) { m_norms = Norms; } + // the heights of the tracks laid on the ground: fitted to it as the tracks go onto the terrain, the points clicked on it with + // even grades between them, level + enum class lay_heights + { + fitted, + points, + level + }; + lay_heights heights() const { return m_heights; } + void heights(lay_heights Heights) { m_heights = Heights; } private: movement_scheme m_movement{movement_scheme::wsad}; @@ -92,6 +102,7 @@ private: int m_top_tracks{0}; // the railway in the top view: whole, without the ballast, the rails alone std::uint32_t m_top_rails{0xffff6e28}; // colour of the rails alone, as ImGui keeps it: ABGR track_norms m_norms{track_norms::ask}; + lay_heights m_heights{lay_heights::fitted}; }; // global editor settings instance diff --git a/lang/editor_pl.po b/lang/editor_pl.po index c3d704cf..55bb9cc6 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11502,3 +11502,27 @@ msgstr "Kolor szyn, żeby odcinały się od ortofotomapy i mapy" msgid "(c) OpenStreetMap contributors, geoportal.gov.pl" msgstr "(c) współtwórcy OpenStreetMap, geoportal.gov.pl" + +msgid "The grade line of the whole track laid in a row fitted to the terrain model, as the tracks go onto the terrain" +msgstr "Pochylenia całego toru kładzionego za jednym razem dopasowane do modelu terenu, tak jak w „Tory na teren”" + +msgid "Height of the track on the ground" +msgstr "Wysokość toru na terenie" + +msgid "Fitted" +msgstr "Dopasowana" + +msgid "Point to point" +msgstr "Punkt–punkt" + +msgid "Flat" +msgstr "Poziomo" + +msgid "The grades fitted to the ground as Tracks onto the terrain fits them: vertical curves of the norms, the bumps of the ground gone past, the earthworks made by the terrain. On the terrain model the whole track laid in a row is fitted again at each click" +msgstr "Pochylenia dopasowane do terenu tak jak w „Tory na teren”: łuki pionowe według norm, drobne nierówności terenu pominięte, nasypy i wykopy robi teren. Na modelu terenu (NMT) cały tor kładziony za jednym razem jest dopasowywany od nowa przy każdym kliknięciu" + +msgid "Each point clicked at the height of the ground, an even grade between them: the grade changes only where you click" +msgstr "Każdy kliknięty punkt na wysokości terenu, między punktami równe pochylenie: pochylenie zmienia się tylko tam, gdzie klikniesz" + +msgid "The track goes on at the height it's laid from, whatever the ground; the first point on the ground" +msgstr "Tor idzie dalej na wysokości, z której go kładziesz, bez względu na teren; pierwszy punkt na terenie" From 7e284b293da834767877a4d597382a145e09e1d7 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Wed, 21 Oct 2026 15:50:00 +0200 Subject: [PATCH 085/145] feat(editor): texture of the rails of path 2 of a switch set apart (cherry picked from commit 16c77ab6fbd750b80d287f2a1d56eaf1f1482012) --- application/editormodetrackprops.cpp | 20 +++++++++++++++++++- editor/editorTrack.cpp | 3 ++- lang/editor_pl.po | 10 ++++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/application/editormodetrackprops.cpp b/application/editormodetrackprops.cpp index 1ca44c6d..03d72dfe 100644 --- a/application/editormodetrackprops.cpp +++ b/application/editormodetrackprops.cpp @@ -375,7 +375,7 @@ std::vector make_properties() // look { - auto &property{add(list, "rails", field::texture, section::look, group::rails, "Rails", "Texture of the rails; of both paths of a switch.\nA material dragged from the texture browser can be dropped here")}; + auto &property{add(list, "rails", field::texture, section::look, group::rails, "Rails", "Texture of the rails; of both paths of a switch, those of its path 2 can then be set apart.\nA material dragged from the texture browser can be dropped here")}; property.rebuild = true; property.texture = 0; property.empty = "No texture"; @@ -390,6 +390,24 @@ std::vector make_properties() return true; }; } + { + // a switch keeps the rails of its path 2 in its second texture, its trackbed is m_material3 + auto &property{add(list, "rails2", field::texture, section::look, group::rails, "Rails of path 2", + "Texture of the rails of path 2 of the switch, from p3 to p4, usually the diverging track.\nPath 1, from p1 to p2, keeps the texture of the rails above.\nA material dragged from the texture browser can be dropped here")}; + property.rebuild = true; + property.late = true; // after the rails, which set both paths + property.texture = 0; + property.empty = "No texture"; + property.applies = [](TTrack const &Track) { return Track.m_visible && Track.eType == tt_Switch; }; + property.get = [](TTrack const &Track) { return editor_track::material_name(Track.m_material2); }; + property.set = [](TTrack &Track, std::string const &Text) { + auto const material{editor_track::fetch_material(Text)}; + if (Track.eType != tt_Switch || material == Track.m_material2) + return false; + Track.m_material2 = material; + return true; + }; + } { // the profiles of the rails the game has (models/tory/railprofile_*), the first: none, the default shape static char const *const profiles[] = {"", "s60", "uic60", "s49", "s42", "s39", "s30pn", "s30din", "s24", "s20", "s18", "s14", "s10", "r-65", "ri60n", "lk1"}; diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index edd60a4d..15a550df 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -3130,7 +3130,8 @@ TTrack *editor_track::create_switch(switch_template const &Template, std::vector { std::ostringstream text; text.precision(std::numeric_limits::digits10); - write_header(text, "switch", Template.length, 10.0, Style, Style.m_material1); + // a switch styled after a switch keeps the rails of its path 2 + write_header(text, "switch", Template.length, 10.0, Style, Style.eType == tt_Switch ? Style.m_material2 : Style.m_material1); write_points(text, Paths[0]); write_points(text, Paths[1]); if (auto const trackbed{ballast_of(Style)}; trackbed != null_handle) diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 55bb9cc6..ef7444c7 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5549,8 +5549,14 @@ msgstr "Ograniczenie prędkości pociągów z opuszczonymi pantografami" msgid "Names of the track circuits (isolated) the track belongs to, separated by spaces.\nA name which doesn't exist yet makes a new circuit. Tracks with vehicles on them keep theirs" msgstr "Nazwy odcinków izolowanych, do których należy tor, rozdzielone spacjami.\nNazwa, której jeszcze nie ma, tworzy nowy odcinek izolowany. Tory z pojazdami zachowują swoje" -msgid "Texture of the rails; of both paths of a switch.\nA material dragged from the texture browser can be dropped here" -msgstr "Tekstura szyn; na rozjeździe obu jego torów.\nMożna tu upuścić materiał przeciągnięty z przeglądarki tekstur" +msgid "Texture of the rails; of both paths of a switch, those of its path 2 can then be set apart.\nA material dragged from the texture browser can be dropped here" +msgstr "Tekstura szyn; na rozjeździe obu jego torów, szyny toru 2 można potem ustawić osobno.\nMożna tu upuścić materiał przeciągnięty z przeglądarki tekstur" + +msgid "Rails of path 2" +msgstr "Szyny toru 2" + +msgid "Texture of the rails of path 2 of the switch, from p3 to p4, usually the diverging track.\nPath 1, from p1 to p2, keeps the texture of the rails above.\nA material dragged from the texture browser can be dropped here" +msgstr "Tekstura szyn toru 2 rozjazdu, od p3 do p4, zwykle toru zwrotnego.\nTor 1, od p1 do p2, zachowuje teksturę szyn podaną wyżej.\nMożna tu upuścić materiał przeciągnięty z przeglądarki tekstur" msgid "Texture of the trackbed (ballast) under the rails.\nA material dragged from the texture browser can be dropped here" msgstr "Tekstura podsypki (tłucznia) pod szynami.\nMożna tu upuścić materiał przeciągnięty z przeglądarki tekstur" From a83d5c6978f9f87d183256ed91401e77c730252a Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Wed, 21 Oct 2026 16:09:05 +0200 Subject: [PATCH 086/145] feat(editor): presets of the track properties, applied on the selected tracks (cherry picked from commit 83c8c23c802ce75ed7037c1700a1322741c283b1) --- CMakeLists.txt | 1 + application/editormode.cpp | 3 + application/editormode.h | 20 +- application/editormodetrackprops.cpp | 283 +++++++++++++++++++++++++-- application/editoruilayer.cpp | 9 + application/editoruilayer.h | 7 + application/uilayer.cpp | 1 + application/uilayer.h | 2 + editor/editorTrackPresets.cpp | 233 ++++++++++++++++++++++ editor/editorTrackPresets.hpp | 58 ++++++ lang/editor_pl.po | 95 +++++++++ 11 files changed, 689 insertions(+), 23 deletions(-) create mode 100644 editor/editorTrackPresets.cpp create mode 100644 editor/editorTrackPresets.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b920162f..acd2e35e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -182,6 +182,7 @@ set(SOURCES "input/editorkeyboardinput.cpp" "editor/editorSettings.cpp" "editor/editorModelSets.cpp" +"editor/editorTrackPresets.cpp" "editor/editorIncludeInfo.cpp" "editor/editorTerrain.cpp" "editor/editorTrack.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 32af41a2..f3a145fc 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -28,6 +28,7 @@ extern char **environ; #include "application/application.h" #include "editor/editorSettings.hpp" #include "editor/editorModelSets.hpp" +#include "editor/editorTrackPresets.hpp" #include "editor/editorIncludeInfo.hpp" #include "editor/editorGroundMesh.hpp" #include "editor/editorFormat.hpp" @@ -542,6 +543,7 @@ editor_mode::editor_mode() { ui()->set_new_scenery([this]() { m_newscenery_asked = true; }); ui()->set_open_scenery([this]() { m_openscenery_asked = true; }); ui()->set_edit_menu([this]() { render_edit_menu(); }); + ui()->set_tools_menu([this]() { render_track_presets_menu(); }); ui()->set_menu_options([this]() { render_object_menu(); render_view_menu(); @@ -648,6 +650,7 @@ bool editor_mode::init() Global.EditorOrthoTracks = EditorSettings.top_tracks(); set_norms_free(EditorSettings.norms() == editorSettings::track_norms::free_laying); EditorModelSets.load(); + EditorTrackPresets.load(); Camera.Init({0, 15, 0}, {glm::radians(-30.0), glm::radians(180.0), 0}, nullptr); return m_input.init(); } diff --git a/application/editormode.h b/application/editormode.h index 72df0eef..f8d3e547 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1837,6 +1837,15 @@ class editor_mode : public application_mode, private editor_track::observer, pri void render_track_style(std::vector const &Tracks); void copy_track_style(TTrack const &Track); std::size_t paste_track_style(std::vector const &Tracks); + // which groups of the properties are pasted or put on the tracks by a preset, in the order of the groups of editormodetrackprops.cpp + using track_groups = std::array; + // the values of the properties of the chosen groups put on the tracks, in one step of the undo. returns: the number of the + // tracks changed, none when no value is of the chosen groups + std::optional apply_track_values(std::vector> const &Values, track_groups const &Groups, std::vector const &Tracks, char const *What); + // presets of the properties of the tracks (editorTrackPresets), made from a track and put on the selected ones + void render_track_presets(std::vector const &Tracks); + void render_track_presets_menu(); + std::size_t apply_track_preset(int const Id, std::vector const &Tracks); std::vector similar_tracks(int const Property, std::vector const &Tracks) const; // values of the properties over the selected tracks, each with the number of the tracks having it struct track_values @@ -1882,12 +1891,19 @@ class editor_mode : public application_mode, private editor_track::observer, pri { std::string from; std::vector> values; - // speed, environment, overhead line, track circuits, rails, trackbed, its shape, sleepers, ride and sound, frogs - std::array groups{false, false, false, false, true, true, true, true, false, true}; + // speed, environment, overhead line, track circuits, rails, trackbed, its shape, sleepers, ride and sound, frogs, sleeper placement + track_groups groups{false, false, false, false, true, true, true, true, false, true, true}; bool open{false}; bool picking{false}; // the next click on a track in the view copies its properties }; track_style m_track_style; + struct track_presets_panel + { + int selected{0}; // id of the preset chosen in the inspector + int nameid{0}; // the preset whose name is in the field + char name[128]{}; + }; + track_presets_panel m_track_presets; std::vector> m_track_similar; TTrack const *m_track_list_hover{nullptr}; // in the list of the selected tracks int m_track_list_hover_frame{-1}; diff --git a/application/editormodetrackprops.cpp b/application/editormodetrackprops.cpp index 03d72dfe..414c0145 100644 --- a/application/editormodetrackprops.cpp +++ b/application/editormodetrackprops.cpp @@ -18,6 +18,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormodeshared.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" +#include "editor/editorTrackPresets.hpp" #include "rendering/renderer.h" #include "world/Track.h" @@ -219,10 +220,13 @@ enum group : int sleepers, ride_sound, frogs, + sleeper_placement, // spacing and shift of the sleepers and of the trackbed under them, apart from their models group_count }; -char const *const kGroupLabels[group_count] = {"Speed", "Environment", "Overhead line", "Track circuits", "Rails", "Trackbed", "Shape of the trackbed", "Sleepers", "Ride and sound", "Frog and check rails"}; +char const *const kGroupLabels[group_count] = {"Speed", "Environment", "Overhead line", "Track circuits", "Rails", "Trackbed", "Shape of the trackbed", "Sleepers", "Ride and sound", "Frog and check rails", "Sleeper placement"}; +// the groups as the presets keep them in their file, by names which don't change with the order of the groups +char const *const kGroupKeys[group_count] = {"speed", "environment", "overhead", "circuits", "rails", "trackbed", "bedshape", "sleepers", "ride", "frogs", "sleeperplacement"}; struct track_property { @@ -630,13 +634,13 @@ std::vector make_properties() }; } } - add_float(list, "sleeperspacing", section::look_sleepers, group::sleepers, "Sleeper spacing (m)", "Distance between the sleepers along the track", &TTrack::m_sleeper_frequency, 3, 0.1f, true); + add_float(list, "sleeperspacing", section::look_sleepers, group::sleeper_placement, "Sleeper spacing (m)", "Distance between the sleepers along the track", &TTrack::m_sleeper_frequency, 3, 0.1f, true); char const *const offsets[][3] = {{"sleeperside", "Sleeper shift sideways (m)", "Shift of each model sideways, to the right of the track"}, {"sleeperalong", "Sleeper shift along (m)", "Shift of each model along the track"}, {"sleeperup", "Sleeper shift up (m)", "Shift of each model up"}}; for (int axis = 0; axis < 3; ++axis) { - auto &property{add(list, offsets[axis][0], field::number, section::look_sleepers, group::sleepers, offsets[axis][1], offsets[axis][2])}; + auto &property{add(list, offsets[axis][0], field::number, section::look_sleepers, group::sleeper_placement, offsets[axis][1], offsets[axis][2])}; property.rebuild = true; property.get = [axis](TTrack const &Track) { return decimal(Track.m_sleeper_offset[axis], 3); }; property.set = [axis](TTrack &Track, std::string const &Text) { @@ -647,7 +651,7 @@ std::vector make_properties() return true; }; } - add_float(list, "bedshift", section::look_sleepers, group::sleepers, "Trackbed shift (m)", "Vertical shift of the drawn trackbed under the sleepers, negative lowers it", &TTrack::m_sleeper_ballast_z, 3, lowest, true); + add_float(list, "bedshift", section::look_sleepers, group::sleeper_placement, "Trackbed shift (m)", "Vertical shift of the drawn trackbed under the sleepers, negative lowers it", &TTrack::m_sleeper_ballast_z, 3, lowest, true); // ride and sound add_float(list, "friction", section::ride, group::ride_sound, "Friction", "Friction coefficient of the rails, 0.15 for the dry rails; lower values make the wheels slip", &TTrack::fFriction, 3, 0.f, false); @@ -748,6 +752,62 @@ void row_tooltip(track_property const &Property, value_counts const &Values, std ImGui::EndTooltip(); } +// the values of the properties of the track, as they're copied and kept by the presets +std::vector> values_of_track(TTrack const &Track) +{ + std::vector> values; + for (auto const &property : properties()) + if (false == property.off && property.applies(Track)) + values.emplace_back(property.key, property.get(Track)); + return values; +} + +// the groups some of the values are of +std::array groups_in(std::vector> const &Values) +{ + std::array groups{}; + for (auto const &[key, value] : Values) + for (auto const &property : properties()) + if (key == property.key) + groups[property.group] = true; + return groups; +} + +// the groups the preset overwrites, from the names it keeps them by +std::array groups_of(editor_track_preset const &Preset) +{ + std::array groups{}; + for (auto const &key : Preset.groups) + for (int group = 0; group < group_count; ++group) + if (key == kGroupKeys[group]) + groups[group] = true; + return groups; +} + +std::vector group_keys(std::array const &Groups) +{ + std::vector keys; + for (int group = 0; group < group_count; ++group) + if (Groups[group]) + keys.emplace_back(kGroupKeys[group]); + return keys; +} + +// what the preset puts on the tracks, for its tooltip +std::string preset_summary(editor_track_preset const &Preset) +{ + auto const groups{groups_of(Preset)}; + auto const available{groups_in(Preset.values)}; + std::string overwrites; + for (int group = 0; group < group_count; ++group) + if (groups[group] && available[group]) + overwrites += (overwrites.empty() ? "" : ", ") + std::string{STR_C(kGroupLabels[group])}; + auto text{overwrites.empty() ? std::string{STR_C("Overwrites nothing: choose what it overwrites in the inspector")} : format(STR_C("Overwrites: %s"), overwrites.c_str())}; + if (false == Preset.from.empty()) + text += "\n" + format(STR_C("Taken from %s"), Preset.from.c_str()); + return text; +} + } // namespace std::vector editor_mode::selected_tracks() @@ -1297,6 +1357,7 @@ void editor_mode::render_track_properties(std::vector const &Tracks) if (any_in(section::look_sleepers, false) && ImGui::TreeNode(STR_C("Sleeper models"))) { rows(section::look_sleepers, false); + render_track_presets(Tracks); ImGui::TreePop(); } } @@ -1307,50 +1368,230 @@ void editor_mode::render_track_properties(std::vector const &Tracks) void editor_mode::copy_track_style(TTrack const &Track) { auto &style{m_track_style}; - style.values.clear(); style.from = name_of(Track); - for (auto const &property : properties()) - if (false == property.off && property.applies(Track)) - style.values.emplace_back(property.key, property.get(Track)); + style.values = values_of_track(Track); ui()->set_status(format(STR_C("Properties of %s copied: Ctrl+Shift+V pastes them on the selected tracks"), style.from.c_str()), false); } std::size_t editor_mode::paste_track_style(std::vector const &Tracks) { - static_assert(group_count == std::tuple_size_v, "a choice for each group of the pasted properties"); auto const &style{m_track_style}; if (style.values.empty() || Tracks.empty()) return 0; + auto const changed{apply_track_values(style.values, style.groups, Tracks, "Pasted properties")}; + if (false == changed.has_value()) + { + ui()->set_status(STR_C("Nothing chosen to paste"), true); + return 0; + } + if (Tracks.size() == 1) + ui()->set_status(*changed > 0 ? format(STR_C("Properties of %s pasted, Ctrl+Z takes them back"), style.from.c_str()) : std::string{STR_C("The track has these properties already")}, false); + return *changed; +} + +std::optional editor_mode::apply_track_values(std::vector> const &Values, track_groups const &Groups, std::vector const &Tracks, char const *What) +{ + static_assert(group_count == std::tuple_size_v, "a choice for each group of the pasted properties"); auto const &list{properties()}; std::vector> pasted; bool rebuild{false}; for (auto const late : {false, true}) - for (auto const &[key, value] : style.values) + for (auto const &[key, value] : Values) { auto const index{track_property_index(key)}; if (index < 0) continue; auto const &property{list[index]}; - if (property.off || property.late != late || false == style.groups[property.group]) + if (property.off || property.late != late || false == Groups[property.group]) continue; pasted.emplace_back(&property, value); rebuild |= property.rebuild; } if (pasted.empty()) - { - ui()->set_status(STR_C("Nothing chosen to paste"), true); - return 0; - } - auto const changed{tracks_apply(Tracks, [&](TTrack &Track) { + return std::nullopt; + return tracks_apply(Tracks, [&](TTrack &Track) { bool any{false}; for (auto const &[property, value] : pasted) if (property->applies(Track)) any |= property->set(Track, value); return any; - }, rebuild, "Pasted properties")}; - if (Tracks.size() == 1) - ui()->set_status(changed > 0 ? format(STR_C("Properties of %s pasted, Ctrl+Z takes them back"), style.from.c_str()) : std::string{STR_C("The track has these properties already")}, false); - return changed; + }, rebuild, What); +} + +std::size_t editor_mode::apply_track_preset(int const Id, std::vector const &Tracks) +{ + auto const *preset{EditorTrackPresets.find(Id)}; + if (preset == nullptr || Tracks.empty()) + return 0; + auto const name{preset->name}; + auto const changed{apply_track_values(preset->values, groups_of(*preset), Tracks, "Applied a track preset")}; + if (false == changed.has_value()) + { + ui()->set_status(format(STR_C("The preset \"%s\" overwrites nothing: choose what it overwrites"), name.c_str()), true); + return 0; + } + if (*changed > 0) + ui()->set_status(format(STR_P("Preset \"%s\" applied on %d track, Ctrl+Z takes it back", "Preset \"%s\" applied on %d tracks, Ctrl+Z takes it back", static_cast(*changed)), name.c_str(), static_cast(*changed)), false); + else + ui()->set_status(STR_C("The tracks have the properties of the preset already"), false); + return *changed; +} + +void editor_mode::render_track_presets(std::vector const &Tracks) +{ + if (Tracks.empty() || false == ImGui::TreeNode(STR_C("Presets"))) + return; + auto &panel{m_track_presets}; + auto const &presets{EditorTrackPresets.presets()}; + if (EditorTrackPresets.find(panel.selected) == nullptr) + panel.selected = presets.empty() ? 0 : presets.front().id; + auto const single{Tracks.size() == 1}; + auto const *selected{EditorTrackPresets.find(panel.selected)}; + + ImGui::SetNextItemWidth(-FLT_MIN); + if (ImGui::BeginCombo("##trackpreset", selected != nullptr ? selected->name.c_str() : STR_C("No presets yet"))) + { + for (auto const &preset : presets) + { + if (ImGui::Selectable(preset.name.c_str(), preset.id == panel.selected)) + panel.selected = preset.id; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", preset_summary(preset).c_str()); + } + ImGui::EndCombo(); + } + if (selected != nullptr && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", preset_summary(*selected).c_str()); + + auto const width{button_width(2)}; + ImGui::BeginDisabled(selected == nullptr); + if (ImGui::Button(format(STR_P("Apply on %d track", "Apply on %d tracks", static_cast(Tracks.size())), static_cast(Tracks.size())).c_str(), ImVec2(width, 0.0f))) + apply_track_preset(panel.selected, Tracks); + ImGui::EndDisabled(); + item_tooltip("Puts the values of the preset on the selected tracks, of what it overwrites; Ctrl+Z takes them back.\nTools in the menu applies the presets too"); + ImGui::SameLine(); + ImGui::BeginDisabled(false == single); + if (ImGui::Button(STR_C("New from the track"), ImVec2(width, 0.0f))) + { + // rails, trackbed, its shape, sleepers, frogs and sleeper placement at first, the look of the track + std::array groups{}; + for (auto const group : {group::rails, group::trackbed, group::trackbed_shape, group::sleepers, group::frogs, group::sleeper_placement}) + groups[group] = true; + panel.selected = EditorTrackPresets.create(STR_C("New preset"), name_of(*Tracks.front()), values_of_track(*Tracks.front()), group_keys(groups)); + panel.nameid = 0; + ui()->set_status(format(STR_C("Properties of %s saved as a preset: give it a name and choose what it overwrites"), name_of(*Tracks.front()).c_str()), false); + } + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", STR_C(single ? "Saves the properties of this track as a new preset" : "Select one track to save its properties as a preset")); + + // pointers to the presets don't outlive a change of their list + selected = EditorTrackPresets.find(panel.selected); + if (selected == nullptr) + { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextDisabled("%s", STR_C("A preset keeps the properties of a track: its rails, trackbed, sleepers... to put them on other tracks,\nwith the choice of what it overwrites")); + ImGui::PopTextWrapPos(); + ImGui::TreePop(); + return; + } + auto const id{selected->id}; + + // the name + if (panel.nameid != id) + { + std::snprintf(panel.name, sizeof(panel.name), "%s", selected->name.c_str()); + panel.nameid = id; + } + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(STR_C("Rename")).x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x); + auto const entered{ImGui::InputText("##presetname", panel.name, IM_ARRAYSIZE(panel.name), ImGuiInputTextFlags_EnterReturnsTrue)}; + ImGui::PopItemWidth(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Rename")) || entered) + { + EditorTrackPresets.rename(id, panel.name); + panel.nameid = 0; // reload, the name could have been adjusted to stay unique + selected = EditorTrackPresets.find(id); + } + + // what it overwrites, of the groups it has values of + ImGui::TextDisabled("%s", STR_C("Overwrites:")); + // its own id scope: the group checkboxes share their labels with the sections of the properties + ImGui::PushID("presetgroups"); + auto const available{groups_in(selected->values)}; + auto groups{groups_of(*selected)}; + auto changed{false}; + int column{0}; + auto const half{ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x * 0.5f}; + for (int group = 0; group < group_count; ++group) + { + if (false == available[group]) + continue; + if (column % 2 == 1) + ImGui::SameLine(half); + changed |= ImGui::Checkbox(STR_C(kGroupLabels[group]), &groups[group]); + ++column; + } + ImGui::PopID(); + if (changed) + { + // the groups it has no values of are kept as they were, for a track they're taken again from + auto kept{groups_of(*selected)}; + for (int group = 0; group < group_count; ++group) + if (available[group]) + kept[group] = groups[group]; + EditorTrackPresets.set_groups(id, group_keys(kept)); + selected = EditorTrackPresets.find(id); + } + + ImGui::BeginDisabled(false == single); + if (ImGui::Button(STR_C("Update from the track"), ImVec2(width, 0.0f))) + { + EditorTrackPresets.update(id, name_of(*Tracks.front()), values_of_track(*Tracks.front())); + ui()->set_status(format(STR_C("Preset \"%s\" takes the properties of %s now"), selected->name.c_str(), name_of(*Tracks.front()).c_str()), false); + } + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", STR_C(single ? "The preset takes the properties of this track in place of the ones it had; what it overwrites stays" : "Select one track to take its properties into the preset")); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Delete"), ImVec2(width, 0.0f))) + ImGui::OpenPopup("##deletepreset"); + if (ImGui::BeginPopup("##deletepreset")) + { + ImGui::Text(STR_C("Delete preset \"%s\"?"), selected->name.c_str()); + if (ImGui::Button(STR_C("Delete##confirm"))) + { + EditorTrackPresets.remove(id); + panel.selected = 0; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + ImGui::TreePop(); +} + +void editor_mode::render_track_presets_menu() +{ + if (false == ImGui::BeginMenu(STR_C("Track presets"))) + return; + auto const &presets{EditorTrackPresets.presets()}; + if (presets.empty()) + ImGui::TextDisabled("%s", STR_C("No presets: save one in the inspector of a track, Look, Sleeper models, Presets")); + auto const tracks{selected_tracks()}; + int chosen{0}; + for (auto const &preset : presets) + { + if (ImGui::MenuItem(preset.name.c_str(), nullptr, false, false == tracks.empty())) + chosen = preset.id; + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", tracks.empty() ? STR_C("Select the tracks to apply the preset on first") : (format(STR_P("Applies the preset on %d selected track", "Applies the preset on %d selected tracks", static_cast(tracks.size())), static_cast(tracks.size())) + "\n" + preset_summary(preset)).c_str()); + } + ImGui::EndMenu(); + if (chosen != 0) + apply_track_preset(chosen, tracks); } void editor_mode::render_track_style(std::vector const &Tracks) @@ -1623,7 +1864,7 @@ editor_mode::selftest_result editor_mode::selftest_props(std::string const &Comm } else if (action == "groups") { - // the groups pasted, as their numbers: 0 speed ... 9 frogs + // the groups pasted, as their numbers: 0 speed ... 9 frogs, 10 sleeper placement m_track_style.groups.fill(false); for (int group; Words >> group;) if (group >= 0 && group < group_count) diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index 0a054eb6..b289467d 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -990,6 +990,15 @@ void editor_ui::render_menu_contents() m_menuoptions(); } +// the tools of the editor mode, in the tools menu after the common ones (logging, screenshot) +void editor_ui::render_tools_menu() +{ + if (!m_toolsmenu) + return; + ImGui::Separator(); + m_toolsmenu(); +} + // the windows of the editor and their layout, in the windows menu after the common ones (log, hierarchy, history, settings) void editor_ui::render_windows_menu() { diff --git a/application/editoruilayer.h b/application/editoruilayer.h index cfb9de43..6858c287 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -80,6 +80,11 @@ class editor_ui : public ui_layer { m_editmenu = std::move(Renderer); } + // draws the items of the editor mode in the tools menu, after the common ones + void set_tools_menu(std::function Renderer) + { + m_toolsmenu = std::move(Renderer); + } // draws the fields of work of the editor mode in a row above the toolbar void set_workspace_bar(std::function Renderer) { @@ -177,6 +182,7 @@ class editor_ui : public ui_layer void scene_row(std::size_t const Entry); void render_menu_contents() override; void render_windows_menu() override; + void render_tools_menu() override; int palette() const override; void render_dockspace() override; void render_() override; @@ -210,6 +216,7 @@ class editor_ui : public ui_layer std::function m_toolbaroptions; std::function m_workspacebar; std::function m_editmenu; + std::function m_toolsmenu; bool m_layoutreset{false}; // the default layout is to be built again on the next frame std::string m_refusal; double m_refusaltime{-100.0}; diff --git a/application/uilayer.cpp b/application/uilayer.cpp index d7f99d3c..998ea46c 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -696,6 +696,7 @@ void ui_layer::render_menu_contents() if (ImGui::MenuItem(STR_C("Screenshot"), "PrtScr")) Application.queue_screenshot(); + render_tools_menu(); ImGui::EndMenu(); } if (ImGui::BeginMenu(STR_C("Windows"))) diff --git a/application/uilayer.h b/application/uilayer.h index 7b68646f..462c8dc1 100644 --- a/application/uilayer.h +++ b/application/uilayer.h @@ -143,6 +143,8 @@ protected: virtual void render_menu_contents(); // items of the windows menu which belong to the ui of a mode, after the common ones virtual void render_windows_menu() {} + // items of the tools menu which belong to the ui of a mode, after the common ones + virtual void render_tools_menu() {} // colours this ui is drawn in, see imgui_colors() virtual int palette() const { diff --git a/editor/editorTrackPresets.cpp b/editor/editorTrackPresets.cpp new file mode 100644 index 00000000..f41506c0 --- /dev/null +++ b/editor/editorTrackPresets.cpp @@ -0,0 +1,233 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorTrackPresets.hpp" +#include "utilities/Logs.h" +#include "utilities/utilities.h" + +#include +#include +#include +#include + +editorTrackPresets EditorTrackPresets; + +// file layout: +// preset +// from +// groups ... +// value +// ... +// endpreset +namespace +{ +namespace fs = std::filesystem; + +fs::path presets_path() +{ + fs::path p = user_config_path("eu07_editor_track_presets.txt"); + return p.empty() ? fs::path("eu07_editor_track_presets.txt") : p; +} + +std::string trimmed(std::string Text) +{ + auto const first = Text.find_first_not_of(" \t"); + if (first == std::string::npos) + return {}; + auto const last = Text.find_last_not_of(" \t"); + return Text.substr(first, last - first + 1); +} + +// one value per line in the file +std::string single_line(std::string Text) +{ + std::replace_if(Text.begin(), Text.end(), [](char const Character) { return Character == '\r' || Character == '\n' || Character == '\t'; }, ' '); + return trimmed(Text); +} + +// the line past its first word, when it starts with the word +bool keyword(std::string const &Line, char const *Word, std::string &Rest) +{ + auto const length{std::char_traits::length(Word)}; + if (Line.compare(0, length, Word) != 0 || (Line.size() > length && Line[length] != ' ')) + return false; + Rest = Line.size() > length ? trimmed(Line.substr(length + 1)) : std::string{}; + return true; +} +} // namespace + +bool editorTrackPresets::load() +{ + m_presets.clear(); + + const fs::path path = presets_path(); + std::error_code ec; + if (!fs::exists(path, ec)) + return false; + + std::ifstream stream(path, std::ios_base::in | std::ios_base::binary); + if (!stream.is_open()) + return false; + + editor_track_preset *current{nullptr}; + std::string line; + std::string rest; + while (std::getline(stream, line)) + { + line = single_line(line); + if (line.empty() || line.rfind("//", 0) == 0) + continue; + + // a preset header also closes a preset with missing "endpreset" + if (keyword(line, "preset", rest)) + { + editor_track_preset preset; + preset.id = m_nextid++; + preset.name = unique_name(rest); + m_presets.push_back(std::move(preset)); + current = &m_presets.back(); + continue; + } + if (line == "endpreset") + { + current = nullptr; + continue; + } + if (current == nullptr) + continue; + if (keyword(line, "from", rest)) + current->from = rest; + else if (keyword(line, "groups", rest)) + { + std::istringstream words{rest}; + current->groups.clear(); + for (std::string group; words >> group;) + current->groups.push_back(group); + } + else if (keyword(line, "value", rest) && false == rest.empty()) + { + auto const split{rest.find(' ')}; + if (split == std::string::npos) + current->values.emplace_back(rest, std::string{}); + else + current->values.emplace_back(rest.substr(0, split), trimmed(rest.substr(split + 1))); + } + } + return true; +} + +bool editorTrackPresets::save() const +{ + const fs::path path = presets_path(); + std::error_code ec; + fs::create_directories(path.parent_path(), ec); + + std::ofstream stream(path, std::ios_base::out | std::ios_base::trunc | std::ios_base::binary); + if (!stream.is_open()) + { + ErrorLog("failed to save editor track presets"); + return false; + } + + stream << "// MaSzyna editor: user-defined presets of the track properties\n"; + for (auto const &preset : m_presets) + { + stream << "\npreset " << preset.name << "\n"; + if (false == preset.from.empty()) + stream << "from " << preset.from << "\n"; + stream << "groups"; + for (auto const &group : preset.groups) + stream << ' ' << group; + stream << "\n"; + for (auto const &[key, value] : preset.values) + stream << "value " << key << ' ' << single_line(value) << "\n"; + stream << "endpreset\n"; + } + return true; +} + +editor_track_preset const *editorTrackPresets::find(int const Id) const +{ + auto const lookup = std::find_if(m_presets.begin(), m_presets.end(), [Id](auto const &Preset) { return Preset.id == Id; }); + return lookup != m_presets.end() ? &(*lookup) : nullptr; +} + +editor_track_preset *editorTrackPresets::find_(int const Id) +{ + auto const lookup = std::find_if(m_presets.begin(), m_presets.end(), [Id](auto const &Preset) { return Preset.id == Id; }); + return lookup != m_presets.end() ? &(*lookup) : nullptr; +} + +int editorTrackPresets::create(std::string const &Name, std::string const &From, std::vector> const &Values, std::vector const &Groups) +{ + editor_track_preset preset; + preset.id = m_nextid++; + preset.name = unique_name(Name); + preset.from = single_line(From); + preset.values = Values; + preset.groups = Groups; + m_presets.push_back(std::move(preset)); + save(); + return m_presets.back().id; +} + +void editorTrackPresets::remove(int const Id) +{ + auto const lookup = std::find_if(m_presets.begin(), m_presets.end(), [Id](auto const &Preset) { return Preset.id == Id; }); + if (lookup == m_presets.end()) + return; + m_presets.erase(lookup); + save(); +} + +void editorTrackPresets::rename(int const Id, std::string const &Name) +{ + auto *preset = find_(Id); + if (preset == nullptr) + return; + auto const name = unique_name(Name, Id); + if (name == preset->name) + return; + preset->name = name; + save(); +} + +void editorTrackPresets::update(int const Id, std::string const &From, std::vector> const &Values) +{ + auto *preset = find_(Id); + if (preset == nullptr) + return; + preset->from = single_line(From); + preset->values = Values; + save(); +} + +void editorTrackPresets::set_groups(int const Id, std::vector const &Groups) +{ + auto *preset = find_(Id); + if (preset == nullptr || preset->groups == Groups) + return; + preset->groups = Groups; + save(); +} + +std::string editorTrackPresets::unique_name(std::string const &Base, int const IgnoreId) const +{ + auto base = single_line(Base); + if (base.empty()) + base = "New preset"; + auto const taken = [&](std::string const &Name) { + return std::any_of(m_presets.begin(), m_presets.end(), [&](auto const &Preset) { return Preset.id != IgnoreId && Preset.name == Name; }); + }; + auto name = base; + for (int suffix = 2; taken(name); ++suffix) + name = base + " " + std::to_string(suffix); + return name; +} diff --git a/editor/editorTrackPresets.hpp b/editor/editorTrackPresets.hpp new file mode 100644 index 00000000..32a24778 --- /dev/null +++ b/editor/editorTrackPresets.hpp @@ -0,0 +1,58 @@ +/* +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 + +// user-defined presets of the properties of tracks (rails, trackbed, sleepers...), taken from a track and put on others +// with the choice of what they overwrite. stored next to the user's ini files, like the model sets +struct editor_track_preset +{ + int id{0}; // runtime identifier, stable while the editor runs (renames don't break references) + std::string name; + std::string from; // name of the track the values were taken from + // the values of the properties as the track inspector reads and writes them, by their keys + std::vector> values; + // the groups of the properties the preset overwrites, by their keys; the other values of it are kept but left out + std::vector groups; +}; + +class editorTrackPresets +{ + public: + bool load(); + bool save() const; + + std::vector const &presets() const + { + return m_presets; + } + editor_track_preset const *find(int Id) const; + // all modifying methods save the presets right away + // returns: id of the new preset + int create(std::string const &Name, std::string const &From, std::vector> const &Values, std::vector const &Groups); + void remove(int Id); + void rename(int Id, std::string const &Name); + // the values taken again, from another track or the same one changed + void update(int Id, std::string const &From, std::vector> const &Values); + void set_groups(int Id, std::vector const &Groups); + // name not used by any other preset, built from specified base + std::string unique_name(std::string const &Base, int IgnoreId = 0) const; + + private: + editor_track_preset *find_(int Id); + + std::vector m_presets; + int m_nextid{1}; +}; + +extern editorTrackPresets EditorTrackPresets; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index ef7444c7..3f277d7a 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11532,3 +11532,98 @@ msgstr "Każdy kliknięty punkt na wysokości terenu, między punktami równe po msgid "The track goes on at the height it's laid from, whatever the ground; the first point on the ground" msgstr "Tor idzie dalej na wysokości, z której go kładziesz, bez względu na teren; pierwszy punkt na terenie" + +msgid "new, not saved" +msgstr "nowy, niezapisany" + +# scenery editor: presets of the track properties + +msgid "Sleeper placement" +msgstr "Rozmieszczenie podkładów" + +msgid "Overwrites nothing: choose what it overwrites in the inspector" +msgstr "Niczego nie nadpisuje: wybierz w inspektorze, co ma nadpisywać" + +msgid "Overwrites: %s" +msgstr "Nadpisuje: %s" + +msgid "Taken from %s" +msgstr "Wzięty z %s" + +msgid "The preset \"%s\" overwrites nothing: choose what it overwrites" +msgstr "Zestaw \"%s\" niczego nie nadpisuje: wybierz, co ma nadpisywać" + +msgid "Preset \"%s\" applied on %d track, Ctrl+Z takes it back" +msgid_plural "Preset \"%s\" applied on %d tracks, Ctrl+Z takes it back" +msgstr[0] "Zestaw \"%s\" zastosowany na %d torze, Ctrl+Z go cofa" +msgstr[1] "Zestaw \"%s\" zastosowany na %d torach, Ctrl+Z go cofa" +msgstr[2] "Zestaw \"%s\" zastosowany na %d torach, Ctrl+Z go cofa" + +msgid "The tracks have the properties of the preset already" +msgstr "Tory mają już właściwości tego zestawu" + +msgid "Presets" +msgstr "Zestawy" + +msgid "No presets yet" +msgstr "Brak zestawów" + +msgid "Apply on %d track" +msgid_plural "Apply on %d tracks" +msgstr[0] "Zastosuj na %d torze" +msgstr[1] "Zastosuj na %d torach" +msgstr[2] "Zastosuj na %d torach" + +msgid "Puts the values of the preset on the selected tracks, of what it overwrites; Ctrl+Z takes them back.\nTools in the menu applies the presets too" +msgstr "Nadaje zaznaczonym torom wartości zestawu, z tego, co nadpisuje; Ctrl+Z je cofa.\nZestawy można też zastosować z menu Narzędzia" + +msgid "New from the track" +msgstr "Nowy z toru" + +msgid "New preset" +msgstr "Nowy zestaw" + +msgid "Properties of %s saved as a preset: give it a name and choose what it overwrites" +msgstr "Właściwości %s zapisane jako zestaw: nadaj mu nazwę i wybierz, co ma nadpisywać" + +msgid "Saves the properties of this track as a new preset" +msgstr "Zapisuje właściwości tego toru jako nowy zestaw" + +msgid "Select one track to save its properties as a preset" +msgstr "Zaznacz jeden tor, żeby zapisać jego właściwości jako zestaw" + +msgid "A preset keeps the properties of a track: its rails, trackbed, sleepers... to put them on other tracks,\nwith the choice of what it overwrites" +msgstr "Zestaw przechowuje właściwości toru: szyny, podsypkę, podkłady... żeby nadać je innym torom,\nz wyborem tego, co nadpisuje" + +msgid "Overwrites:" +msgstr "Nadpisuje:" + +msgid "Update from the track" +msgstr "Aktualizuj z toru" + +msgid "Preset \"%s\" takes the properties of %s now" +msgstr "Zestaw \"%s\" ma teraz właściwości %s" + +msgid "The preset takes the properties of this track in place of the ones it had; what it overwrites stays" +msgstr "Zestaw przejmuje właściwości tego toru w miejsce dotychczasowych; wybór tego, co nadpisuje, zostaje" + +msgid "Select one track to take its properties into the preset" +msgstr "Zaznacz jeden tor, żeby przenieść jego właściwości do zestawu" + +msgid "Delete preset \"%s\"?" +msgstr "Usunąć zestaw \"%s\"?" + +msgid "Track presets" +msgstr "Zestawy torów" + +msgid "No presets: save one in the inspector of a track, Look, Sleeper models, Presets" +msgstr "Brak zestawów: zapisz zestaw w inspektorze toru, Wygląd, Modele podkładów, Zestawy" + +msgid "Select the tracks to apply the preset on first" +msgstr "Najpierw zaznacz tory, na których zastosować zestaw" + +msgid "Applies the preset on %d selected track" +msgid_plural "Applies the preset on %d selected tracks" +msgstr[0] "Stosuje zestaw na %d zaznaczonym torze" +msgstr[1] "Stosuje zestaw na %d zaznaczonych torach" +msgstr[2] "Stosuje zestaw na %d zaznaczonych torach" From 36904c2815bd660096e91a3ae38863f21c6db754 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 24 Oct 2026 01:40:47 +0200 Subject: [PATCH 087/145] feat(editor): buttons in the switch tool setting a model of timbers on all the switches and crossings of the scenery at once, one for each model of timbers of the scenery data and one for the model of eu07.ini, in one step of the undo; the selftest switchtimbers does the same (cherry picked from commit 81a6b855d05be0ccaa57d5f9b85c8cfca4558ee6) --- application/editormode.h | 4 + application/editormodeselftest.cpp | 1 + application/editormodeselftestrails.cpp | 9 ++ application/editormodeswitchtool.cpp | 31 ++++++ lang/editor_pl.po | 135 ++++++++++++++++++++++++ 5 files changed, 180 insertions(+) diff --git a/application/editormode.h b/application/editormode.h index f8d3e547..987ba5d3 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1419,6 +1419,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::vector lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const; alignment::result lay_shape(std::vector const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &Design) const; void render_switch_ui(); + void render_switch_timbers(); void render_rail_profile_hint(TTrack &Track); bool start_switch_placement(); void finish_switch_placement(); @@ -1821,6 +1822,9 @@ class editor_mode : public application_mode, private editor_track::observer, pri void select_tracks(std::vector const &Tracks); // changes each track which can be changed, in one step of the undo; false from Change: the track stays as it was std::size_t tracks_apply(std::vector const &Tracks, std::function const &Change, bool const Rebuild, char const *What); + // the model of the timbers, with the fastenings where it has them, set on all the switches and crossings of the scenery, in one + // step of the undo; empty: the one of eu07.ini. returns: the tracks changed + std::size_t set_switch_timbers(std::string const &Model); int track_property_index(std::string const &Key) const; std::size_t track_property_apply(int const Property, std::vector const &Tracks, std::string const &Text); void track_field_settle(); diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 3617b307..749ad391 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -71,6 +71,7 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c // railtime lays out the rails of every track, as when its geometry is made, and logs how long it took // throw <0|1> sets every switch to the path and moves its blades there at once // crossings [name] logs where rails of tracks cross rails of other tracks and of what kind (acute, obtuse, middle acute or obtuse, wide), checks that each acute crossing has one partner and one nose +// switchtimbers the model of the timbers set on all the switches and crossings, as the buttons of the switch tool do it // frogkey name [on|off] sets or clears the frogs key of a switch (one undo step); logs the key, whether it is written, the frog and the crossings // geomhash [name] logs the number of vertices and a hash of the drawn geometry of each rail track (rails, trackbed, cut faces), to compare builds; exact: the bits of every value, userdata and tread materials too // sweep =... model, params (separated by commas), lateral, height, side (origin, near, far, diff --git a/application/editormodeselftestrails.cpp b/application/editormodeselftestrails.cpp index 86d69280..ee1e7b4f 100644 --- a/application/editormodeselftestrails.cpp +++ b/application/editormodeselftestrails.cpp @@ -324,6 +324,15 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm WriteLog(format("SELFTEST geom total: tracks %d, vertices %zu, hash %016llx, exact %016llx", tracks, vertices, static_cast(total), static_cast(exacttotal))); return selftest_result::handled; } + if (command == "switchtimbers") + { + std::string model; + words >> model; + if (model == "none") + model.clear(); + WriteLog(format("SELFTEST switchtimbers %s: %zu tracks", model.c_str(), set_switch_timbers(model))); + return selftest_result::handled; + } if (command == "frogkey") { std::string name, value; diff --git a/application/editormodeswitchtool.cpp b/application/editormodeswitchtool.cpp index 169dde34..41f163a5 100644 --- a/application/editormodeswitchtool.cpp +++ b/application/editormodeswitchtool.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "utilities/translation.h" #include "world/Track.h" +#include "world/TrackSleepers.h" #include "imgui/imgui.h" #include @@ -390,6 +391,7 @@ void editor_mode::render_switch_ui() arm_switch(); } item_tooltip("Looks for the switches of the scenery, to use their shapes as the templates"); + render_switch_timbers(); ImGui::TreePop(); } ImGui::PopID(); @@ -405,6 +407,35 @@ void editor_mode::render_switch_ui() item_tooltip("Own switch templates: diverging tracks of straights, arcs and transition curves, double switches"); } +// the timbers of all the switches and crossings of the scenery at once: a button for each model of timbers of the scenery data +void editor_mode::render_switch_timbers() +{ + ImGui::TextDisabled("%s", STR_C("Timbers of all the switches")); + auto first{true}; + auto const button = [&](std::string const &Model, std::string const &Label) { + if (false == first) + ImGui::SameLine(); + first = false; + if (ImGui::Button((Label + "##switchtimbers" + Model).c_str())) + m_switch.status = format(STR_C("Timbers %s set on %d tracks"), Label.c_str(), static_cast(set_switch_timbers(Model))); + item_tooltip("Sets the model of the timbers, with the fastenings where it has them, on all the switches and crossings of the scenery, in one step of the undo"); + }; + for (auto const &model : track_sleepers::pack_models()) + if (model.timber) + button(model.name, model.name.substr(model.name.find_last_of('/') + 1)); + button({}, STR_C("As in eu07.ini")); +} + +std::size_t editor_mode::set_switch_timbers(std::string const &Model) +{ + std::vector tracks; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && (track->iCategoryFlag & 15) == 1 && (track->eType == tt_Switch || false == track->m_crossings.empty()) + && track->m_timber_model_name != Model) + tracks.push_back(track); + return tracks_apply(tracks, [&](TTrack &Track) { Track.m_timber_model_name = Model; return true; }, true, "Timber model"); +} + // a track with no rail profile, as the sceneries of the older editors have them: the templates of all the rails are offered. // the profile set at once on all the tracks laid with the same rails and no profile, so a scenery is put right in a few clicks void editor_mode::render_rail_profile_hint(TTrack &Track) diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 3f277d7a..f1e8205d 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -11627,3 +11627,138 @@ msgid_plural "Applies the preset on %d selected tracks" msgstr[0] "Stosuje zestaw na %d zaznaczonym torze" msgstr[1] "Stosuje zestaw na %d zaznaczonych torach" msgstr[2] "Stosuje zestaw na %d zaznaczonych torach" + +# scenery editor: texture blending of the tracks + +msgid "Texture blending" +msgstr "Mieszanie tekstur" + +msgid "Ballast blending at p1 (m)" +msgstr "Mieszanie podsypki przy p1 (m)" + +msgid "Ballast blending at p2 (m)" +msgstr "Mieszanie podsypki przy p2 (m)" + +msgid "Ballast blending at p4 (m)" +msgstr "Mieszanie podsypki przy p4 (m)" + +msgid "Rails blending at p1 (m)" +msgstr "Mieszanie szyn przy p1 (m)" + +msgid "Rails blending at p2 (m)" +msgstr "Mieszanie szyn przy p2 (m)" + +msgid "How far from the end p1 the ballast fades into that of the track joined there, where their textures differ; 0: the joint stays sharp, whatever the track joined there has" +msgstr "Na jakiej długości od końca p1 podsypka płynnie przechodzi w podsypkę toru tam dołączonego, gdy ich tekstury są różne; 0: ostra granica na styku, niezależnie od ustawienia toru tam dołączonego" + +msgid "How far from the end p2 the ballast fades into that of the track joined there, where their textures differ; 0: the joint stays sharp, whatever the track joined there has" +msgstr "Na jakiej długości od końca p2 podsypka płynnie przechodzi w podsypkę toru tam dołączonego, gdy ich tekstury są różne; 0: ostra granica na styku, niezależnie od ustawienia toru tam dołączonego" + +msgid "How far from the end p4 of the switch its trackbed fades into the ballast of the track joined there, where their textures differ; 0: the joint stays sharp, whatever the track joined there has" +msgstr "Na jakiej długości od końca p4 rozjazdu jego podsypka płynnie przechodzi w podsypkę toru tam dołączonego, gdy ich tekstury są różne; 0: ostra granica na styku, niezależnie od ustawienia toru tam dołączonego" + +msgid "How far from the end p1 the rails fade into those of the track joined there, where their textures differ; 0: the joint stays sharp, whatever the track joined there has" +msgstr "Na jakiej długości od końca p1 szyny płynnie przechodzą w szyny toru tam dołączonego, gdy ich tekstury są różne; 0: ostra granica na styku, niezależnie od ustawienia toru tam dołączonego" + +msgid "How far from the end p2 the rails fade into those of the track joined there, where their textures differ; 0: the joint stays sharp, whatever the track joined there has" +msgstr "Na jakiej długości od końca p2 szyny płynnie przechodzą w szyny toru tam dołączonego, gdy ich tekstury są różne; 0: ostra granica na styku, niezależnie od ustawienia toru tam dołączonego" + +# scenery editor: level crossings told by memory cells + +msgid "Memory cell" +msgstr "Komórka pamięci" + +msgid "(named after the crossing)" +msgstr "(nazwana od przejazdu)" + +msgid "Events of the scenery close the entry onto the crossing with the first value of this memory cell:\nabove 0 the vehicles heading for the crossing are stopped, below 0 it's kept open whatever comes by the rails,\n0 leaves it to the trains. The vehicles past the stops, on the crossing or leaving it, drive on.\nThe crossing sets the second value: 1 while the entry is closed, 0 while it's open.\nLeft empty, the cell is the one named after the crossing with \":block\" added, made if the scenery doesn't have it" +msgstr "Eventy scenerii zamykają wjazd na przejazd pierwszą wartością tej komórki pamięci:\npowyżej 0 pojazdy jadące na przejazd są zatrzymywane, poniżej 0 jest on otwarty bez względu na to, co jedzie po torach,\n0 zostawia to pociągom. Pojazdy za miejscami zatrzymania, na przejeździe albo zjeżdżające z niego, jadą dalej.\nPrzejazd ustawia drugą wartość: 1, gdy wjazd jest zamknięty, 0, gdy jest otwarty.\nGdy pole jest puste, jest to komórka nazwana od przejazdu z dodanym \":block\", tworzona, jeśli sceneria jej nie ma" + +msgid "The crossing has no name, so there's no memory cell to tell it by" +msgstr "Przejazd nie ma nazwy, więc nie ma komórki pamięci, którą można nim sterować" + +msgid "The entry is closed" +msgstr "Wjazd zamknięty" + +msgid "The entry is open" +msgstr "Wjazd otwarty" + +msgid "By the trains" +msgstr "Według pociągów" + +msgid "Closed" +msgstr "Zamknięty" + +msgid "Kept open" +msgstr "Otwarty" + +msgid "Sets the first value of the memory cell, the way an event of the scenery would. It isn't saved with the scenery" +msgstr "Ustawia pierwszą wartość komórki pamięci, tak jak zrobiłby to event scenerii. Nie jest zapisywane w scenerii" + +# scenery editor: memory cells window + +msgid "Debug" +msgstr "Debugowanie" + +msgid "Memory cells" +msgstr "Komórki pamięci" + +msgid "Looks up the memory cells of the scenery, shows what's in them and sets it by hand" +msgstr "Wyszukuje komórki pamięci scenerii, pokazuje ich zawartość i pozwala ją ustawić ręcznie" + +msgid "In the texts too" +msgstr "Także w tekstach" + +msgid "Log all" +msgstr "Wszystkie do logu" + +msgid "Writes the content of all memory cells to the log" +msgstr "Zapisuje zawartość wszystkich komórek pamięci do logu" + +msgid "Memory cells: %d, shown: %d" +msgstr "Komórek pamięci: %d, pokazanych: %d" + +msgid "Text" +msgstr "Tekst" + +msgid "Value 1" +msgstr "Wartość 1" + +msgid "Value 2" +msgstr "Wartość 2" + +msgid "Made by the simulator, it isn't saved with the scenery" +msgstr "Utworzona przez symulator, nie jest zapisywana w scenerii" + +msgid "Pick a memory cell from the list to set what's in it. A double click shows where it is" +msgstr "Wybierz komórkę pamięci z listy, aby ustawić jej zawartość. Podwójne kliknięcie pokazuje, gdzie jest" + +msgid "\"%s\": %s %g %g" +msgstr "\"%s\": %s %g %g" + +msgid "At %.1f %.1f %.1f" +msgstr "Położenie %.1f %.1f %.1f" + +msgid "Puts the ticked fields in the cell, the way an UpdateValues event does. The scenery files aren't changed" +msgstr "Wpisuje zaznaczone pola do komórki, tak jak event UpdateValues. Pliki scenerii nie są zmieniane" + +msgid "Add to the values" +msgstr "Dodaj do wartości" + +msgid "Read again" +msgstr "Odczytaj ponownie" + +msgid "Pick a memory cell from the list to set what's in it" +msgstr "Wybierz komórkę pamięci z listy, aby ustawić jej zawartość" + +msgid "Timbers of all the switches" +msgstr "Podrozjazdnice wszystkich rozjazdów" + +msgid "Timbers %s set on %d tracks" +msgstr "Podrozjazdnice %s ustawione na %d torach" + +msgid "As in eu07.ini" +msgstr "Jak w eu07.ini" + +msgid "Sets the model of the timbers, with the fastenings where it has them, on all the switches and crossings of the scenery, in one step of the undo" +msgstr "Ustawia model podrozjazdnic, razem z przytwierdzeniami tam, gdzie je ma, na wszystkich rozjazdach i skrzyżowaniach scenerii, jednym krokiem cofania" From e35487f767f117c397b4b156c8837d8009406a72 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 24 Oct 2026 17:25:26 +0200 Subject: [PATCH 088/145] fix(editor): the map of the scenery wizard north up from its first image: the thread loading it keeps the north edge in the first row, not flipped or not as the texture loader left stb by then --- editor/editorGeoMap.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/editor/editorGeoMap.cpp b/editor/editorGeoMap.cpp index 52e837e5..fe989c66 100644 --- a/editor/editorGeoMap.cpp +++ b/editor/editorGeoMap.cpp @@ -245,7 +245,7 @@ bool editor_map::render(glm::vec2 const &Size) 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(static_cast(m_texture), to_screen({m_image_low.x, m_image_high.y}), to_screen({m_image_high.x, m_image_low.y}), ImVec2(0, 1), ImVec2(1, 0)); + drawlist->AddImage(static_cast(m_texture), to_screen({m_image_low.x, m_image_high.y}), to_screen({m_image_high.x, m_image_low.y}), ImVec2(0, 0), ImVec2(1, 1)); if (m_picked) { auto const at{to_screen(m_point)}; @@ -294,6 +294,9 @@ void editor_map::request(glm::vec2 const &Size) if (false == editor_http_get(url, data, result.error)) return result; int w{0}, h{0}, components{0}; + // the texture loader turns the stb flip on for the whole process only once it loads its first texture: this thread + // keeps the north edge in the first row whatever it is by then + stbi_set_flip_vertically_on_load_thread(0); auto *pixels{stbi_load_from_memory(data.data(), static_cast(data.size()), &w, &h, &components, 4)}; if (pixels == nullptr) { From e32294730008834fd659e4e4652f4a1cc333d510 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 24 Oct 2026 18:00:21 +0200 Subject: [PATCH 089/145] fix(editor): the track shown while it's laid over the ground no longer holds up the frames: its grade line is fitted on a thread of its own, the line fitted a moment ago stretched over the track meanwhile, and the click takes the line fitted for the same ground instead of fitting it again; the terrain follows the tracks once the laying ends, not after each click of a chain Fitting the grade line took 3 ms for 250 m of track, 30 ms for 1 km, 65 ms for 2 km and 200 ms for 4 km, and the preview of the lay tool and of the drag from a free end fitted it every frame, twice for the drag; on the terrain model the preview of some 800 m took 10 to 17 ms a frame, the cursor standing or not. Now the frame waits under 1.5 ms for 4 km. The formations of the tracks led into the terrain went through every chunk under them 0.3 s after each click, some 50 ms, while the cursor went on to the next point; they wait for the laying to end. The tracks laid and the terrain are the same as before. Selftests: perf logs the longest frame and those over 33 ms, fittime the time of the fit by length, terrainhash a hash of the chunks in memory. --- application/editormode.h | 7 + application/editormodelay.cpp | 10 + application/editormodenmt.cpp | 4 + application/editormodeselftest.cpp | 16 +- application/editormodeselftest.h | 13 ++ application/editormodeselftestjoin.cpp | 29 +++ application/editormodeselftestscene.cpp | 4 +- application/editormodeselftestterrain.cpp | 18 ++ application/editormodetrack.cpp | 6 +- application/editormodetrackbed.cpp | 12 +- editor/editorGroundFollow.cpp | 223 ++++++++++++++++++---- editor/editorGroundFollow.hpp | 55 +++++- 12 files changed, 354 insertions(+), 43 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 987ba5d3..d55ffc09 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -39,6 +39,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorSpeed.hpp" #include "editor/editorRoad.hpp" #include "editor/editorProfile.hpp" +#include "editor/editorGroundFollow.hpp" #include "editor/editorInfra.hpp" #include "editor/editorGauge.hpp" #include "editor/editorClips.hpp" @@ -1512,6 +1513,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool start_extend(editor_track::snap_target const &End); void finish_extend(); std::vector extend_pieces() const; + // the same for the track shown while it's dragged: its grade line fitted on the thread of the fitter, the frame not waiting for it + std::vector extend_preview() const; // the free end of the path as the start of a new track; false if it isn't free or has no direction static bool free_end(TTrack &Track, int const Path, bool const Atend, extend_tool &Tool); // the free end new pieces are added at: the selected point if it's one, else the end, else the start @@ -1732,6 +1735,10 @@ class editor_mode : public application_mode, private editor_track::observer, pri static std::pair trackbed(TTrack const *Style); std::vector bed_heights(std::vector const &Points, std::vector const &Directions, TTrack const *Style) const; void follow_ground(std::vector &Pieces, double const Height0, double const Grade0, double const Height1, double const Grade1, TTrack const *Style) const; + ground_follow::grading laying_grading(TTrack const *Style) const; + // the grade lines of the tracks shown while they're laid are fitted on a thread of their own, not to hold up the frames + mutable ground_follow::fitter m_fitter; + mutable bool m_follow_preview{false}; glm::dvec3 lay_bed(glm::dvec3 const &Point); std::optional m_cursor_override; bool sweep_grabs(glm::dvec3 const &Point); diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index 9e4e6bcb..53e3a7fa 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -101,7 +101,9 @@ void editor_mode::update_build_tools() if (plan_distance(lay.mouse, points.back()) > 1.0) points.push_back(lay.mouse); } + m_follow_preview = true; lay.preview = lay_pieces(points, end, lay.preview_length, lay.preview_error); + m_follow_preview = false; } bool editor_mode::free_end(TTrack &Track, int const Path, bool const Atend, extend_tool &Tool) @@ -490,6 +492,14 @@ std::vector editor_mode::extend_pieces() const return pieces; } +std::vector editor_mode::extend_preview() const +{ + m_follow_preview = true; + auto pieces{extend_pieces()}; + m_follow_preview = false; + return pieces; +} + TTrack const *editor_mode::speed_track(TTrack const *From, TTrack const *To) const { if (m_lay.own_speed && m_lay.style.velocity > 0.0) diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index e13e5c2e..9630cb94 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -691,6 +691,10 @@ void editor_mode::nmt_follow_tracks() return; if (state.stage != nmt_state::automatic::idle || state.fitting || ImGui::GetTime() - state.changed_time < 0.3 || ImGui::IsMouseDown(ImGuiMouseButton_Left) || ImGui::IsMouseDown(ImGuiMouseButton_Right)) return; + // laying on, from the end of the track laid or dragged from a free end, the terrain waits for the laying to end: shaping it + // goes through every chunk under the tracks, a stall each click + if ((m_lay.active && false == m_lay.points.empty()) || m_extend.active) + return; if (false == m_streamer.active()) { state.changed.clear(); diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 749ad391..b74389cb 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -151,7 +151,11 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c // screenshot // save // log -// perf

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�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 {g^`)L!fqBfpwOl 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§{`nq8jPQo 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