mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-26 19:24:54 +01:00
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
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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<track_mode, 11> const &track_modes();
|
||||
static std::array<track_mode, 12> 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<editor_tool *> 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<editor_tool *> 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();
|
||||
|
||||
642
application/editormodesrk.cpp
Normal file
642
application/editormodesrk.cpp
Normal file
@@ -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 <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <unordered_set>
|
||||
|
||||
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<unsigned char>(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<scene::layer_handle> 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<std::string> 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<int>(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<int>(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<int>(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<TTrack const *> 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<char, 128> 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<float>(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<double>(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<float>((Chainage - view.from) / across); };
|
||||
auto const y_of = [&](double const Offset) { return corner.y + size.y * 0.5f - static_cast<float>((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<int>(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=<name>: 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<TTrack const *> 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=<code> line=<line> name=<words joined by _>
|
||||
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;
|
||||
}
|
||||
81
application/editormodesrk.h
Normal file
81
application/editormodesrk.h
Normal file
@@ -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 <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// 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<record> m_records;
|
||||
std::optional<station::layout> 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<std::pair<std::string, std::size_t>> 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();
|
||||
};
|
||||
@@ -411,9 +411,9 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve
|
||||
profile_restore();
|
||||
}
|
||||
|
||||
std::array<editor_mode::track_mode, 11> const &editor_mode::track_modes()
|
||||
std::array<editor_mode::track_mode, 12> const &editor_mode::track_modes()
|
||||
{
|
||||
static std::array<track_mode, 11> const modes{{
|
||||
static std::array<track_mode, 12> 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<editor_mode::track_mode, 11> 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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -177,10 +177,10 @@ void editor_mode::work_area_tools(std::vector<tool_button> &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<tool_button> &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);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -51,6 +51,7 @@ enum class icon
|
||||
gauge,
|
||||
turntable,
|
||||
parallel,
|
||||
station,
|
||||
more,
|
||||
// the fields of work
|
||||
work_surroundings,
|
||||
|
||||
399
editor/editorStation.cpp
Normal file
399
editor/editorStation.cpp
Normal file
@@ -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 <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
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<int>(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<std::pair<int, bool>> ends_of(TTrack const &Track)
|
||||
{
|
||||
std::vector<std::pair<int, bool>> result;
|
||||
for (int path = 0; path < static_cast<int>(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<TTrack *> 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<TTrack *> switches;
|
||||
std::vector<TTrack *> plain;
|
||||
std::vector<chain> 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<TTrack const *> 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<glm::dvec3> 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<int>(Length / 2.0), 2, 64)};
|
||||
std::vector<glm::dvec3> result;
|
||||
for (int i = 0; i <= count; ++i)
|
||||
result.push_back(curve.point(static_cast<double>(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<layout> find(TTrack &From, options const &Options)
|
||||
{
|
||||
auto const found{collect(From, Options)};
|
||||
if (found.switches.empty())
|
||||
return std::nullopt;
|
||||
std::unordered_set<TTrack const *> 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<TTrack *> 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<std::pair<double, double>> marks; // the chainage and the km, in m
|
||||
for (auto *track : found.switches)
|
||||
{
|
||||
auto const start{end_of(*track, 0, false)};
|
||||
std::optional<double> 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<double> 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<int>(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<double>::max();
|
||||
result.to = std::numeric_limits<double>::lowest();
|
||||
result.low = std::numeric_limits<double>::max();
|
||||
result.high = std::numeric_limits<double>::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<double>::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<TTrack *> stations(options const &Options)
|
||||
{
|
||||
std::vector<TTrack *> result;
|
||||
std::unordered_set<TTrack const *> 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<TTrack *> switches_of(TTrack &Switch, options const &Options)
|
||||
{
|
||||
return collect(Switch, Options).switches;
|
||||
}
|
||||
|
||||
} // namespace station
|
||||
65
editor/editorStation.hpp
Normal file
65
editor/editorStation.hpp
Normal file
@@ -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 <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
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<glm::dvec3> points; // along the path, world space
|
||||
std::vector<glm::dvec2> 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<TTrack *> switches; // switches, crossings and turntables, by the chainage of their start
|
||||
std::vector<path> paths;
|
||||
std::vector<glm::dvec3> axis; // the main route through the station
|
||||
std::vector<double> 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<layout> 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<TTrack *> stations(options const &Options = {});
|
||||
// the switches of the station of the switch, without laying it out
|
||||
std::vector<TTrack *> switches_of(TTrack &Switch, options const &Options = {});
|
||||
|
||||
} // namespace station
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 46 KiB |
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -843,16 +843,17 @@ bool node_layers::pending() const
|
||||
namespace
|
||||
{
|
||||
// marks of the lines the editor keeps track of
|
||||
std::array<char const *, 6> const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c", "//$o"};
|
||||
std::array<char const *, 7> 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<std::pair<std::string_view, std::array<std::string_view, 9>>, 3> const words{{
|
||||
static std::array<std::pair<std::string_view, std::array<std::string_view, 9>>, 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())
|
||||
|
||||
Reference in New Issue
Block a user