17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-20 16:14:52 +02:00
Files
maszyna/application/editormodeswitchtool.cpp
maj00r ff1710d9ea feat(editor): labels for a layman, light at the start; whole sceneries fitted to the terrain model
Labels:
- every button, field, tooltip and message of the editor reread for a beginner: short nouns for the fields with the
  sentence in the tooltip, the less used options under More settings, one glossary (field of work, template, track,
  grade), sides told from directions, degrees and per mille as signs, messages built from one format string
- plural forms through STR_P; the last entry of a po file is read too, and the Polish plural rule is in the header
- lang/editor_pl.po: the code pass entries merged, 391 entries no code uses and 3 duplicates dropped
- roads: the notes and errors naming a road translated; the build options and the layout fold the texture length,
  kerb sizes, traffic share, roundabout part, height over the ground and junction speed limit under More settings, the
  lanes start folded

Grade lines fitted to the terrain model (NMT), a line or the whole scenery at once:
- fit_ground: a robust fit on nodes 5 m apart; the ground parting from the line (a bridge, a viaduct, a wall) is gone
  past rather than followed, the bends keep off the switches as far as their curves reach, a run of bends the same way
  is one point of intersection, a curve with no room is as sharp as the room lets it be
- the ends of the line and the other paths of its switches leading into lines fitted already bound the line by what
  the tracks between can climb; ends meeting in the plan, laid apart in height by hand, are joined before the run
- apply_profile: the paths are split where split_path takes the parameter (a straight was cut in the wrong place, into
  pieces of a few centimetres the game takes for zero length), no piece shorter than the margin; the paths off the
  ends of the route are brought to its height
- Bytom: 1600 lines, grade breaks at the joints 89 -> 20; Tarnowskie Gory: 848 lines, 223 -> 3; no step over 2 cm
2026-10-19 15:27:50 +02:00

