17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-05 01:55:43 +01:00

Merge branch 'feat/track-join-ux' into feat/switch-blades

This commit is contained in:
maj00r
2026-10-15 14:25:27 +02:00
19 changed files with 3655 additions and 185 deletions

View File

@@ -178,6 +178,9 @@ set(SOURCES
"editor/editorGroundFollow.cpp" "editor/editorGroundFollow.cpp"
"editor/editorAlignmentStore.cpp" "editor/editorAlignmentStore.cpp"
"editor/editorSwitchStore.cpp" "editor/editorSwitchStore.cpp"
"editor/editorSwitchDesign.cpp"
"editor/editorConnections.cpp"
"application/editormodeconnection.cpp"
"editor/editorSpeed.cpp" "editor/editorSpeed.cpp"
"editor/editorProfile.cpp" "editor/editorProfile.cpp"
"editor/editorGauge.cpp" "editor/editorGauge.cpp"
@@ -192,6 +195,7 @@ set(SOURCES
"application/editormoderecords.cpp" "application/editormoderecords.cpp"
"application/editormodeswitchdrive.cpp" "application/editormodeswitchdrive.cpp"
"application/editormodeswitchslide.cpp" "application/editormodeswitchslide.cpp"
"application/editormodeswitchdesign.cpp"
"application/editormodetrackbed.cpp" "application/editormodetrackbed.cpp"
"application/editormodetrackux.cpp" "application/editormodetrackux.cpp"
"application/editormoderoad.cpp" "application/editormoderoad.cpp"

View File

@@ -1483,6 +1483,7 @@ bool editor_mode::update()
render_shortcuts_window(); render_shortcuts_window();
render_first_steps(); render_first_steps();
render_orthophoto_window(); render_orthophoto_window();
render_switch_designer();
render_environment_window(); render_environment_window();
draw_selection_overlay(); draw_selection_overlay();
render_context_menu(); render_context_menu();

View File

@@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/.
#include "editor/editorTerrainStreamer.hpp" #include "editor/editorTerrainStreamer.hpp"
#include "editor/editorOrthophoto.hpp" #include "editor/editorOrthophoto.hpp"
#include "editor/editorTrack.hpp" #include "editor/editorTrack.hpp"
#include "editor/editorConnections.hpp"
#include "editor/editorAlignment.hpp" #include "editor/editorAlignment.hpp"
#include "editor/editorAlignmentStore.hpp" #include "editor/editorAlignmentStore.hpp"
#include "editor/editorSwitchStore.hpp" #include "editor/editorSwitchStore.hpp"
@@ -725,6 +726,8 @@ class editor_mode : public application_mode, private editor_track::observer
void track_gizmo_take_switch_joints(TTrack &Track, bool const Controlmode); 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<TTrack *, editor_track::point_ref> const &Connection, std::function<bool(TTrack const *)> const &Movable); track_gizmo_drag::switch_joint track_gizmo_link(TTrack &Track, editor_track::point_ref const &Own, std::pair<TTrack *, editor_track::point_ref> const &Connection, std::function<bool(TTrack const *)> const &Movable);
void track_gizmo_take_switches(TTrack &Track); 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_rotation(TTrack &Track, bool const Pointmode);
void track_gizmo_measure(TTrack &Track, bool const Pointmode, gizmo_operation const Operation); 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); void track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_operation const Operation);
@@ -832,6 +835,19 @@ class editor_mode : public application_mode, private editor_track::observer
void route_read_stored(TTrack &Track); void route_read_stored(TTrack &Track);
void route_take_stored(int const Index); void route_take_stored(int const Index);
void record_switch(TTrack &Track, editor_track::switch_template const &Shape); 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_switch> double_of(TTrack const &Track) const;
// the paths of the set in the order editor_track::place_switch gives them
static std::vector<segment_data> 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<TTrack *> create_switch_set(editor_track::switch_template const &Shape, std::vector<segment_data> const &Paths, TTrack const &Style, std::string const &Name);
void record_slip(std::vector<TTrack *> const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base); void record_slip(std::vector<TTrack *> 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 render_switch_record(TTrack const &Track) const;
void route_store(); void route_store();
@@ -978,15 +994,33 @@ class editor_mode : public application_mode, private editor_track::observer
double low{0.0}; double low{0.0};
double high{0.0}; double high{0.0};
double distance{0.0}; double distance{0.0};
double length{0.0};
std::vector<double> joints; std::vector<double> joints;
}; };
slide_state slide; slide_state slide;
double slide_typed{0.0}; double slide_typed{0.0};
int connection{-1};
int connection_second{-1};
double connection_radius{0.0};
double connection_gap{6.0};
bool connection_together{false};
char connection_spacings[64]{"4.75;4.75;5.0"};
double connection_stub{25.0};
}; };
switch_tool m_switch; switch_tool m_switch;
bool plan_connection(connection::request &Request, connection::result &Result) const;
void insert_connection();
void render_connection_ui();
std::string connection_readout(connection::result const &Plan) const;
editor_track::straight straight_near(glm::dvec3 const &Point, std::vector<TTrack const *> const &Exclude, double const Reach) const;
editor_track::straight straight_through(glm::dvec3 const &Point, glm::dvec2 const &Direction) const;
void record_connection(connection::kind const Kind, std::vector<TTrack *> const &Switches, connection::result const &Plan, editor_track::switch_template const &Shape);
bool switch_room(TTrack &Track, double &Low, double &High, std::string &Reason); 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 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<segment_data> const &Paths, std::string &Status); bool move_switch_paths(TTrack &Track, std::vector<segment_data> 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<segment_data> const &Paths, std::string &Status);
bool slide_switch(TTrack &Track, double const Distance); bool slide_switch(TTrack &Track, double const Distance);
bool switch_slides(TTrack const &Track) const; bool switch_slides(TTrack const &Track) const;
void switch_slide_start(TTrack &Track); void switch_slide_start(TTrack &Track);
@@ -1213,6 +1247,29 @@ class editor_mode : public application_mode, private editor_track::observer
int m_lineside_tab{0}; int m_lineside_tab{0};
void render_lineside_ui(); void render_lineside_ui();
void arm_switch(); 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<switch_design::design> catalogue;
std::vector<switch_design::design> 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}; 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 // 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(); void render_track_inspector();

View File

