17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-22 14:23:20 +02:00

feat(editor): connections of tracks laid whole from the switch tool: a crossover with a straight insert or an aligning curve between switches of different angles or tracks that aren't parallel, a double crossover meeting on either track, a scissors crossover with the crossing of its inserts, and a switch ladder onto the parallel tracks of a fan or onto new ones at given spacings; press on a straight and drag onto the other track, Shift+wheel the kind, Ctrl+wheel the R of the aligning curve, Alt+wheel where a double crossover meets; the ghost shows the whole set with the spacing, the inserts and the distance of the mathematical points; switches get their drives and a //$r connection record; the crossover drawn from a diverging end uses the same solver; self-test connection

This commit is contained in:
maj00r
2026-10-14 23:38:31 +02:00
parent 52a289b15b
commit aa20a88ed0
11 changed files with 1521 additions and 30 deletions

View File

@@ -183,6 +183,8 @@ set(SOURCES
"editor/editorAlignmentStore.cpp"
"editor/editorSwitchStore.cpp"
"editor/editorSwitchDesign.cpp"
"editor/editorConnections.cpp"
"application/editormodeconnection.cpp"
"editor/editorSpeed.cpp"
"editor/editorProfile.cpp"
"editor/editorGauge.cpp"

View File

@@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/.
#include "editor/editorTerrainStreamer.hpp"
#include "editor/editorOrthophoto.hpp"
#include "editor/editorTrack.hpp"
#include "editor/editorConnections.hpp"
#include "editor/editorAlignment.hpp"
#include "editor/editorAlignmentStore.hpp"
#include "editor/editorSwitchStore.hpp"
@@ -998,8 +999,22 @@ class editor_mode : public application_mode, private editor_track::observer
};
slide_state slide;
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;
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_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);

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

@@ -278,6 +278,26 @@ void editor_mode::record_double(TTrack &First, TTrack &Second, editor_track::swi
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> {

View File

@@ -1317,6 +1317,107 @@ 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);
return selftest_result::handled;
}
if (command == "connection" || command == "connectionplan")
{
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 (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 = 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};
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")
{
std::string name;

View File

@@ -3863,7 +3863,9 @@ std::vector<editor_mode::key_hint> editor_mode::track_key_hints(bool const All)
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"}};
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)
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))
@@ -4823,6 +4825,25 @@ void editor_mode::render_layout_combo()
bool editor_mode::track_wheel(double const Offset)
{
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)
return false;
bool const laying{m_lay.active && false == m_lay.points.empty()};
@@ -5416,6 +5437,18 @@ bool editor_mode::start_switch_placement()
void editor_mode::finish_switch_placement()
{
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;
auto const linked{plan_switch_link(plan)};
tool.placing = false;
@@ -5765,7 +5798,40 @@ void editor_mode::draw_build_overlay() const
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())
drawpath(piece, overlay_color::marked);
@@ -5892,6 +5958,7 @@ void editor_mode::render_switch_ui()
list(true);
}
}
render_connection_ui();
ImGui::PushItemWidth(120.0f);
ImGui::InputTextWithHint(STR_C("Name##switchname"), switch_name_for_new().c_str(), tool.name, sizeof(tool.name));
ImGui::PopItemWidth();
@@ -6164,9 +6231,7 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const
if (std::abs(lateral) < 0.01)
return false;
auto const side{lateral > 0.0 ? 1.0 : -1.0};
auto const normal{perpendicular * side};
auto const offset{std::abs(lateral)};
auto const &control{diverging.points[segment_data::point::control2]};
auto const &control{(diverging.pieces.empty() ? diverging : diverging.pieces.back()).points[segment_data::point::control2]};
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))};
if (angle < 1e-4)
@@ -6204,22 +6269,6 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const
auto const &line{Plan.target};
if (line.tracks.empty() || std::abs(cross(line.direction, axis)) > std::sin(glm::radians(1.0)))
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;
shape.source = tool.track;
shape.length = glm::dot(plan_of(main.points[segment_data::point::end] - origin), axis);
@@ -6238,11 +6287,14 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const
}
}
Plan.shape = shape;
auto const station{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * Plan.along};
Plan.paths = editor_track::place_switch(shape, station, direction, Plan.side, line.grade * Plan.direction);
Plan.insert = segment_data{};
Plan.insert.points[segment_data::point::start] = frog;
Plan.insert.points[segment_data::point::end] = Plan.paths[1].points[segment_data::point::end];
auto const faced{connection::face(frog, tangent, connection::of(line), 1, shape)};
if (false == faced.valid)
return false;
Plan.along = faced.partner.along;
Plan.direction = faced.partner.forward;
Plan.side = faced.partner.side;
Plan.paths = faced.partner.paths;
Plan.insert = faced.insert;
return true;
}
@@ -6265,7 +6317,7 @@ bool editor_mode::stretchable(editor_track::snap_target const &Target) const
bool editor_mode::plan_switch_link(switch_link &Plan) const
{
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;
auto const &shape{tool.templates[tool.armed]};
auto const &line{tool.line};

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

@@ -156,7 +156,7 @@ void load()
std::string km;
std::size_t count{0};
line >> entry.kind;
if (entry.kind != "switch" && entry.kind != "slip" && entry.kind != "crossing" && entry.kind != "double")
if (entry.kind != "switch" && entry.kind != "slip" && entry.kind != "crossing" && entry.kind != "double" && entry.kind != "connection")
continue;
line >> entry.name >> entry.type >> entry.rail >> entry.radius >> entry.ratio >> hand >> entry.a >> entry.b >> entry.main_radius >> keyword >> count;
if (line.fail() || keyword != "parts" || count > 64)
@@ -205,11 +205,24 @@ std::optional<record> named(TTrack const &Track)
if (name.empty())
return std::nullopt;
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 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)
{
auto const found{named(Track)};

View File

@@ -26,7 +26,7 @@ namespace switch_store
struct record
{
scene::layer_handle layer{null_handle};
std::string kind{"switch"}; // switch, slip, crossing, double
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 type{"Rz"}; // Rz, Rlj, Rld, Rkp, Rkpd, Skrz, Rpd, Rpj, - when unknown
std::string rail{"-"};
@@ -55,6 +55,8 @@ std::vector<record> const &records();
std::optional<record> of(TTrack const &Track);
// the record of the track by its name alone, also when the switch lost its shape
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
std::vector<std::string> follow();
// radius of the diverging track: of the type, bent with the main track for an arc switch

View File

@@ -6010,3 +6010,168 @@ 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"