441 lines
20 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
// the panel of the switch tool: what it places, the templates and the options of that only, and how to place it
#include "stdafx.h"
#include "application/editormode.h"
#include "application/editorgestures.h"
#include "application/editormodeshared.h"
#include "editor/editorConnections.hpp"
#include "editor/editorFormat.hpp"
#include "editor/editorTrack.hpp"
#include "simulation/simulation.h"
#include "utilities/translation.h"
#include "world/Track.h"
#include "imgui/imgui.h"
#include <algorithm>
#include <string>
// the name of the template in the language of the editor: the words of the catalogue translated, the kind of a slip or
// a crossing left out where its type says it
std::string switch_name_translated(std::string Title)
{
auto const replace = [&](std::string const &From, std::string const &To) {
for (auto at{Title.find(From)}; at != std::string::npos; at = Title.find(From, at + To.size()))
Title.replace(at, From.size(), To);
};
for (auto const *redundant : {" single slip", " double slip", " crossing"})
replace(redundant, "");
for (auto const *word : {"the blades outside of the diamond", "movable nose", "long", "industrial"})
replace(word, STR(word));
if (auto const at{Title.find("gauge ")}; at != std::string::npos)
if (auto const gauge{std::atoi(Title.c_str() + at + 6)}; gauge > 0)
{
auto const end{Title.find_first_not_of("0123456789", at + 6)};
Title.replace(at, (end == std::string::npos ? Title.size() : end) - at, format(STR_C("gauge %d"), gauge));
}
return Title;
}
namespace
{
// a kind to place as a tile: a sketch of its tracks on the left, its name beside it; true when clicked
bool kind_tile(char const *Id, char const *Label, int const Picture, bool const Chosen, float const Width)
{
auto const font{ImGui::GetFontSize()};
auto const height{std::round(font * 2.4f)};
auto const top{ImGui::GetCursorScreenPos()};
auto const clicked{ImGui::InvisibleButton(Id, ImVec2(Width, height))};
auto const hovered{ImGui::IsItemHovered()};
auto *list{ImGui::GetWindowDrawList()};
auto const accent{family_colour(0)};
ImVec2 const bottom{top.x + Width, top.y + height};
auto const rounding{std::round(font * 0.35f)};
list->AddRectFilled(top, bottom, Chosen ? (accent & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 46) : ImGui::GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), rounding);
list->AddRect(top, bottom, Chosen ? accent : ImGui::GetColorU32(ImGuiCol_Border), rounding, 0, Chosen ? 2.0f : 1.0f);
auto const pad{std::round(font * 0.4f)};
auto const w{std::round(font * 2.2f)};
auto const h{height - 2.0f * pad};
ImVec2 const o{top.x + pad, top.y + pad};
auto const at = [&](float const X, float const Y) { return ImVec2(o.x + X * w, o.y + Y * h); };
auto const line{Chosen ? accent : ImGui::GetColorU32(ImGuiCol_Text, 0.7f)};
auto const branch{Chosen ? IM_COL32(255, 210, 60, 255) : ImGui::GetColorU32(ImGuiCol_Text, 0.45f)};
switch (Picture)
{
case 0: // a switch: the straight track and the diverging one
list->AddLine(at(0.0f, 0.75f), at(1.0f, 0.75f), line, 2.0f);
list->AddBezierQuadratic(at(0.15f, 0.75f), at(0.6f, 0.75f), at(1.0f, 0.1f), branch, 2.0f);
break;
case 1: // a slip switch: a crossing with the slip curves inside
list->AddLine(at(0.0f, 0.85f), at(1.0f, 0.15f), line, 2.0f);
list->AddLine(at(0.0f, 0.15f), at(1.0f, 0.85f), line, 2.0f);
list->AddBezierQuadratic(at(0.08f, 0.8f), at(0.5f, 0.45f), at(0.92f, 0.8f), branch, 1.5f);
list->AddBezierQuadratic(at(0.08f, 0.2f), at(0.5f, 0.55f), at(0.92f, 0.2f), branch, 1.5f);
break;
case 2: // a crossing
list->AddLine(at(0.0f, 0.85f), at(1.0f, 0.15f), line, 2.0f);
list->AddLine(at(0.0f, 0.15f), at(1.0f, 0.85f), line, 2.0f);
break;
case 5: // a double crossover: two crossovers facing each other
list->AddLine(at(0.0f, 0.2f), at(1.0f, 0.2f), line, 2.0f);
list->AddLine(at(0.0f, 0.8f), at(1.0f, 0.8f), line, 2.0f);
list->AddLine(at(0.05f, 0.8f), at(0.45f, 0.2f), branch, 2.0f);
list->AddLine(at(0.55f, 0.2f), at(0.95f, 0.8f), branch, 2.0f);
break;
case 6: // scissors: two crossovers through each other
list->AddLine(at(0.0f, 0.2f), at(1.0f, 0.2f), line, 2.0f);
list->AddLine(at(0.0f, 0.8f), at(1.0f, 0.8f), line, 2.0f);
list->AddLine(at(0.15f, 0.8f), at(0.85f, 0.2f), branch, 2.0f);
list->AddLine(at(0.15f, 0.2f), at(0.85f, 0.8f), branch, 2.0f);
break;
case 7: // a ladder: the tracks of a fan going off one by one
list->AddLine(at(0.0f, 0.9f), at(1.0f, 0.9f), line, 2.0f);
for (float const y : {0.6f, 0.35f, 0.1f})
list->AddLine(at(0.95f - y, y), at(1.0f, y), line, 2.0f);
list->AddLine(at(0.05f, 0.9f), at(0.85f, 0.1f), branch, 2.0f);
break;
default: // a connection of two tracks
list->AddLine(at(0.0f, 0.2f), at(1.0f, 0.2f), line, 2.0f);
list->AddLine(at(0.0f, 0.8f), at(1.0f, 0.8f), line, 2.0f);
list->AddLine(at(0.2f, 0.8f), at(0.8f, 0.2f), branch, 2.0f);
break;
}
auto const text_left{o.x + w + pad};
auto const wrap{bottom.x - pad - text_left};
auto const size{ImGui::CalcTextSize(Label, nullptr, false, wrap)};
list->AddText(nullptr, 0.0f, ImVec2(text_left, std::round(top.y + (height - size.y) * 0.5f)), ImGui::GetColorU32(Chosen ? ImGuiCol_Text : ImGuiCol_TextDisabled), Label, nullptr, wrap);
return clicked;
}
// whether the template is one of those the kind places
bool of_kind(editor_track::switch_template const &Template, int const Kind)
{
switch (Kind)
{
case 1: return Template.double_slip && Template.type != "Skrz";
case 2: return Template.double_slip && Template.type == "Skrz";
case 3: return false == Template.double_slip && false == Template.twofold();
default: return false == Template.double_slip;
}
}
std::string template_tooltip(editor_track::switch_template const &Template)
{
if (Template.source != nullptr)
return Template.label;
auto text{switch_name_translated(Template.title())};
if (Template.double_slip)
{
text += "\n";
text += Template.type == "Rkp" ? STR_C("Single slip (Rkp)") : Template.type == "Rkpd" ? STR_C("Double slip (Rkpd)") : STR_C("Crossing (Skrz)");
text += "\n" + format(STR_C("a %.3f m from the middle of the crossing to the joint"), Template.a) + "\n";
text += Template.type == "Rkp" ? STR_C("Click at a crossing of two straights, on the side of the tracks to join") : STR_C("Click at a crossing of two straights");
return text;
}
text += "\n" + format(STR_C("a %.3f m to the centre of the switch, b %.3f m on to its end"), Template.a, Template.b);
if (auto const note{Template.label.rfind(" (")}; false == Template.own && note != std::string::npos && Template.label.back() == ')')
text += " " + STR(Template.label.substr(note + 1));
return text;
}
// the angle of the template at a glance, steeper than it is so 1:9 and 1:18.5 differ: the main track and the diverging
// one, or the two crossing tracks of a slip or a crossing
void template_glyph(ImDrawList &Draw, ImVec2 const Low, ImVec2 const High, editor_track::switch_template const &Template, bool const Chosen)
{
auto const line{ImGui::GetColorU32(ImGuiCol_TextDisabled)};
auto const made{Chosen ? IM_COL32(255, 255, 255, 255) : IM_COL32(120, 200, 255, 255)};
auto const ratio{Template.ratio > 0.0 ? Template.ratio : Template.radius > 0.0 && Template.length > 0.0 ? 2.0 * Template.radius / Template.length : 9.0};
auto const slope{static_cast<float>(std::min(0.9, 3.0 / ratio))};
auto const width{High.x - Low.x};
auto const middle{std::round((Low.y + High.y) * 0.5f)};
if (Template.double_slip)
{
auto const rise{std::min(width * slope * 0.5f, (High.y - Low.y) * 0.5f)};
Draw.AddLine(ImVec2(Low.x, middle), ImVec2(High.x, middle), line, 2.0f);
Draw.AddLine(ImVec2(Low.x, middle + rise), ImVec2(High.x, middle - rise), made, 2.0f);
return;
}
auto const bottom{std::round(High.y - 2.0f)};
auto const rise{std::min(width * 0.75f * slope, High.y - Low.y - 4.0f)};
Draw.AddLine(ImVec2(Low.x, bottom), ImVec2(High.x, bottom), line, 2.0f);
Draw.AddBezierQuadratic(ImVec2(Low.x + width * 0.15f, bottom), ImVec2(Low.x + width * 0.5f, bottom), ImVec2(High.x, bottom - rise), made, 2.0f);
}
} // namespace
std::string editor_mode::switch_template_label(editor_track::switch_template const &Template)
{
if (Template.source != nullptr)
return format("%s: %s (%dx)", STR_C("From the scenery"), Template.title().c_str(), Template.count);
return (Template.own ? STR("Own: ") : std::string{"PLK: "}) + switch_name_translated(Template.title());
}
editor_mode::switch_kind editor_mode::current_switch_kind() const
{
auto const &tool{m_switch};
if (tool.connection >= 0)
return switch_kind::connection;
if (tool.armed >= 0 && tool.armed < static_cast<int>(tool.templates.size()) && tool.templates[tool.armed].double_slip)
return tool.templates[tool.armed].type == "Skrz" ? switch_kind::crossing : switch_kind::slip;
return switch_kind::single;
}
bool editor_mode::switch_kind_offered(switch_kind const Kind) const
{
auto const &templates{m_switch.templates};
return std::any_of(templates.begin(), templates.end(), [&](editor_track::switch_template const &Template) { return of_kind(Template, static_cast<int>(Kind)); });
}
void editor_mode::choose_switch_kind(switch_kind const Kind)
{
auto &tool{m_switch};
tool.connection = (Kind == switch_kind::connection ? std::clamp(tool.connection_kind, 0, connection::kind_count - 1) : -1);
// the template chosen stays if it's of the kind, else the first one of the kind
auto const fits = [&](int const Index) { return Index >= 0 && Index < static_cast<int>(tool.templates.size()) && of_kind(tool.templates[Index], static_cast<int>(Kind)); };
if (fits(tool.armed))
return;
for (int i = 0; i < static_cast<int>(tool.templates.size()); ++i)
if (fits(i))
{
tool.last = i;
tool.armed = i;
tool.status.clear();
return;
}
tool.armed = -1;
}
void editor_mode::render_switch_ui()
{
auto &tool{m_switch};
auto const kind{current_switch_kind()};
// what to place, each with the templates and the options of its own only
struct choice
{
switch_kind kind;
char const *label;
char const *tooltip;
};
choice const choices[] = {{switch_kind::single, "Switch", "A switch in a straight or a curve"},
{switch_kind::slip, "Slip switch", "Rkp or Rkpd at a crossing of two straights"},
{switch_kind::crossing, "Crossing", "A crossing of two straights"},
{switch_kind::connection, "Connection", "Crossover, double, scissors, ladder"}};
{
auto const gap{std::round(ImGui::GetFontSize() * 0.4f)};
auto const width{std::floor((ImGui::GetContentRegionAvail().x - gap) * 0.5f)};
int shown{0};
for (auto const &entry : choices)
{
if (entry.kind != switch_kind::single && false == switch_kind_offered(entry.kind))
continue;
if (shown % 2 == 1)
ImGui::SameLine(0.0f, gap);
else if (shown > 0)
ImGui::Dummy(ImVec2(0.0f, gap * 0.25f));
if (kind_tile(entry.label, STR_C(entry.label), static_cast<int>(&entry - choices), kind == entry.kind, width) && kind != entry.kind)
choose_switch_kind(entry.kind);
item_tooltip(entry.tooltip);
++shown;
}
}
if (kind == switch_kind::connection)
{
ImGui::SeparatorText(STR_C("Kind of the connection"));
auto const gap{std::round(ImGui::GetFontSize() * 0.4f)};
auto const width{std::floor((ImGui::GetContentRegionAvail().x - gap) * 0.5f)};
for (int i = 0; i < connection::kind_count; ++i)
{
if (i % 2 == 1)
ImGui::SameLine(0.0f, gap);
else if (i > 0)
ImGui::Dummy(ImVec2(0.0f, gap * 0.25f));
ImGui::PushID(i);
if (kind_tile("##connectionkind", connection_name(i), 4 + i, tool.connection == i, width))
{
tool.connection = i;
tool.connection_kind = i;
}
ImGui::PopID();
}
}
// how to place it with the mouse
std::vector<std::pair<char const *, char const *>> how;
switch (kind)
{
case switch_kind::slip:
case switch_kind::crossing: how = {{"LMB on a crossing", "of two straights: it goes there"}}; break;
case switch_kind::connection:
how = {{"Drag", "from a straight onto the other track"}, {"Shift+wheel", "the kind"}, {"Ctrl+wheel", "R of the aligning curve"}};
if (tool.connection == static_cast<int>(connection::kind::double_crossover))
how.push_back({"Alt+wheel", "where they meet"});
break;
default: how = {{"Drag on a track", "along: the direction, sideways: the side"}}; break;
}
gesture_rows(how);
// the templates of the kind, by their names, for the rail of the selected track unless all are asked for; the lengths
// and the rest of the label in the tooltip
ImGui::TextDisabled("%s", STR_C(kind == switch_kind::connection ? "The first switch" : "Template"));
auto const rail{editor_track::rail_of(selected_track())};
if (false == rail.empty())
{
ImGui::SameLine();
auto const label{format(STR_C("Rail of the track: %s"), rail.c_str())};
toggle_chips("railonly", {{label.c_str(), &tool.rail_only, "Only the templates of the rail the selected track is laid with"}});
}
else if (auto *track{selected_track()}; track != nullptr && (track->iCategoryFlag & 15) == 1 && track->m_profile1.first.empty())
render_rail_profile_hint(*track);
auto const shown = [&](editor_track::switch_template const &Template) {
return of_kind(Template, static_cast<int>(kind)) && (rail.empty() || false == tool.rail_only || Template.source != nullptr || Template.rail == rail || Template.rail == "-");
};
// in groups: the switches of the scenery, the own templates, then the catalogue rail by rail
auto const group_of = [](editor_track::switch_template const &Template) {
return Template.source != nullptr ? std::string{STR("From the scenery")} : Template.own ? std::string{STR("Own templates")} : "PLK " + (Template.rail == "-" ? std::string{} : Template.rail);
};
std::vector<std::string> groups;
int listed{0};
for (auto const &entry : tool.templates)
if (shown(entry))
{
++listed;
if (auto const group{group_of(entry)}; std::find(groups.begin(), groups.end(), group) == groups.end())
groups.push_back(group);
}
auto const row{std::round(ImGui::GetTextLineHeight() * 1.3f)};
auto const header{groups.size() > 1 ? ImGui::GetTextLineHeightWithSpacing() * static_cast<float>(groups.size()) : 0.0f};
ImGui::BeginChild("##switchtemplates", ImVec2(0.0f, std::min(std::round(ImGui::GetFontSize() * 18.0f), (listed + 0.5f) * (row + ImGui::GetStyle().ItemSpacing.y) + header + 8.0f)), ImGuiChildFlags_Borders);
auto const glyph{std::round(row * 1.8f)};
for (auto const &group : groups)
{
if (groups.size() > 1)
ImGui::SeparatorText(group.c_str());
for (int i = 0; i < static_cast<int>(tool.templates.size()); ++i)
{
auto const &entry{tool.templates[i]};
if (false == shown(entry) || group_of(entry) != group)
continue;
ImGui::PushID(i);
auto const low{ImGui::GetCursorScreenPos()};
if (ImGui::Selectable("##switch", tool.armed == i, ImGuiSelectableFlags_None, ImVec2(0.0f, row)))
{
tool.last = i;
tool.armed = i;
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", template_tooltip(entry).c_str());
if (ImGui::IsItemVisible())
{
auto &draw{*ImGui::GetWindowDrawList()};
template_glyph(draw, ImVec2(low.x + 2.0f, low.y + 2.0f), ImVec2(low.x + glyph, low.y + row - 2.0f), entry, tool.armed == i);
auto const label{entry.source != nullptr ? entry.title() + " (" + std::to_string(entry.count) + "x)" : switch_name_translated(entry.title())};
draw.AddText(ImVec2(low.x + glyph + ImGui::GetStyle().ItemSpacing.x, std::round(low.y + (row - ImGui::GetTextLineHeight()) * 0.5f)), ImGui::GetColorU32(ImGuiCol_Text), label.c_str());
}
ImGui::PopID();
}
}
ImGui::EndChild();
if (kind == switch_kind::connection && tool.connection == static_cast<int>(connection::kind::ladder) && begin_fields("##connectionladder"))
{
field_row("Spacings, m##connectionspacings");
ImGui::InputText("##connectionspacings", tool.connection_spacings, sizeof(tool.connection_spacings));
item_tooltip("Distances between the tracks of the fan from this track on, such as 4.75;4.75;5.0; used when the drag doesn't end on a parallel track, otherwise the parallel tracks up to that one make the fan");
ImGui::EndTable();
}
if (kind == switch_kind::connection && tool.connection == static_cast<int>(connection::kind::double_crossover))
{
ImGui::TextDisabled("%s", STR_C("Where the two crossovers meet"));
int together{tool.connection_together ? 0 : 1};
if (segmented("connectiontogether", together,
{{0, STR_C("On this track"), "The two crossovers meet on the track the drag starts on; Alt+wheel while dragging switches it"},
{1, STR_C("On the other track"), "The two crossovers meet on the other track; Alt+wheel while dragging switches it"}}))
tool.connection_together = together == 0;
}
ImGui::PushID("switchmore");
if (ImGui::TreeNode(STR_C("More settings")))
{
if (kind == switch_kind::connection)
render_connection_options();
// the name and the drive of the next switch; a crossing has no moving parts
if (kind != switch_kind::crossing)
{
if (begin_fields("##switchnamefields", 0.3f))
{
field_row("Name##switchname");
ImGui::InputTextWithHint("##switchname", switch_name_for_new().c_str(), tool.name, sizeof(tool.name));
item_tooltip("Name of the next switch, its drive events are named after it; empty: the next free z1, z2...; a number at the end goes up with each switch");
ImGui::EndTable();
}
if (render_switch_drive_choice())
if (auto *track{selected_track()}; track != nullptr && track->eType == tt_Switch && false == track->m_editorremoved)
apply_switch_drive_choice(*track);
}
if (ImGui::Button(STR_C("Collect from the scenery")))
{
tool.templates = editor_track::standard_switch_templates();
auto const found{editor_track::find_switch_templates()};
tool.templates.insert(tool.templates.end(), found.begin(), found.end());
tool.collected = true;
arm_switch();
}
item_tooltip("Looks for the switches of the scenery, to use their shapes as the templates");
ImGui::TreePop();
}
ImGui::PopID();
if (false == tool.status.empty())
{
ImGui::PushTextWrapPos(0.0f);
ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", tool.status.c_str());
ImGui::PopTextWrapPos();
}
if (ImGui::Button(STR_C("Template editor...")))
m_switch_designer.open = true;
item_tooltip("Own switch templates: diverging tracks of straights, arcs and transition curves, double switches");
}
// 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)
{
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", STR_C("Track with no rail profile"));
item_tooltip("The track doesn't say its rail, so the templates of all the rails are offered.\nSet the profile and the list keeps to the templates of the rail; the rails of the track take its shape");
ImGui::SameLine();
if (ImGui::SmallButton(STR_C("Set...##railprofile")))
ImGui::OpenPopup("##railprofileset");
if (false == ImGui::BeginPopup("##railprofileset"))
return;
auto const material{Track.m_material1};
auto const same = [&](TTrack const &Other) { return false == Other.m_editorremoved && (Other.iCategoryFlag & 15) == 1 && Other.m_material1 == material && Other.m_profile1.first.empty(); };
std::vector<TTrack *> tracks;
for (auto *other : simulation::Paths.sequence())
if (other != nullptr && same(*other))
tracks.push_back(other);
ImGui::TextUnformatted(format(STR_C("Tracks with the rails %s and no profile: %d"), editor_track::material_name(material).c_str(), static_cast<int>(tracks.size())).c_str());
static std::pair<char const *, char const *> const profiles[] = {{"S60 (60E1)", "s60"}, {"S49 (49E1)", "s49"}, {"S42", "s42"}};
for (auto const &[label, name] : profiles)
{
if (&label != &profiles[0].first)
ImGui::SameLine();
if (false == ImGui::Button(label))
continue;
auto const changed{tracks_apply(tracks, [&](TTrack &Other) { Other.rail_profile(name); return true; }, true, "Rail profile")};
m_switch.status = format(STR_C("Rail profile %s set on %d tracks"), label, static_cast<int>(changed));
ImGui::CloseCurrentPopup();
}
ImGui::TextDisabled("%s", STR_C("Other profiles: the properties of the track"));
ImGui::EndPopup();
}