diff --git a/CMakeLists.txt b/CMakeLists.txt index 8524fd8d..c63147cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -182,6 +182,7 @@ set(SOURCES "editor/editorGroundFollow.cpp" "editor/editorAlignmentStore.cpp" "editor/editorSwitchStore.cpp" +"editor/editorSwitchDesign.cpp" "editor/editorSpeed.cpp" "editor/editorProfile.cpp" "editor/editorGauge.cpp" @@ -196,6 +197,7 @@ set(SOURCES "application/editormoderecords.cpp" "application/editormodeswitchdrive.cpp" "application/editormodeswitchslide.cpp" +"application/editormodeswitchdesign.cpp" "application/editormodetrackbed.cpp" "application/editormodetrackux.cpp" "application/editormoderoad.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 1fdb70cf..27aa1a4a 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1483,6 +1483,7 @@ bool editor_mode::update() render_shortcuts_window(); render_first_steps(); render_orthophoto_window(); + render_switch_designer(); render_environment_window(); draw_selection_overlay(); render_context_menu(); diff --git a/application/editormode.h b/application/editormode.h index 7f4f12b1..89a6a411 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -725,6 +725,8 @@ class editor_mode : public application_mode, private editor_track::observer void track_gizmo_take_switch_joints(TTrack &Track, bool const Controlmode); track_gizmo_drag::switch_joint track_gizmo_link(TTrack &Track, editor_track::point_ref const &Own, std::pair const &Connection, std::function const &Movable); void track_gizmo_take_switches(TTrack &Track); + // the other switch of a double switch and the track between them go with the moved or turned switch as a whole + void track_gizmo_take_double(TTrack &Track); void track_gizmo_measure_rotation(TTrack &Track, bool const Pointmode); void track_gizmo_measure(TTrack &Track, bool const Pointmode, gizmo_operation const Operation); void track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_operation const Operation); @@ -832,6 +834,19 @@ class editor_mode : public application_mode, private editor_track::observer void route_read_stored(TTrack &Track); void route_take_stored(int const Index); void record_switch(TTrack &Track, editor_track::switch_template const &Shape); + void record_double(TTrack &First, TTrack &Second, editor_track::switch_template const &Shape); + // the two switches of a double switch and the plain track between them, found from any of the three + struct double_switch + { + TTrack *first{nullptr}; + TTrack *link{nullptr}; + TTrack *second{nullptr}; + }; + std::optional double_of(TTrack const &Track) const; + // the paths of the set in the order editor_track::place_switch gives them + static std::vector double_paths(double_switch const &Set); + // the switch of the template, or the two switches of a double one with the plain track between them, committed and recorded + std::vector create_switch_set(editor_track::switch_template const &Shape, std::vector const &Paths, TTrack const &Style, std::string const &Name); void record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base); void render_switch_record(TTrack const &Track) const; void route_store(); @@ -978,6 +993,7 @@ class editor_mode : public application_mode, private editor_track::observer double low{0.0}; double high{0.0}; double distance{0.0}; + double length{0.0}; std::vector joints; }; slide_state slide; @@ -987,6 +1003,9 @@ class editor_mode : public application_mode, private editor_track::observer bool switch_room(TTrack &Track, double &Low, double &High, std::string &Reason); bool switch_vacate(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, double const Shift, editor_track::straight const &Line, editor_track::edit &Edit, std::string &Status); bool move_switch_paths(TTrack &Track, std::vector const &Paths, std::string &Status); + // room on the straight of the whole double switch along the axis of its first switch, and the length of the set + bool double_room(double_switch const &Set, double &Low, double &High, double &Length, std::string &Reason); + bool move_double_paths(double_switch const &Set, std::vector const &Paths, std::string &Status); bool slide_switch(TTrack &Track, double const Distance); bool switch_slides(TTrack const &Track) const; void switch_slide_start(TTrack &Track); @@ -1213,6 +1232,29 @@ class editor_mode : public application_mode, private editor_track::observer int m_lineside_tab{0}; void render_lineside_ui(); void arm_switch(); + // the window of the switch templates: the catalogue and the own ones, made of straights, arcs and transition curves + struct switch_designer + { + bool open{false}; + bool loaded{false}; + std::vector catalogue; + std::vector own; + bool from_catalogue{true}; + int selected{-1}; + bool dirty{false}; + bool stretched{true}; + double quick_radius{300.0}; + double quick_ratio{9.0}; + double quick_straight{0.0}; + double blade_length{5.0}; + double blade_gap{0.15}; + std::string status; + }; + switch_designer m_switch_designer; + void switch_designs_load(); + // the list of the switch tool built again with the own templates, keeping the armed one or arming the one whose label starts with Arm + void switch_templates_refresh(std::string const &Arm = {}); + void render_switch_designer(); bool m_track_window_open{false}; // the inspector is pinned to the right edge of the screen, the profile strip to the bottom one, both resized from the inner edge void render_track_inspector(); diff --git a/application/editormoderecords.cpp b/application/editormoderecords.cpp index a5b05e4c..84b8f76f 100644 --- a/application/editormoderecords.cpp +++ b/application/editormoderecords.cpp @@ -264,6 +264,68 @@ void editor_mode::record_switch(TTrack &Track, editor_track::switch_template con switch_store::put(entry, alignment_store::layer_of(Track)); } +// a double switch is kept as one record of its two switches, with the type, the size and the side of the first one +void editor_mode::record_double(TTrack &First, TTrack &Second, editor_track::switch_template const &Shape) +{ + if (First.eType != tt_Switch || Second.eType != tt_Switch || First.m_paths.size() < 2 || First.name().empty() || Second.name().empty() || scene::Layers.empty()) + return; + auto entry{switch_entry(Shape)}; + entry.kind = "double"; + entry.name = First.name(); + entry.parts = {First.name(), Second.name()}; + switch_store::place(entry, First); + switch_store::classify(entry, First); + switch_store::put(entry, alignment_store::layer_of(First)); +} + +std::optional editor_mode::double_of(TTrack const &Track) const +{ + auto const of_switch = [](TTrack const &Switch) -> std::optional { + if (Switch.eType != tt_Switch || Switch.m_editorremoved) + return std::nullopt; + auto const known{switch_store::named(Switch)}; + if (false == known.has_value() || known->kind != "double" || known->parts.size() != 2) + return std::nullopt; + auto *first{simulation::Paths.find(known->parts[0])}; + auto *second{simulation::Paths.find(known->parts[1])}; + for (auto const *part : {first, second}) + if (part == nullptr || part->m_editorremoved || part->eType != tt_Switch || part->m_paths.size() != 2) + return std::nullopt; + if (first == second) + return std::nullopt; + auto const start{second->m_paths.front().points[segment_data::point::start]}; + for (auto const &[other, point] : editor_track::connected_points(*first, first->m_paths.front().points[segment_data::point::end])) + { + if (other->m_editorremoved || other->eType != tt_Normal || other->m_paths.size() != 1) + continue; + auto const &path{other->m_paths.front()}; + auto const opposite{path.points[point.kind == editor_track::point_kind::start ? segment_data::point::end : segment_data::point::start]}; + if (glm::distance(opposite, start) < geometry::same_point) + return double_switch{first, other, second}; + } + return std::nullopt; + }; + if (Track.eType == tt_Switch) + return of_switch(Track); + if (Track.eType != tt_Normal || Track.m_editorremoved || Track.m_paths.size() != 1) + return std::nullopt; + for (auto const index : {segment_data::point::start, segment_data::point::end}) + for (auto const &[other, point] : editor_track::connected_points(Track, Track.m_paths.front().points[index])) + if (other->eType == tt_Switch) + if (auto const found{of_switch(*other)}; found.has_value() && found->link == &Track) + return found; + return std::nullopt; +} + +std::vector editor_mode::double_paths(double_switch const &Set) +{ + auto link{Set.link->m_paths.front()}; + auto const joint{Set.first->m_paths.front().points[segment_data::point::end]}; + if (glm::distance(link.points[segment_data::point::start], joint) > glm::distance(link.points[segment_data::point::end], joint)) + link = geometry::reversed(link); + return {Set.first->m_paths[0], Set.first->m_paths[1], Set.second->m_paths[0], Set.second->m_paths[1], link}; +} + void editor_mode::record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base) { if (Parts.empty() || scene::Layers.empty()) diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 656ba41b..6a3cdd22 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -1625,7 +1625,9 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com double low{0.0}; double high{0.0}; std::string reason; - auto const room{switch_room(*track, low, high, reason)}; + auto const set{double_of(*track)}; + double length{0.0}; + auto const room{set.has_value() ? double_room(*set, low, high, length, reason) : switch_room(*track, low, high, reason)}; auto const done{slide_switch(*track, distance)}; WriteLog(format("SELFTEST switchslide %s %.3f: room %d %.3f .. %.3f, done %d, %s", name.c_str(), distance, room ? 1 : 0, low, high, done ? 1 : 0, m_switch.status.c_str())); return selftest_result::handled; @@ -1641,14 +1643,24 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com WriteLog("SELFTEST switchreplace: no template " + label); return selftest_result::handled; } + auto const set{double_of(*track)}; + if (set.has_value()) + track = set->first; auto const &main{track->m_paths[0]}; auto const origin{main.points[segment_data::point::start]}; auto const chord{geometry::plan_of(main.points[segment_data::point::end] - origin)}; auto const length{glm::length(chord)}; auto paths{editor_track::place_switch(*found, origin, chord / length, side, (main.points[segment_data::point::end].y - origin.y) / length)}; std::string status; - auto const done{paths.size() >= 2 && move_switch_paths(*track, paths, status)}; - if (done) + auto const done{set.has_value() ? found->twofold() && paths.size() >= 5 && move_double_paths(*set, paths, status) : paths.size() >= 2 && move_switch_paths(*track, paths, status)}; + if (done && set.has_value()) + { + record_double(*set->first, *set->second, *found); + switch_store::follow(); + records_to_history(); + straight_refresh(); + } + else if (done) { record_switch(*track, *found); records_to_history(); @@ -1719,6 +1731,61 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com } return selftest_result::handled; } + if (command == "switchlinks") + { + auto const name_of = [](TTrack const *Track) { return Track == nullptr ? std::string{"-"} : Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->SwitchExtension == nullptr) + continue; + auto const &extension{*track->SwitchExtension}; + WriteLog(format("SELFTEST switchlinks %s: prev %s / %s, next %s / %s, lengths %.3f / %.3f m", track->name().c_str(), name_of(extension.pPrevs[0]).c_str(), name_of(extension.pPrevs[1]).c_str(), name_of(extension.pNexts[0]).c_str(), name_of(extension.pNexts[1]).c_str(), + extension.Segments[0]->GetLength(), extension.Segments[1]->GetLength())); + } + return selftest_result::handled; + } + if (command == "switchtemplates") + { + for (auto const &entry : editor_track::standard_switch_templates()) + { + if (entry.double_slip) + continue; + auto const paths{editor_track::place_switch(entry, glm::dvec3{0.0}, glm::dvec2{0.0, 1.0}, 1, 0.0)}; + double gap{0.0}; + double kink{0.0}; + std::size_t pieces{0}; + for (auto const &path : paths) + { + pieces += path.pieces.size(); + for (std::size_t i = 0; i + 1 < path.pieces.size(); ++i) + { + auto const &one{path.pieces[i]}; + auto const &two{path.pieces[i + 1]}; + gap = std::max(gap, glm::distance(one.points[segment_data::point::end], two.points[segment_data::point::start])); + geometry::bezier const a{one}; + geometry::bezier const b{two}; + auto const ta{glm::normalize(geometry::plan_of(a.first(1.0)))}; + auto const tb{glm::normalize(geometry::plan_of(b.first(0.0)))}; + kink = std::max(kink, std::abs(geometry::signed_angle(ta, tb))); + } + if (false == path.pieces.empty()) + gap = std::max({gap, glm::distance(path.pieces.front().points[segment_data::point::start], path.points[segment_data::point::start]), glm::distance(path.pieces.back().points[segment_data::point::end], path.points[segment_data::point::end])}); + } + std::string ends; + for (auto const &path : paths) + ends += format(" (%.3f %.3f)", path.points[segment_data::point::end].x, path.points[segment_data::point::end].z); + std::string starts; + for (std::size_t i = 1; i < paths.size() && i <= 3; i += 2) + { + geometry::bezier const first{paths[i].pieces.empty() ? paths[i] : paths[i].pieces.front()}; + auto const tangent{glm::normalize(geometry::plan_of(first.first(0.0)))}; + starts += format(" %.4f", glm::degrees(std::abs(geometry::signed_angle(glm::dvec2{0.0, 1.0}, tangent)))); + } + WriteLog(format("SELFTEST switchtemplate %s: %s%s, L %.3f 1:%.3f R %.1f a %.3f b %.3f stagger %.3f, %zu paths, %zu pieces, gap %.6f m, kink %.6f deg, start deg%s, ends%s", entry.label.c_str(), entry.type.c_str(), entry.own ? " own" : "", entry.length, entry.ratio, entry.radius, entry.a, entry.b, entry.stagger, paths.size(), pieces, gap, + glm::degrees(kink), starts.c_str(), ends.c_str())); + } + return selftest_result::handled; + } if (command == "switches") { for (auto const &entry : switch_store::records()) diff --git a/application/editormodeswitchdesign.cpp b/application/editormodeswitchdesign.cpp new file mode 100644 index 00000000..1e3f47dc --- /dev/null +++ b/application/editormodeswitchdesign.cpp @@ -0,0 +1,542 @@ +/* +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/editormode.h" +#include "application/editoruilayer.h" +#include "application/editormodeshared.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorSwitchDesign.hpp" + +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include +#include + +namespace +{ + +enum class part_kind +{ + straight, + arc, + transition +}; + +part_kind kind_of(switch_design::part const &Part) +{ + if (Part.radius1 == 0.0 && Part.radius2 == 0.0) + return part_kind::straight; + return Part.radius1 == Part.radius2 ? part_kind::arc : part_kind::transition; +} + +void set_kind(switch_design::part &Part, part_kind const Kind) +{ + auto const radius{Part.radius2 != 0.0 ? Part.radius2 : Part.radius1 != 0.0 ? Part.radius1 : 300.0}; + switch (Kind) + { + case part_kind::straight: + Part.radius1 = Part.radius2 = 0.0; + break; + case part_kind::arc: + Part.radius1 = Part.radius2 = radius; + break; + case part_kind::transition: + Part.radius1 = 0.0; + Part.radius2 = radius; + break; + } +} + +// the parts of the diverging track as a table; true when something changed +bool edit_parts(std::vector &Parts, char const *Id, bool const Editable) +{ + bool changed{false}; + char const *kinds[] = {STR_C("Straight"), STR_C("Arc"), STR_C("Transition curve")}; + ImGui::PushID(Id); + if (false == Editable) + ImGui::BeginDisabled(); + int erase{-1}; + int up{-1}; + for (int i = 0; i < static_cast(Parts.size()); ++i) + { + auto &part{Parts[i]}; + ImGui::PushID(i); + ImGui::AlignTextToFramePadding(); + ImGui::Text("%d.", i + 1); + ImGui::SameLine(); + auto kind{static_cast(kind_of(part))}; + ImGui::SetNextItemWidth(130.0f); + if (ImGui::Combo("##kind", &kind, kinds, 3)) + { + set_kind(part, static_cast(kind)); + changed = true; + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(90.0f); + changed |= ImGui::InputDouble(STR_C("m##length"), &part.length, 0.0, 0.0, "%.3f"); + item_tooltip("Length of the part, measured along it"); + switch (static_cast(kind)) + { + case part_kind::arc: + ImGui::SameLine(); + ImGui::SetNextItemWidth(90.0f); + if (ImGui::InputDouble(STR_C("R##radius"), &part.radius1, 0.0, 0.0, "%.1f")) + { + part.radius2 = part.radius1; + changed = true; + } + item_tooltip("Radius of the arc; negative turns back towards the main track"); + if (part.radius1 != 0.0) + { + ImGui::SameLine(); + ImGui::TextDisabled("%.3f deg", glm::degrees(part.length / part.radius1)); + } + break; + case part_kind::transition: + ImGui::SameLine(); + ImGui::SetNextItemWidth(90.0f); + changed |= ImGui::InputDouble(STR_C("R from##radius1"), &part.radius1, 0.0, 0.0, "%.1f"); + item_tooltip("Radius at the start of the transition curve, 0: from a straight"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(90.0f); + changed |= ImGui::InputDouble(STR_C("to##radius2"), &part.radius2, 0.0, 0.0, "%.1f"); + item_tooltip("Radius at the end of the transition curve, 0: into a straight; the curvature changes evenly along it"); + break; + default: + break; + } + if (Editable) + { + ImGui::SameLine(); + if (ImGui::SmallButton("^") && i > 0) + up = i; + ImGui::SameLine(); + if (ImGui::SmallButton("x")) + erase = i; + } + ImGui::PopID(); + } + if (up > 0) + { + std::swap(Parts[up], Parts[up - 1]); + changed = true; + } + if (erase >= 0) + { + Parts.erase(Parts.begin() + erase); + changed = true; + } + if (Editable) + { + auto const radius{Parts.empty() ? 300.0 : std::max({std::abs(Parts.back().radius1), std::abs(Parts.back().radius2), 0.0})}; + if (ImGui::SmallButton(STR_C("+ Straight"))) + { + Parts.push_back({5.0, 0.0, 0.0}); + changed = true; + } + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("+ Arc"))) + { + auto const r{radius > 0.0 ? radius : 300.0}; + Parts.push_back({20.0, r, r}); + changed = true; + } + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("+ Transition curve"))) + { + Parts.push_back({10.0, 0.0, radius > 0.0 ? radius : 300.0}); + changed = true; + } + } + if (false == Editable) + ImGui::EndDisabled(); + ImGui::PopID(); + return changed; +} + +// plan of the switch in its own space, the main track along the width of the box +void draw_shape(switch_design::shape const &Shape, float const Height, bool const Stretched) +{ + auto const width{ImGui::GetContentRegionAvail().x}; + ImGui::BeginChild("##switchshape", ImVec2(0.0f, Height), true, ImGuiWindowFlags_NoScrollbar); + auto *drawlist{ImGui::GetWindowDrawList()}; + auto const origin{ImGui::GetCursorScreenPos()}; + if (Shape.paths.empty() || Shape.length <= 0.0) + { + ImGui::EndChild(); + return; + } + double reach{0.0}; + double across{0.0}; + for (auto const &path : Shape.paths) + { + reach = std::max(reach, path.points[segment_data::point::end].z); + across = std::max(across, std::abs(path.points[segment_data::point::end].x)); + } + auto const margin{12.0f}; + auto const scale{(width - margin * 2.0f) / static_cast(std::max(1.0, reach))}; + auto const stretch{Stretched ? std::min(8.0f, (Height * 0.5f - margin) / std::max(0.01f, static_cast(across) * scale)) : 1.0f}; + auto const middle{origin.y + Height * 0.5f}; + auto const screen = [&](glm::dvec3 const &Point) { return ImVec2{origin.x + margin + static_cast(Point.z) * scale, middle - static_cast(Point.x) * scale * std::max(1.0f, stretch)}; }; + ImU32 const colours[] = {IM_COL32(200, 200, 200, 255), IM_COL32(90, 200, 255, 255), IM_COL32(200, 200, 200, 255), IM_COL32(255, 170, 60, 255)}; + for (std::size_t i = 0; i < Shape.paths.size(); ++i) + { + auto const &path{Shape.paths[i]}; + std::vector pieces{path.pieces}; + if (pieces.empty()) + pieces.push_back(path); + for (auto const &piece : pieces) + { + geometry::bezier const curve{piece}; + auto previous{screen(curve.point(0.0))}; + for (int j = 1; j <= 24; ++j) + { + auto const next{screen(curve.point(j / 24.0))}; + drawlist->AddLine(previous, next, colours[i % 4], 2.0f); + previous = next; + } + drawlist->AddCircleFilled(screen(piece.points[segment_data::point::end]), 3.0f, colours[i % 4]); + } + } + drawlist->AddCircleFilled(screen({0.0, 0.0, 0.0}), 4.0f, IM_COL32(255, 255, 255, 255)); + if (Shape.stagger > 0.0) + drawlist->AddCircleFilled(screen({0.0, 0.0, Shape.stagger}), 4.0f, IM_COL32(255, 170, 60, 255)); + for (auto const tip : {Shape.tip, Shape.stagger > 0.0 ? Shape.tip2 : 0.0}) + if (tip > 0.0) + { + auto const at{screen({0.0, 0.0, tip})}; + drawlist->AddTriangleFilled({at.x, at.y}, {at.x - 4.0f, at.y + 8.0f}, {at.x + 4.0f, at.y + 8.0f}, IM_COL32(120, 255, 120, 220)); + } + if (Shape.a > 0.0) + { + auto const at{screen({0.0, 0.0, Shape.a})}; + drawlist->AddLine({at.x, at.y - 6.0f}, {at.x, at.y + 6.0f}, IM_COL32(255, 255, 255, 160), 1.0f); + } + if (Stretched && stretch > 1.01f) + drawlist->AddText({origin.x + 4.0f, origin.y + 2.0f}, IM_COL32(255, 255, 255, 120), format(STR_C("across x%.1f"), stretch).c_str()); + ImGui::EndChild(); +} + +} // namespace + +void editor_mode::switch_designs_load() +{ + auto &designer{m_switch_designer}; + if (designer.loaded) + return; + designer.loaded = true; + designer.catalogue = switch_design::catalogue(); + designer.own = switch_design::own(); +} + +void editor_mode::switch_templates_refresh(std::string const &Arm) +{ + auto &tool{m_switch}; + std::vector scenery; + for (auto const &entry : tool.templates) + if (entry.source != nullptr) + scenery.push_back(entry); + auto const armed{false == Arm.empty() ? Arm : tool.armed >= 0 && tool.armed < static_cast(tool.templates.size()) ? tool.templates[tool.armed].label : std::string{}}; + tool.templates = editor_track::standard_switch_templates(); + tool.templates.insert(tool.templates.end(), scenery.begin(), scenery.end()); + tool.collected = true; + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + if (false == armed.empty() && tool.templates[i].label.rfind(armed, 0) == 0) + { + tool.last = i; + if (tool.armed >= 0) + tool.armed = i; + break; + } + m_turnout_template = 0; +} + +void editor_mode::render_switch_designer() +{ + auto &designer{m_switch_designer}; + if (false == designer.open) + return; + switch_designs_load(); + ImGui::SetNextWindowSize(ImVec2(820.0f, 560.0f), ImGuiCond_FirstUseEver); + if (false == ImGui::Begin((std::string(STR_C("Switch templates")) + "###switchdesigner").c_str(), &designer.open, ImGuiWindowFlags_NoCollapse)) + { + ImGui::End(); + return; + } + ImGui::BeginChild("##designlist", ImVec2(230.0f, 0.0f), true); + ImGui::TextDisabled("%s", STR_C("Catalogue (read only)")); + item_tooltip("The catalogue switches start tangentially at the joint before the blades: without the pre-blade stretch and the angle of the blade, which a copy can be given"); + for (int i = 0; i < static_cast(designer.catalogue.size()); ++i) + if (ImGui::Selectable((designer.catalogue[i].name + "##catalogue" + std::to_string(i)).c_str(), designer.from_catalogue && designer.selected == i)) + { + designer.from_catalogue = true; + designer.selected = i; + } + ImGui::Separator(); + ImGui::TextDisabled("%s", STR_C("Own templates")); + for (int i = 0; i < static_cast(designer.own.size()); ++i) + if (ImGui::Selectable((designer.own[i].name + "##own" + std::to_string(i)).c_str(), false == designer.from_catalogue && designer.selected == i)) + { + designer.from_catalogue = false; + designer.selected = i; + } + if (designer.own.empty()) + ImGui::TextDisabled("%s", STR_C("(none yet)")); + if (ImGui::Button(STR_C("New"))) + { + switch_design::design entry; + entry.own = true; + entry.name = format("%s %zu", STR_C("Switch"), designer.own.size() + 1); + auto const angle{std::atan(1.0 / 9.0)}; + entry.parts = {{190.0 * angle, 190.0, 190.0}, {6.0, 0.0, 0.0}}; + designer.own.push_back(entry); + designer.from_catalogue = false; + designer.selected = static_cast(designer.own.size()) - 1; + designer.dirty = true; + } + item_tooltip("A new own template, an Rz 1:9 R190 to start from"); + ImGui::EndChild(); + ImGui::SameLine(); + ImGui::BeginChild("##designedit", ImVec2(0.0f, 0.0f), false); + auto const &list{designer.from_catalogue ? designer.catalogue : designer.own}; + if (designer.selected < 0 || designer.selected >= static_cast(list.size())) + { + ImGui::TextWrapped("%s", STR_C("Choose a template on the left. The catalogue ones can be copied to own templates and changed there. A template is a straight main track and the diverging track from the start of the switch as a chain of straights, arcs and transition curves; a double switch is two switches, the second one starting on the main track of the first.")); + ImGui::EndChild(); + ImGui::End(); + return; + } + bool const editable{false == designer.from_catalogue}; + auto &entry{designer.from_catalogue ? designer.catalogue[designer.selected] : designer.own[designer.selected]}; + bool changed{false}; + if (false == editable) + { + if (ImGui::Button(STR_C("Copy to own templates"))) + { + auto copy{entry}; + copy.own = true; + copy.note.clear(); + copy.name += STR(" (copy)"); + designer.own.push_back(copy); + designer.from_catalogue = false; + designer.selected = static_cast(designer.own.size()) - 1; + designer.dirty = true; + ImGui::EndChild(); + ImGui::End(); + return; + } + ImGui::SameLine(); + ImGui::TextDisabled("%s", STR_C("A catalogue template can't be changed")); + } + if (false == editable) + ImGui::BeginDisabled(); + char name[96]; + std::snprintf(name, sizeof(name), "%s", entry.name.c_str()); + ImGui::SetNextItemWidth(240.0f); + if (ImGui::InputText(STR_C("Name##designname"), name, sizeof(name))) + { + entry.name = name; + changed = true; + } + char const *types[] = {STR_C("Rz ordinary"), STR_C("Rpd double, to both sides"), STR_C("Rpj double, to one side")}; + char const *codes[] = {"Rz", "Rpd", "Rpj"}; + int type{entry.type == "Rpd" ? 1 : entry.type == "Rpj" ? 2 : 0}; + ImGui::SetNextItemWidth(240.0f); + if (ImGui::Combo(STR_C("Type##designtype"), &type, types, 3)) + { + entry.type = codes[type]; + if (type > 0 && entry.stagger <= 0.0) + entry.stagger = 5.0; + changed = true; + } + item_tooltip("A double switch is laid as two switches: the first one with its diverging track and the main track up to the second one, which starts there with the other diverging track"); + char rail[32]; + std::snprintf(rail, sizeof(rail), "%s", entry.rail == "-" ? "" : entry.rail.c_str()); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputText(STR_C("Rail##designrail"), rail, sizeof(rail))) + { + entry.rail = rail[0] != '\0' ? std::string{rail} : std::string{"-"}; + changed = true; + } + item_tooltip("Rail of the switch kept in its record: 60E1, 49E1..."); + ImGui::PushItemWidth(90.0f); + changed |= ImGui::InputDouble(STR_C("Pre-blade stretch, m##designlead"), &entry.lead, 0.0, 0.0, "%.3f"); + item_tooltip("From the start of the switch (the joint before the blades) to the tip of the blade; the diverging track runs along the main one there"); + ImGui::SameLine(); + changed |= ImGui::InputDouble(STR_C("Blade angle, deg##designentry"), &entry.entry, 0.0, 0.0, "%.4f"); + item_tooltip("The angle at which the blade meets the stock rail at its tip: the diverging track starts there at this angle instead of tangentially. Curved blades a fraction of a degree, straight blades 1 to 2 degrees; 0: tangential"); + ImGui::PopItemWidth(); + if (editable) + { + ImGui::PushItemWidth(70.0f); + ImGui::TextDisabled("%s", STR_C("Straight blade:")); + ImGui::SameLine(); + ImGui::InputDouble(STR_C("l, m##bladelength"), &designer.blade_length, 0.0, 0.0, "%.2f"); + ImGui::SameLine(); + ImGui::InputDouble(STR_C("z, m##bladegap"), &designer.blade_gap, 0.0, 0.0, "%.3f"); + ImGui::PopItemWidth(); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Angle from l and z")) && designer.blade_length > 0.1 && designer.blade_gap > 0.0 && designer.blade_gap < designer.blade_length) + { + entry.entry = glm::degrees(std::asin(designer.blade_gap / designer.blade_length)); + changed = true; + } + item_tooltip("sin angle = z / l: l the length of a straight blade, z the gap between the running edges of the stock rail and the blade at its heel"); + } + if (false == editable) + ImGui::EndDisabled(); + + ImGui::Separator(); + ImGui::TextDisabled("%s", STR_C("Diverging track from the start of the switch")); + if (editable) + { + ImGui::PushItemWidth(80.0f); + ImGui::InputDouble(STR_C("R##quickradius"), &designer.quick_radius, 0.0, 0.0, "%.0f"); + ImGui::SameLine(); + ImGui::InputDouble(STR_C("1:n##quickratio"), &designer.quick_ratio, 0.0, 0.0, "%.2f"); + ImGui::SameLine(); + ImGui::InputDouble(STR_C("straight after, m##quickstraight"), &designer.quick_straight, 0.0, 0.0, "%.3f"); + ImGui::PopItemWidth(); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Fill in")) && designer.quick_radius >= 10.0 && designer.quick_ratio > 1.0) + { + auto const angle{std::atan(1.0 / designer.quick_ratio)}; + entry.parts = {{designer.quick_radius * angle, designer.quick_radius, designer.quick_radius}}; + if (designer.quick_straight > 0.01) + entry.parts.push_back({designer.quick_straight, 0.0, 0.0}); + changed = true; + } + item_tooltip("Replaces the parts with an arc of R up to the angle 1:n and the straight after it, the shape of an ordinary switch"); + } + changed |= edit_parts(entry.parts, "##parts", editable); + bool const twofold{switch_design::double_switch(entry.type)}; + if (false == editable) + ImGui::BeginDisabled(); + if (twofold) + { + ImGui::Separator(); + ImGui::SetNextItemWidth(90.0f); + changed |= ImGui::InputDouble(STR_C("Second switch starts at, m##stagger"), &entry.stagger, 0.0, 0.0, "%.3f"); + item_tooltip("Distance from the start of the first switch to the start of the second one, along the main track"); + bool own{false == entry.parts2.empty()}; + if (ImGui::Checkbox(STR_C("The second switch has a diverging track of its own"), &own)) + { + entry.parts2 = own ? entry.parts : std::vector{}; + changed = true; + } + item_tooltip("Otherwise the second switch diverges the same way as the first, to the other side for Rpd, to the same side for Rpj"); + if (own) + { + ImGui::PushItemWidth(90.0f); + auto lead2{entry.lead2 >= 0.0 ? entry.lead2 : entry.lead}; + if (ImGui::InputDouble(STR_C("Pre-blade stretch, m##designlead2"), &lead2, 0.0, 0.0, "%.3f")) + { + entry.lead2 = std::max(0.0, lead2); + changed = true; + } + ImGui::SameLine(); + auto entry2{entry.entry2 >= 0.0 ? entry.entry2 : entry.entry}; + if (ImGui::InputDouble(STR_C("Blade angle, deg##designentry2"), &entry2, 0.0, 0.0, "%.4f")) + { + entry.entry2 = std::max(0.0, entry2); + changed = true; + } + ImGui::PopItemWidth(); + changed |= edit_parts(entry.parts2, "##parts2", editable); + } + else if (entry.lead2 >= 0.0 || entry.entry2 >= 0.0) + { + entry.lead2 = entry.entry2 = -1.0; + changed = true; + } + } + ImGui::Separator(); + ImGui::PushItemWidth(90.0f); + changed |= ImGui::InputDouble(STR_C("Main track, m##designmain"), &entry.main, 0.0, 0.0, "%.3f"); + item_tooltip("Length of the main track, 0: as far as the diverging tracks reach"); + ImGui::SameLine(); + changed |= ImGui::InputDouble(STR_C("a##designa"), &entry.a, 0.0, 0.0, "%.3f"); + item_tooltip("From the start to the point where the tangents of both tracks meet, as the catalogue gives it; 0: from the geometry"); + ImGui::SameLine(); + changed |= ImGui::InputDouble(STR_C("b##designb"), &entry.b, 0.0, 0.0, "%.3f"); + item_tooltip("From the point where the tangents meet to the end, as the catalogue gives it; 0: the main track less a"); + ImGui::PopItemWidth(); + if (false == editable) + ImGui::EndDisabled(); + + std::string error; + auto const shape{switch_design::build(entry, error)}; + ImGui::Separator(); + if (false == error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "%s", STR_C(error.c_str())); + else + { + ImGui::Text(STR_C("Angle 1:%.3f (%.4f deg), sharpest R %.1f m, a %.3f m, b %.3f m"), shape.ratio, glm::degrees(std::atan(1.0 / shape.ratio)), shape.radius, shape.a, shape.b); + ImGui::Text(STR_C("Length %.3f m, the diverging track ends %.3f m beside the main one, in %zu pieces"), shape.length, shape.offset, std::max(1, shape.paths[1].pieces.size())); + if (entry.lead > 0.0 || entry.entry > 0.0) + ImGui::Text(STR_C("Tip of the blade %.3f m from the start, the diverging track leaves there at %.4f deg"), shape.tip, entry.entry); + } + ImGui::Checkbox(STR_C("Stretch across"), &designer.stretched); + item_tooltip("Draws the offsets from the main track larger, so the shape of the diverging track can be seen"); + draw_shape(shape, 150.0f, designer.stretched); + + if (changed) + designer.dirty = true; + if (editable) + { + if (ImGui::Button(designer.dirty ? STR_C("Save *") : STR_C("Save"))) + { + std::string problem; + if (switch_design::save_own(designer.own, problem)) + { + designer.dirty = false; + designer.status = STR("Saved, the templates are on the list of the switch tool"); + switch_templates_refresh(); + } + else + designer.status = problem; + } + item_tooltip("Keeps the own templates in eu07_switch_templates.txt in the configuration directory of the simulator and puts them on the list of the switch tool"); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Delete"))) + { + designer.own.erase(designer.own.begin() + designer.selected); + designer.selected = -1; + designer.dirty = true; + } + item_tooltip("Removes the template; Save keeps the change"); + } + if (error.empty()) + { + if (editable) + ImGui::SameLine(); + if (ImGui::Button(STR_C("Place this switch"))) + { + if (designer.dirty && editable) + { + std::string problem; + if (switch_design::save_own(designer.own, problem)) + designer.dirty = false; + else + designer.status = problem; + } + switch_templates_refresh(entry.name + " "); + show_track_tab(track_tab::turnout); + arm_switch(); + } + item_tooltip("Saves the own templates and chooses this one in the switch tool: then click a straight and drag to the side the switch goes"); + } + if (false == designer.status.empty()) + ImGui::TextWrapped("%s", designer.status.c_str()); + ImGui::EndChild(); + ImGui::End(); +} diff --git a/application/editormodeswitchslide.cpp b/application/editormodeswitchslide.cpp index 15ea0945..5a13861c 100644 --- a/application/editormodeswitchslide.cpp +++ b/application/editormodeswitchslide.cpp @@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/. #include "imgui/imgui.h" #include +#include #include #include @@ -218,15 +219,129 @@ bool editor_mode::move_switch_paths(TTrack &Track, std::vector con return true; } +bool editor_mode::double_room(double_switch const &Set, double &Low, double &High, double &Length, std::string &Reason) +{ + double low{0.0}; + double high{0.0}; + double low2{0.0}; + double high2{0.0}; + auto const axis{editor_track::main_axis(*Set.first)}; + if (false == axis.has_value() || false == switch_room(*Set.first, low, high, Reason) || false == switch_room(*Set.second, low2, high2, Reason)) + return false; + auto const &main{Set.second->m_paths.front()}; + Low = low; + High = axis->along(main.points[segment_data::point::start]) + high2; + Length = axis->along(main.points[segment_data::point::end]); + return true; +} + +bool editor_mode::move_double_paths(double_switch const &Set, std::vector const &Paths, std::string &Status) +{ + auto const axis{editor_track::main_axis(*Set.first)}; + if (Paths.size() < 5 || false == axis.has_value()) + return false; + double low{0.0}; + double high{0.0}; + double length{0.0}; + std::string reason; + bool const online{double_room(Set, low, high, length, reason)}; + if (false == online && false == reason.empty()) + { + Status = reason; + return false; + } + struct outer_end + { + TTrack *track{nullptr}; + switch_end_move move; + }; + std::vector ends; + auto const outer = [&](TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, bool const Atstart) { + if (false == online || glm::distance(From, To) < 0.005 || axis->lateral(To) > m_straights.tolerance.offset) + return true; + auto const target{axis->along(To)}; + if (target < low - 0.01 || target > high + 0.01) + { + Status = STR_C("The switch would run into another switch or past the end of the straight"); + return false; + } + ends.push_back({&Track, {From, To, Atstart ? axis->along(From) - target : target - axis->along(From)}}); + return true; + }; + if (false == outer(*Set.first, Set.first->m_paths.front().points[segment_data::point::start], Paths[0].points[segment_data::point::start], true) || + false == outer(*Set.second, Set.second->m_paths.front().points[segment_data::point::end], Paths[2].points[segment_data::point::end], false)) + return false; + std::stable_sort(ends.begin(), ends.end(), [](outer_end const &A, outer_end const &B) { return A.move.shift > B.move.shift; }); + std::array const switches{Set.first, Set.second}; + std::array, 2> joints; + std::array, 2> positions; + std::array, 2> handled; + for (std::size_t k = 0; k < 2; ++k) + { + joints[k] = editor_track::joints(*switches[k]); + for (auto const &joint : joints[k]) + { + positions[k].push_back(editor_track::point_position(*switches[k], joint.own)); + handled[k].push_back(joint.other == Set.link || joint.other == Set.first || joint.other == Set.second); + } + } + auto const joint{Set.first->m_paths.front().points[segment_data::point::end]}; + auto const &link{Set.link->m_paths.front()}; + bool const reversed{glm::distance(link.points[segment_data::point::start], joint) > glm::distance(link.points[segment_data::point::end], joint)}; + editor_track::edit edit; + for (auto *track : {Set.first, Set.link, Set.second}) + edit.capture(*track); + for (auto const &end : ends) + { + for (std::size_t k = 0; k < 2; ++k) + for (std::size_t i = 0; i < joints[k].size(); ++i) + if (glm::distance(positions[k][i], end.move.from) < geometry::same_point) + handled[k][i] = true; + auto const line{editor_track::find_straight(*end.track, m_straights.tolerance)}; + if (false == switch_vacate(*end.track, end.move.from, end.move.to, end.move.shift, line, edit, Status)) + return false; + } + Set.first->m_paths.assign(Paths.begin(), Paths.begin() + 2); + Set.second->m_paths.assign(Paths.begin() + 2, Paths.begin() + 4); + Set.link->m_paths = {reversed ? geometry::reversed(Paths[4]) : Paths[4]}; + std::string loose; + for (std::size_t k = 0; k < 2; ++k) + { + auto const left{switch_rejoin(*switches[k], joints[k], handled[k], edit)}; + if (false == left.empty()) + loose += (loose.empty() ? "" : ", ") + left; + } + editor_track::commit(edit.changed); + push_track_snapshot(std::move(edit.states), std::move(edit.created), std::move(edit.removed)); + if (false == loose.empty()) + Status = STR_C("Too far from the new ends of the switch, left unjoined: ") + loose; + return true; +} + bool editor_mode::slide_switch(TTrack &Track, double const Distance) { - if (Track.m_paths.size() < 2 || std::abs(Distance) < 0.005) + if (std::abs(Distance) < 0.005) return false; - auto const paths{editor_track::slid(Track, Distance)}; + auto const set{double_of(Track)}; + auto &first{set.has_value() ? *set->first : Track}; + if (first.m_paths.size() < 2) + return false; + auto paths{editor_track::slid(first, Distance)}; if (paths.empty()) return false; std::string status; - if (false == move_switch_paths(Track, paths, status)) + bool moved{false}; + if (set.has_value()) + { + auto const shift{paths.front().points[segment_data::point::start] - first.m_paths.front().points[segment_data::point::start]}; + paths = double_paths(*set); + for (auto &path : paths) + path.translate(shift); + moved = move_double_paths(*set, paths, status); + } + else + moved = move_switch_paths(Track, paths, status); + if (false == moved) { m_switch.status = status; ui()->set_status(status, true); @@ -235,13 +350,18 @@ bool editor_mode::slide_switch(TTrack &Track, double const Distance) switch_store::follow(); records_to_history(); straight_refresh(); - m_switch.status = status.empty() ? format(STR_C("Switch %s moved %.2f m along the straight"), Track.name().c_str(), Distance) : status; + m_switch.status = status.empty() ? format(STR_C("Switch %s moved %.2f m along the straight"), first.name().c_str(), Distance) : status; ui()->set_status(m_switch.status, false == status.empty()); return true; } bool editor_mode::switch_slides(TTrack const &Track) const { + if (Track.eType == tt_Normal) + { + auto const set{double_of(Track)}; + return set.has_value() && switch_slides(*set->first) && switch_slides(*set->second); + } return Track.eType == tt_Switch && Track.m_paths.size() == 2 && false == Track.m_editorremoved && false == has_controls(Track.m_paths.front()); } @@ -250,26 +370,39 @@ void editor_mode::switch_slide_start(TTrack &Track) auto &slide{m_switch.slide}; slide = {}; std::string reason; - if (false == switch_room(Track, slide.low, slide.high, reason)) + auto const set{double_of(Track)}; + auto &first{set.has_value() ? *set->first : Track}; + auto const axis{editor_track::main_axis(first)}; + if (false == axis.has_value()) + return; + slide.length = axis->length; + if (false == (set.has_value() ? double_room(*set, slide.low, slide.high, slide.length, reason) : switch_room(first, slide.low, slide.high, reason))) { if (false == reason.empty()) ui()->set_status(reason, true); return; } - auto const axis{editor_track::main_axis(Track)}; auto const &mouse{ImGui::GetIO().MousePos}; slide.active = true; - slide.track = &Track; + slide.track = &first; slide.pressed = {mouse.x, mouse.y}; slide.grip = axis->along(cursor_level(axis->origin.y)); - auto const line{editor_track::find_straight(Track, m_straights.tolerance)}; - for (std::size_t i = 0; i < line.tracks.size(); ++i) + std::vector members{&first}; + if (set.has_value()) + members = {set->first, set->link, set->second}; + for (auto *member : members) { - if (line.tracks[i] == &Track) + if (member->eType != tt_Switch) continue; - auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; - for (auto const index : {segment_data::point::start, segment_data::point::end}) - slide.joints.push_back(axis->along(path.points[index])); + auto const line{editor_track::find_straight(*member, m_straights.tolerance)}; + for (std::size_t i = 0; i < line.tracks.size(); ++i) + { + if (std::find(members.begin(), members.end(), line.tracks[i]) != members.end()) + continue; + auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; + for (auto const index : {segment_data::point::start, segment_data::point::end}) + slide.joints.push_back(axis->along(path.points[index])); + } } } @@ -284,7 +417,7 @@ void editor_mode::switch_slide_update() slide = {}; return; } - auto const length{axis->length}; + auto const length{slide.length}; auto const distance{std::clamp(axis->along(cursor_level(axis->origin.y)) - slide.grip, slide.low, slide.high - length)}; double best{1.0}; double snapped{distance}; @@ -320,9 +453,16 @@ void editor_mode::draw_switch_slide() const auto const &mouse{ImGui::GetIO().MousePos}; if (glm::length(glm::vec2{mouse.x, mouse.y} - slide.pressed) <= kDragPixels) return; - auto const paths{editor_track::slid(*slide.track, slide.distance)}; + auto paths{editor_track::slid(*slide.track, slide.distance)}; if (paths.empty()) return; + if (auto const set{double_of(*slide.track)}) + { + auto const shift{paths.front().points[segment_data::point::start] - slide.track->m_paths.front().points[segment_data::point::start]}; + paths = double_paths(*set); + for (auto &path : paths) + path.translate(shift); + } screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; for (auto const &path : paths) diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index fbbc6058..1ec9166e 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -57,6 +57,13 @@ using geometry::level; using geometry::reversed; using geometry::straight_piece; +std::string template_label(editor_track::switch_template const &Template) +{ + if (Template.source != nullptr) + return STR("Scenery: ") + Template.label + " (" + std::to_string(Template.count) + "x)"; + return (Template.own ? STR("Own: ") : std::string{"PLK: "}) + Template.label; +} + double constexpr kParallelFindAngle{15.0}; // deg, neighbours counted as beside the straight double constexpr kParallelSnapAngle{5.0}; // deg, close enough to snap parallel double constexpr kParallelSnapWindow{0.45}; // m from 4.00 / 4.50 / 4.75 m @@ -910,6 +917,9 @@ void editor_mode::render_turnout_ui() auto *track{selected_track()}; if (track == nullptr || track->eType != tt_Switch || track->m_paths.size() < 2) return; + auto const set{double_of(*track)}; + if (set.has_value()) + track = set->first; auto const &main{track->m_paths[0]}; auto const &diverging{track->m_paths[1]}; auto const origin{main.points[segment_data::point::start]}; @@ -928,43 +938,59 @@ void editor_mode::render_turnout_ui() auto const offset{glm::dot(end, normal)}; glm::dvec2 const tangent{glm::normalize(glm::dvec2{-control.x, -control.z})}; auto const angle{std::atan2(std::abs(tangent.x * axis.y - tangent.y * axis.x), glm::dot(tangent, axis))}; - ImGui::Text(STR_C("Length %.3f m, diverging %s, end offset %.3f m"), length, offset > 0.0 ? STR_C("right") : STR_C("left"), std::abs(offset)); + if (set.has_value()) + { + auto const &second{set->second->m_paths.front()}; + ImGui::Text(STR_C("Double switch %s + %s: length %.3f m, the second switch starts %.3f m on"), set->first->name().c_str(), set->second->name().c_str(), glm::dot(plan_of(second.points[segment_data::point::end] - origin), axis), glm::dot(plan_of(second.points[segment_data::point::start] - origin), axis)); + ImGui::TextDisabled(STR_C("First switch: diverging %s, end offset %.3f m"), offset > 0.0 ? STR_C("right") : STR_C("left"), std::abs(offset)); + } + else + ImGui::Text(STR_C("Length %.3f m, diverging %s, end offset %.3f m"), length, offset > 0.0 ? STR_C("right") : STR_C("left"), std::abs(offset)); render_track_name(*track); render_switch_record(*track); - if (auto const drive{find_switch_drive(*track)}) - { - auto const family{drive->family}; - auto const variant{drive->index}; - ImGui::TextDisabled(STR_C("Drive: events %s+ and %s- switch it, also in the map"), track->name().c_str(), track->name().c_str()); - ImGui::PushItemWidth(200.0f); - if (ImGui::BeginCombo(STR_C("Drive##standing"), m_switch.drives[family].name.c_str())) + auto const drive_ui = [&](TTrack &Switch) { + ImGui::PushID(&Switch); + if (set.has_value()) + ImGui::TextUnformatted(Switch.name().c_str()); + if (auto const drive{find_switch_drive(Switch)}) { - for (int i = 0; i < static_cast(m_switch.drives.size()); ++i) - if (ImGui::Selectable(m_switch.drives[i].name.c_str(), family == i) && i != family) - replace_switch_drive(*track, i, false); - ImGui::EndCombo(); + auto const family{drive->family}; + auto const variant{drive->index}; + ImGui::TextDisabled(STR_C("Drive: events %s+ and %s- switch it, also in the map"), Switch.name().c_str(), Switch.name().c_str()); + ImGui::PushItemWidth(200.0f); + if (ImGui::BeginCombo(STR_C("Drive##standing"), m_switch.drives[family].name.c_str())) + { + for (int i = 0; i < static_cast(m_switch.drives.size()); ++i) + if (ImGui::Selectable(m_switch.drives[i].name.c_str(), family == i) && i != family) + replace_switch_drive(Switch, i, false); + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + item_tooltip("Puts another drive in place of the drive of this switch, on the same side"); + int side{((variant >> 1) == (variant & 1)) ? 0 : 1}; + ImGui::SameLine(); + auto const before{side}; + ImGui::RadioButton(STR_C("diverging side##standing"), &side, 0); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("other side##standing"), &side, 1); + if (side != before) + flip_switch_drive(Switch); + item_tooltip("Puts the drive of the switch on the other side of the track: the same drive, its variant for the other side"); } - ImGui::PopItemWidth(); - item_tooltip("Puts another drive in place of the drive of this switch, on the same side"); - int side{((variant >> 1) == (variant & 1)) ? 0 : 1}; - ImGui::SameLine(); - auto const before{side}; - ImGui::RadioButton(STR_C("diverging side##standing"), &side, 0); - ImGui::SameLine(); - ImGui::RadioButton(STR_C("other side##standing"), &side, 1); - if (side != before) - flip_switch_drive(*track); - item_tooltip("Puts the drive of the switch on the other side of the track: the same drive, its variant for the other side"); - } - else if ((simulation::Events.FindEvent(track->name() + "+") != nullptr && simulation::Events.FindEvent(track->name() + "-") != nullptr) || (m_track_name.track == track && m_track_name.uses.includes > 0)) - ImGui::TextDisabled(STR_C("Drive: events %s+ and %s- switch it, also in the map"), track->name().c_str(), track->name().c_str()); - else - { - ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", STR_C("No drive: the switch can't be switched during the game")); - render_switch_drive_choice(); - if (m_switch.drive >= 0 && ImGui::Button(STR_C("Put the drive by the switch"))) - place_switch_drive(*track); - } + else if ((simulation::Events.FindEvent(Switch.name() + "+") != nullptr && simulation::Events.FindEvent(Switch.name() + "-") != nullptr) || (m_track_name.track == &Switch && m_track_name.uses.includes > 0)) + ImGui::TextDisabled(STR_C("Drive: events %s+ and %s- switch it, also in the map"), Switch.name().c_str(), Switch.name().c_str()); + else + { + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", STR_C("No drive: the switch can't be switched during the game")); + render_switch_drive_choice(); + if (m_switch.drive >= 0 && ImGui::Button(STR_C("Put the drive by the switch"))) + place_switch_drive(Switch); + } + ImGui::PopID(); + }; + drive_ui(*track); + if (set.has_value()) + drive_ui(*set->second); if (false == m_switch.status.empty()) ImGui::TextWrapped("%s", m_switch.status.c_str()); if (angle > 1e-6) @@ -979,9 +1005,10 @@ void editor_mode::render_turnout_ui() if (tool.templates.empty()) return; m_turnout_template = std::clamp(m_turnout_template, 0, static_cast(tool.templates.size()) - 1); - if (tool.templates[m_turnout_template].double_slip) + auto const fits = [&](editor_track::switch_template const &Template) { return false == Template.double_slip && Template.twofold() == set.has_value(); }; + if (false == fits(tool.templates[m_turnout_template])) { - auto const ordinary{std::find_if(tool.templates.begin(), tool.templates.end(), [](auto const &Template) { return false == Template.double_slip; })}; + auto const ordinary{std::find_if(tool.templates.begin(), tool.templates.end(), fits)}; if (ordinary == tool.templates.end()) return; m_turnout_template = static_cast(ordinary - tool.templates.begin()); @@ -992,18 +1019,43 @@ void editor_mode::render_turnout_ui() { for (int i = 0; i < static_cast(tool.templates.size()); ++i) { - if (tool.templates[i].double_slip) + if (false == fits(tool.templates[i])) continue; - auto const label{(tool.templates[i].source != nullptr ? "Scenery: " : "PLK: ") + tool.templates[i].label}; + auto const label{template_label(tool.templates[i])}; if (ImGui::Selectable(label.c_str(), i == m_turnout_template)) m_turnout_template = i; } ImGui::EndCombo(); } ImGui::PopItemWidth(); + auto const finish_set = [&](std::string const &Status) { + switch_store::follow(); + records_to_history(); + straight_refresh(); + if (false == Status.empty()) + ui()->set_status(Status, true); + }; auto const replace = [&](editor_track::switch_template const &Template, int const Side) { auto const grade{length > 0.0 ? (main.points[segment_data::point::end].y - origin.y) / length : 0.0}; auto paths{editor_track::place_switch(Template, origin, axis, Side, grade)}; + if (set.has_value()) + { + if (paths.size() < 5) + return; + auto const before{double_paths(*set)}; + for (std::size_t i = 0; i < paths.size(); ++i) + if (paths[i].rolls[0] == 0.f && paths[i].rolls[1] == 0.f) + paths[i].rolls = before[i < 4 ? i & ~std::size_t{1} : 4].rolls; + std::string status; + if (false == move_double_paths(*set, paths, status)) + { + ui()->set_status(status, true); + return; + } + record_double(*set->first, *set->second, Template); + finish_set(status); + return; + } if (paths.size() < 2) return; for (int i = 0; i < 2; ++i) @@ -1026,7 +1078,30 @@ void editor_mode::render_turnout_ui() replace(tool.templates[m_turnout_template], side); item_tooltip("Keeps the start and the direction of the switch, takes the shape of the chosen template"); ImGui::SameLine(); - if (ImGui::Button(STR_C("Flip the side"))) + if (set.has_value() && ImGui::Button(STR_C("Flip the side"))) + { + auto const mirror = [&](glm::dvec3 const &Vector) { + auto const across{glm::dot(plan_of(Vector), normal)}; + return glm::dvec3{Vector.x - 2.0 * across * normal.x, Vector.y, Vector.z - 2.0 * across * normal.y}; + }; + auto paths{double_paths(*set)}; + for (auto &path : paths) + path.transform([&](glm::dvec3 const &Point) { return origin + mirror(Point - origin); }, mirror); + std::string status; + if (false == move_double_paths(*set, paths, status)) + ui()->set_status(status, true); + else + { + if (auto entry{switch_store::named(*set->first)}) + { + switch_store::place(*entry, *set->first); + switch_store::classify(*entry, *set->first); + switch_store::put(*entry, alignment_store::layer_of(*set->first)); + } + finish_set(status); + } + } + else if (false == set.has_value() && ImGui::Button(STR_C("Flip the side"))) { editor_track::switch_template own; own.source = track; @@ -1046,16 +1121,16 @@ void editor_mode::render_turnout_ui() replace(own, -side); } item_tooltip("Mirrors the switch: the diverging track goes to the other side"); - if (double low, high; switch_slides(*track) && switch_room(*track, low, high, m_switch.status)) + if (double low, high, reach{length}; switch_slides(*track) && (set.has_value() ? switch_slides(*set->second) && double_room(*set, low, high, reach, m_switch.status) : switch_room(*track, low, high, m_switch.status))) { ImGui::PushItemWidth(100.0f); ImGui::InputDouble("##switchslide", &tool.slide_typed, 0.0, 0.0, "%.2f m"); ImGui::PopItemWidth(); ImGui::SameLine(); if (ImGui::Button(STR_C("Slide along the straight"))) - slide_switch(*track, std::clamp(tool.slide_typed, low, high - length)); + slide_switch(*track, std::clamp(tool.slide_typed, low, high - reach)); item_tooltip("Moves the switch along its straight by the distance typed, + towards the frog; the straight paths on both sides get shorter or longer. Dragging the switch with the mouse does the same"); - ImGui::TextDisabled(STR_C("Room on the straight: %.2f m back, %.2f m ahead"), -low, high - length); + ImGui::TextDisabled(STR_C("Room on the straight: %.2f m back, %.2f m ahead"), -low, high - reach); } ImGui::Separator(); render_path_parameters(*track); @@ -3698,6 +3773,39 @@ void editor_mode::delete_selected_track() ui()->set_status(STR_C("The track can't be deleted while vehicles stand on it"), true); return; } + if (auto const set{double_of(*track)}) + { + for (auto *part : {set->first, set->link, set->second}) + if (false == part->Dynamics.empty()) + { + ui()->set_status(STR_C("The track can't be deleted while vehicles stand on it"), true); + return; + } + if (auto const known{switch_store::named(*set->first)}) + switch_store::forget(known->name); + auto main{set->first->m_paths.front()}; + main.points[segment_data::point::end] = set->second->m_paths.front().points[segment_data::point::end]; + auto const velocity{speed_check::path_velocity(*set->first, 0)}; + for (auto *part : {set->first, set->link, set->second}) + editor_track::retire(*part); + std::vector created; + if (false == Global.shiftState) + { + auto *straight{editor_track::create_path(*set->first, main)}; + editor_track::velocity(*straight, velocity); + editor_track::commit({straight}); + created.push_back(straight); + ui()->set_status(STR_C("The double switch is replaced by its main track, Shift+Del removes all of it"), false); + } + push_track_snapshot({}, created, {set->first, set->link, set->second}); + if (std::any_of(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), [&](TTrack const *Each) { return Each == set->first || Each == set->link || Each == set->second; })) + m_route = {}; + m_node = nullptr; + ui()->set_node(nullptr); + m_track_point = {}; + straight_refresh(); + return; + } // retired first, so the infrastructure it carried and moves elsewhere is in the same undo step if (auto const known{switch_store::of(*track)}) switch_store::forget(known->name); @@ -5363,7 +5471,25 @@ void editor_mode::switch_placed(std::vector const &Created) m_node = *found; ui()->set_node(m_node); if (slip) + { + auto const known{switch_store::named(**found)}; + if (false == known.has_value() || known->kind != "double") + return; + std::vector driven; + for (auto *track : Created) + if (track != nullptr && track->eType == tt_Switch && place_switch_drive(*track)) + { + join_last_steps(); + driven.push_back(track->name()); + } + if (driven.size() == 2) + { + m_switch.status = format(STR_C("Double switch %s + %s with drives: events %s+, %s- and %s+, %s- switch them, also in the map"), driven[0].c_str(), driven[1].c_str(), driven[0].c_str(), driven[0].c_str(), driven[1].c_str(), driven[1].c_str()); + ui()->set_status(m_switch.status, false); + } + advance_switch_name(); return; + } if (place_switch_drive(**found)) join_last_steps(); advance_switch_name(); @@ -5514,30 +5640,59 @@ bool editor_mode::place_switch_on_straight(editor_track::straight const &Line, e auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; }; glm::dvec2 const direction{Line.direction * static_cast(Direction)}; auto const paths{editor_track::place_switch(shape, point(origin), direction, Side, Line.grade * Direction)}; - auto *track{editor_track::create_switch(shape, paths, Style != nullptr ? *Style : *first, Name)}; - if (track != nullptr) - { - editor_track::commit({track}); - record_switch(*track, shape); - Created.push_back(track); - m_switch.status = STR_C("Switch ") + track->name() + STR_C(" inserted"); - } - else - m_switch.status = STR_C("The switch couldn't be created from this template"); + auto const made{create_switch_set(shape, paths, Style != nullptr ? *Style : *first, Name)}; + Created.insert(Created.end(), made.begin(), made.end()); return true; } +std::vector editor_mode::create_switch_set(editor_track::switch_template const &Shape, std::vector const &Paths, TTrack const &Style, std::string const &Name) +{ + if (Shape.twofold() && Paths.size() >= 5) + { + auto firstshape{Shape}; + firstshape.length = glm::distance(Paths[0].points[segment_data::point::start], Paths[0].points[segment_data::point::end]); + auto secondshape{Shape}; + secondshape.length = Shape.length - Shape.stagger; + auto *one{editor_track::create_switch(firstshape, Paths, Style, Name)}; + auto *two{one != nullptr ? editor_track::create_switch(secondshape, {Paths[2], Paths[3]}, Style, Name + "b") : nullptr}; + auto *link{two != nullptr ? editor_track::create_path(Style, Paths[4]) : nullptr}; + if (link == nullptr) + { + m_switch.status = STR_C("The switch couldn't be created from this template"); + return {}; + } + editor_track::commit({one, two, link}); + record_double(*one, *two, Shape); + m_switch.status = format(STR_C("Double switch %s with its second switch %s inserted"), one->name().c_str(), two->name().c_str()); + return {one, two, link}; + } + auto *track{editor_track::create_switch(Shape, Paths, Style, Name)}; + if (track == nullptr) + { + m_switch.status = STR_C("The switch couldn't be created from this template"); + return {}; + } + editor_track::commit({track}); + record_switch(*track, Shape); + m_switch.status = STR_C("Switch ") + track->name() + STR_C(" inserted"); + return {track}; +} + void editor_mode::draw_build_overlay() const { screen_projection const projection; ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); auto const drawpath = [&](segment_data const &Path, ImU32 const Color) { - auto previous{bezier{Path}.point(0.0)}; - for (int i = 1; i <= 24; ++i) + for (auto const &piece : Path.pieces.empty() ? std::vector{Path} : Path.pieces) { - auto const next{bezier{Path}.point(i / 24.0)}; - projection.line(drawlist, previous, next, Color, 3.0f); - previous = next; + bezier const curve{piece}; + auto previous{curve.point(0.0)}; + for (int i = 1; i <= 24; ++i) + { + auto const next{curve.point(i / 24.0)}; + projection.line(drawlist, previous, next, Color, 3.0f); + previous = next; + } } }; if (false == GfxRenderer->Mouse_Hit() && false == ImGui::GetIO().WantCaptureMouse) @@ -5714,7 +5869,7 @@ void editor_mode::render_switch_ui() auto const &entry{tool.templates[i]}; if (entry.double_slip != Slips) continue; - auto label{entry.source != nullptr ? STR("Scenery: ") + entry.label + " (" + std::to_string(entry.count) + "x)" : "PLK: " + entry.label}; + auto label{template_label(entry)}; label += "##switch" + std::to_string(i); if (ImGui::Selectable(label.c_str(), tool.armed == i)) { @@ -5755,6 +5910,10 @@ void editor_mode::render_switch_ui() arm_switch(); } item_tooltip("Looks for the switches of the scenery, to use their shapes as the templates"); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Template editor..."))) + m_switch_designer.open = true; + item_tooltip("Own switch templates: diverging tracks of straights, arcs and transition curves, double switches"); } std::vector editor_mode::detour_outline() const @@ -6687,13 +6846,11 @@ std::vector editor_mode::curved_switch_paths(editor_track::switch_ path.points[segment_data::point::control2] = {-Enddirection.x * handle, -(End.y - Start.y) / 3.0, -Enddirection.y * handle}; return path; }; - std::vector result; - for (auto const &local : Shape.local) - { - auto const &start{local.points[segment_data::point::start]}; - auto const &end{local.points[segment_data::point::end]}; - auto const &control1{local.points[segment_data::point::control1]}; - auto const &control2{local.points[segment_data::point::control2]}; + auto const bend = [&](segment_data const &Local) { + auto const &start{Local.points[segment_data::point::start]}; + auto const &end{Local.points[segment_data::point::end]}; + auto const &control1{Local.points[segment_data::point::control1]}; + auto const &control2{Local.points[segment_data::point::control2]}; glm::dvec2 const chord{end.x - start.x, end.z - start.z}; auto const localstart{glm::normalize(control1 != glm::dvec3{} ? plan_of(control1) : chord)}; auto const localend{glm::normalize(control2 != glm::dvec3{} ? -plan_of(control2) : chord)}; @@ -6719,19 +6876,35 @@ std::vector editor_mode::curved_switch_paths(editor_track::switch_ auto const rollstart{sample_at(Frame, Station + Direction * start.z).roll * Direction}; auto const rollend{sample_at(Frame, Station + Direction * end.z).roll * Direction}; path.rolls = {static_cast(rollstart), static_cast(rollend)}; - path.radius = local.radius; + path.radius = Local.radius; { bezier const curve{path}; auto const begin{plan_of(curve.first(0.0))}; auto const finish{plan_of(curve.first(1.0))}; auto const turn{glm::length(begin) > 1e-9 && glm::length(finish) > 1e-9 ? geometry::signed_angle(begin, finish) : 0.0}; - if (local.radius != 0.f && std::abs(turn) > 1e-6) - path.radius = static_cast(std::copysign(curve.plan_length() / std::abs(turn), static_cast(local.radius))); + if (Local.radius != 0.f && std::abs(turn) > 1e-6) + path.radius = static_cast(std::copysign(curve.plan_length() / std::abs(turn), static_cast(Local.radius))); } + return path; + }; + std::vector locals{Shape.local.begin(), Shape.local.end()}; + if (Shape.twofold()) + { + locals.insert(locals.end(), Shape.second.begin(), Shape.second.end()); + locals.push_back(Shape.link); + } + std::vector result; + for (std::size_t i = 0; i < locals.size(); ++i) + { + auto const &local{locals[i]}; + auto path{bend(local)}; + for (auto const &piece : local.pieces) + path.pieces.push_back(bend(piece)); + // the main tracks follow the curve they are laid into + if (i % 2 == 0) + path.radius = sample_at(Frame, Station + Direction * 0.5 * (local.points[segment_data::point::start].z + local.points[segment_data::point::end].z)).radius; result.push_back(path); } - if (false == result.empty()) - result.front().radius = sample_at(Frame, Station).radius; return result; } @@ -6789,16 +6962,10 @@ void editor_mode::insert_curved_switch(int const Direction, int const Side) } } auto const paths{curved_switch_paths(shape, tool.frame, station, Direction, Side)}; - auto *track{editor_track::create_switch(shape, paths, *tool.frame_tracks.front(), switch_name_for_new())}; - if (track != nullptr) - { - editor_track::commit({track}); - record_switch(*track, shape); - created.push_back(track); - tool.status = STR_C("Switch ") + track->name() + STR_C(" inserted into the curve"); - } - else - tool.status = STR_C("The switch couldn't be created from this template"); + auto const made{create_switch_set(shape, paths, *tool.frame_tracks.front(), switch_name_for_new())}; + created.insert(created.end(), made.begin(), made.end()); + if (made.size() == 1) + tool.status = STR_C("Switch ") + made.front()->name() + STR_C(" inserted into the curve"); auto const placed{created}; push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); diff --git a/application/editormodetrackgizmo.cpp b/application/editormodetrackgizmo.cpp index 9fb40a3b..18d01f8b 100644 --- a/application/editormodetrackgizmo.cpp +++ b/application/editormodetrackgizmo.cpp @@ -405,6 +405,27 @@ void editor_mode::track_gizmo_take_switches(TTrack &Track) } } +void editor_mode::track_gizmo_take_double(TTrack &Track) +{ + auto const set{double_of(Track)}; + if (false == set.has_value() || Track.eType != tt_Switch) + return; + auto *other{&Track == set->first ? set->second : set->first}; + if (false == editable(*set->link) || false == editable(*other) || track_gizmo_clustered(set->link)) + return; + editor_track::point_ref const own{0, &Track == set->first ? editor_track::point_kind::end : editor_track::point_kind::start}; + auto const position{editor_track::point_position(Track, own)}; + for (auto const &connection : editor_track::connected_points(Track, position)) + { + if (connection.first != set->link) + continue; + std::erase_if(m_track_joints, [&](editor_track::joint const &Joint) { return Joint.other == set->link; }); + auto const movable = [&](TTrack const *Each) { return Each == set->link || Each == other; }; + m_track_gizmo_drag.switches.push_back(track_gizmo_link(Track, own, connection, movable)); + return; + } +} + void editor_mode::track_gizmo_measure_rotation(TTrack &Track, bool const Pointmode) { auto &drag{m_track_gizmo_drag}; @@ -482,6 +503,8 @@ void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_o track_gizmo_take_partners(Track); if (false == whole && Track.eType == tt_Switch && m_track_drag_connected) track_gizmo_take_switch_joints(Track, controlmode); + if (whole) + track_gizmo_take_double(Track); if (m_track_drag_connected) track_gizmo_take_switches(Track); std::vector> states; diff --git a/editor/editorSwitchDesign.cpp b/editor/editorSwitchDesign.cpp new file mode 100644 index 00000000..5ec0ab6c --- /dev/null +++ b/editor/editorSwitchDesign.cpp @@ -0,0 +1,519 @@ +/* +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/editorSwitchDesign.hpp" +#include "editor/editorFormat.hpp" + +#include "utilities/utilities.h" + +#include +#include +#include +#include +#include + +namespace switch_design +{ + +namespace +{ + +double constexpr kPi{3.14159265358979323846}; +double constexpr kMinRadius{10.0}; +double constexpr kTransitionStep{5.0}; +double constexpr kLink{1.0}; + +glm::dvec2 heading_of(double const Angle) +{ + return {std::sin(Angle), std::cos(Angle)}; +} + +glm::dvec3 spatial(glm::dvec2 const &Plan) +{ + return {Plan.x, 0.0, Plan.y}; +} + +double curvature_of(double const Radius) +{ + return Radius != 0.0 ? 1.0 / Radius : 0.0; +} + +segment_data piece(glm::dvec2 const &From, glm::dvec2 const &To, double const Heading0, double const Heading1, double const Handle0, double const Handle1, double const Radius) +{ + segment_data result; + result.points[segment_data::point::start] = spatial(From); + result.points[segment_data::point::end] = spatial(To); + if (Handle0 > 0.0 || Handle1 > 0.0) + { + result.points[segment_data::point::control1] = spatial(heading_of(Heading0) * Handle0); + result.points[segment_data::point::control2] = spatial(-heading_of(Heading1) * Handle1); + } + result.radius = static_cast(std::abs(Radius)); + return result; +} + +struct walker +{ + glm::dvec2 at{0.0}; + double heading{0.0}; + double sharpest{0.0}; + std::vector pieces; + + void note(double const Radius) + { + if (Radius != 0.0 && (sharpest == 0.0 || std::abs(Radius) < sharpest)) + sharpest = std::abs(Radius); + } + + void straight(double const Length) + { + auto const to{at + heading_of(heading) * Length}; + pieces.push_back(piece(at, to, heading, heading, 0.0, 0.0, 0.0)); + at = to; + } + + void arc(double const Length, double const Radius) + { + note(Radius); + auto const angle{Length / Radius}; + auto const count{std::max(1, static_cast(std::ceil(std::abs(angle) / (kPi * 0.5) - 1e-9)))}; + auto const step{angle / count}; + auto const handle{4.0 / 3.0 * std::abs(Radius * std::tan(step * 0.25))}; + for (int i = 0; i < count; ++i) + { + auto const chord{2.0 * std::abs(Radius * std::sin(step * 0.5))}; + auto const to{at + heading_of(heading + step * 0.5) * chord}; + pieces.push_back(piece(at, to, heading, heading + step, handle, handle, Radius)); + at = to; + heading += step; + } + } + + void transition(double const Length, double const Radius1, double const Radius2) + { + note(Radius1); + note(Radius2); + auto const k1{curvature_of(Radius1)}; + auto const k2{curvature_of(Radius2)}; + auto const start{heading}; + auto const heading_at = [&](double const S) { return start + k1 * S + (k2 - k1) * S * S / (2.0 * Length); }; + auto const count{std::max(2, static_cast(std::ceil(Length / kTransitionStep)))}; + auto const step{Length / count}; + for (int i = 0; i < count; ++i) + { + auto const s0{step * i}; + int const samples{32}; + glm::dvec2 offset{0.0}; + for (int j = 0; j <= samples; ++j) + { + auto const weight{j == 0 || j == samples ? 1.0 : (j % 2 == 1 ? 4.0 : 2.0)}; + offset += heading_of(heading_at(s0 + step * j / samples)) * weight; + } + offset *= step / (3.0 * samples); + auto const h0{heading_at(s0)}; + auto const h1{heading_at(s0 + step)}; + auto const middle{curvature_of(Radius1) + (k2 - k1) * (s0 + step * 0.5) / Length}; + auto const handle{glm::length(offset) / 3.0}; + pieces.push_back(piece(at, at + offset, h0, h1, handle, handle, middle != 0.0 ? 1.0 / middle : 0.0)); + at += offset; + } + heading = heading_at(Length); + } +}; + +void mirror(segment_data &Path) +{ + auto const flip = [](glm::dvec3 const &Point) { return glm::dvec3{-Point.x, Point.y, Point.z}; }; + Path.transform(flip, flip); +} + +void shift(segment_data &Path, double const Along) +{ + Path.transform([&](glm::dvec3 const &Point) { return Point + glm::dvec3{0.0, 0.0, Along}; }, [](glm::dvec3 const &Vector) { return Vector; }); +} + +segment_data straight_path(double const From, double const To) +{ + segment_data result; + result.points[segment_data::point::start] = {0.0, 0.0, From}; + result.points[segment_data::point::end] = {0.0, 0.0, To}; + return result; +} + +std::string checked(std::vector const &Parts) +{ + if (Parts.empty()) + return "The diverging track has no parts"; + for (auto const &part : Parts) + { + if (part.length < 0.05) + return "A part of the diverging track is shorter than 5 cm"; + for (auto const radius : {part.radius1, part.radius2}) + if (radius != 0.0 && std::abs(radius) < kMinRadius) + return "A radius of the diverging track is under 10 m"; + } + return {}; +} + +// the point where the tangent at the end of the diverging track meets the main track, as the distance along it +double tangent_point(glm::dvec2 const &End, double const Angle) +{ + return End.y - End.x / std::tan(Angle); +} + +std::string word(std::string const &Text) +{ + if (Text.empty()) + return "-"; + auto result{Text}; + std::replace(result.begin(), result.end(), ' ', '_'); + return result; +} + +std::string unword(std::string const &Text) +{ + if (Text == "-") + return {}; + auto result{Text}; + std::replace(result.begin(), result.end(), '_', ' '); + return result; +} + +std::filesystem::path own_path() +{ + auto path{user_config_path("eu07_switch_templates.txt")}; + return path.empty() ? std::filesystem::path{"eu07_switch_templates.txt"} : path; +} + +void write_parts(std::ostream &Output, std::vector const &Parts) +{ + Output << ' ' << Parts.size(); + for (auto const &part : Parts) + Output << ' ' << format("%.4f %.4f %.4f", part.length, part.radius1, part.radius2); +} + +bool read_parts(std::istream &Input, std::vector &Parts) +{ + std::size_t count{0}; + Input >> count; + if (Input.fail() || count > 64) + return false; + Parts.resize(count); + for (auto &part : Parts) + Input >> part.length >> part.radius1 >> part.radius2; + return false == Input.fail(); +} + +} // namespace + +bool double_switch(std::string const &Type) +{ + return Type == "Rpd" || Type == "Rpj"; +} + +path diverging(std::vector const &Parts, double const Lead, double const Entry) +{ + walker walk; + if (Lead > 0.0) + walk.straight(Lead); + auto const start{Lead > 0.0 ? 0.0 : glm::radians(Entry)}; + walk.heading += glm::radians(Entry); + for (auto const &part : Parts) + { + if (part.radius1 == 0.0 && part.radius2 == 0.0) + walk.straight(part.length); + else if (part.radius1 == part.radius2) + walk.arc(part.length, part.radius1); + else + walk.transition(part.length, part.radius1, part.radius2); + } + path result; + result.angle = walk.heading; + result.radius = walk.sharpest; + if (walk.pieces.size() == 1) + { + result.whole = walk.pieces.front(); + return result; + } + auto const end{walk.at}; + auto handle0{glm::length(end) / 3.0}; + auto handle1{handle0}; + if (std::abs(walk.heading) > 1e-6) + { + auto const along{tangent_point(end, walk.heading)}; + auto const back{end.x / std::sin(walk.heading)}; + if (along > 0.0 && back > 0.0) + { + handle0 = along * 2.0 / 3.0; + handle1 = back * 2.0 / 3.0; + } + } + result.whole = piece({0.0, 0.0}, end, start, walk.heading, handle0, handle1, walk.sharpest); + result.whole.pieces = std::move(walk.pieces); + return result; +} + +shape build(design const &Design, std::string &Error) +{ + shape result; + Error.clear(); + bool const twofold{double_switch(Design.type)}; + if (Design.type != "Rz" && false == twofold) + { + Error = "Unknown type of switch"; + return result; + } + Error = checked(Design.parts); + if (Error.empty() && twofold && false == Design.parts2.empty()) + Error = checked(Design.parts2); + if (Error.empty() && (Design.lead < 0.0 || Design.lead > 30.0)) + Error = "The pre-blade stretch has to be between 0 and 30 m"; + if (Error.empty() && (Design.entry < 0.0 || Design.entry > 5.0)) + Error = "The angle of the blade has to be between 0 and 5 degrees"; + if (false == Error.empty()) + return result; + auto const first{diverging(Design.parts, Design.lead, Design.entry)}; + if (first.angle <= 1e-4 || first.angle >= kPi / 4.0) + { + Error = "The diverging track has to leave to the side at an angle between 0 and 45 degrees"; + return result; + } + auto const end{glm::dvec2{first.whole.points[segment_data::point::end].x, first.whole.points[segment_data::point::end].z}}; + result.ratio = 1.0 / std::tan(first.angle); + result.radius = first.radius; + result.offset = end.x; + result.tip = Design.lead; + result.a = Design.a > 0.0 ? Design.a : tangent_point(end, first.angle); + if (false == twofold) + { + result.length = Design.main > 0.0 ? Design.main : end.y; + if (result.length < 1.0) + { + Error = "The main track is shorter than 1 m"; + return result; + } + result.b = Design.b > 0.0 ? Design.b : result.length - result.a; + result.paths = {straight_path(0.0, result.length), first.whole}; + return result; + } + auto const reach{end.y}; + if (Design.stagger < 1.0 || Design.stagger > reach - 1.0) + { + Error = "The second switch has to start at least 1 m along the main track and 1 m before the end of the diverging track"; + return result; + } + auto const lead2{Design.lead2 >= 0.0 ? Design.lead2 : Design.lead}; + auto const entry2{Design.entry2 >= 0.0 ? Design.entry2 : Design.entry}; + if (lead2 > 30.0 || entry2 > 5.0) + { + Error = "The pre-blade stretch of the second switch has to be at most 30 m and the angle of its blade at most 5 degrees"; + return result; + } + auto second{diverging(Design.parts2.empty() ? Design.parts : Design.parts2, lead2, entry2)}; + if (second.angle <= 1e-4 || second.angle >= kPi / 4.0) + { + Error = "The diverging track of the second switch has to leave to the side at an angle between 0 and 45 degrees"; + return result; + } + if (Design.type == "Rpd") + mirror(second.whole); + shift(second.whole, Design.stagger); + auto const secondend{second.whole.points[segment_data::point::end].z}; + result.length = Design.main > 0.0 ? Design.main : std::max(reach, secondend); + if (result.length < Design.stagger + 1.0) + { + Error = "The main track ends less than 1 m after the start of the second switch"; + return result; + } + result.b = Design.b > 0.0 ? Design.b : reach - result.a; + result.stagger = Design.stagger; + result.tip2 = Design.stagger + lead2; + result.radius = std::min(first.radius, second.radius > 0.0 ? second.radius : first.radius); + // the simulation joins switches only to plain tracks, so a short one lies between the two + auto const link{std::min(kLink, Design.stagger * 0.5)}; + result.paths = {straight_path(0.0, Design.stagger - link), first.whole, straight_path(Design.stagger, result.length), second.whole, straight_path(Design.stagger - link, Design.stagger)}; + return result; +} + +std::vector catalogue() +{ + struct ordinary + { + char const *name; + double radius; + double ratio; + double a; + double b; + }; + ordinary const ordinaries[] = { + {"Rz 1:9 R190", 190.0, 9.0, 10.523, 16.615}, + {"Rz 1:9 R300", 300.0, 9.0, 16.615, 16.615}, + {"Rz 1:12 R500", 500.0, 12.0, 20.797, 0.0}, + {"Rz 1:14 R760", 760.0, 14.0, 0.0, 0.0}, + {"Rz 1:18,5 R1200", 1200.0, 18.5, 32.409, 34.943}, + {"Rz 1:26,5 R2500", 2500.0, 26.5, 0.0, 0.0}, + }; + auto const make = [](ordinary const &Ordinary) { + design entry; + entry.name = Ordinary.name; + auto const angle{std::atan(1.0 / Ordinary.ratio)}; + entry.a = Ordinary.a > 0.0 ? Ordinary.a : Ordinary.radius * std::tan(angle * 0.5); + auto const estimated{Ordinary.b <= 0.0}; + entry.b = estimated ? 1.87 * Ordinary.ratio : Ordinary.b; + entry.parts.push_back({Ordinary.radius * angle, Ordinary.radius, Ordinary.radius}); + auto const rest{(entry.a + entry.b - Ordinary.radius * std::sin(angle)) / std::cos(angle)}; + if (rest > 0.01) + { + entry.parts.push_back({rest, 0.0, 0.0}); + entry.main = entry.a + entry.b; + } + else + entry.b = 0.0; + if (estimated) + entry.note = "(b est.)"; + return entry; + }; + std::vector result; + for (auto const &ordinary : ordinaries) + result.push_back(make(ordinary)); + { + design curved; + curved.name = "Rz 1:7,5 R190"; + auto const angle{std::atan(1.0 / 7.5)}; + curved.parts.push_back({190.0 * angle, 190.0, 190.0}); + curved.note = "(curved to the end)"; + result.push_back(curved); + } + struct twofold + { + char const *name; + char const *type; + std::size_t ordinary; + double stagger; + }; + twofold const twofolds[] = { + {"Rpd 1:9 R190", "Rpd", 0, 5.0}, + {"Rpj 1:9 R190", "Rpj", 0, 5.0}, + {"Rpd 1:9 R300", "Rpd", 1, 6.0}, + {"Rpj 1:9 R300", "Rpj", 1, 6.0}, + }; + for (auto const &entry : twofolds) + { + auto made{make(ordinaries[entry.ordinary])}; + made.name = entry.name; + made.type = entry.type; + made.stagger = entry.stagger; + made.main = 0.0; + made.b = 0.0; + made.note = format("(second switch %.1f m on, assumed)", entry.stagger); + result.push_back(made); + } + return result; +} + +std::vector own() +{ + std::vector result; + std::ifstream file{own_path()}; + if (false == file.is_open()) + return result; + std::string text; + while (std::getline(file, text)) + { + if (text.empty() || text.front() == '#') + continue; + std::istringstream line{text}; + design entry; + entry.own = true; + std::string keyword; + std::string name; + line >> keyword >> name; + if (keyword != "switch") + continue; + entry.name = unword(name); + bool good{true}; + while (good && line >> keyword) + { + if (keyword == "type") + line >> entry.type; + else if (keyword == "rail") + { + line >> entry.rail; + if (entry.rail.empty()) + entry.rail = "-"; + } + else if (keyword == "main") + line >> entry.main; + else if (keyword == "stagger") + line >> entry.stagger; + else if (keyword == "lead") + line >> entry.lead; + else if (keyword == "entry") + line >> entry.entry; + else if (keyword == "lead2") + line >> entry.lead2; + else if (keyword == "entry2") + line >> entry.entry2; + else if (keyword == "a") + line >> entry.a; + else if (keyword == "b") + line >> entry.b; + else if (keyword == "parts") + good = read_parts(line, entry.parts); + else if (keyword == "parts2") + good = read_parts(line, entry.parts2); + else + good = false; + good = good && false == line.fail(); + } + if (good && false == entry.name.empty()) + result.push_back(std::move(entry)); + } + return result; +} + +bool save_own(std::vector const &Designs, std::string &Error) +{ + auto const path{own_path()}; + std::error_code error; + if (path.has_parent_path()) + std::filesystem::create_directories(path.parent_path(), error); + std::ofstream file{path, std::ios::trunc}; + if (false == file.is_open()) + { + Error = "Can't write " + path.string(); + return false; + } + file << "# switch templates made in the track editor of MaSzyna\n"; + file << "# switch type rail main stagger a b lead entry [lead2 entry2 ] parts ( )... parts2 ...\n"; + file << "# R 0 is a straight, the same R at both ends an arc, different ones a transition curve; 0 for main, a, b: from the geometry\n"; + for (auto const &entry : Designs) + { + file << "switch " << word(entry.name) << " type " << entry.type << " rail " << word(entry.rail) << ' ' << format("main %.4f stagger %.4f a %.4f b %.4f lead %.4f entry %.4f", entry.main, entry.stagger, entry.a, entry.b, entry.lead, entry.entry); + if (entry.lead2 >= 0.0 || entry.entry2 >= 0.0) + file << format(" lead2 %.4f entry2 %.4f", entry.lead2, entry.entry2); + file << " parts"; + write_parts(file, entry.parts); + if (false == entry.parts2.empty()) + { + file << " parts2"; + write_parts(file, entry.parts2); + } + file << '\n'; + } + if (file.fail()) + { + Error = "Can't write " + path.string(); + return false; + } + return true; +} + +} // namespace switch_design diff --git a/editor/editorSwitchDesign.hpp b/editor/editorSwitchDesign.hpp new file mode 100644 index 00000000..c3671b5e --- /dev/null +++ b/editor/editorSwitchDesign.hpp @@ -0,0 +1,82 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include + +#include "world/Segment.h" + +// switches described by their geometry, of the catalogue and made by the user in the template editor: the main track is a +// straight, the diverging track a chain of parts of linearly changing curvature from the start of the switch; a double switch +// (Rpd to both sides, Rpj to one side) is two switches, the second starting on the main track of the first +namespace switch_design +{ + +struct part +{ + double length{0.0}; + double radius1{0.0}; // at the start of the part, 0 straight + double radius2{0.0}; // at its end; the same as radius1 for an arc, different for a transition curve +}; + +struct design +{ + std::string name; + std::string type{"Rz"}; // Rz, Rpd, Rpj + std::string rail{"-"}; + double main{0.0}; // length of the main track, 0: as far as the diverging tracks reach + std::vector parts; + double lead{0.0}; // the pre-blade stretch: the diverging track runs along the main one up to the tip of the blade + double entry{0.0}; // degrees, the angle of the blade to the stock rail at its tip, 0: the diverging track leaves tangentially + double stagger{0.0}; // of a double switch: where its second switch starts on the main track + std::vector parts2; // of the second switch, empty: the same as the first + double lead2{-1.0}; // of the second switch, negative: the same as the first + double entry2{-1.0}; + double a{0.0}; // as the catalogue gives them, 0: from the geometry + double b{0.0}; + std::string note; // shown after the name on the list + bool own{false}; // made in the template editor, kept in the file of the user +}; + +bool double_switch(std::string const &Type); + +// diverging track of the chain of parts from the start, with the first direction along +z and turning to +x +struct path +{ + segment_data whole; // the classic bezier, with the parts as its pieces when one bezier doesn't make them + double angle{0.0}; // at the end, radians + double radius{0.0}; // the sharpest +}; +path diverging(std::vector const &Parts, double Lead = 0.0, double Entry = 0.0); + +// the paths of the switch in its own space: the main and the diverging track of the first switch, then of the second one of a +// double switch and the plain track joining the two; empty with the reason in Error when the design can't make a switch +struct shape +{ + std::vector paths; + double length{0.0}; + double ratio{0.0}; + double radius{0.0}; + double a{0.0}; + double b{0.0}; + double offset{0.0}; // of the end of the diverging track from the main one + double stagger{0.0}; + double tip{0.0}; // of the blade, along the main track + double tip2{0.0}; +}; +shape build(design const &Design, std::string &Error); + +std::vector catalogue(); +// templates of the user, kept in eu07_switch_templates.txt in the configuration directory +std::vector own(); +bool save_own(std::vector const &Designs, std::string &Error); + +} // namespace switch_design diff --git a/editor/editorSwitchStore.cpp b/editor/editorSwitchStore.cpp index ffc09bc4..f004c24c 100644 --- a/editor/editorSwitchStore.cpp +++ b/editor/editorSwitchStore.cpp @@ -39,7 +39,7 @@ bool g_changed{false}; bool g_loaded{false}; // format of the lines, coordinates in the space of the file: -// +// // parts ... shape
start dir // km number source std::string word(std::string const &Text) @@ -156,7 +156,7 @@ void load() std::string km; std::size_t count{0}; line >> entry.kind; - if (entry.kind != "switch" && entry.kind != "slip" && entry.kind != "crossing") + if (entry.kind != "switch" && entry.kind != "slip" && entry.kind != "crossing" && entry.kind != "double") continue; line >> entry.name >> entry.type >> entry.rail >> entry.radius >> entry.ratio >> hand >> entry.a >> entry.b >> entry.main_radius >> keyword >> count; if (line.fail() || keyword != "parts" || count > 64) diff --git a/editor/editorSwitchStore.hpp b/editor/editorSwitchStore.hpp index 5ec9d60b..3917c3a9 100644 --- a/editor/editorSwitchStore.hpp +++ b/editor/editorSwitchStore.hpp @@ -26,9 +26,9 @@ namespace switch_store struct record { scene::layer_handle layer{null_handle}; - std::string kind{"switch"}; // switch, slip, crossing + std::string kind{"switch"}; // switch, slip, crossing, double std::string name; // the switch, or the base name of a slip or a crossing - std::string type{"Rz"}; // Rz, Rlj, Rld, Rkp, Rkpd, Skrz, - when unknown + std::string type{"Rz"}; // Rz, Rlj, Rld, Rkp, Rkpd, Skrz, Rpd, Rpj, - when unknown std::string rail{"-"}; double radius{0.0}; // of the diverging track of the straight switch the type is made of double ratio{0.0}; // n of 1:n @@ -36,7 +36,7 @@ struct record double a{0.0}; double b{0.0}; double main_radius{0.0}; // of the main track, signed: + to the right, - to the left, 0 straight - std::vector parts; // the switch, the switches of a slip, the paths of a crossing + std::vector parts; // the switch, the switches of a slip or of a double switch, the paths of a crossing glm::dvec3 start{0.0}; // of the first part glm::dvec2 direction{0.0, 1.0}; // ends of the main and the diverging track of the first part seen from its start: along, across, along, across. it stays the diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 56650e34..7428c9e9 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -25,6 +25,7 @@ http: #include "utilities/Globals.h" #include "utilities/utilities.h" #include "utilities/parser.h" +#include "utilities/Logs.h" #include #include @@ -2593,77 +2594,72 @@ std::vector editor_track::place_switch(switch_template const &Temp auto const planar{Direction * Local.z + normal * (Local.x * side)}; return glm::dvec3{planar.x, Local.y + Grade * Local.z, planar.y}; }; - for (auto const &local : Template.local) - { - segment_data path{local}; + auto const put = [&](segment_data const &Local) { + segment_data path{Local}; path.transform([&](glm::dvec3 const &Point) { return Origin + vector(Point); }, vector); if (Side < 0) { - path.rolls = {-local.rolls[0], -local.rolls[1]}; + path.rolls = {-Local.rolls[0], -Local.rolls[1]}; for (auto &piece : path.pieces) piece.rolls = {-piece.rolls[0], -piece.rolls[1]}; } result.push_back(path); + }; + for (auto const &local : Template.local) + put(local); + if (Template.twofold()) + { + for (auto const &local : Template.second) + put(local); + put(Template.link); } return result; } +std::optional editor_track::template_of(switch_design::design const &Design, std::string &Error) +{ + auto const shape{switch_design::build(Design, Error)}; + if (shape.paths.size() < 2) + return std::nullopt; + switch_template entry; + entry.type = Design.type; + entry.rail = Design.rail.empty() ? std::string{"-"} : Design.rail; + entry.length = shape.length; + entry.ratio = shape.ratio; + entry.radius = shape.radius; + entry.a = shape.a; + entry.b = shape.b; + entry.own = Design.own; + entry.local = {shape.paths[0], shape.paths[1]}; + entry.local[1].radius = static_cast(shape.radius); + if (shape.paths.size() >= 5) + { + entry.second = {shape.paths[2], shape.paths[3]}; + entry.link = shape.paths[4]; + entry.stagger = shape.stagger; + } + auto const decimals = [](double const Value) { return format("%.3f", Value); }; + entry.label = Design.name + " a " + decimals(entry.a) + " b " + decimals(entry.b) + (Design.note.empty() ? std::string{} : " " + Design.note); + return entry; +} + std::vector editor_track::standard_switch_templates() { - struct definition - { - char const *name; - double radius; - double ratio; - double a; - double b; - }; - definition const definitions[] = { - {"Rz 1:9 R190", 190.0, 9.0, 10.523, 16.615}, - {"Rz 1:9 R300", 300.0, 9.0, 16.615, 16.615}, - {"Rz 1:12 R500", 500.0, 12.0, 20.797, 0.0}, - {"Rz 1:14 R760", 760.0, 14.0, 0.0, 0.0}, - {"Rz 1:18,5 R1200", 1200.0, 18.5, 32.409, 34.943}, - {"Rz 1:26,5 R2500", 2500.0, 26.5, 0.0, 0.0}, - }; std::vector result; - for (auto const &definition : definitions) + auto designs{switch_design::catalogue()}; + auto const own{switch_design::own()}; + designs.insert(designs.end(), own.begin(), own.end()); + for (std::size_t i = 0; i < designs.size(); ++i) { - auto const angle{std::atan(1.0 / definition.ratio)}; - auto const a{definition.a > 0.0 ? definition.a : definition.radius * std::tan(angle * 0.5)}; - auto const estimated{definition.b <= 0.0}; - auto const b{estimated ? 1.87 * definition.ratio : definition.b}; - auto const offset{b * std::tan(angle)}; - switch_template entry; - entry.a = a; - entry.b = b; - entry.length = a + b; - entry.ratio = definition.ratio; - entry.radius = definition.radius; - auto &main{entry.local[0]}; - main.points[segment_data::point::end] = {0.0, 0.0, a + b}; - auto &diverging{entry.local[1]}; - diverging.points[segment_data::point::end] = {offset, 0.0, a + b}; - diverging.points[segment_data::point::control1] = {0.0, 0.0, a * 2.0 / 3.0}; - diverging.points[segment_data::point::control2] = {-offset * 2.0 / 3.0, 0.0, -b * 2.0 / 3.0}; - diverging.radius = static_cast(definition.radius); - auto const tangent{definition.radius * std::tan(angle * 0.5)}; - glm::dvec3 const arcend{tangent * std::sin(angle), 0.0, tangent + tangent * std::cos(angle)}; - if (glm::distance(arcend, diverging.points[segment_data::point::end]) > 0.5) + std::string error; + auto entry{template_of(designs[i], error)}; + if (false == entry.has_value()) { - auto const handle{4.0 / 3.0 * definition.radius * std::tan(angle * 0.25)}; - segment_data arc; - arc.points[segment_data::point::end] = arcend; - arc.points[segment_data::point::control1] = {0.0, 0.0, handle}; - arc.points[segment_data::point::control2] = {-handle * std::sin(angle), 0.0, -handle * std::cos(angle)}; - arc.radius = static_cast(definition.radius); - segment_data straight; - straight.points[segment_data::point::start] = arcend; - straight.points[segment_data::point::end] = diverging.points[segment_data::point::end]; - diverging.pieces = {arc, straight}; + WriteLog("Editor: switch template \"" + designs[i].name + "\" left out: " + error, logtype::generic); + continue; } - entry.label = std::string{definition.name} + " a " + std::to_string(a).substr(0, std::to_string(a).find('.') + 4) + " b " + std::to_string(b).substr(0, std::to_string(b).find('.') + 4) + (estimated ? " (b est.)" : ""); - result.push_back(entry); + entry->design = static_cast(i); + result.push_back(std::move(*entry)); } for (auto const &slipping : {std::pair{190.0, "1:9"}, {300.0, "1:9"}}) { diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 194f9d49..ca21c335 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -19,6 +19,7 @@ http: #include "world/Segment.h" #include "editor/editorAlignment.hpp" +#include "editor/editorSwitchDesign.hpp" #include "utilities/Classes.h" class TTrack; @@ -406,8 +407,16 @@ class editor_track bool double_slip{false}; std::string type{"Rz"}; std::string rail{"-"}; + // the second switch of a double one, starting on the main track of the first at the stagger + std::array second; + segment_data link; // plain track from the main track of the first switch to the start of the second one + double stagger{0.0}; + bool own{false}; + int design{-1}; // in the list of the designs the template was built from + bool twofold() const { return stagger > 0.0; } }; static std::vector standard_switch_templates(); + static std::optional template_of(switch_design::design const &Design, std::string &Error); static std::vector find_switch_templates(); static TTrack *load_path(std::string const &Text, TTrack const &Template, std::string const &Name = {}); static TTrack *create_path(TTrack const &Style, segment_data const &Path); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 2f896bf2..ef0f7786 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5742,3 +5742,271 @@ msgstr "stawia nową obrotnicę" msgid "Shifts a piece of the straight sideways around an obstacle and back: four clicks on the straight (Shift+click in the view does the same)" msgstr "Przesuwa kawałek prostej w bok, wokół przeszkody, i z powrotem: cztery kliknięcia na prostej (Shift+klik w widoku robi to samo)" + +msgid "1:n##quickratio" +msgstr "1:n##quickratio" + +msgid "across x%.1f" +msgstr "w poprzek x%.1f" + +msgid "a##designa" +msgstr "a##designa" + +msgid "A double switch is laid as two switches: the first one with its diverging track and the main track up to the second one, which starts there with the other diverging track" +msgstr "Rozjazd podwójny kładzie się jako dwie zwrotnice: pierwszą z jej torem zwrotnym i torem zasadniczym aż do drugiej, która zaczyna się tam z drugim torem zwrotnym" + +msgid "A new own template, an Rz 1:9 R190 to start from" +msgstr "Nowy własny szablon, na początek Rz 1:9 R190" + +msgid "Angle 1:%.3f (%.4f deg), sharpest R %.1f m, a %.3f m, b %.3f m" +msgstr "Skos 1:%.3f (%.4f°), najmniejszy R %.1f m, a %.3f m, b %.3f m" + +msgid "Angle from l and z" +msgstr "Kąt z l i z" + +msgid "+ Arc" +msgstr "+ Łuk" + +msgid "Arc" +msgstr "Łuk" + +msgid "b##designb" +msgstr "b##designb" + +msgid "Blade angle, deg##designentry" +msgstr "Kąt iglicy, °##designentry" + +msgid "Blade angle, deg##designentry2" +msgstr "Kąt iglicy, °##designentry2" + +msgid "Choose a template on the left. The catalogue ones can be copied to own templates and changed there. A template is a straight main track and the diverging track from the start of the switch as a chain of straights, arcs and transition curves; a double switch is two switches, the second one starting on the main track of the first." +msgstr "Wybierz szablon po lewej. Katalogowe można skopiować do własnych i tam zmieniać. Szablon to prosty tor zasadniczy i tor zwrotny od początku rozjazdu jako ciąg prostych, łuków i krzywych przejściowych; rozjazd podwójny to dwie zwrotnice, druga zaczyna się na torze zasadniczym pierwszej." + +msgid " (copy)" +msgstr " (kopia)" + +msgid "Copy to own templates" +msgstr "Kopiuj do własnych szablonów" + +msgid "Distance from the start of the first switch to the start of the second one, along the main track" +msgstr "Odległość od początku pierwszej zwrotnicy do początku drugiej, wzdłuż toru zasadniczego" + +msgid "Double switch %s + %s: length %.3f m, the second switch starts %.3f m on" +msgstr "Rozjazd podwójny %s + %s: długość %.3f m, druga zwrotnica zaczyna się po %.3f m" + +msgid "Double switch %s + %s with drives: events %s+, %s- and %s+, %s- switch them, also in the map" +msgstr "Rozjazd podwójny %s + %s z napędami: zdarzenia %s+, %s- i %s+, %s- je przestawiają, także w mapie" + +msgid "Double switch %s with its second switch %s inserted" +msgstr "Wstawiono rozjazd podwójny %s z drugą zwrotnicą %s" + +msgid "Draws the offsets from the main track larger, so the shape of the diverging track can be seen" +msgstr "Rysuje odsunięcia od toru zasadniczego większe, żeby było widać kształt toru zwrotnego" + +msgid "Fill in" +msgstr "Wypełnij" + +msgid "First switch: diverging %s, end offset %.3f m" +msgstr "Pierwsza zwrotnica: zwrotny w %s, odsunięcie końca %.3f m" + +msgid "From the point where the tangents meet to the end, as the catalogue gives it; 0: the main track less a" +msgstr "Od punktu przecięcia stycznych do końca, jak podaje katalog; 0: tor zasadniczy minus a" + +msgid "From the start of the switch (the joint before the blades) to the tip of the blade; the diverging track runs along the main one there" +msgstr "Od początku rozjazdu (styku przediglicowego) do ostrza iglicy; tor zwrotny biegnie tam wzdłuż zasadniczego" + +msgid "From the start to the point where the tangents of both tracks meet, as the catalogue gives it; 0: from the geometry" +msgstr "Od początku do punktu przecięcia stycznych obu torów, jak podaje katalog; 0: z geometrii" + +msgid "Keeps the own templates in eu07_switch_templates.txt in the configuration directory of the simulator and puts them on the list of the switch tool" +msgstr "Zapisuje własne szablony w eu07_switch_templates.txt w katalogu konfiguracji symulatora i dodaje je do listy narzędzia rozjazdów" + +msgid "Length %.3f m, the diverging track ends %.3f m beside the main one, in %zu pieces" +msgstr "Długość %.3f m, tor zwrotny kończy się %.3f m obok zasadniczego, w %zu kawałkach" + +msgid "Length of the main track, 0: as far as the diverging tracks reach" +msgstr "Długość toru zasadniczego, 0: tak daleko, jak sięgają tory zwrotne" + +msgid "Length of the part, measured along it" +msgstr "Długość części, mierzona wzdłuż niej" + +msgid "l, m##bladelength" +msgstr "l, m##bladelength" + +msgid "Main track, m##designmain" +msgstr "Tor zasadniczy, m##designmain" + +msgid "m##length" +msgstr "m##length" + +msgid "Name##designname" +msgstr "Nazwa##designname" + +msgid "Otherwise the second switch diverges the same way as the first, to the other side for Rpd, to the same side for Rpj" +msgstr "W przeciwnym razie druga zwrotnica odgałęzia się tak samo jak pierwsza, w drugą stronę dla Rpd, w tę samą dla Rpj" + +msgid "Own: " +msgstr "Własny: " + +msgid "Own switch templates: diverging tracks of straights, arcs and transition curves, double switches" +msgstr "Własne szablony rozjazdów: tory zwrotne z prostych, łuków i krzywych przejściowych, rozjazdy podwójne" + +msgid "Place this switch" +msgstr "Postaw ten rozjazd" + +msgid "Pre-blade stretch, m##designlead" +msgstr "Odcinek przediglicowy, m##designlead" + +msgid "Pre-blade stretch, m##designlead2" +msgstr "Odcinek przediglicowy, m##designlead2" + +msgid "Radius at the end of the transition curve, 0: into a straight; the curvature changes evenly along it" +msgstr "Promień na końcu krzywej przejściowej, 0: w prostą; krzywizna zmienia się wzdłuż niej równomiernie" + +msgid "Radius at the start of the transition curve, 0: from a straight" +msgstr "Promień na początku krzywej przejściowej, 0: z prostej" + +msgid "Radius of the arc; negative turns back towards the main track" +msgstr "Promień łuku; ujemny skręca z powrotem ku torowi zasadniczemu" + +msgid "Rail##designrail" +msgstr "Szyna##designrail" + +msgid "Rail of the switch kept in its record: 60E1, 49E1..." +msgstr "Szyna rozjazdu zapisywana w jego rekordzie: 60E1, 49E1..." + +msgid "Removes the template; Save keeps the change" +msgstr "Usuwa szablon; Zapisz utrwala zmianę" + +msgid "Replaces the parts with an arc of R up to the angle 1:n and the straight after it, the shape of an ordinary switch" +msgstr "Zastępuje części łukiem R do skosu 1:n i prostą za nim, kształtem rozjazdu zwyczajnego" + +msgid "R from##radius1" +msgstr "R od##radius1" + +msgid "Rpd double, to both sides" +msgstr "Rpd podwójny dwustronny" + +msgid "Rpj double, to one side" +msgstr "Rpj podwójny jednostronny" + +msgid "R##quickradius" +msgstr "R##quickradius" + +msgid "R##radius" +msgstr "R##radius" + +msgid "Rz ordinary" +msgstr "Rz zwyczajny" + +msgid "Save *" +msgstr "Zapisz *" + +msgid "Saved, the templates are on the list of the switch tool" +msgstr "Zapisano, szablony są na liście narzędzia rozjazdów" + +msgid "Saves the own templates and chooses this one in the switch tool: then click a straight and drag to the side the switch goes" +msgstr "Zapisuje własne szablony i wybiera ten w narzędziu rozjazdów: potem kliknij prostą i przeciągnij w stronę, w którą idzie rozjazd" + +msgid "Second switch starts at, m##stagger" +msgstr "Druga zwrotnica zaczyna się po, m##stagger" + +msgid "sin angle = z / l: l the length of a straight blade, z the gap between the running edges of the stock rail and the blade at its heel" +msgstr "sin kąta = z / l: l długość iglicy prostej, z odstęp krawędzi tocznych opornicy i iglicy w osadzie" + +msgid "A catalogue template can't be changed" +msgstr "Szablonu katalogowego nie można zmieniać" + +msgid "Catalogue (read only)" +msgstr "Katalog (tylko do odczytu)" + +msgid "Diverging track from the start of the switch" +msgstr "Tor zwrotny od początku rozjazdu" + +msgid "(none yet)" +msgstr "(jeszcze brak)" + +msgid "Own templates" +msgstr "Własne szablony" + +msgid "Straight blade:" +msgstr "Iglica prosta:" + +msgid "+ Straight" +msgstr "+ Prosta" + +msgid "straight after, m##quickstraight" +msgstr "prosta za łukiem, m##quickstraight" + +msgid "Stretch across" +msgstr "Rozciągnij w poprzek" + +msgid "Switch templates" +msgstr "Szablony rozjazdów" + +msgid "Template editor..." +msgstr "Edytor szablonów..." + +msgid "The angle at which the blade meets the stock rail at its tip: the diverging track starts there at this angle instead of tangentially. Curved blades a fraction of a degree, straight blades 1 to 2 degrees; 0: tangential" +msgstr "Kąt oparcia iglicy o opornicę przy ostrzu: tor zwrotny zaczyna się tam pod tym kątem zamiast stycznie. Iglice krzywe ułamek stopnia, proste 1 do 2 stopni; 0: stycznie" + +msgid "The catalogue switches start tangentially at the joint before the blades: without the pre-blade stretch and the angle of the blade, which a copy can be given" +msgstr "Rozjazdy katalogowe zaczynają się stycznie na styku przediglicowym: bez odcinka przediglicowego i kąta iglicy, które można nadać kopii" + +msgid "The double switch is replaced by its main track, Shift+Del removes all of it" +msgstr "Rozjazd podwójny zastąpiono jego torem zasadniczym, Shift+Del usuwa całość" + +msgid "The second switch has a diverging track of its own" +msgstr "Druga zwrotnica ma własny tor zwrotny" + +msgid "Tip of the blade %.3f m from the start, the diverging track leaves there at %.4f deg" +msgstr "Ostrze iglicy %.3f m od początku, tor zwrotny odchodzi tam pod kątem %.4f°" + +msgid "to##radius2" +msgstr "do##radius2" + +msgid "+ Transition curve" +msgstr "+ Krzywa przejściowa" + +msgid "Type##designtype" +msgstr "Typ##designtype" + +msgid "z, m##bladegap" +msgstr "z, m##bladegap" + +msgid "Unknown type of switch" +msgstr "Nieznany typ rozjazdu" + +msgid "The pre-blade stretch has to be between 0 and 30 m" +msgstr "Odcinek przediglicowy musi mieć od 0 do 30 m" + +msgid "The angle of the blade has to be between 0 and 5 degrees" +msgstr "Kąt iglicy musi wynosić od 0 do 5 stopni" + +msgid "The diverging track has to leave to the side at an angle between 0 and 45 degrees" +msgstr "Tor zwrotny musi odchodzić w bok pod kątem od 0 do 45 stopni" + +msgid "The main track is shorter than 1 m" +msgstr "Tor zasadniczy jest krótszy niż 1 m" + +msgid "The second switch has to start at least 1 m along the main track and 1 m before the end of the diverging track" +msgstr "Druga zwrotnica musi zaczynać się co najmniej 1 m od początku toru zasadniczego i 1 m przed końcem toru zwrotnego" + +msgid "The pre-blade stretch of the second switch has to be at most 30 m and the angle of its blade at most 5 degrees" +msgstr "Odcinek przediglicowy drugiej zwrotnicy może mieć najwyżej 30 m, a kąt jej iglicy najwyżej 5 stopni" + +msgid "The diverging track of the second switch has to leave to the side at an angle between 0 and 45 degrees" +msgstr "Tor zwrotny drugiej zwrotnicy musi odchodzić w bok pod kątem od 0 do 45 stopni" + +msgid "The main track ends less than 1 m after the start of the second switch" +msgstr "Tor zasadniczy kończy się mniej niż 1 m za początkiem drugiej zwrotnicy" + +msgid "The diverging track has no parts" +msgstr "Tor zwrotny nie ma części" + +msgid "A part of the diverging track is shorter than 5 cm" +msgstr "Część toru zwrotnego jest krótsza niż 5 cm" + +msgid "A radius of the diverging track is under 10 m" +msgstr "Promień toru zwrotnego jest mniejszy niż 10 m" +