@@ -0,0 +1,388 @@
/*
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/editorAlignmentStore.hpp"
#include "editor/editorSwitchStore.hpp"
#include "editor/editorGeometry.hpp"
#include "scene/scene.h"
#include "simulation/simulation.h"
#include "world/Track.h"
#include "utilities/translation.h"
#include "imgui/imgui.h"
#include <algorithm>
#include <map>
#include <sstream>
namespace
{
using geometry::cross;
using geometry::plan_of;
char const *connection_name(int const Kind)
{
switch (static_cast<connection::kind>(Kind))
{
case connection::kind::double_crossover: return STR_C("Double crossover");
case connection::kind::scissors: return STR_C("Scissors crossover");
case connection::kind::ladder: return STR_C("Switch ladder");
default: return STR_C("Crossover");
}
}
std::vector<double> spacings_of(char const *Text)
{
std::string text{Text};
for (auto &letter : text)
if (letter == ';')
letter = ' ';
else if (letter == ',')
letter = '.';
std::istringstream words{text};
std::vector<double> result;
double value{0.0};
while (words >> value)
result.push_back(value);
return result;
}
std::string switch_label(editor_track::switch_template const &Template)
{
if (Template.source != nullptr)
return STR("Scenery: ") + Template.label;
return (Template.own ? STR("Own: ") : std::string{"PLK: "}) + Template.label;
}
bool usable_in_connection(editor_track::switch_template const &Template)
{
return false == Template.double_slip && false == Template.twofold();
}
} // namespace
editor_track::straight editor_mode::straight_near(glm::dvec3 const &Point, std::vector<TTrack const *> const &Exclude, double const Reach) const
{
TTrack *found{nullptr};
auto best{Reach};
for (auto *section : simulation::Region->sections(Point, static_cast<float>(std::max(40.0, Reach))))
for (auto const &cell : section->m_cells)
for (auto *track : cell.m_paths)
{
if (track->eType != tt_Normal || track->m_editorremoved || track->m_paths.empty() || std::find(Exclude.begin(), Exclude.end(), track) != Exclude.end() || false == editor_track::is_straight(*track, m_straights.tolerance))
continue;
auto const &path{track->m_paths.front()};
auto const a{plan_of(path.points[segment_data::point::start])};
auto const segment{plan_of(path.points[segment_data::point::end]) - a};
auto const t{std::clamp(glm::dot(plan_of(Point) - a, segment) / std::max(1e-9, glm::dot(segment, segment)), 0.0, 1.0)};
auto const distance{glm::distance(plan_of(Point), a + segment * t)};
if (distance < best)
{
best = distance;
found = track;
}
}
return found != nullptr ? editor_track::find_straight(*found, m_straights.tolerance) : editor_track::straight{};
}
editor_track::straight editor_mode::straight_through(glm::dvec3 const &Point, glm::dvec2 const &Direction) const
{
for (auto *section : simulation::Region->sections(Point, 40.f))
for (auto const &cell : section->m_cells)
for (auto *track : cell.m_paths)
{
if (track->eType != tt_Normal || track->m_editorremoved || track->m_paths.empty() || false == editor_track::is_straight(*track, m_straights.tolerance))
continue;
auto const &path{track->m_paths.front()};
auto const a{plan_of(path.points[segment_data::point::start])};
auto const segment{plan_of(path.points[segment_data::point::end]) - a};
auto const length{glm::length(segment)};
if (length < 1e-6 || std::abs(cross(segment / length, Direction)) > 1e-3 || std::abs(cross(segment / length, plan_of(Point) - a)) > 0.02)
continue;
auto const t{glm::dot(plan_of(Point) - a, segment) / (length * length)};
if (t >= -1e-6 && t <= 1.0 + 1e-6)
return editor_track::find_straight(*track, m_straights.tolerance);
}
return {};
}
bool editor_mode::plan_connection(connection::request &Request, connection::result &Result) const
{
auto const &tool{m_switch};
Result = {};
if (false == tool.placing || tool.curved || tool.connection < 0 || tool.armed < 0 || tool.armed >= static_cast<int>(tool.templates.size()) || tool.line.tracks.empty())
return false;
auto const &line{tool.line};
Request = {};
Request.type = static_cast<connection::kind>(tool.connection);
Request.from = connection::of(line);
Request.first = tool.templates[tool.armed];
auto const second{tool.connection_second >= 0 && tool.connection_second < static_cast<int>(tool.templates.size()) ? tool.connection_second : tool.armed};
Request.second = tool.templates[second];
Request.radius = tool.connection_radius;
Request.gap = tool.connection_gap;
Request.together = tool.connection_together;
Request.stub = tool.connection_stub;
Request.spacings = spacings_of(tool.connection_spacings);
Request.along = tool.along;
glm::dvec2 const offset{plan_of(tool.mouse - tool.point)};
Request.forward = glm::dot(offset, line.direction) >= 0.0 ? 1 : -1;
glm::dvec2 const travel{line.direction * static_cast<double>(Request.forward)};
Request.side = cross(travel, offset) >= 0.0 ? 1 : -1;
std::vector<TTrack const *> const exclude(line.tracks.begin(), line.tracks.end());
auto const other{straight_near(tool.mouse, exclude, 8.0)};
if (false == other.tracks.empty())
{
auto const target{connection::of(other)};
if (Request.type != connection::kind::ladder)
{
Request.to.push_back(target);
Request.target = target.along(tool.mouse);
}
else if (std::abs(cross(target.direction, line.direction)) < 1e-3)
{
auto const side{cross(travel, plan_of(target.point(target.along(tool.point)) - tool.point)) > 0.0 ? 1.0 : -1.0};
auto const lateral = [&](glm::dvec3 const &Point) { return side * cross(travel, plan_of(Point - tool.point)); };
auto const outermost{lateral(target.point(target.along(tool.point)))};
std::map<long, std::pair<double, TTrack *>> fan;
auto const better = [](double const One, double const Two) { return (One < 0.0) != (Two < 0.0) ? One >= 0.0 : std::abs(One) < std::abs(Two); };
auto const centre{(tool.point + tool.mouse) * 0.5};
for (auto *section : simulation::Region->sections(centre, static_cast<float>(glm::length(offset) * 0.5 + 200.0)))
for (auto const &cell : section->m_cells)
for (auto *track : cell.m_paths)
{
if (track->eType != tt_Normal || track->m_editorremoved || track->m_paths.empty() || std::find(exclude.begin(), exclude.end(), track) != exclude.end() || false == editor_track::is_straight(*track, m_straights.tolerance))
continue;
auto const &path{track->m_paths.front()};
auto const a{path.points[segment_data::point::start]};
auto const b{path.points[segment_data::point::end]};
auto const direction{plan_of(b - a)};
if (glm::length(direction) < 1e-6 || std::abs(cross(glm::normalize(direction), line.direction)) > 1e-3)
continue;
auto const distance{lateral(a)};
auto const along1{glm::dot(travel, plan_of(a - tool.point))};
auto const along2{glm::dot(travel, plan_of(b - tool.point))};
if (distance < 1.0 || distance > outermost + 0.05 || std::max(along1, along2) <= 0.0)
continue;
auto const nearer{std::min(along1, along2)};
auto &entry{fan[std::lround(distance * 20.0)]};
if (entry.second == nullptr || better(nearer, entry.first))
entry = {nearer, track};
}
for (auto const &entry : fan)
Request.to.push_back(connection::of(editor_track::find_straight(*entry.second.second, m_straights.tolerance)));
}
}
Result = connection::plan(Request);
return Result.valid;
}
std::string editor_mode::connection_readout(connection::result const &Plan) const
{
if (false == Plan.valid)
return Plan.error;
auto text{format(STR_C("%s: %d switches, spacing %.2f m"), connection_name(m_switch.connection), static_cast<int>(Plan.switches.size()), Plan.spacing)};
if (false == Plan.straights.empty())
{
text += STR_C(", straight inserts");
for (auto const length : Plan.straights)
text += format(" %.2f", length);
text += " m";
}
if (Plan.points > 0.0)
text += format(STR_C(", mathematical points %.2f m apart"), Plan.points);
if (Plan.radius > 0.0)
text += format(STR_C(", aligning curve R %.0f m"), Plan.radius);
if (Plan.crossing && Plan.angle > 1e-6)
text += format(STR_C(", crossing 1:%.2f"), 1.0 / std::tan(Plan.angle));
if (false == Plan.fan.empty())
{
text += STR_C(", tracks at");
for (auto const offset : Plan.fan)
text += format(" %.2f", offset);
text += " m";
}
if (false == Plan.warning.empty())
text += "\n" + Plan.warning;
return text;
}
void editor_mode::insert_connection()
{
auto &tool{m_switch};
connection::request request;
connection::result plan;
auto const planned{plan_connection(request, plan)};
tool.placing = false;
if (false == planned)
{
tool.status = plan.error.empty() ? std::string{STR_C("Drag onto the other track")} : plan.error;
ui()->set_status(tool.status, true);
return;
}
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> created;
std::vector<TTrack *> removed;
auto *style{tool.line.tracks.front()};
auto const fail = [&](std::string const &Reason) {
for (auto *track : created)
editor_track::retire(*track);
for (auto entry{states.rbegin()}; entry != states.rend(); ++entry)
{
editor_track::apply(*entry->first, entry->second);
editor_track::commit({entry->first});
}
for (auto *track : removed)
{
track->m_editorremoved = false;
editor_track::commit({track});
}
if (auto before{switch_store::take_changes()})
switch_store::restore(std::move(*before));
if (auto before{alignment_store::take_changes()})
alignment_store::restore(std::move(*before));
straight_refresh();
tool.status = Reason;
ui()->set_status(Reason, true);
};
std::vector<TTrack *> switches;
for (auto const &entry : plan.switches)
{
auto const &shape{entry.second ? request.second : request.first};
auto const name{switch_name_for_new()};
auto const before{created.size()};
auto const start{entry.paths[0].points[segment_data::point::start]};
auto const end{entry.paths[0].points[segment_data::point::end]};
if (entry.line >= 0)
{
auto const &target{entry.line == 0 ? request.from : request.to[entry.line - 1]};
auto const found{straight_through((start + end) * 0.5, target.direction)};
if (found.tracks.empty())
{
fail(STR_C("A switch of the connection goes where its track isn't a plain straight"));
return;
}
auto const forward{glm::dot(found.direction, plan_of(end - start)) > 0.0 ? 1 : -1};
if (false == place_switch_on_straight(found, shape, nullptr, found.along(start), forward, entry.side, false, states, created, removed, name))
{
fail(tool.status);
return;
}
}
else
{
auto const made{create_switch_set(shape, entry.paths, *style, name)};
if (made.empty())
{
fail(tool.status);
return;
}
created.insert(created.end(), made.begin(), made.end());
}
auto const made{std::find_if(created.begin() + static_cast<std::ptrdiff_t>(before), created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; })};
if (made == created.end() || glm::distance((*made)->m_paths.front().points[segment_data::point::start], start) > 0.01)
{
fail(STR_C("A switch of the connection doesn't fit where it was planned, its straight is too short"));
return;
}
switches.push_back(*made);
advance_switch_name();
}
std::vector<TTrack *> inserts;
for (auto const &insert : plan.inserts)
if (auto *track{editor_track::create_path(*style, insert)})
inserts.push_back(track);
editor_track::commit(inserts);
digitize_drawn(inserts);
created.insert(created.end(), inserts.begin(), inserts.end());
if (plan.crossing)
{
auto const first{glm::normalize(plan_of(plan.ends[1] - plan.ends[0]))};
auto second{glm::normalize(plan_of(plan.ends[3] - plan.ends[2]))};
if (glm::dot(first, second) < 0.0)
second = -second;
auto const parts{build_double_slip(plan.ends[0], plan.ends[1], plan.ends[2], plan.ends[3], plan_of(plan.centre), plan.centre.y, first, second, plan.angle, 0.0, *style, "Skrz")};
editor_track::commit(parts);
created.insert(created.end(), parts.begin(), parts.end());
}
record_connection(request.type, switches, plan, request.first);
push_track_snapshot(std::move(states), std::move(created), std::move(removed));
straight_refresh();
first_step_done(first_step::turnout);
m_track_point = {};
m_node = switches.front();
ui()->set_node(m_node);
std::string names;
for (auto *track : switches)
{
if (place_switch_drive(*track))
join_last_steps();
names += (names.empty() ? "" : ", ") + track->name();
}
tool.status = format(STR_C("%s laid, switches %s"), connection_name(tool.connection), names.c_str()) + "\n" + connection_readout(plan);
ui()->set_status(tool.status, false);
}
void editor_mode::render_connection_ui()
{
auto &tool{m_switch};
ImGui::PushItemWidth(220.0f);
if (ImGui::BeginCombo(STR_C("Connection##switchconnection"), tool.connection < 0 ? STR_C("Single switch") : connection_name(tool.connection)))
{
if (ImGui::Selectable(STR_C("Single switch"), tool.connection < 0))
tool.connection = -1;
for (int i = 0; i < connection::kind_count; ++i)
if (ImGui::Selectable(connection_name(i), tool.connection == i))
tool.connection = i;
ImGui::EndCombo();
}
item_tooltip("A connection of tracks laid whole: press on a straight and drag onto the other track. Its switches are cut into the tracks with the inserts between them and get their drives; Shift+wheel while dragging: another kind, Ctrl+wheel: R of the aligning curve");
if (tool.connection < 0)
{
ImGui::PopItemWidth();
return;
}
auto const label = [&](int const Index) { return Index < 0 || Index >= static_cast<int>(tool.templates.size()) ? std::string{STR_C("The same as the first one")} : switch_label(tool.templates[Index]); };
if (ImGui::BeginCombo(STR_C("Other switches##connectionsecond"), label(tool.connection_second).c_str()))
{
if (ImGui::Selectable(STR_C("The same as the first one"), tool.connection_second < 0))
tool.connection_second = -1;
for (int i = 0; i < static_cast<int>(tool.templates.size()); ++i)
if (usable_in_connection(tool.templates[i]) && ImGui::Selectable((switch_label(tool.templates[i]) + "##connectionsecond" + std::to_string(i)).c_str(), tool.connection_second == i))
tool.connection_second = i;
ImGui::EndCombo();
}
item_tooltip("Template of the switches on the other track and along a ladder; the first switch is the one chosen on the list above");
ImGui::InputDouble(STR_C("Aligning curve R, m##connectionradius"), &tool.connection_radius, 10.0, 100.0, "%.0f");
tool.connection_radius = std::clamp(tool.connection_radius, 0.0, 10000.0);
item_tooltip("Of the curve between switches of different angles or on tracks that aren't parallel, and at the end of a ladder; 0: the radius of the sharper switch");
if (tool.connection == static_cast<int>(connection::kind::double_crossover))
{
ImGui::InputDouble(STR_C("Gap between the switches, m##connectiongap"), &tool.connection_gap, 0.5, 1.0, "%.2f");
tool.connection_gap = std::clamp(tool.connection_gap, 0.0, 100.0);
item_tooltip("Straight between the starts of the two switches meeting on one track");
ImGui::Checkbox(STR_C("The switches meet on this track##connectiontogether"), &tool.connection_together);
item_tooltip("Otherwise the two crossovers meet on the other track; Alt+wheel while dragging switches it");
}
if (tool.connection == static_cast<int>(connection::kind::ladder))
{
ImGui::InputText(STR_C("Spacings, m##connectionspacings"), tool.connection_spacings, sizeof(tool.connection_spacings));
item_tooltip("Distances between the tracks of the fan from this track on, such as 4.75;4.75;5.0; used when the drag doesn't end on a parallel track, otherwise the parallel tracks up to that one make the fan");
ImGui::InputDouble(STR_C("New tracks, m##connectionstub"), &tool.connection_stub, 5.0, 25.0, "%.0f");
tool.connection_stub = std::clamp(tool.connection_stub, 1.0, 1000.0);
item_tooltip("Length of the new tracks of the fan where there are none to lead onto");
}
ImGui::PopItemWidth();
}

View File

@@ -264,6 +264,88 @@ void editor_mode::record_switch(TTrack &Track, editor_track::switch_template con
switch_store::put(entry, alignment_store::layer_of(Track)); 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));
}
void editor_mode::record_connection(connection::kind const Kind, std::vector<TTrack *> const &Switches, connection::result const &Plan, editor_track::switch_template const &Shape)
{
if (Switches.empty() || Switches.front()->eType != tt_Switch || Switches.front()->m_paths.size() < 2 || scene::Layers.empty())
return;
auto entry{switch_entry(Shape)};
entry.kind = "connection";
entry.name = Switches.front()->name();
for (auto const *track : Switches)
if (track->name().empty())
return;
else
entry.parts.push_back(track->name());
switch_store::place(entry, *Switches.front());
switch_store::classify(entry, *Switches.front());
entry.type = connection::word(Kind);
entry.a = Plan.spacing;
entry.b = Plan.straights.empty() ? 0.0 : Plan.straights.front();
switch_store::put(entry, alignment_store::layer_of(*Switches.front()));
}
std::optional<editor_mode::double_switch> editor_mode::double_of(TTrack const &Track) const
{
auto const of_switch = [](TTrack const &Switch) -> std::optional<double_switch> {
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<segment_data> 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<TTrack *> const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base) void editor_mode::record_slip(std::vector<TTrack *> 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()) if (Parts.empty() || scene::Layers.empty())

View File

@@ -155,7 +155,7 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm
float extent{0.0f}; float extent{0.0f};
words >> extent; words >> extent;
choose_work_area(work_area::tracks); choose_work_area(work_area::tracks);
if (false == Global.EditorOrtho) if (Global.EditorOrtho == extent < 0.0f)
toggle_ortho(); toggle_ortho();
if (extent > 0.0f) if (extent > 0.0f)
Global.EditorOrthoExtent = std::clamp(extent, 5.0f, 50000.0f); Global.EditorOrthoExtent = std::clamp(extent, 5.0f, 50000.0f);
@@ -1318,6 +1318,166 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com
WriteLog("SELFTEST curvedswitch " + (tool.armed >= 0 ? tool.templates[tool.armed].label : std::string{}) + ": " + tool.status); WriteLog("SELFTEST curvedswitch " + (tool.armed >= 0 ? tool.templates[tool.armed].label : std::string{}) + ": " + tool.status);
return selftest_result::handled; return selftest_result::handled;
} }
if (command == "designer")
{
std::string list;
int index{0};
words >> list >> index;
auto &designer{m_switch_designer};
designer.open = list != "close";
designer.from_catalogue = list != "own";
designer.selected = index;
return selftest_result::handled;
}
if (command == "release")
{
m_switch.placing = false;
lay_cancel();
m_cursor_override.reset();
return selftest_result::handled;
}
if (command == "hover")
{
glm::dvec3 at{0.0};
if (words >> at.x >> at.z)
m_cursor_override = at;
else
m_cursor_override.reset();
return selftest_result::handled;
}
if (command == "layhold")
{
int layout{0};
words >> layout >> m_lay.radius;
m_track_layout = static_cast<track_layout>(std::clamp(layout, 0, 3));
m_lay.active = true;
lay_cancel();
std::vector<glm::dvec3> points;
for (glm::dvec3 point{0.0}; words >> point.x >> point.z;)
points.push_back(point);
for (std::size_t i = 0; i + 1 < points.size(); ++i)
{
auto point{points[i]};
point.y = 5000.0;
std::vector<char> found;
auto const ground{ground_heights({point}, true, &found)};
point.y = found[0] != 0 ? ground[0] : m_lay.points.empty() ? 0.0 : m_lay.points.back().y;
m_lay.mouse_snap = editor_track::find_free_end(nullptr, 1, point, 3.0, {});
m_lay.mouse = m_lay.mouse_snap.track != nullptr ? point : lay_bed(point);
lay_click();
}
if (false == points.empty())
m_cursor_override = points.back();
WriteLog("SELFTEST layhold: " + m_lay.status);
return selftest_result::handled;
}
if (command == "connection" || command == "connectionplan" || command == "connectionhold")
{
std::string kindword;
std::string label;
std::string second;
glm::dvec3 at{0.0};
glm::dvec3 toward{0.0};
words >> kindword >> at.x >> at.z >> toward.x >> toward.z >> label >> second;
connection::kind kind{connection::kind::crossover};
if (kindword != "single" && false == connection::parse(kindword, kind))
{
WriteLog("SELFTEST connection: unknown kind " + kindword);
return selftest_result::handled;
}
auto &tool{m_switch};
for (std::string key; words >> key;)
{
if (key == "radius")
words >> tool.connection_radius;
else if (key == "gap")
words >> tool.connection_gap;
else if (key == "together")
words >> tool.connection_together;
else if (key == "stub")
words >> tool.connection_stub;
else if (key == "spacings")
{
std::string value;
words >> value;
std::snprintf(tool.connection_spacings, sizeof(tool.connection_spacings), "%s", value.c_str());
}
}
show_track_tab(track_tab::turnout);
arm_switch();
auto const find = [&](std::string Label) {
std::replace(Label.begin(), Label.end(), '_', ' ');
for (int i = 0; i < static_cast<int>(tool.templates.size()); ++i)
if (false == Label.empty() && Label != "-" && false == tool.templates[i].double_slip && false == tool.templates[i].twofold() && tool.templates[i].label.find(Label) != std::string::npos)
return i;
return -1;
};
auto const first{find(label)};
if (first < 0)
{
WriteLog("SELFTEST connection: no template " + label);
return selftest_result::handled;
}
tool.armed = first;
tool.connection_second = find(second);
tool.connection = kindword == "single" ? -1 : static_cast<int>(kind);
auto const line{straight_near(at, {}, 3.0)};
if (line.tracks.empty())
{
WriteLog("SELFTEST connection: no straight at the start");
return selftest_result::handled;
}
m_node = line.tracks.front();
ui()->set_node(m_node);
m_cursor_override = at;
auto const started{start_switch_placement()};
m_cursor_override.reset();
if (false == started)
{
WriteLog("SELFTEST connection: not started, " + tool.status);
return selftest_result::handled;
}
tool.mouse = glm::dvec3{toward.x, tool.point.y, toward.z};
if (command == "connectionhold")
{
m_cursor_override = toward;
WriteLog("SELFTEST connectionhold: " + tool.status);
return selftest_result::handled;
}
connection::request request;
connection::result plan;
auto const valid{plan_connection(request, plan)};
WriteLog("SELFTEST connection " + kindword + ": " + (valid ? connection_readout(plan) : "invalid, " + plan.error));
for (auto const &entry : plan.switches)
{
auto const &start{entry.paths[0].points[segment_data::point::start]};
auto const &end{entry.paths[1].points[segment_data::point::end]};
WriteLog(format("SELFTEST connection switch on %d at %.3f, forward %d side %d%s: %.3f %.3f %.3f -> branch end %.3f %.3f %.3f", entry.line, entry.along, entry.forward, entry.side, entry.second ? " (second)" : "", start.x, start.y, start.z, end.x, end.y,
end.z));
}
for (auto const &insert : plan.inserts)
{
auto const &start{insert.points[segment_data::point::start]};
auto const &end{insert.points[segment_data::point::end]};
WriteLog(format("SELFTEST connection insert %.3f %.3f %.3f -> %.3f %.3f %.3f, %.3f m, R %.1f", start.x, start.y, start.z, end.x, end.y, end.z, geometry::bezier{insert}.plan_length(), std::abs(insert.radius)));
}
if (command == "connectionplan" || false == valid)
{
tool.placing = false;
return selftest_result::handled;
}
insert_connection();
WriteLog("SELFTEST connection built: " + tool.status);
for (auto const &entry : switch_store::records())
if (entry.kind == "connection")
{
std::string parts;
for (auto const &part : entry.parts)
parts += ' ' + part;
WriteLog(format("SELFTEST connection record %s %s: spacing %.3f, insert %.3f, parts%s", entry.type.c_str(), entry.name.c_str(), entry.a, entry.b, parts.c_str()));
}
return selftest_result::handled;
}
if (command == "conecheck") if (command == "conecheck")
{ {
std::string name; std::string name;
@@ -1626,7 +1786,9 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com
double low{0.0}; double low{0.0};
double high{0.0}; double high{0.0};
std::string reason; 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)}; 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())); 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; return selftest_result::handled;
@@ -1642,14 +1804,24 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com
WriteLog("SELFTEST switchreplace: no template " + label); WriteLog("SELFTEST switchreplace: no template " + label);
return selftest_result::handled; 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 &main{track->m_paths[0]};
auto const origin{main.points[segment_data::point::start]}; auto const origin{main.points[segment_data::point::start]};
auto const chord{geometry::plan_of(main.points[segment_data::point::end] - origin)}; auto const chord{geometry::plan_of(main.points[segment_data::point::end] - origin)};
auto const length{glm::length(chord)}; 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)}; auto paths{editor_track::place_switch(*found, origin, chord / length, side, (main.points[segment_data::point::end].y - origin.y) / length)};
std::string status; std::string status;
auto const done{paths.size() >= 2 && move_switch_paths(*track, paths, status)}; 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) 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); record_switch(*track, *found);
records_to_history(); records_to_history();
@@ -1720,6 +1892,61 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com
} }
return selftest_result::handled; 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") if (command == "switches")
{ {
for (auto const &entry : switch_store::records()) for (auto const &entry : switch_store::records())

View File

@@ -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 <algorithm>
#include <cmath>
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<switch_design::part> &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<int>(Parts.size()); ++i)
{
auto &part{Parts[i]};
ImGui::PushID(i);
ImGui::AlignTextToFramePadding();
ImGui::Text("%d.", i + 1);
ImGui::SameLine();
auto kind{static_cast<int>(kind_of(part))};
ImGui::SetNextItemWidth(130.0f);
if (ImGui::Combo("##kind", &kind, kinds, 3))
{
set_kind(part, static_cast<part_kind>(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<part_kind>(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<float>(std::max(1.0, reach))};
auto const stretch{Stretched ? std::min(8.0f, (Height * 0.5f - margin) / std::max(0.01f, static_cast<float>(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<float>(Point.z) * scale, middle - static_cast<float>(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<segment_data> 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<editor_track::switch_template> 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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<switch_design::part>{};
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<std::size_t>(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();
}

View File

@@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/.
#include "imgui/imgui.h" #include "imgui/imgui.h"
#include <algorithm> #include <algorithm>
#include <array>
#include <optional> #include <optional>
#include <tuple> #include <tuple>
@@ -218,15 +219,129 @@ bool editor_mode::move_switch_paths(TTrack &Track, std::vector<segment_data> con
return true; 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<segment_data> 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<outer_end> 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<TTrack *, 2> const switches{Set.first, Set.second};
std::array<std::vector<editor_track::joint>, 2> joints;
std::array<std::vector<glm::dvec3>, 2> positions;
std::array<std::vector<bool>, 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) 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; 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()) if (paths.empty())
return false; return false;
std::string status; 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; m_switch.status = status;
ui()->set_status(status, true); ui()->set_status(status, true);
@@ -235,13 +350,18 @@ bool editor_mode::slide_switch(TTrack &Track, double const Distance)
switch_store::follow(); switch_store::follow();
records_to_history(); records_to_history();
straight_refresh(); 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()); ui()->set_status(m_switch.status, false == status.empty());
return true; return true;
} }
bool editor_mode::switch_slides(TTrack const &Track) const 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()); 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}; auto &slide{m_switch.slide};
slide = {}; slide = {};
std::string reason; 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()) if (false == reason.empty())
ui()->set_status(reason, true); ui()->set_status(reason, true);
return; return;
} }
auto const axis{editor_track::main_axis(Track)};
auto const &mouse{ImGui::GetIO().MousePos}; auto const &mouse{ImGui::GetIO().MousePos};
slide.active = true; slide.active = true;
slide.track = &Track; slide.track = &first;
slide.pressed = {mouse.x, mouse.y}; slide.pressed = {mouse.x, mouse.y};
slide.grip = axis->along(cursor_level(axis->origin.y)); slide.grip = axis->along(cursor_level(axis->origin.y));
auto const line{editor_track::find_straight(Track, m_straights.tolerance)}; std::vector<TTrack *> members{&first};
for (std::size_t i = 0; i < line.tracks.size(); ++i) 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; continue;
auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; auto const line{editor_track::find_straight(*member, m_straights.tolerance)};
for (auto const index : {segment_data::point::start, segment_data::point::end}) for (std::size_t i = 0; i < line.tracks.size(); ++i)
slide.joints.push_back(axis->along(path.points[index])); {
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 = {}; slide = {};
return; 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)}; auto const distance{std::clamp(axis->along(cursor_level(axis->origin.y)) - slide.grip, slide.low, slide.high - length)};
double best{1.0}; double best{1.0};
double snapped{distance}; double snapped{distance};
@@ -320,9 +453,16 @@ void editor_mode::draw_switch_slide() const
auto const &mouse{ImGui::GetIO().MousePos}; auto const &mouse{ImGui::GetIO().MousePos};
if (glm::length(glm::vec2{mouse.x, mouse.y} - slide.pressed) <= kDragPixels) if (glm::length(glm::vec2{mouse.x, mouse.y} - slide.pressed) <= kDragPixels)
return; return;
auto const paths{editor_track::slid(*slide.track, slide.distance)}; auto paths{editor_track::slid(*slide.track, slide.distance)};
if (paths.empty()) if (paths.empty())
return; 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; screen_projection const projection;
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
for (auto const &path : paths) for (auto const &path : paths)

View File

@@ -57,6 +57,13 @@ using geometry::level;
using geometry::reversed; using geometry::reversed;
using geometry::straight_piece; 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 kParallelFindAngle{15.0}; // deg, neighbours counted as beside the straight
double constexpr kParallelSnapAngle{5.0}; // deg, close enough to snap parallel 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 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()}; auto *track{selected_track()};
if (track == nullptr || track->eType != tt_Switch || track->m_paths.size() < 2) if (track == nullptr || track->eType != tt_Switch || track->m_paths.size() < 2)
return; return;
auto const set{double_of(*track)};
if (set.has_value())
track = set->first;
auto const &main{track->m_paths[0]}; auto const &main{track->m_paths[0]};
auto const &diverging{track->m_paths[1]}; auto const &diverging{track->m_paths[1]};
auto const origin{main.points[segment_data::point::start]}; 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)}; auto const offset{glm::dot(end, normal)};
glm::dvec2 const tangent{glm::normalize(glm::dvec2{-control.x, -control.z})}; 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))}; 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_track_name(*track);
render_switch_record(*track); render_switch_record(*track);
if (auto const drive{find_switch_drive(*track)}) auto const drive_ui = [&](TTrack &Switch) {
{ ImGui::PushID(&Switch);
auto const family{drive->family}; if (set.has_value())
auto const variant{drive->index}; ImGui::TextUnformatted(Switch.name().c_str());
ImGui::TextDisabled(STR_C("Drive: events %s+ and %s- switch it, also in the map"), track->name().c_str(), track->name().c_str()); if (auto const drive{find_switch_drive(Switch)})
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<int>(m_switch.drives.size()); ++i) auto const family{drive->family};
if (ImGui::Selectable(m_switch.drives[i].name.c_str(), family == i) && i != family) auto const variant{drive->index};
replace_switch_drive(*track, i, false); ImGui::TextDisabled(STR_C("Drive: events %s+ and %s- switch it, also in the map"), Switch.name().c_str(), Switch.name().c_str());
ImGui::EndCombo(); 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<int>(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(); 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))
item_tooltip("Puts another drive in place of the drive of this switch, on the same side"); ImGui::TextDisabled(STR_C("Drive: events %s+ and %s- switch it, also in the map"), Switch.name().c_str(), Switch.name().c_str());
int side{((variant >> 1) == (variant & 1)) ? 0 : 1}; else
ImGui::SameLine(); {
auto const before{side}; 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"));
ImGui::RadioButton(STR_C("diverging side##standing"), &side, 0); render_switch_drive_choice();
ImGui::SameLine(); if (m_switch.drive >= 0 && ImGui::Button(STR_C("Put the drive by the switch")))
ImGui::RadioButton(STR_C("other side##standing"), &side, 1); place_switch_drive(Switch);
if (side != before) }
flip_switch_drive(*track); ImGui::PopID();
item_tooltip("Puts the drive of the switch on the other side of the track: the same drive, its variant for the other side"); };
} drive_ui(*track);
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)) if (set.has_value())
ImGui::TextDisabled(STR_C("Drive: events %s+ and %s- switch it, also in the map"), track->name().c_str(), track->name().c_str()); drive_ui(*set->second);
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);
}
bool frogs{track->SwitchExtension->m_frogs}; bool frogs{track->SwitchExtension->m_frogs};
if (ImGui::Checkbox(STR_C("Frog and check rails made from the rails"), &frogs)) if (ImGui::Checkbox(STR_C("Frog and check rails made from the rails"), &frogs))
{ {
@@ -989,9 +1015,10 @@ void editor_mode::render_turnout_ui()
if (tool.templates.empty()) if (tool.templates.empty())
return; return;
m_turnout_template = std::clamp(m_turnout_template, 0, static_cast<int>(tool.templates.size()) - 1); m_turnout_template = std::clamp(m_turnout_template, 0, static_cast<int>(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()) if (ordinary == tool.templates.end())
return; return;
m_turnout_template = static_cast<int>(ordinary - tool.templates.begin()); m_turnout_template = static_cast<int>(ordinary - tool.templates.begin());
@@ -1002,18 +1029,43 @@ void editor_mode::render_turnout_ui()
{ {
for (int i = 0; i < static_cast<int>(tool.templates.size()); ++i) for (int i = 0; i < static_cast<int>(tool.templates.size()); ++i)
{ {
if (tool.templates[i].double_slip) if (false == fits(tool.templates[i]))
continue; 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)) if (ImGui::Selectable(label.c_str(), i == m_turnout_template))
m_turnout_template = i; m_turnout_template = i;
} }
ImGui::EndCombo(); ImGui::EndCombo();
} }
ImGui::PopItemWidth(); 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 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 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)}; 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) if (paths.size() < 2)
return; return;
for (int i = 0; i < 2; ++i) for (int i = 0; i < 2; ++i)
@@ -1036,7 +1088,30 @@ void editor_mode::render_turnout_ui()
replace(tool.templates[m_turnout_template], side); 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"); item_tooltip("Keeps the start and the direction of the switch, takes the shape of the chosen template");
ImGui::SameLine(); 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; editor_track::switch_template own;
own.source = track; own.source = track;
@@ -1056,16 +1131,16 @@ void editor_mode::render_turnout_ui()
replace(own, -side); replace(own, -side);
} }
item_tooltip("Mirrors the switch: the diverging track goes to the other 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::PushItemWidth(100.0f);
ImGui::InputDouble("##switchslide", &tool.slide_typed, 0.0, 0.0, "%.2f m"); ImGui::InputDouble("##switchslide", &tool.slide_typed, 0.0, 0.0, "%.2f m");
ImGui::PopItemWidth(); ImGui::PopItemWidth();
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button(STR_C("Slide along the straight"))) 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"); 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(); ImGui::Separator();
render_path_parameters(*track); render_path_parameters(*track);
@@ -3708,6 +3783,39 @@ void editor_mode::delete_selected_track()
ui()->set_status(STR_C("The track can't be deleted while vehicles stand on it"), true); ui()->set_status(STR_C("The track can't be deleted while vehicles stand on it"), true);
return; 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<TTrack *> 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 // retired first, so the infrastructure it carried and moves elsewhere is in the same undo step
if (auto const known{switch_store::of(*track)}) if (auto const known{switch_store::of(*track)})
switch_store::forget(known->name); switch_store::forget(known->name);
@@ -3765,7 +3873,9 @@ std::vector<editor_mode::key_hint> editor_mode::track_key_hints(bool const All)
else if (m_extend.active) else if (m_extend.active)
hints = {{"Drag", "lays the shape to the cursor, to a free end: joins it"}, {"Ctrl+wheel", "radius"}, {"Shift+wheel", "another shape"}, {"Release", "build"}, {"Esc", "cancel"}}; hints = {{"Drag", "lays the shape to the cursor, to a free end: joins it"}, {"Ctrl+wheel", "radius"}, {"Shift+wheel", "another shape"}, {"Release", "build"}, {"Esc", "cancel"}};
else if (m_switch.placing) else if (m_switch.placing)
hints = {{"Drag along", "direction of the switch"}, {"Drag sideways", "side of the diverging track"}, {"Esc", "cancel"}}; hints = m_switch.connection >= 0 && false == m_switch.curved ?
std::vector<key_hint>{{"Drag onto the other track", "the connection there"}, {"Shift+wheel", "another kind"}, {"Ctrl+wheel", "R of the aligning curve"}, {"Alt+wheel", "where a double crossover meets"}, {"Esc", "cancel"}} :
std::vector<key_hint>{{"Drag along", "direction of the switch"}, {"Drag sideways", "side of the diverging track"}, {"Esc", "cancel"}};
else if (m_track_tab == track_tab::lay) else if (m_track_tab == track_tab::lay)
hints = {{"LMB", "start, on the ground or at a free end"}}; hints = {{"LMB", "start, on the ground or at a free end"}};
else if (m_track_tab == track_tab::turnout && (selected_track() == nullptr || selected_track()->eType == tt_Switch)) else if (m_track_tab == track_tab::turnout && (selected_track() == nullptr || selected_track()->eType == tt_Switch))
@@ -4725,6 +4835,25 @@ void editor_mode::render_layout_combo()
bool editor_mode::track_wheel(double const Offset) bool editor_mode::track_wheel(double const Offset)
{ {
auto const &io{ImGui::GetIO()}; auto const &io{ImGui::GetIO()};
if (m_switch.placing && m_switch.connection >= 0 && Offset != 0.0 && (io.KeyCtrl || io.KeyShift || io.KeyAlt))
{
auto &tool{m_switch};
int const step{Offset > 0.0 ? 1 : -1};
if (io.KeyShift)
tool.connection = (tool.connection + step + connection::kind_count) % connection::kind_count;
else if (io.KeyCtrl)
{
static double const radii[]{0.0, 150.0, 190.0, 250.0, 300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1200.0, 1500.0, 2000.0, 3000.0};
int index{0};
for (int i = 0; i < static_cast<int>(std::size(radii)); ++i)
if (radii[i] <= tool.connection_radius + 1e-6)
index = i;
tool.connection_radius = radii[std::clamp(index + step, 0, static_cast<int>(std::size(radii)) - 1)];
}
else
tool.connection_together = false == tool.connection_together;
return true;
}
if ((false == io.KeyCtrl && false == io.KeyShift) || Offset == 0.0) if ((false == io.KeyCtrl && false == io.KeyShift) || Offset == 0.0)
return false; return false;
bool const laying{m_lay.active && false == m_lay.points.empty()}; bool const laying{m_lay.active && false == m_lay.points.empty()};
@@ -5318,6 +5447,18 @@ bool editor_mode::start_switch_placement()
void editor_mode::finish_switch_placement() void editor_mode::finish_switch_placement()
{ {
auto &tool{m_switch}; auto &tool{m_switch};
if (tool.connection >= 0)
{
if (false == tool.curved)
{
insert_connection();
return;
}
tool.placing = false;
tool.status = STR_C("A connection is laid between straights");
ui()->set_status(tool.status, true);
return;
}
switch_link plan; switch_link plan;
auto const linked{plan_switch_link(plan)}; auto const linked{plan_switch_link(plan)};
tool.placing = false; tool.placing = false;
@@ -5373,7 +5514,25 @@ void editor_mode::switch_placed(std::vector<TTrack *> const &Created)
m_node = *found; m_node = *found;
ui()->set_node(m_node); ui()->set_node(m_node);
if (slip) if (slip)
{
auto const known{switch_store::named(**found)};
if (false == known.has_value() || known->kind != "double")
return;
std::vector<std::string> 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; return;
}
if (place_switch_drive(**found)) if (place_switch_drive(**found))
join_last_steps(); join_last_steps();
advance_switch_name(); advance_switch_name();
@@ -5524,30 +5683,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; }; 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<double>(Direction)}; glm::dvec2 const direction{Line.direction * static_cast<double>(Direction)};
auto const paths{editor_track::place_switch(shape, point(origin), direction, Side, Line.grade * 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)}; auto const made{create_switch_set(shape, paths, Style != nullptr ? *Style : *first, Name)};
if (track != nullptr) Created.insert(Created.end(), made.begin(), made.end());
{
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");
return true; return true;
} }
std::vector<TTrack *> editor_mode::create_switch_set(editor_track::switch_template const &Shape, std::vector<segment_data> 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 void editor_mode::draw_build_overlay() const
{ {
screen_projection const projection; screen_projection const projection;
ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport());
auto const drawpath = [&](segment_data const &Path, ImU32 const Color) { auto const drawpath = [&](segment_data const &Path, ImU32 const Color) {
auto previous{bezier{Path}.point(0.0)}; for (auto const &piece : Path.pieces.empty() ? std::vector<segment_data>{Path} : Path.pieces)
for (int i = 1; i <= 24; ++i)
{ {
auto const next{bezier{Path}.point(i / 24.0)}; bezier const curve{piece};
projection.line(drawlist, previous, next, Color, 3.0f); auto previous{curve.point(0.0)};
previous = next; 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) if (false == GfxRenderer->Mouse_Hit() && false == ImGui::GetIO().WantCaptureMouse)
@@ -5620,7 +5808,40 @@ void editor_mode::draw_build_overlay() const
drawpath(piece, overlay_color::marked); drawpath(piece, overlay_color::marked);
} }
} }
if (m_switch.placing) if (m_switch.placing && m_switch.connection >= 0 && false == m_switch.curved)
{
connection::request request;
connection::result plan;
auto const valid{plan_connection(request, plan)};
if (valid)
{
for (auto const &entry : plan.switches)
for (auto const &path : entry.paths)
drawpath(path, overlay_color::marked);
for (auto const &insert : plan.inserts)
drawpath(insert, overlay_color::grip);
if (plan.crossing)
for (int i = 0; i < 4; i += 2)
{
segment_data path;
path.points[segment_data::point::start] = plan.ends[i];
path.points[segment_data::point::end] = plan.ends[i + 1];
drawpath(path, overlay_color::grip);
}
}
else
for (auto const &piece : switch_preview())
drawpath(piece, overlay_color::invalid);
ImVec2 screen;
auto const text{valid ? connection_readout(plan) : plan.error.empty() ? std::string{STR_C("Drag onto the other track")} : plan.error};
if (projection.project(m_switch.mouse, screen))
{
auto const size{ImGui::CalcTextSize(text.c_str())};
drawlist->AddRectFilled(ImVec2(screen.x + 14.0f, screen.y + 10.0f), ImVec2(screen.x + 22.0f + size.x, screen.y + 14.0f + size.y), IM_COL32(0, 0, 0, 180), 3.0f);
drawlist->AddText(ImVec2(screen.x + 18.0f, screen.y + 12.0f), valid ? (plan.warning.empty() ? overlay_color::highlight : overlay_color::warning) : overlay_color::invalid, text.c_str());
}
}
else if (m_switch.placing)
{ {
for (auto const &piece : switch_preview()) for (auto const &piece : switch_preview())
drawpath(piece, overlay_color::marked); drawpath(piece, overlay_color::marked);
@@ -5724,7 +5945,7 @@ void editor_mode::render_switch_ui()
auto const &entry{tool.templates[i]}; auto const &entry{tool.templates[i]};
if (entry.double_slip != Slips) if (entry.double_slip != Slips)
continue; 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); label += "##switch" + std::to_string(i);
if (ImGui::Selectable(label.c_str(), tool.armed == i)) if (ImGui::Selectable(label.c_str(), tool.armed == i))
{ {
@@ -5747,6 +5968,7 @@ void editor_mode::render_switch_ui()
list(true); list(true);
} }
} }
render_connection_ui();
ImGui::PushItemWidth(120.0f); ImGui::PushItemWidth(120.0f);
ImGui::InputTextWithHint(STR_C("Name##switchname"), switch_name_for_new().c_str(), tool.name, sizeof(tool.name)); ImGui::InputTextWithHint(STR_C("Name##switchname"), switch_name_for_new().c_str(), tool.name, sizeof(tool.name));
ImGui::PopItemWidth(); ImGui::PopItemWidth();
@@ -5765,6 +5987,10 @@ void editor_mode::render_switch_ui()
arm_switch(); arm_switch();
} }
item_tooltip("Looks for the switches of the scenery, to use their shapes as the templates"); 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<glm::dvec3> editor_mode::detour_outline() const std::vector<glm::dvec3> editor_mode::detour_outline() const
@@ -6015,9 +6241,7 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const
if (std::abs(lateral) < 0.01) if (std::abs(lateral) < 0.01)
return false; return false;
auto const side{lateral > 0.0 ? 1.0 : -1.0}; auto const side{lateral > 0.0 ? 1.0 : -1.0};
auto const normal{perpendicular * side}; auto const &control{(diverging.pieces.empty() ? diverging : diverging.pieces.back()).points[segment_data::point::control2]};
auto const offset{std::abs(lateral)};
auto const &control{diverging.points[segment_data::point::control2]};
auto const tangent{glm::normalize(control != glm::dvec3{} ? -plan_of(control) : plan_of(frog - origin))}; auto const tangent{glm::normalize(control != glm::dvec3{} ? -plan_of(control) : plan_of(frog - origin))};
auto const angle{std::atan2(std::abs(cross(axis, tangent)), glm::dot(axis, tangent))}; auto const angle{std::atan2(std::abs(cross(axis, tangent)), glm::dot(axis, tangent))};
if (angle < 1e-4) if (angle < 1e-4)
@@ -6055,22 +6279,6 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const
auto const &line{Plan.target}; auto const &line{Plan.target};
if (line.tracks.empty() || std::abs(cross(line.direction, axis)) > std::sin(glm::radians(1.0))) if (line.tracks.empty() || std::abs(cross(line.direction, axis)) > std::sin(glm::radians(1.0)))
return false; return false;
auto const distance{glm::dot(plan_of(line.start) - plan_of(origin), normal)};
auto const remaining{distance - 2.0 * offset};
if (remaining < 0.01)
return false;
auto const length{remaining / std::sin(angle)};
glm::dvec2 const crossing{axis * std::cos(angle) + normal * std::sin(angle)};
auto const alongfrog{glm::dot(plan_of(frog - origin), axis)};
auto const otherfrog{plan_of(frog) + crossing * length};
auto const otherorigin{otherfrog + axis * alongfrog + normal * offset};
Plan.along = glm::dot(otherorigin - plan_of(line.start), line.direction);
if (Plan.along < 0.0 || Plan.along > line.length)
return false;
Plan.direction = glm::dot(-axis, line.direction) > 0.0 ? 1 : -1;
glm::dvec2 const direction{line.direction * static_cast<double>(Plan.direction)};
Plan.side = glm::dot(glm::dvec2{-direction.y, direction.x}, -normal) > 0.0 ? 1 : -1;
editor_track::switch_template shape; editor_track::switch_template shape;
shape.source = tool.track; shape.source = tool.track;
shape.length = glm::dot(plan_of(main.points[segment_data::point::end] - origin), axis); shape.length = glm::dot(plan_of(main.points[segment_data::point::end] - origin), axis);
@@ -6089,11 +6297,14 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const
} }
} }
Plan.shape = shape; Plan.shape = shape;
auto const station{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * Plan.along}; auto const faced{connection::face(frog, tangent, connection::of(line), 1, shape)};
Plan.paths = editor_track::place_switch(shape, station, direction, Plan.side, line.grade * Plan.direction); if (false == faced.valid)
Plan.insert = segment_data{}; return false;
Plan.insert.points[segment_data::point::start] = frog; Plan.along = faced.partner.along;
Plan.insert.points[segment_data::point::end] = Plan.paths[1].points[segment_data::point::end]; Plan.direction = faced.partner.forward;
Plan.side = faced.partner.side;
Plan.paths = faced.partner.paths;
Plan.insert = faced.insert;
return true; return true;
} }
@@ -6116,7 +6327,7 @@ bool editor_mode::stretchable(editor_track::snap_target const &Target) const
bool editor_mode::plan_switch_link(switch_link &Plan) const bool editor_mode::plan_switch_link(switch_link &Plan) const
{ {
auto const &tool{m_switch}; auto const &tool{m_switch};
if (false == tool.placing || tool.curved || tool.armed < 0 || tool.armed >= static_cast<int>(tool.templates.size()) || tool.line.tracks.empty() || tool.templates[tool.armed].double_slip) if (false == tool.placing || tool.curved || tool.connection >= 0 || tool.armed < 0 || tool.armed >= static_cast<int>(tool.templates.size()) || tool.line.tracks.empty() || tool.templates[tool.armed].double_slip)
return false; return false;
auto const &shape{tool.templates[tool.armed]}; auto const &shape{tool.templates[tool.armed]};
auto const &line{tool.line}; auto const &line{tool.line};
@@ -6697,13 +6908,11 @@ std::vector<segment_data> 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}; path.points[segment_data::point::control2] = {-Enddirection.x * handle, -(End.y - Start.y) / 3.0, -Enddirection.y * handle};
return path; return path;
}; };
std::vector<segment_data> result; auto const bend = [&](segment_data const &Local) {
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 &start{local.points[segment_data::point::start]}; auto const &control1{Local.points[segment_data::point::control1]};
auto const &end{local.points[segment_data::point::end]}; auto const &control2{Local.points[segment_data::point::control2]};
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}; 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 localstart{glm::normalize(control1 != glm::dvec3{} ? plan_of(control1) : chord)};
auto const localend{glm::normalize(control2 != glm::dvec3{} ? -plan_of(control2) : chord)}; auto const localend{glm::normalize(control2 != glm::dvec3{} ? -plan_of(control2) : chord)};
@@ -6729,19 +6938,35 @@ std::vector<segment_data> editor_mode::curved_switch_paths(editor_track::switch_
auto const rollstart{sample_at(Frame, Station + Direction * start.z).roll * Direction}; auto const rollstart{sample_at(Frame, Station + Direction * start.z).roll * Direction};
auto const rollend{sample_at(Frame, Station + Direction * end.z).roll * Direction}; auto const rollend{sample_at(Frame, Station + Direction * end.z).roll * Direction};
path.rolls = {static_cast<float>(rollstart), static_cast<float>(rollend)}; path.rolls = {static_cast<float>(rollstart), static_cast<float>(rollend)};
path.radius = local.radius; path.radius = Local.radius;
{ {
bezier const curve{path}; bezier const curve{path};
auto const begin{plan_of(curve.first(0.0))}; auto const begin{plan_of(curve.first(0.0))};
auto const finish{plan_of(curve.first(1.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}; 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) if (Local.radius != 0.f && std::abs(turn) > 1e-6)
path.radius = static_cast<float>(std::copysign(curve.plan_length() / std::abs(turn), static_cast<double>(local.radius))); path.radius = static_cast<float>(std::copysign(curve.plan_length() / std::abs(turn), static_cast<double>(Local.radius)));
} }
return path;
};
std::vector<segment_data> 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<segment_data> 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); result.push_back(path);
} }
if (false == result.empty())
result.front().radius = sample_at(Frame, Station).radius;
return result; return result;
} }
@@ -6799,16 +7024,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 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())}; auto const made{create_switch_set(shape, paths, *tool.frame_tracks.front(), switch_name_for_new())};
if (track != nullptr) created.insert(created.end(), made.begin(), made.end());
{ if (made.size() == 1)
editor_track::commit({track}); tool.status = STR_C("Switch ") + made.front()->name() + STR_C(" inserted into the curve");
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 placed{created}; auto const placed{created};
push_track_snapshot(std::move(states), std::move(created), std::move(removed)); push_track_snapshot(std::move(states), std::move(created), std::move(removed));
straight_refresh(); straight_refresh();

View File

@@ -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) void editor_mode::track_gizmo_measure_rotation(TTrack &Track, bool const Pointmode)
{ {
auto &drag{m_track_gizmo_drag}; 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); track_gizmo_take_partners(Track);
if (false == whole && Track.eType == tt_Switch && m_track_drag_connected) if (false == whole && Track.eType == tt_Switch && m_track_drag_connected)
track_gizmo_take_switch_joints(Track, controlmode); track_gizmo_take_switch_joints(Track, controlmode);
if (whole)
track_gizmo_take_double(Track);
if (m_track_drag_connected) if (m_track_drag_connected)
track_gizmo_take_switches(Track); track_gizmo_take_switches(Track);
std::vector<std::pair<TTrack *, editor_track::state>> states; std::vector<std::pair<TTrack *, editor_track::state>> states;

View File

@@ -0,0 +1,626 @@
/*
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/editorConnections.hpp"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "utilities/translation.h"
#include <algorithm>
#include <cmath>
#include <limits>
namespace connection
{
namespace
{
using geometry::bezier;
using geometry::cross;
using geometry::grade_of;
using geometry::has_controls;
using geometry::plan_of;
using geometry::signed_angle;
double constexpr kStraightTolerance{2e-3};
double constexpr kCrossingHalf{2.0};
struct branch
{
glm::dvec3 end{0.0};
glm::dvec2 heading{0.0, 1.0};
double grade{0.0};
double tail{0.0}; // straight at the end of the diverging track
};
bool plain(segment_data const &Piece)
{
if (false == has_controls(Piece))
return true;
bezier const curve{Piece};
auto const first{plan_of(curve.first(0.0))};
auto const last{plan_of(curve.first(1.0))};
if (glm::length(first) < 1e-9 || glm::length(last) < 1e-9)
return true;
return std::abs(signed_angle(first, last)) < 1e-6;
}
branch branch_of(std::vector<segment_data> const &Paths)
{
branch result;
if (Paths.size() < 2)
return result;
auto const &path{Paths[1]};
result.end = path.points[segment_data::point::end];
auto const &last{path.pieces.empty() ? path : path.pieces.back()};
auto tangent{bezier{last}.first(1.0)};
if (glm::length(plan_of(tangent)) < 1e-9)
tangent = last.points[segment_data::point::end] - last.points[segment_data::point::start];
result.heading = glm::normalize(plan_of(tangent));
result.grade = grade_of(tangent);
for (auto piece{path.pieces.rbegin()}; piece != path.pieces.rend() && plain(*piece); ++piece)
result.tail += bezier{*piece}.plan_length();
return result;
}
glm::dvec2 middle_point(placed const &Switch)
{
auto const &main{Switch.paths[0]};
auto const origin{plan_of(main.points[segment_data::point::start])};
auto const axis{glm::normalize(plan_of(main.points[segment_data::point::end] - main.points[segment_data::point::start]))};
auto const diverging{branch_of(Switch.paths)};
auto const determinant{cross(diverging.heading, axis)};
if (std::abs(determinant) < 1e-9)
return origin;
return origin + axis * (cross(diverging.heading, plan_of(diverging.end) - origin) / determinant);
}
glm::dvec3 start_of(placed const &Switch)
{
return Switch.paths[0].points[segment_data::point::start];
}
bool fits(line const &Line, double const Along, int const Forward, double const Length)
{
auto const from{Forward > 0 ? Along : Along - Length};
return from >= -1e-3 && from + Length <= Line.length + 1e-3;
}
placed put(line const &Line, int const Index, double const Along, int const Forward, int const Side, editor_track::switch_template const &Shape, bool const Second)
{
placed result;
result.line = Index;
result.along = Along;
result.forward = Forward;
result.side = Side;
result.second = Second;
result.paths = editor_track::place_switch(Shape, Line.point(Along), Line.direction * static_cast<double>(Forward), Side, Line.grade * Forward);
return result;
}
segment_data straight(glm::dvec3 const &From, glm::dvec3 const &To)
{
segment_data path;
path.points[segment_data::point::start] = From;
path.points[segment_data::point::end] = To;
return path;
}
double sharper(editor_track::switch_template const &One, editor_track::switch_template const &Two)
{
double result{0.0};
for (auto const radius : {One.radius, Two.radius})
if (radius > 1.0 && (result == 0.0 || radius < result))
result = radius;
return result > 0.0 ? result : 190.0;
}
struct crossing_over
{
bool valid{false};
bool straight{false};
std::string error;
std::string warning;
placed one;
placed two;
std::vector<segment_data> inserts;
std::vector<double> straights;
double radius{0.0};
};
// the first switch on From diverging towards To and the switch on To facing it, joined by a straight insert when their angles
// agree, by an aligning curve between straight inserts when they don't, the switch on To then starting as near Target as it can
crossing_over cross_over(line const &From, int const Fromindex, double const Along, int const Forward, editor_track::switch_template const &Fromshape, bool const Fromsecond, line const &To, int const Toindex,
editor_track::switch_template const &Toshape, bool const Tosecond, double const Target, request const &Request)
{
crossing_over result;
if (false == fits(From, Along, Forward, Fromshape.length))
{
result.error = STR_C("A switch of the connection runs off the end of its straight");
return result;
}
glm::dvec2 const direction{From.direction * static_cast<double>(Forward)};
auto const origin{From.point(Along)};
auto const nearest{To.point(To.along(origin))};
auto const side{cross(direction, plan_of(nearest) - plan_of(origin)) > 0.0 ? 1 : -1};
result.one = put(From, Fromindex, Along, Forward, side, Fromshape, Fromsecond);
if (result.one.paths.size() < 2)
{
result.error = STR_C("The switch template has no diverging track");
return result;
}
auto const diverging{branch_of(result.one.paths)};
auto faced{face(diverging.end, diverging.heading, To, Toindex, Toshape)};
if (faced.valid)
{
faced.partner.second = Tosecond;
result.two = faced.partner;
result.straight = true;
if (faced.length >= 0.01)
result.inserts.push_back(faced.insert);
result.straights.push_back(faced.length);
auto const curves{faced.length + diverging.tail + branch_of(result.two.paths).tail};
if (curves < Request.insert - 1e-3)
result.warning = format(STR_C("The straight between the curves of the switches is %.2f m, under %.0f m"), curves, Request.insert);
result.valid = true;
return result;
}
if (faced.partner.paths.empty())
{
result.error = faced.error;
return result;
}
auto const forward{faced.partner.forward};
auto const otherside{faced.partner.side};
auto const radius{Request.radius > 0.0 ? Request.radius : sharper(Fromshape, Toshape)};
auto const trial{branch_of(put(To, Toindex, 0.0, forward, otherside, Toshape, Tosecond).paths)};
auto const into{-trial.heading};
auto const turn{signed_angle(diverging.heading, into)};
auto const determinant{cross(diverging.heading, into)};
if (std::abs(determinant) < 1e-9 || std::abs(turn) > glm::radians(60.0))
{
result.error = STR_C("The switches can't face each other across these tracks");
return result;
}
auto const tangent{radius * std::tan(std::abs(turn) * 0.5)};
auto const offset{plan_of(trial.end) - plan_of(diverging.end)};
auto const reach{cross(offset, into) / determinant};
auto const after{cross(diverging.heading, offset) / determinant};
auto const reachrate{cross(To.direction, into) / determinant};
auto const afterrate{cross(diverging.heading, To.direction) / determinant};
auto low{forward > 0 ? 0.0 : Toshape.length};
auto high{forward > 0 ? To.length - Toshape.length : To.length};
auto const bound = [&](double const Value, double const Rate, double const Need) {
if (std::abs(Rate) < 1e-12)
{
if (Value - tangent < Need - 1e-6)
{
low = std::numeric_limits<double>::infinity();
high = -low;
}
return;
}
auto const limit{(Need + tangent - Value) / Rate};
if (Rate > 0.0)
low = std::max(low, limit);
else
high = std::min(high, limit);
};
bound(reach, reachrate, std::max(0.0, Request.insert - diverging.tail));
bound(after, afterrate, std::max(0.0, Request.insert - trial.tail));
if (low > high + 1e-6)
{
result.error = format(STR_C("The aligning curve R %.0f m with %.0f m straights doesn't fit between the switches"), radius, Request.insert);
return result;
}
result.two = put(To, Toindex, std::clamp(Target, low, std::max(low, high)), forward, otherside, Toshape, Tosecond);
auto const arrived{branch_of(result.two.paths)};
auto const link{editor_track::link_branch({diverging.end, diverging.heading, diverging.grade, 0.0}, radius, {arrived.end, -arrived.heading, -arrived.grade, 0.0}, 1e-4, Request.insert, false)};
if (false == link.valid || link.pieces.empty() || link.trim != 0.0 || link.reverse)
{
result.error = format(STR_C("The aligning curve R %.0f m with %.0f m straights doesn't fit between the switches"), radius, Request.insert);
return result;
}
result.inserts = link.pieces;
result.straights = {link.before, link.after};
result.radius = radius;
result.valid = true;
return result;
}
void take(result &Result, crossing_over const &Part, bool const Inserts)
{
Result.switches.push_back(Part.one);
Result.switches.push_back(Part.two);
if (Inserts)
Result.inserts.insert(Result.inserts.end(), Part.inserts.begin(), Part.inserts.end());
Result.straights.insert(Result.straights.end(), Part.straights.begin(), Part.straights.end());
if (Part.radius > 0.0)
Result.radius = Part.radius;
if (Result.warning.empty())
Result.warning = Part.warning;
}
bool overlapping(std::vector<placed> const &Switches, std::vector<line> const &Lines)
{
for (std::size_t i = 0; i < Switches.size(); ++i)
for (std::size_t j = i + 1; j < Switches.size(); ++j)
{
auto const &one{Switches[i]};
auto const &two{Switches[j]};
if (one.line != two.line || one.line < 0 || one.line >= static_cast<int>(Lines.size()))
continue;
auto const &line{Lines[one.line]};
auto const span = [&](placed const &Switch) {
auto const a{line.along(Switch.paths[0].points[segment_data::point::start])};
auto const b{line.along(Switch.paths[0].points[segment_data::point::end])};
return std::pair{std::min(a, b), std::max(a, b)};
};
auto const [from1, to1]{span(one)};
auto const [from2, to2]{span(two)};
if (from2 < to1 - 0.01 && from1 < to2 - 0.01)
return true;
}
return false;
}
} // namespace
char const *word(kind const Kind)
{
switch (Kind)
{
case kind::double_crossover: return "double_crossover";
case kind::scissors: return "scissors";
case kind::ladder: return "ladder";
default: return "crossover";
}
}
bool parse(std::string const &Word, kind &Kind)
{
for (int i = 0; i < kind_count; ++i)
if (Word == word(static_cast<kind>(i)))
{
Kind = static_cast<kind>(i);
return true;
}
return false;
}
glm::dvec3 line::point(double const Along) const
{
return start + glm::dvec3{direction.x, grade, direction.y} * Along;
}
double line::along(glm::dvec3 const &Point) const
{
return glm::dot(plan_of(Point) - plan_of(start), direction);
}
double line::lateral(glm::dvec3 const &Point) const
{
return cross(direction, plan_of(Point) - plan_of(start));
}
line of(editor_track::straight const &Straight)
{
line result;
result.start = Straight.start;
result.direction = Straight.direction;
result.grade = Straight.grade;
result.length = Straight.length;
return result;
}
facing face(glm::dvec3 const &End, glm::dvec2 const &Heading, line const &To, int const Index, editor_track::switch_template const &Shape)
{
facing result;
auto const forward{glm::dot(To.direction, Heading) > 0.0 ? -1 : 1};
glm::dvec2 const direction{To.direction * static_cast<double>(forward)};
auto const side{cross(direction, plan_of(End) - plan_of(To.start)) > 0.0 ? 1 : -1};
auto const trial{put(To, Index, 0.0, forward, side, Shape, true)};
if (trial.paths.size() < 2)
{
result.error = STR_C("The switch template has no diverging track");
return result;
}
auto const diverging{branch_of(trial.paths)};
auto const mismatch{signed_angle(Heading, -diverging.heading)};
if (std::abs(mismatch) > kStraightTolerance)
{
result.partner = trial;
result.error = format(STR_C("The diverging tracks meet at %.2f deg, a straight insert can't join them"), glm::degrees(std::abs(mismatch)));
return result;
}
auto const across{cross(Heading, To.direction)};
if (std::abs(across) < 1e-9)
{
result.error = STR_C("The diverging track runs along the other track");
return result;
}
auto const shift{-cross(Heading, plan_of(diverging.end) - plan_of(End)) / across};
auto const meeting{plan_of(diverging.end) + To.direction * shift};
result.length = glm::dot(Heading, meeting - plan_of(End));
if (result.length < -0.005)
{
result.error = format(STR_C("The tracks are %.2f m too close for these switches"), -result.length * std::abs(across));
return result;
}
result.length = std::max(0.0, result.length);
if (false == fits(To, shift, forward, Shape.length))
{
result.error = STR_C("The switch on the other track runs off the end of its straight");
return result;
}
result.partner = put(To, Index, shift, forward, side, Shape, true);
result.insert = straight(End, result.partner.paths[1].points[segment_data::point::end]);
result.valid = true;
return result;
}
result plan(request const &Request)
{
result out;
auto const usable = [](editor_track::switch_template const &Shape) { return false == Shape.double_slip && false == Shape.twofold() && Shape.length > 0.5; };
if (false == usable(Request.first) || false == usable(Request.second))
{
out.error = STR_C("A connection is made of ordinary switches, not of slips, crossings or double switches");
return out;
}
auto const &from{Request.from};
glm::dvec2 const travel{from.direction * static_cast<double>(Request.forward)};
auto const origin{from.point(Request.along)};
std::vector<line> lines{from};
lines.insert(lines.end(), Request.to.begin(), Request.to.end());
if (Request.type == kind::ladder)
{
auto side{Request.side >= 0 ? 1 : -1};
std::vector<double> offsets;
if (false == Request.to.empty())
{
auto const &first{Request.to.front()};
side = cross(travel, plan_of(first.point(first.along(origin))) - plan_of(origin)) > 0.0 ? 1 : -1;
for (auto const &fan : Request.to)
{
auto const offset{side * cross(travel, plan_of(fan.point(fan.along(origin))) - plan_of(origin))};
if (offset < 1.0 || (false == offsets.empty() && offset < offsets.back() + 1.0) || std::abs(cross(fan.direction, from.direction)) > 1e-3)
{
out.error = STR_C("The tracks of a fan have to be parallel, on one side and at least 1 m apart");
return out;
}
offsets.push_back(offset);
}
}
else
{
double sum{0.0};
for (auto const spacing : Request.spacings)
{
if (spacing < 1.0)
{
out.error = STR_C("The spacings of the tracks of a ladder have to be at least 1 m");
return out;
}
offsets.push_back(sum += spacing);
}
if (offsets.empty())
{
out.error = STR_C("Give the spacings of the tracks of the ladder");
return out;
}
}
if (false == fits(from, Request.along, Request.forward, Request.first.length))
{
out.error = STR_C("A switch of the connection runs off the end of its straight");
return out;
}
auto const start{put(from, 0, Request.along, Request.forward, side, Request.first, false)};
auto const ladder{branch_of(start.paths)};
auto const climb{side * cross(travel, ladder.heading)};
if (climb < 1e-4)
{
out.error = STR_C("The switch template has no diverging track");
return out;
}
out.switches.push_back(start);
auto const lateral = [&](glm::dvec3 const &Point) { return side * cross(travel, plan_of(Point) - plan_of(origin)); };
glm::dvec3 const step{ladder.heading.x, ladder.grade, ladder.heading.y};
auto const rung = [&](double const Along) { return ladder.end + step * Along; };
auto const back{cross(ladder.heading, travel) > 0.0 ? 1 : -1};
auto const radius{Request.radius > 0.0 ? Request.radius : sharper(Request.second, Request.second)};
auto const settle = [&](glm::dvec3 const &Point, glm::dvec2 const &Heading, double const Grade) {
auto const turn{signed_angle(Heading, travel)};
return std::abs(turn) > kStraightTolerance ? geometry::arc_pieces(Point, Heading, Grade, radius, std::abs(turn), turn > 0.0 ? 1 : -1) : std::vector<segment_data>{};
};
double reached{0.0};
for (std::size_t i = 0; i < offsets.size(); ++i)
{
bool const last{i + 1 == offsets.size()};
std::vector<segment_data> paths;
std::vector<segment_data> bend;
glm::dvec3 landing{0.0};
if (false == last)
{
paths = editor_track::place_switch(Request.second, rung(0.0), ladder.heading, back, ladder.grade);
auto const diverging{branch_of(paths)};
bend = settle(diverging.end, diverging.heading, diverging.grade);
landing = bend.empty() ? diverging.end : bend.back().points[segment_data::point::end];
}
else
{
bend = settle(rung(0.0), ladder.heading, ladder.grade);
landing = bend.empty() ? rung(0.0) : bend.back().points[segment_data::point::end];
}
auto const along{(offsets[i] - lateral(landing)) / climb};
if (along < reached - 1e-3)
{
out.error = format(STR_C("Track %d of the fan is %.2f m too close to the previous one for these switches"), static_cast<int>(i + 1), (reached - along) * climb);
return out;
}
auto const shift{step * along};
if (along - reached >= 0.01)
{
out.inserts.push_back(straight(rung(reached), rung(along)));
out.straights.push_back(along - reached);
}
for (auto &path : paths)
path.translate(shift);
for (auto &piece : bend)
piece.translate(shift);
if (false == last)
{
placed rungswitch;
rungswitch.line = -1;
rungswitch.along = along;
rungswitch.side = back;
rungswitch.second = true;
rungswitch.paths = paths;
out.switches.push_back(rungswitch);
reached = along + Request.second.length;
}
if (false == bend.empty())
out.radius = radius;
out.inserts.insert(out.inserts.end(), bend.begin(), bend.end());
auto const fanstart{landing + shift};
if (i < Request.to.size())
{
auto const &fan{Request.to[i]};
auto end{fan.start};
auto ahead{glm::dot(travel, plan_of(fan.start) - plan_of(fanstart))};
auto const other{fan.point(fan.length)};
if (auto const further{glm::dot(travel, plan_of(other) - plan_of(fanstart))}; further < ahead)
{
end = other;
ahead = further;
}
if (ahead < -0.01)
{
out.error = format(STR_C("Track %d of the fan reaches into the ladder, it has to start %.2f m further on"), static_cast<int>(i + 1), -ahead);
return out;
}
if (ahead >= 0.01)
out.inserts.push_back(straight(fanstart, end));
}
else
out.inserts.push_back(straight(fanstart, fanstart + glm::dvec3{travel.x, from.grade * Request.forward, travel.y} * std::max(1.0, Request.stub)));
}
out.fan = offsets;
out.spacing = offsets.front();
out.valid = true;
return out;
}
if (Request.to.empty())
{
out.error = STR_C("Drag onto the other track");
return out;
}
auto const &to{Request.to.front()};
out.spacing = std::abs(to.lateral(origin));
auto const one{cross_over(from, 0, Request.along, Request.forward, Request.first, false, to, 1, Request.second, true, Request.target, Request)};
if (false == one.valid)
{
out.error = one.error;
return out;
}
out.points = glm::distance(middle_point(one.one), middle_point(one.two));
if (Request.type == kind::crossover)
{
take(out, one, true);
out.valid = true;
return out;
}
auto const towards{glm::dot(to.direction, travel) > 0.0 ? 1 : -1};
auto const span{glm::dot(travel, plan_of(start_of(one.two)) - plan_of(start_of(one.one)))};
if (Request.type == kind::double_crossover)
{
crossing_over two;
if (false == Request.together)
{
auto const along{one.two.along + towards * std::max(0.0, Request.gap)};
two = cross_over(to, 1, along, towards, Request.second, true, from, 0, Request.first, false, from.along(to.point(along)) + Request.forward * span, Request);
}
else
{
auto const along{Request.along - Request.forward * std::max(0.0, Request.gap)};
two = cross_over(from, 0, along, -Request.forward, Request.first, false, to, 1, Request.second, true, to.along(from.point(along)) - towards * span, Request);
}
if (false == two.valid)
{
out.error = two.error;
return out;
}
take(out, one, true);
take(out, two, true);
if (overlapping(out.switches, lines))
{
out.error = STR_C("The switches of the two crossovers overlap, the gap between them has to be longer");
return out;
}
out.valid = true;
return out;
}
if (false == one.straight || std::abs(cross(from.direction, to.direction)) > 1e-4)
{
out.error = STR_C("A scissors crossover needs parallel tracks and switches of the same angle");
return out;
}
auto const p1{branch_of(one.one.paths).end};
auto const p2{branch_of(one.two.paths).end};
auto const middle{(p1 + p2) * 0.5};
auto const two{cross_over(from, 0, 2.0 * from.along(middle) - Request.along, -Request.forward, Request.first, false, to, 1, Request.second, true, 0.0, Request)};
if (false == two.valid || false == two.straight)
{
out.error = two.valid ? std::string{STR_C("A scissors crossover needs parallel tracks and switches of the same angle")} : two.error;
return out;
}
take(out, one, false);
take(out, two, false);
if (overlapping(out.switches, lines))
{
out.error = STR_C("The tracks are too close for a scissors crossover with these switches, its switches overlap");
return out;
}
auto const q1{branch_of(two.one.paths).end};
auto const q2{branch_of(two.two.paths).end};
auto const first{plan_of(p2 - p1)};
auto const second{plan_of(q2 - q1)};
auto const determinant{cross(first, second)};
auto const sine{std::abs(cross(glm::normalize(travel), glm::length(first) > 1e-9 ? glm::normalize(first) : travel))};
auto const shortfall{(2.0 * kCrossingHalf - one.straights.front()) * sine};
if (std::abs(determinant) < 1e-9 || glm::length(first) < 2.0 * kCrossingHalf || glm::length(second) < 2.0 * kCrossingHalf)
{
out.error = format(STR_C("The tracks are too close for the crossing of a scissors crossover: %.2f m more spacing is needed"), std::max(0.01, shortfall));
return out;
}
auto const fraction{cross(plan_of(q1) - plan_of(p1), second) / determinant};
auto const meeting{plan_of(p1) + first * fraction};
auto const half{std::min({glm::distance(meeting, plan_of(p1)), glm::distance(meeting, plan_of(p2)), glm::distance(meeting, plan_of(q1)), glm::distance(meeting, plan_of(q2))})};
if (half < kCrossingHalf)
{
out.error = format(STR_C("The tracks are too close for the crossing of a scissors crossover: %.2f m more spacing is needed"), std::max(0.01, shortfall));
return out;
}
out.crossing = true;
out.ends = {p1, p2, q1, q2};
out.centre = {meeting.x, p1.y + (p2.y - p1.y) * fraction, meeting.y};
auto const unitone{glm::normalize(first)};
auto unittwo{glm::normalize(second)};
if (glm::dot(unitone, unittwo) < 0.0)
unittwo = -unittwo;
out.angle = std::atan2(std::abs(cross(unitone, unittwo)), glm::dot(unitone, unittwo));
out.valid = true;
return out;
}
} // namespace connection

View File

@@ -0,0 +1,107 @@
/*
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 <array>
#include <string>
#include <vector>
#include "editor/editorTrack.hpp"
// connections of the straight tracks of a station made of switches: a crossover of two switches joined by a straight insert or
// by an aligning curve between straight inserts, a double crossover of two crossovers one after the other, a scissors crossover
// of two crossovers around one centre with a crossing between their inserts, a ladder of switches leading onto a fan of tracks
namespace connection
{
enum class kind
{
crossover,
double_crossover,
scissors,
ladder
};
int constexpr kind_count{4};
char const *word(kind const Kind);
bool parse(std::string const &Word, kind &Kind);
struct line
{
glm::dvec3 start{0.0};
glm::dvec2 direction{0.0, 1.0};
double grade{0.0};
double length{0.0};
glm::dvec3 point(double const Along) const;
double along(glm::dvec3 const &Point) const;
// to the left of the direction
double lateral(glm::dvec3 const &Point) const;
};
line of(editor_track::straight const &Straight);
struct placed
{
int line{0}; // 0 the starting track, 1 the other one, or the tracks of a fan in their order from 1, -1 the ladder track
double along{0.0}; // of the start of the switch along its line
int forward{1};
int side{1};
bool second{false}; // made of the template of the other tracks
std::vector<segment_data> paths;
};
// the switch on a track facing the end of a diverging track and the straight insert joining the two
struct facing
{
bool valid{false};
placed partner;
segment_data insert;
double length{0.0};
std::string error;
};
facing face(glm::dvec3 const &End, glm::dvec2 const &Heading, line const &To, int const Index, editor_track::switch_template const &Shape);
struct request
{
kind type{kind::crossover};
line from; // the track the connection starts on
std::vector<line> to; // the other track; for a ladder the tracks of the fan, the nearest first, or none
double along{0.0}; // of the start of the first switch on the starting track
int forward{1};
int side{0}; // of a ladder with no tracks of the fan to lead onto
double target{0.0}; // where the switch on the other track starts along it when no straight insert fixes the place
editor_track::switch_template first;
editor_track::switch_template second; // on the other tracks and along the ladder
double radius{0.0}; // of the aligning curves and of the end of a ladder, 0: of the sharper switch
double insert{6.0}; // the shortest straight between curves turning the opposite ways
double gap{6.0}; // of a double crossover: between the starts of the switches meeting on one track
bool together{false}; // of a double crossover: the switches meet on the starting track, not on the other one
std::vector<double> spacings; // of a ladder with no tracks of the fan: from the starting track to the first one and on
double stub{25.0}; // of a ladder: length of the new tracks of the fan
};
struct result
{
bool valid{false};
std::string error;
std::string warning;
std::vector<placed> switches;
std::vector<segment_data> inserts;
std::vector<double> straights; // lengths of the straight inserts between the switches
double spacing{0.0}; // of the tracks at the start of the first switch
double points{0.0}; // between the mathematical points of the switches of the first crossover
double radius{0.0}; // of the aligning curve, 0 when a straight insert joins the switches
std::vector<double> fan; // offsets of the tracks of a ladder from the starting track
bool crossing{false}; // of a scissors crossover, at the meeting of its two inserts
std::array<glm::dvec3, 4> ends{}; // of the two inserts crossing: along the first one, then along the second one
glm::dvec3 centre{0.0};
double angle{0.0};
};
result plan(request const &Request);
} // namespace connection

View File

@@ -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 <algorithm>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <sstream>
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<float>(std::abs(Radius));
return result;
}
struct walker
{
glm::dvec2 at{0.0};
double heading{0.0};
double sharpest{0.0};
std::vector<segment_data> 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<int>(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<int>(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<part> 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<part> 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<part> &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<part> 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<design> 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<design> 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<design> own()
{
std::vector<design> 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<design> 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 <name> type <Rz|Rpd|Rpj> rail <rail> main <m> stagger <m> a <m> b <m> lead <m> entry <deg> [lead2 <m> entry2 <deg>] parts <count> (<length> <R at the start> <R at the end>)... parts2 <count> ...\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

View File

@@ -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 <string>
#include <vector>
#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<part> 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<part> 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<part> 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<segment_data> 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<design> catalogue();
// templates of the user, kept in eu07_switch_templates.txt in the configuration directory
std::vector<design> own();
bool save_own(std::vector<design> const &Designs, std::string &Error);
} // namespace switch_design

View File

@@ -39,7 +39,7 @@ bool g_changed{false};
bool g_loaded{false}; bool g_loaded{false};
// format of the lines, coordinates in the space of the file: // format of the lines, coordinates in the space of the file:
// <switch|slip|crossing> <name> <type> <rail> <R> <n of 1:n> <hand L|R|-> <a> <b> <R of the main track, + right, - left, 0 straight> // <switch|slip|crossing|double> <name> <type> <rail> <R> <n of 1:n> <hand L|R|-> <a> <b> <R of the main track, + right, - left, 0 straight>
// parts <count> <names>... shape <main end along, across> <diverging end along, across> start <x y z> dir <x z> // parts <count> <names>... shape <main end along, across> <diverging end along, across> start <x y z> dir <x z>
// km <km|-> number <label|-> source <designed|measured> // km <km|-> number <label|-> source <designed|measured>
std::string word(std::string const &Text) std::string word(std::string const &Text)
@@ -156,7 +156,7 @@ void load()
std::string km; std::string km;
std::size_t count{0}; std::size_t count{0};
line >> entry.kind; 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" && entry.kind != "connection")
continue; continue;
line >> entry.name >> entry.type >> entry.rail >> entry.radius >> entry.ratio >> hand >> entry.a >> entry.b >> entry.main_radius >> keyword >> count; line >> entry.name >> entry.type >> entry.rail >> entry.radius >> entry.ratio >> hand >> entry.a >> entry.b >> entry.main_radius >> keyword >> count;
if (line.fail() || keyword != "parts" || count > 64) if (line.fail() || keyword != "parts" || count > 64)
@@ -205,11 +205,24 @@ std::optional<record> named(TTrack const &Track)
if (name.empty()) if (name.empty())
return std::nullopt; return std::nullopt;
for (auto const &entry : g_records) for (auto const &entry : g_records)
if (std::find(entry.parts.begin(), entry.parts.end(), name) != entry.parts.end()) if (entry.kind != "connection" && std::find(entry.parts.begin(), entry.parts.end(), name) != entry.parts.end())
return entry; return entry;
return std::nullopt; return std::nullopt;
} }
std::vector<record> connections(TTrack const &Track)
{
load();
std::vector<record> result;
auto const &name{Track.name()};
if (name.empty())
return result;
for (auto const &entry : g_records)
if (entry.kind == "connection" && std::find(entry.parts.begin(), entry.parts.end(), name) != entry.parts.end())
result.push_back(entry);
return result;
}
std::optional<record> of(TTrack const &Track) std::optional<record> of(TTrack const &Track)
{ {
auto const found{named(Track)}; auto const found{named(Track)};

View File

@@ -26,9 +26,9 @@ namespace switch_store
struct record struct record
{ {
scene::layer_handle layer{null_handle}; scene::layer_handle layer{null_handle};
std::string kind{"switch"}; // switch, slip, crossing std::string kind{"switch"}; // switch, slip, crossing, double, connection
std::string name; // the switch, or the base name of a slip or a crossing 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{"-"}; std::string rail{"-"};
double radius{0.0}; // of the diverging track of the straight switch the type is made of 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 double ratio{0.0}; // n of 1:n
@@ -36,7 +36,7 @@ struct record
double a{0.0}; double a{0.0};
double b{0.0}; double b{0.0};
double main_radius{0.0}; // of the main track, signed: + to the right, - to the left, 0 straight double main_radius{0.0}; // of the main track, signed: + to the right, - to the left, 0 straight
std::vector<std::string> parts; // the switch, the switches of a slip, the paths of a crossing std::vector<std::string> 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::dvec3 start{0.0}; // of the first part
glm::dvec2 direction{0.0, 1.0}; 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 // ends of the main and the diverging track of the first part seen from its start: along, across, along, across. it stays the
@@ -55,6 +55,8 @@ std::vector<record> const &records();
std::optional<record> of(TTrack const &Track); std::optional<record> of(TTrack const &Track);
// the record of the track by its name alone, also when the switch lost its shape // the record of the track by its name alone, also when the switch lost its shape
std::optional<record> named(TTrack const &Track); std::optional<record> named(TTrack const &Track);
// the connections of switches the switch is a part of
std::vector<record> connections(TTrack const &Track);
// records whose switches were moved or turned as a whole take their new place and km; returns their names // records whose switches were moved or turned as a whole take their new place and km; returns their names
std::vector<std::string> follow(); std::vector<std::string> follow();
// radius of the diverging track: of the type, bent with the main track for an arc switch // radius of the diverging track: of the type, bent with the main track for an arc switch

View File

@@ -25,6 +25,7 @@ http:
#include "utilities/Globals.h" #include "utilities/Globals.h"
#include "utilities/utilities.h" #include "utilities/utilities.h"
#include "utilities/parser.h" #include "utilities/parser.h"
#include "utilities/Logs.h"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
@@ -2595,77 +2596,72 @@ std::vector<segment_data> editor_track::place_switch(switch_template const &Temp
auto const planar{Direction * Local.z + normal * (Local.x * side)}; auto const planar{Direction * Local.z + normal * (Local.x * side)};
return glm::dvec3{planar.x, Local.y + Grade * Local.z, planar.y}; return glm::dvec3{planar.x, Local.y + Grade * Local.z, planar.y};
}; };
for (auto const &local : Template.local) auto const put = [&](segment_data const &Local) {
{ segment_data path{Local};
segment_data path{local};
path.transform([&](glm::dvec3 const &Point) { return Origin + vector(Point); }, vector); path.transform([&](glm::dvec3 const &Point) { return Origin + vector(Point); }, vector);
if (Side < 0) if (Side < 0)
{ {
path.rolls = {-local.rolls[0], -local.rolls[1]}; path.rolls = {-Local.rolls[0], -Local.rolls[1]};
for (auto &piece : path.pieces) for (auto &piece : path.pieces)
piece.rolls = {-piece.rolls[0], -piece.rolls[1]}; piece.rolls = {-piece.rolls[0], -piece.rolls[1]};
} }
result.push_back(path); 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; return result;
} }
std::optional<editor_track::switch_template> 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<float>(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::switch_template> editor_track::standard_switch_templates() std::vector<editor_track::switch_template> 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<switch_template> result; std::vector<switch_template> 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)}; std::string error;
auto const a{definition.a > 0.0 ? definition.a : definition.radius * std::tan(angle * 0.5)}; auto entry{template_of(designs[i], error)};
auto const estimated{definition.b <= 0.0}; if (false == entry.has_value())
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<float>(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)
{ {
auto const handle{4.0 / 3.0 * definition.radius * std::tan(angle * 0.25)}; WriteLog("Editor: switch template \"" + designs[i].name + "\" left out: " + error, logtype::generic);
segment_data arc; continue;
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<float>(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};
} }
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.)" : ""); entry->design = static_cast<int>(i);
result.push_back(entry); result.push_back(std::move(*entry));
} }
for (auto const &slipping : {std::pair<double, char const *>{190.0, "1:9"}, {300.0, "1:9"}}) for (auto const &slipping : {std::pair<double, char const *>{190.0, "1:9"}, {300.0, "1:9"}})
{ {

View File

@@ -19,6 +19,7 @@ http:
#include "world/Segment.h" #include "world/Segment.h"
#include "editor/editorAlignment.hpp" #include "editor/editorAlignment.hpp"
#include "editor/editorSwitchDesign.hpp"
#include "utilities/Classes.h" #include "utilities/Classes.h"
class TTrack; class TTrack;
@@ -407,8 +408,16 @@ class editor_track
bool double_slip{false}; bool double_slip{false};
std::string type{"Rz"}; std::string type{"Rz"};
std::string rail{"-"}; std::string rail{"-"};
// the second switch of a double one, starting on the main track of the first at the stagger
std::array<segment_data, 2> 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<switch_template> standard_switch_templates(); static std::vector<switch_template> standard_switch_templates();
static std::optional<switch_template> template_of(switch_design::design const &Design, std::string &Error);
static std::vector<switch_template> find_switch_templates(); static std::vector<switch_template> find_switch_templates();
static TTrack *load_path(std::string const &Text, TTrack const &Template, std::string const &Name = {}); 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); static TTrack *create_path(TTrack const &Style, segment_data const &Path);

View File

@@ -5747,3 +5747,436 @@ msgstr "Krzyżownica i kierownice z szyn"
msgid "Cuts the rails of the switch at its frog and lays wing rails and check rails, for rail profiles S60, UIC60 and S39.\nOff for a switch whose include has its own model of them" msgid "Cuts the rails of the switch at its frog and lays wing rails and check rails, for rail profiles S60, UIC60 and S39.\nOff for a switch whose include has its own model of them"
msgstr "Tnie szyny rozjazdu na krzyżownicy i układa szyny skrzydłowe i kierownice, dla profili szyn S60, UIC60 i S39.\nWyłącz dla rozjazdu, którego include ma ich własny model" msgstr "Tnie szyny rozjazdu na krzyżownicy i układa szyny skrzydłowe i kierownice, dla profili szyn S60, UIC60 i S39.\nWyłącz dla rozjazdu, którego include ma ich własny model"
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"
msgid "%s laid, switches %s"
msgstr "Ułożono: %s, zwrotnice %s"
msgid "%s: %d switches, spacing %.2f m"
msgstr "%s: zwrotnic %d, rozstaw %.2f m"
msgid ", aligning curve R %.0f m"
msgstr ", łuk wyrównujący R %.0f m"
msgid ", crossing 1:%.2f"
msgstr ", skrzyżowanie 1:%.2f"
msgid ", mathematical points %.2f m apart"
msgstr ", punkty matematyczne co %.2f m"
msgid ", straight inserts"
msgstr ", wstawki proste"
msgid ", tracks at"
msgstr ", tory w odległości"
msgid "A connection is laid between straights"
msgstr "Połączenie układa się między prostymi"
msgid "A connection is made of ordinary switches, not of slips, crossings or double switches"
msgstr "Połączenie składa się z rozjazdów zwyczajnych, nie z krzyżowych, skrzyżowań ani podwójnych"
msgid "A connection of tracks laid whole: press on a straight and drag onto the other track. Its switches are cut into the tracks with the inserts between them and get their drives; Shift+wheel while dragging: another kind, Ctrl+wheel: R of the aligning curve"
msgstr "Połączenie torów układane w całości: wciśnij na prostej i przeciągnij na drugi tor. Zwrotnice są wcinane w tory razem ze wstawkami między nimi i dostają napędy; Shift+kółko podczas przeciągania: inny rodzaj, Ctrl+kółko: R łuku wyrównującego"
msgid "A scissors crossover needs parallel tracks and switches of the same angle"
msgstr "Nożyce wymagają torów równoległych i rozjazdów o tym samym skosie"
msgid "A switch of the connection doesn't fit where it was planned, its straight is too short"
msgstr "Zwrotnica połączenia nie mieści się tam, gdzie ją zaplanowano, jej prosta jest za krótka"
msgid "A switch of the connection goes where its track isn't a plain straight"
msgstr "Zwrotnica połączenia wypada tam, gdzie tor nie jest zwykłą prostą"
msgid "A switch of the connection runs off the end of its straight"
msgstr "Zwrotnica połączenia wychodzi poza koniec swojej prostej"
msgid "Aligning curve R, m##connectionradius"
msgstr "R łuku wyrównującego, m##connectionradius"
msgid "Alt+wheel"
msgstr "Alt+kółko"
msgid "Connection##switchconnection"
msgstr "Połączenie##switchconnection"
msgid "Crossover"
msgstr "Przejście rozjazdowe"
msgid "Distances between the tracks of the fan from this track on, such as 4.75;4.75;5.0; used when the drag doesn't end on a parallel track, otherwise the parallel tracks up to that one make the fan"
msgstr "Odległości między torami wachlarza od tego toru, np. 4.75;4.75;5.0; używane, gdy przeciąganie nie kończy się na torze równoległym, w przeciwnym razie wachlarz tworzą tory równoległe aż do tamtego"
msgid "Double crossover"
msgstr "Podwójne przejście rozjazdowe"
msgid "Drag onto the other track"
msgstr "Przeciągnij na drugi tor"
msgid "Gap between the switches, m##connectiongap"
msgstr "Odstęp między zwrotnicami, m##connectiongap"
msgid "Give the spacings of the tracks of the ladder"
msgstr "Podaj rozstawy torów ulicy zwrotnicowej"
msgid "Length of the new tracks of the fan where there are none to lead onto"
msgstr "Długość nowych torów wachlarza tam, gdzie nie ma torów do połączenia"
msgid "New tracks, m##connectionstub"
msgstr "Nowe tory, m##connectionstub"
msgid "Of the curve between switches of different angles or on tracks that aren't parallel, and at the end of a ladder; 0: the radius of the sharper switch"
msgstr "Łuku między rozjazdami o różnych skosach lub na torach nierównoległych oraz na końcu ulicy zwrotnicowej; 0: promień ostrzejszego rozjazdu"
msgid "Other switches##connectionsecond"
msgstr "Pozostałe zwrotnice##connectionsecond"
msgid "Otherwise the two crossovers meet on the other track; Alt+wheel while dragging switches it"
msgstr "W przeciwnym razie oba przejścia spotykają się na drugim torze; Alt+kółko podczas przeciągania to przełącza"
msgid "R of the aligning curve"
msgstr "R łuku wyrównującego"
msgid "Scissors crossover"
msgstr "Nożyce"
msgid "Single switch"
msgstr "Pojedynczy rozjazd"
msgid "Spacings, m##connectionspacings"
msgstr "Rozstawy, m##connectionspacings"
msgid "Straight between the starts of the two switches meeting on one track"
msgstr "Prosta między początkami dwóch zwrotnic spotykających się na jednym torze"
msgid "Switch ladder"
msgstr "Ulica zwrotnicowa"
msgid "Template of the switches on the other track and along a ladder; the first switch is the one chosen on the list above"
msgstr "Szablon zwrotnic na drugim torze i wzdłuż ulicy zwrotnicowej; pierwsza zwrotnica to ta wybrana na liście powyżej"
msgid "The aligning curve R %.0f m with %.0f m straights doesn't fit between the switches"
msgstr "Łuk wyrównujący R %.0f m z prostymi %.0f m nie mieści się między zwrotnicami"
msgid "The diverging track runs along the other track"
msgstr "Tor zwrotny biegnie wzdłuż drugiego toru"
msgid "The diverging tracks meet at %.2f deg, a straight insert can't join them"
msgstr "Tory zwrotne spotykają się pod kątem %.2f°, wstawka prosta ich nie połączy"
msgid "The same as the first one"
msgstr "Taka sama jak pierwsza"
msgid "The spacings of the tracks of a ladder have to be at least 1 m"
msgstr "Rozstawy torów ulicy zwrotnicowej muszą mieć co najmniej 1 m"
msgid "The straight between the curves of the switches is %.2f m, under %.0f m"
msgstr "Prosta między łukami zwrotnic ma %.2f m, mniej niż %.0f m"
msgid "The switch on the other track runs off the end of its straight"
msgstr "Zwrotnica na drugim torze wychodzi poza koniec swojej prostej"
msgid "The switch template has no diverging track"
msgstr "Szablon rozjazdu nie ma toru zwrotnego"
msgid "The switches can't face each other across these tracks"
msgstr "Zwrotnice nie mogą stanąć naprzeciw siebie na tych torach"
msgid "The switches meet on this track##connectiontogether"
msgstr "Zwrotnice spotykają się na tym torze##connectiontogether"
msgid "The switches of the two crossovers overlap, the gap between them has to be longer"
msgstr "Zwrotnice obu przejść nachodzą na siebie, odstęp między nimi musi być dłuższy"
msgid "The tracks are %.2f m too close for these switches"
msgstr "Tory są o %.2f m za blisko dla tych rozjazdów"
msgid "The tracks are too close for a scissors crossover with these switches, its switches overlap"
msgstr "Tory są za blisko dla nożyc z tymi rozjazdami, ich zwrotnice nachodzą na siebie"
msgid "The tracks are too close for the crossing of a scissors crossover: %.2f m more spacing is needed"
msgstr "Tory są za blisko dla skrzyżowania nożyc: potrzeba o %.2f m większego rozstawu"
msgid "The tracks of a fan have to be parallel, on one side and at least 1 m apart"
msgstr "Tory wachlarza muszą być równoległe, po jednej stronie i co najmniej 1 m od siebie"
msgid "Track %d of the fan is %.2f m too close to the previous one for these switches"
msgstr "Tor %d wachlarza jest o %.2f m za blisko poprzedniego dla tych rozjazdów"
msgid "Track %d of the fan reaches into the ladder, it has to start %.2f m further on"
msgstr "Tor %d wachlarza wchodzi w ulicę zwrotnicową, musi zaczynać się %.2f m dalej"
msgid "another kind"
msgstr "inny rodzaj"
msgid "the connection there"
msgstr "połączenie tam"
msgid "where a double crossover meets"
msgstr "gdzie spotyka się podwójne przejście"