/* 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 #include // 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(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(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(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(tool.templates.size()) && of_kind(tool.templates[Index], static_cast(Kind)); }; if (fits(tool.armed)) return; for (int i = 0; i < static_cast(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(&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> 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(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(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 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(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(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(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(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 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(tracks.size())).c_str()); static std::pair 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(changed)); ImGui::CloseCurrentPopup(); } ImGui::TextDisabled("%s", STR_C("Other profiles: the properties of the track")); ImGui::EndPopup(); }