17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-20 13:54:53 +02:00
Files
maszyna/application/editormodeswitchplace.cpp
maj00r 93a45fb632 fix(editor): a slip goes to the crossing under the cursor or nowhere, shown before the click
A slip went to the crossing of the selected straight with another straight within 40 m of the click. When the track under the cursor wasn't taken as a straight (a slight curve, a switch), the slip silently went to another crossing up to 40 m away. A double slip within 25 m of the click was replaced, even when the click was on a free crossing.

- The crossing nearest the cursor, within 12 m, is found among all the rail tracks. A slip goes only there; when that track is a curve or a switch, the tool says so.
- A double slip is replaced only when it is nearer the cursor than any crossing.
- The cursor is taken at the height of the straight under it, so on a grade the 3D view doesn't put the click metres away from the track.
- Before the click, the view marks the crossing the slip goes to, with its two straights, or the reason it can't go there.
2026-10-19 18:20:41 +02:00

1405 lines
60 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http:
*/
#include "stdafx.h"
#include "application/editormode.h"
#include "application/editoruilayer.h"
#include "application/editorprojection.h"
#include "application/editormodeshared.h"
#include "editor/editorFormat.hpp"
#include "editor/editorAlignmentStore.hpp"
#include "editor/editorSwitchStore.hpp"
#include "editor/editorGeometry.hpp"
#include "editor/editorIncludeInfo.hpp"
#include "utilities/Globals.h"
#include "rendering/renderer.h"
#include "world/Track.h"
#include "world/TrackFeatures.h"
#include "scene/scene.h"
#include "scene/scenelayers.h"
#include "simulation/simulation.h"
#include "utilities/Logs.h"
#include "utilities/translation.h"
#include "imgui/imgui.h"
#include "imgui/ImGuizmo.h"
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstring>
#include <functional>
#include <iterator>
#include <set>
#include "application/editormodetrackshared.h"
using namespace track_mode;
std::vector<segment_data> editor_mode::switch_preview() const
{
auto const &tool{m_switch};
if (false == tool.placing || tool.armed < 0 || tool.armed >= static_cast<int>(tool.templates.size()))
return {};
if (tool.curved)
{
auto const sample{sample_at(tool.frame, tool.along)};
glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z};
auto const forward{glm::dot(offset, sample.tangent) >= 0.0 ? 1 : -1};
glm::dvec2 const direction{sample.tangent * static_cast<double>(forward)};
auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1};
return curved_switch_paths(tool.templates[tool.armed], tool.frame, tool.along, forward, side);
}
if (switch_link plan; plan_switch_link(plan))
{
plan.paths.insert(plan.paths.end(), plan.link.pieces.begin(), plan.link.pieces.end());
return plan.paths;
}
auto const &line{tool.line};
glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z};
auto const forward{glm::dot(offset, line.direction) >= 0.0 ? 1 : -1};
glm::dvec2 const direction{line.direction * static_cast<double>(forward)};
auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1};
return editor_track::place_switch(tool.templates[tool.armed], tool.point, direction, side, line.grade * forward);
}
bool editor_mode::start_switch_placement()
{
auto &tool{m_switch};
if (tool.armed < 0 || m_track_tab != track_tab::turnout)
return false;
// with no plain track selected, or a press away from it, the click selects
auto *selected{selected_track()};
if (selected == nullptr || selected->eType != tt_Normal || selected->m_paths.empty())
return false;
glm::dvec3 const ground{cursor_level(selected->m_paths.front().points[segment_data::point::start].y)};
tool.curved = false;
tool.status.clear();
if (current_straight().tracks.empty())
{
auto *track{selected};
if (tool.templates[tool.armed].double_slip)
{
tool.status = STR_C("A double slip goes only into a straight crossing another one");
return false;
}
editor_track::curve curve;
editor_track::chain chain;
std::string error;
if (false == editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve))
{
tool.status = STR_C("The selected track is neither a recognized straight nor a curve");
return false;
}
if (false == editor_track::find_chain(curve.from, curve.to, chain, error, true))
{
tool.status = error.empty() ? std::string{STR_C("The curve can't be followed")} : error;
return false;
}
for (auto const &part : editor_track::chain_parts(chain))
if (std::find(part.tracks.begin(), part.tracks.end(), track) != part.tracks.end())
{
chain = part;
break;
}
tool.frame = sample_chain(chain);
tool.frame_tracks = chain.tracks;
if (tool.frame.size() < 2)
return false;
tool.curved = true;
tool.along = nearest_station(tool.frame, ground);
tool.point = sample_at(tool.frame, tool.along).position;
if (plan_distance(tool.point, ground) > kSwitchReach)
return false;
tool.mouse = ground;
tool.placing = true;
return true;
}
auto const &line{current_straight()};
auto const cursor{cursor_on_straight(line)};
auto const along{std::clamp(line.along(cursor), 0.0, line.length)};
auto const point{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * along};
if (plan_distance(point, cursor) > kSwitchReach)
return false;
if (tool.templates[tool.armed].double_slip)
{
insert_double_slip(line, cursor, tool.templates[tool.armed]);
return true;
}
tool.along = along;
tool.point = point;
tool.mouse = cursor;
tool.line = line;
tool.placing = true;
return true;
}
void editor_mode::finish_switch_placement()
{
auto &tool{m_switch};
if (tool.connection >= 0)
{
if (false == tool.curved)
{
insert_connection();
return;
}
insert_curved_connection();
return;
}
switch_link plan;
auto const linked{plan_switch_link(plan)};
tool.placing = false;
if (tool.armed < 0 || tool.armed >= static_cast<int>(tool.templates.size()))
return;
if (linked)
{
insert_linked_switch(plan);
return;
}
if (tool.curved)
{
auto const sample{sample_at(tool.frame, tool.along)};
glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z};
auto const forward{glm::dot(offset, sample.tangent) >= 0.0 ? 1 : -1};
glm::dvec2 const direction{sample.tangent * static_cast<double>(forward)};
auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1};
insert_curved_switch(forward, side);
return;
}
glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z};
auto const forward{glm::dot(offset, tool.line.direction) >= 0.0 ? 1 : -1};
glm::dvec2 const direction{tool.line.direction * static_cast<double>(forward)};
auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1};
insert_switch(tool.line, tool.along, forward, side);
if (false == plan.reason.empty())
ui()->set_status(plan.reason, true);
}
void editor_mode::insert_switch(editor_track::straight const &Line, double const Along, int const Direction, int const Side)
{
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> created;
std::vector<TTrack *> removed;
if (false == place_switch_on_straight(Line, m_switch.templates[m_switch.armed], nullptr, Along, Direction, Side, true, states, created, removed, switch_name_for_new()))
return;
auto const placed{created};
push_track_snapshot(std::move(states), std::move(created), std::move(removed));
straight_refresh();
switch_placed(placed);
}
void editor_mode::switch_placed(std::vector<TTrack *> const &Created)
{
auto const found{std::find_if(Created.begin(), Created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; })};
if (found == Created.end())
return;
auto const slip{std::count_if(Created.begin(), Created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; }) > 1};
m_switch.placing = false;
first_step_done(first_step::turnout);
m_track_point = {};
m_node = *found;
ui()->set_node(m_node);
if (slip)
{
auto const known{switch_store::named(**found)};
if (false == known.has_value() || (known->kind != "double" && known->kind != "slip"))
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());
}
// a slip has a drive at each of its switches
if (known->kind == "slip")
{
if (false == driven.empty())
{
std::string names;
for (auto const &name : driven)
names += (names.empty() ? "" : ", ") + name;
m_switch.status = format(STR_C("Slip with drives at its switches %s: the events of the name of a switch with + and - switch it, also in the map"), names.c_str());
ui()->set_status(m_switch.status, false);
}
return;
}
if (driven.size() == 2)
{
m_switch.status = format(STR_C("Double switch %s + %s with drives: events %s+, %s- and %s+, %s- switch them, also in the map"), driven[0].c_str(), driven[1].c_str(), driven[0].c_str(), driven[0].c_str(), driven[1].c_str(), driven[1].c_str());
ui()->set_status(m_switch.status, false);
}
advance_switch_name();
return;
}
if (place_switch_drive(**found))
join_last_steps();
advance_switch_name();
}
void editor_mode::advance_switch_name()
{
if (false == m_switch.keep_names.empty())
{
m_switch.keep_names.erase(m_switch.keep_names.begin());
return;
}
std::string name{m_switch.name};
auto digits{name.size()};
while (digits > 0 && std::isdigit(static_cast<unsigned char>(name[digits - 1])))
--digits;
if (digits < name.size() && name.size() - digits < 9)
name = name.substr(0, digits) + std::to_string(std::stoi(name.substr(digits)) + 1);
else
name.clear();
std::snprintf(m_switch.name, sizeof(m_switch.name), "%s", name.c_str());
}
std::string editor_mode::switch_name_for_new() const
{
if (false == m_switch.keep_names.empty() && simulation::Paths.find(m_switch.keep_names.front()) == nullptr)
return m_switch.keep_names.front();
if (m_switch.name[0] != '\0')
return m_switch.name;
for (int i = 1;; ++i)
{
auto const name{"z" + std::to_string(i)};
if (simulation::Paths.find(name) == nullptr && simulation::Events.FindEvent(name + "+") == nullptr)
return name;
}
}
void editor_mode::join_last_steps()
{
if (m_history.size() < 2)
return;
auto &last{m_history.back()};
// a step of more than the includes placed, as the paths laid after others were taken out, is undone together with the one before
auto const more{false == last.tracks.empty() || false == last.created.empty() || false == last.removed.empty() || false == last.directives.empty() || false == last.sweeps.empty() || false == last.sweeps_toggled.empty() ||
false == last.infra.empty() || last.alignments.has_value() || last.switch_records.has_value() || last.profiles || false == last.copies.empty() || last.terrain != nullptr};
if (more)
{
last.joined = true;
return;
}
auto instances{std::move(last.instances)};
m_history.pop_back();
auto &previous{m_history.back()};
previous.instances.insert(previous.instances.end(), instances.begin(), instances.end());
}
bool editor_mode::cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Created, std::vector<TTrack *> &Removed, TTrack **Style)
{
auto const from{From};
auto const to{To};
std::string reason;
if (false == tracks_editable(Line.tracks, reason))
{
m_switch.status = reason;
ui()->set_status(reason, true);
return false;
}
struct span
{
TTrack *track;
int path;
double from;
double to;
bool forward;
};
std::vector<span> spans;
for (std::size_t i = 0; i < Line.tracks.size(); ++i)
{
auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]};
auto const a{Line.along(path.points[segment_data::point::start])};
auto const b{Line.along(path.points[segment_data::point::end])};
spans.push_back({Line.tracks[i], Line.paths[i], std::min(a, b), std::max(a, b), a < b});
}
std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; });
int first{-1};
int last{-1};
for (int i = 0; i < static_cast<int>(spans.size()); ++i)
{
if (spans[i].to > from + 1e-3 && spans[i].from < to - 1e-3)
{
if (first < 0)
first = i;
last = i;
}
}
if (first < 0)
return false;
editor_track::chain chain;
for (int i = first; i <= last; ++i)
{
if (spans[i].track->eType != tt_Normal)
return false;
chain.tracks.push_back(spans[i].track);
chain.forward.push_back(spans[i].forward);
}
if (Style != nullptr)
*Style = spans[first].track;
auto const before{std::max(0.0, from - spans[first].from)};
auto const after{std::max(0.0, spans[last].to - to)};
bool const hasbefore{before > 0.01};
bool const hasafter{after > 0.01};
auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; };
if (false == hasbefore && false == hasafter)
{
for (auto *track : chain.tracks)
editor_track::retire(*track);
Removed.insert(Removed.end(), chain.tracks.begin(), chain.tracks.end());
return true;
}
auto const count{std::max<std::size_t>(chain.tracks.size(), (hasbefore ? 1 : 0) + (hasafter ? 1 : 0))};
std::size_t countbefore{0};
if (hasbefore && hasafter)
countbefore = std::clamp<std::size_t>(static_cast<std::size_t>(std::round(count * before / (before + after))), 1, count - 1);
else if (hasbefore)
countbefore = count;
std::vector<segment_data> pieces;
auto const straight = [&](double const Start, double const End, std::size_t const Count) {
for (std::size_t i = 0; i < Count; ++i)
{
segment_data path;
path.points[segment_data::point::start] = point(Start + (End - Start) * i / Count);
path.points[segment_data::point::end] = point(Start + (End - Start) * (i + 1) / Count);
pieces.push_back(path);
}
};
if (hasbefore)
straight(spans[first].from, from, countbefore);
if (hasafter)
straight(to, spans[last].to, count - countbefore);
for (auto *track : chain.tracks)
States.emplace_back(track, editor_track::capture(*track));
auto const relaid{editor_track::relay(chain, pieces)};
Created.insert(Created.end(), relaid.begin(), relaid.end());
return true;
}
bool editor_mode::place_switch_on_straight(editor_track::straight const &Line, editor_track::switch_template const &Shape, TTrack const *Style, double const Along, int const Direction, int const Side, bool const Snap, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Created, std::vector<TTrack *> &Removed, std::string const &Name)
{
auto const &shape{Shape};
if (shape.length > Line.length - 0.02)
{
m_switch.status = format(STR_C("The straight is %.1f m long, too short for the %.1f m switch"), Line.length, shape.length);
return false;
}
auto const from{switch_from(Line, shape.length, Along, Direction, Snap)};
auto const to{from + shape.length};
auto const origin{Direction > 0 ? from : to};
TTrack *first{nullptr};
if (false == cut_straight(Line, from, to, States, Created, Removed, &first))
{
m_switch.status = STR_C("The switch would overlap another switch, move it onto plain track");
return false;
}
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)};
auto const paths{editor_track::place_switch(shape, point(origin), direction, Side, Line.grade * Direction)};
auto const made{create_switch_set(shape, paths, Style != nullptr ? *Style : *first, Name)};
Created.insert(Created.end(), made.begin(), made.end());
return true;
}
std::vector<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 = format(STR_C("Switch %s inserted, Ctrl+Z takes it back"), track->name().c_str());
return {track};
}
bool editor_mode::plan_crossover(crossover_plan &Plan) const
{
auto const &tool{m_extend};
if (false == tool.active || tool.track == nullptr || tool.track->eType != tt_Switch || tool.track->m_paths.size() < 2 || tool.path != 1 || false == tool.atend || m_track_layout == track_layout::straight)
return false;
auto const &own{*tool.track};
auto const &main{own.m_paths[0]};
auto const &diverging{own.m_paths[1]};
auto const origin{main.points[segment_data::point::start]};
auto const axis{glm::normalize(plan_of(main.points[segment_data::point::end] - origin))};
glm::dvec2 const perpendicular{-axis.y, axis.x};
auto const &frog{diverging.points[segment_data::point::end]};
auto const lateral{glm::dot(plan_of(frog - origin), perpendicular)};
if (std::abs(lateral) < 0.01)
return false;
auto const side{lateral > 0.0 ? 1.0 : -1.0};
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)
return false;
auto const ground{tool.mouse};
TTrack *target{nullptr};
double best{8.0};
auto const sections{simulation::Region->sections(ground, 40.f)};
for (auto *section : sections)
{
for (auto const &cell : section->m_cells)
{
for (auto *track : cell.m_paths)
{
if (track == &own || track->eType != tt_Normal || track->m_editorremoved || 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 b{plan_of(path.points[segment_data::point::end])};
auto const segment{b - a};
auto const t{std::clamp(glm::dot(plan_of(ground) - a, segment) / std::max(1e-9, glm::dot(segment, segment)), 0.0, 1.0)};
auto const distance{glm::distance(plan_of(ground), a + segment * t)};
if (distance < best)
{
best = distance;
target = track;
}
}
}
}
if (target == nullptr)
return false;
Plan.target = editor_track::find_straight(*target, m_straights.tolerance);
auto const &line{Plan.target};
if (line.tracks.empty() || std::abs(cross(line.direction, axis)) > std::sin(glm::radians(1.0)))
return false;
editor_track::switch_template shape;
shape.source = tool.track;
shape.length = glm::dot(plan_of(main.points[segment_data::point::end] - origin), axis);
for (int i = 0; i < 2; ++i)
{
auto const &path{own.m_paths[i]};
auto &local{shape.local[i]};
local = path;
auto const tolocal = [&](glm::dvec3 const &Vector) { return glm::dvec3{glm::dot(plan_of(Vector), perpendicular) * side, Vector.y, glm::dot(plan_of(Vector), axis)}; };
local.transform([&](glm::dvec3 const &Point) { return tolocal(Point - origin); }, tolocal);
if (side < 0.0)
{
local.rolls = {-local.rolls[0], -local.rolls[1]};
for (auto &piece : local.pieces)
piece.rolls = {-piece.rolls[0], -piece.rolls[1]};
}
}
Plan.shape = shape;
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;
}
std::string editor_mode::link_problem(editor_track::snap_target const &Target, editor_track::branch_link const &Link) const
{
auto const name{Target.track != nullptr && false == Target.track->name().empty() ? Target.track->name() : std::string{STR_C("(no name)")}};
if (false == Link.valid)
return Link.shortfall > 0.0 ? format(STR_C("The branch can't lead into %s: the R %.0f m curve needs %.2f m more room"), name.c_str(), Link.radius, Link.shortfall) :
format(STR_C("The branch can't lead into %s"), name.c_str());
if (Link.trim > 0.0 && (false == stretchable(Target) || geometry::chord_length(Target.track->m_paths[Target.point.path]) < Link.trim + 1.0))
return format(STR_C("%s would have to be shortened by %.2f m, it's too short or not straight"), name.c_str(), Link.trim);
return {};
}
bool editor_mode::stretchable(editor_track::snap_target const &Target) const
{
return Target.track != nullptr && Target.track->eType == tt_Normal && Target.track->m_paths.size() == 1 && editor_track::is_straight(*Target.track, m_straights.tolerance);
}
bool editor_mode::plan_switch_link(switch_link &Plan) const
{
auto const &tool{m_switch};
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};
std::vector<TTrack const *> const exclude(line.tracks.begin(), line.tracks.end());
auto const category{line.tracks.front()->iCategoryFlag & 15};
auto const orient = [&](glm::dvec3 const &Toward) {
glm::dvec2 const offset{plan_of(Toward - tool.point)};
Plan.forward = glm::dot(offset, line.direction) >= 0.0 ? 1 : -1;
Plan.side = cross(line.direction * static_cast<double>(Plan.forward), offset) >= 0.0 ? 1 : -1;
};
auto const place = [&](double const Along) {
auto const point{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * Along};
return editor_track::place_switch(shape, point, line.direction * static_cast<double>(Plan.forward), Plan.side, line.grade * Plan.forward);
};
auto const &io{ImGui::GetIO()};
if (false == m_cursor_override.has_value())
Plan.target = snap_free_end(tool.mouse, {io.MousePos.x, io.MousePos.y}, nullptr, category, exclude);
if (Plan.target.track == nullptr)
Plan.target = editor_track::find_free_end(nullptr, category, tool.mouse, kLaySnapRadius, exclude);
if (Plan.target.track != nullptr)
orient(Plan.target.position);
else
{
orient(tool.mouse);
auto const paths{place(tool.along)};
if (paths.size() < 2)
return false;
Plan.target = editor_track::find_free_end(nullptr, category, paths[1].points[segment_data::point::end], kLaySnapRadius, exclude);
if (Plan.target.track == nullptr)
return false;
}
if (shape.length > line.length - 0.02)
return false;
auto const kink{glm::radians(m_joints.angle * 0.5)};
auto const plan = [&](double const Along) {
Plan.along = Along;
Plan.paths = place(Along);
if (Plan.paths.size() < 2)
return false;
auto const radius{editor_track::branch_radius(Plan.paths[1])};
Plan.link = editor_track::link_branch(branch_start(Plan.paths[1]), radius, branch_target(Plan.target), kink,
editor_track::reverse_insert(std::min(editor_track::diverging_speed(radius), design_speed(nullptr, Plan.target.track))), stretchable(Plan.target));
Plan.reason = link_problem(Plan.target, Plan.link);
return Plan.reason.empty();
};
auto const from{switch_from(line, shape.length, tool.along, Plan.forward, true)};
auto const snapped{Plan.forward > 0 ? from : from + shape.length};
auto const paths{place(snapped)};
if (paths.size() < 2)
return false;
if (auto const shift{editor_track::branch_shift(branch_start(paths[1]), line.direction * static_cast<double>(Plan.forward), editor_track::branch_radius(paths[1]), branch_target(Plan.target), kink)})
{
auto const along{snapped + *shift * Plan.forward};
auto const begin{Plan.forward > 0 ? along : along - shape.length};
Plan.moved = std::abs(*shift);
if (begin >= -1e-6 && begin + shape.length <= line.length + 1e-6 && plan(along) && false == Plan.link.reverse)
return true;
}
Plan.moved = 0.0;
return plan(snapped);
}
void editor_mode::insert_linked_switch(switch_link const &Plan)
{
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> created;
std::vector<TTrack *> removed;
if (false == place_switch_on_straight(m_switch.line, m_switch.templates[m_switch.armed], nullptr, Plan.along, Plan.forward, Plan.side, false, states, created, removed, switch_name_for_new()))
return;
auto const placed{created};
link_tracks(Plan.target, Plan.link, Plan.target.track->eType == tt_Normal ? *Plan.target.track : *m_switch.line.tracks.front(), states, created);
push_track_snapshot(std::move(states), std::move(created), std::move(removed));
straight_refresh();
m_switch.status.clear();
switch_placed(placed);
auto summary{link_summary(Plan.target, Plan.link, editor_track::branch_radius(Plan.paths[1]))};
if (Plan.moved >= 0.005)
summary += format(STR_C(", the switch moved %.2f m"), Plan.moved);
m_switch.status = summary + (m_switch.status.empty() ? std::string{} : "\n" + m_switch.status);
ui()->set_status(m_switch.status, false);
}
bool editor_mode::plan_branch_link(editor_track::branch_link &Link, std::string &Reason) const
{
auto const &tool{m_extend};
editor_track::snap_target target;
Reason.clear();
if (false == extend_snap(target) || tool.track == nullptr || tool.track->eType != tt_Switch || tool.track->m_paths.size() != 2 || tool.path != 1 || false == tool.atend)
return false;
if (std::find(tool.line.begin(), tool.line.end(), target.track) != tool.line.end())
return false;
auto const &diverging{tool.track->m_paths[1]};
auto const radius{editor_track::branch_radius(diverging)};
Link = editor_track::link_branch(branch_start(diverging), radius, branch_target(target), glm::radians(m_joints.angle * 0.5),
editor_track::reverse_insert(std::min(editor_track::diverging_speed(radius), design_speed(tool.track, target.track))), stretchable(target));
Reason = link_problem(target, Link);
return Reason.empty();
}
void editor_mode::finish_branch_link(editor_track::branch_link const &Link)
{
auto *own{m_extend.track};
auto const target{m_extend.snap};
m_extend.active = false;
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> created;
auto const &style{target.track->eType == tt_Normal || m_extend.line.empty() ? *target.track : *m_extend.line.front()};
auto const linked{link_tracks(target, Link, style, states, created)};
push_track_snapshot(std::move(states), std::move(created));
m_node = linked.empty() ? own : linked.back();
ui()->set_node(m_node);
straight_refresh();
ui()->set_status(link_summary(target, Link, editor_track::branch_radius(own->m_paths[1])), false);
}
std::vector<TTrack *> editor_mode::link_tracks(editor_track::snap_target const &Target, editor_track::branch_link const &Link, TTrack const &Style, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Created)
{
auto *target{Target.track};
if (Link.trim != 0.0)
{
States.emplace_back(target, editor_track::capture(*target));
editor_track::move_point(*target, Target.point, Link.landing);
auto &path{target->m_paths[Target.point.path]};
path.points[segment_data::point::control1] = glm::dvec3{0.0};
path.points[segment_data::point::control2] = glm::dvec3{0.0};
editor_track::commit({target});
}
std::vector<TTrack *> pieces;
for (auto const &piece : Link.pieces)
pieces.push_back(editor_track::create_path(Style, piece));
editor_track::commit(pieces);
digitize_drawn(pieces);
Created.insert(Created.end(), pieces.begin(), pieces.end());
return pieces;
}
std::vector<TTrack *> editor_mode::build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, editor_track::slip_curve const &Curve, TTrack &Style, std::string const &Type)
{
auto const at = [](glm::dvec2 const &Planar, double const Y) { return glm::dvec3{Planar.x, Y, Planar.y}; };
auto const straight = [](glm::dvec3 const &Start, glm::dvec3 const &End) {
segment_data path;
path.points[segment_data::point::start] = Start;
path.points[segment_data::point::end] = End;
return path;
};
// the ends of the two diagonals at the crossing are kept apart further than the simulation joins the ends, 2 cm
auto const halfsine{std::sqrt(std::max(1e-6, (1.0 - glm::dot(First, Second)) * 0.5))};
auto const gap{std::max(0.25, 0.06 / halfsine)};
auto const side{cross(First, Second) > 0.0 ? 1.0 : -1.0};
auto const whole{Curve.length()};
// the part of the slip curve from Start along Heading, turning by Turn to Finish, between the distances From and To along it;
// where it changes the radius made of pieces, with one curve from end to end for the versions which know no pieces
auto const arc = [&](glm::dvec3 const &Start, glm::dvec2 const &Heading, double const Turn, glm::dvec3 const &Finish, double const From, double const To) {
auto const place = [&](double const Along) {
auto const local{Curve.point(Along)};
auto const planar{plan_of(Start) + Heading * local.x + glm::dvec2{-Heading.y, Heading.x} * (Turn * local.y)};
return glm::dvec3{planar.x, Start.y + (Finish.y - Start.y) * Along / whole, planar.y};
};
auto const bend = [&](double const Begin, double const End, double const Radius, double const Handle0, double const Handle1) {
auto const d0{turned(Heading, Turn * Curve.heading(Begin))};
auto const d3{turned(Heading, Turn * Curve.heading(End))};
segment_data path;
path.points[segment_data::point::start] = place(Begin);
path.points[segment_data::point::end] = place(End);
if (Radius > 0.0)
{
path.points[segment_data::point::control1] = {d0.x * Handle0, 0.0, d0.y * Handle0};
path.points[segment_data::point::control2] = {-d3.x * Handle1, 0.0, -d3.y * Handle1};
}
path.radius = static_cast<float>(Radius);
return path;
};
std::vector<std::pair<double, double>> zones;
for (auto const &zone : {std::pair{Curve.lead, 0.0}, {Curve.lead + Curve.ends, Curve.radius}, {Curve.lead + Curve.ends + Curve.between, Curve.middle}, {whole - Curve.lead, Curve.radius}, {whole, 0.0}})
{
if (zone.first <= (zones.empty() ? 0.0 : zones.back().first) + 1e-9)
continue;
if (false == zones.empty() && zone.second == zones.back().second)
zones.back().first = zone.first;
else
zones.push_back(zone);
}
// broken at the tips of the blades of the tables, so the model of the switch finds them there
if (Curve.tip > 0.0)
for (auto const cut : {Curve.tip, whole - Curve.tip})
for (std::size_t i = 0; i < zones.size(); ++i)
if (cut > (i > 0 ? zones[i - 1].first : 0.0) + 0.05 && cut < zones[i].first - 0.05)
{
zones.insert(zones.begin() + i, {cut, zones[i].second});
break;
}
std::vector<segment_data> pieces;
auto from{From};
for (auto const &[to, radius] : zones)
{
auto const upto{std::min(To, to)};
if (upto <= from + 1e-6)
continue;
auto const handle{radius > 0.0 ? 4.0 / 3.0 * std::tan((upto - from) / radius / 4.0) * radius : 0.0};
pieces.push_back(bend(from, upto, radius, handle, handle));
from = upto;
}
if (pieces.size() == 1)
return pieces.front();
// the handles of the curve from end to end reach 2/3 of the way to the crossing of the tangents
auto const chord{plan_of(place(To) - place(From))};
auto const d0{turned(Heading, Turn * Curve.heading(From))};
auto const d3{turned(Heading, Turn * Curve.heading(To))};
auto const sine{cross(d0, d3)};
auto path{bend(From, To, Curve.radius, 2.0 / 3.0 * cross(chord, d3) / sine, 2.0 / 3.0 * cross(d0, chord) / sine)};
path.pieces = std::move(pieces);
return path;
};
auto const back = [](segment_data const &Path) {
auto result{reversed(Path)};
std::reverse(result.pieces.begin(), result.pieces.end());
for (auto &piece : result.pieces)
piece = reversed(piece);
return result;
};
auto const middle{whole * 0.5};
auto const split{gap};
auto const centreA{at(Crossing - First * gap, Height)};
auto const centreB{at(Crossing + First * gap, Height)};
auto const centreC{at(Crossing - Second * gap, Height)};
auto const centreD{at(Crossing + Second * gap, Height)};
std::string base;
auto const prefix{Type == "Skrz" ? "editor_skrz" : Type == "Rkp" ? "editor_dkp" : "editor_dks"};
// a crossing keeps the names of its tracks, only its record holds the name
auto const recorded = [](std::string const &Name) {
auto const &records{switch_store::records()};
return std::any_of(records.begin(), records.end(), [&](switch_store::record const &Record) { return Record.name == Name; });
};
for (int i = 1; base.empty() || simulation::Paths.find(base + "_a") != nullptr || simulation::Paths.find(base + "_d") != nullptr || recorded(base); ++i)
base = prefix + std::to_string(i);
std::vector<TTrack *> parts;
if (Type == "Skrz")
{
// the two tracks just cross, their ends far from the crossing
parts.push_back(editor_track::create_path(Style, straight(PointA, PointB)));
parts.push_back(editor_track::create_path(Style, straight(PointC, PointD)));
parts.erase(std::remove(parts.begin(), parts.end(), nullptr), parts.end());
record_slip(parts, "crossing", Type, 0.0, Angle, base);
return parts;
}
editor_track::switch_template shape;
shape.length = glm::distance(plan_of(PointA), Crossing);
bool const single{Type == "Rkp"};
parts.push_back(editor_track::create_switch(shape, {straight(PointA, centreA), arc(PointA, First, side, PointD, 0.0, middle - split)}, Style, base + "_a"));
if (single)
parts.push_back(editor_track::create_path(Style, straight(PointB, centreB)));
else
parts.push_back(editor_track::create_switch(shape, {straight(PointB, centreB), back(arc(PointC, Second, -side, PointB, middle + split, whole))}, Style, base + "_b"));
if (single)
parts.push_back(editor_track::create_path(Style, straight(PointC, centreC)));
else
parts.push_back(editor_track::create_switch(shape, {straight(PointC, centreC), arc(PointC, Second, -side, PointB, 0.0, middle - split)}, Style, base + "_c"));
parts.push_back(editor_track::create_switch(shape, {straight(PointD, centreD), back(arc(PointA, First, side, PointD, middle + split, whole))}, Style, base + "_d"));
parts.push_back(editor_track::create_path(Style, straight(centreA, centreB)));
parts.push_back(editor_track::create_path(Style, straight(centreC, centreD)));
parts.push_back(editor_track::create_path(Style, arc(PointA, First, side, PointD, middle - split, middle + split)));
if (false == single)
parts.push_back(editor_track::create_path(Style, arc(PointC, Second, -side, PointB, middle - split, middle + split)));
parts.erase(std::remove(parts.begin(), parts.end(), nullptr), parts.end());
record_slip(parts, "slip", Type, Curve.radius, Angle, base, Curve.middle);
return parts;
}
glm::dvec3 editor_mode::cursor_on_straight(editor_track::straight const &Line) const
{
// the height of the straight under the cursor, worked out anew till it settles: on a grade the plane of the start of the
// straight would put the cursor metres away from where it points at the track
auto cursor{cursor_level(Line.start.y)};
for (int i = 0; i < 4; ++i)
cursor = cursor_level(Line.start.y + Line.grade * std::clamp(Line.along(cursor), 0.0, Line.length));
return cursor;
}
bool editor_mode::plan_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape, slip_plan &Plan) const
{
Plan = slip_plan{};
// the crossing of the straight with the rail track nearest the cursor, whatever that track is: the slip goes there or
// nowhere, never to another crossing around
TTrack *nearest{nullptr};
glm::dvec2 crossing{0.0};
double best{kSlipReach};
auto const category{Line.tracks.empty() ? 1 : Line.tracks.front()->iCategoryFlag & 15};
for (auto *section : simulation::Region->sections(Point, 300.f))
for (auto const &cell : section->m_cells)
for (auto *track : cell.m_paths)
{
if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != category || std::find(Line.tracks.begin(), Line.tracks.end(), track) != Line.tracks.end())
continue;
for (auto const &path : track->m_paths)
{
bezier const curve{path};
auto const straight{path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}};
int const pieces{straight ? 1 : 16};
auto previous{plan_of(path.points[segment_data::point::start])};
for (int i = 1; i <= pieces; ++i)
{
auto const next{straight ? plan_of(path.points[segment_data::point::end]) : plan_of(curve.point(static_cast<double>(i) / pieces))};
auto const segment{next - previous};
auto const determinant{cross(Line.direction, segment)};
if (std::abs(determinant) > 1e-9)
{
auto const offset{previous - plan_of(Line.start)};
auto const along{cross(offset, segment) / determinant};
auto const fraction{cross(offset, Line.direction) / determinant};
auto const position{plan_of(Line.start) + Line.direction * along};
if (fraction >= -0.01 && fraction <= 1.01 && along >= 0.0 && along <= Line.length && glm::distance(position, plan_of(Point)) < best)
{
best = glm::distance(position, plan_of(Point));
nearest = track;
crossing = position;
}
}
previous = next;
}
}
}
// a double slip nearer the cursor than any crossing is replaced by the one chosen
if (Shape.type == "Rkpd")
{
TTrack *slip{nullptr};
double closest{25.0};
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_Switch && false == track->m_editorremoved && track->DoubleSlip() && glm::distance(plan_of(track->location()), plan_of(Point)) < closest)
{
closest = glm::distance(plan_of(track->location()), plan_of(Point));
slip = track;
}
if (slip != nullptr && (nearest == nullptr || closest < best))
{
Plan.replaces = slip;
return true;
}
}
if (nearest == nullptr)
{
Plan.problem = STR_C("No track crosses the selected straight under the cursor");
return false;
}
if (nearest->eType != tt_Normal)
{
Plan.problem = STR_C("The track crossing here is a switch: a slip goes only where two straights cross");
return false;
}
if (false == editor_track::is_straight(*nearest, m_straights.tolerance))
{
Plan.problem = STR_C("The track crossing here isn't straight: a slip goes only where two straights cross");
return false;
}
auto const second{editor_track::find_straight(*nearest, m_straights.tolerance)};
if (second.tracks.empty())
{
Plan.problem = STR_C("The track crossing here isn't straight: a slip goes only where two straights cross");
return false;
}
auto const first{Line.direction};
auto direction{second.direction};
if (glm::dot(first, direction) < 0.0)
direction = -direction;
auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))};
bool const plain{Shape.type == "Skrz"};
if (angle < glm::radians(0.5) || angle > glm::radians(plain ? 89.0 : 30.0))
{
Plan.problem = STR_C("The straights cross at an angle unsuitable for a double slip");
return false;
}
auto const alongfirst{glm::dot(crossing - plan_of(Line.start), Line.direction)};
auto const alongsecond{glm::dot(crossing - plan_of(second.start), second.direction)};
auto const available{std::min({alongfirst, Line.length - alongfirst, alongsecond, second.length - alongsecond}) - 0.5};
auto const *gauged{Line.tracks.front()};
auto const blade{switch_design::secant_blade(Shape.rail, Shape.radius, Shape.ratio)};
auto curve{editor_track::slip_of(Shape.radius, Shape.middle, angle, 0.5 * std::abs(gauged->fTrackWidth) + gauged->rail_dimensions().head + 0.002, blade.first, blade.second)};
curve.tip = switch_design::cut_tip(Shape.rail, curve.radius);
auto half{plain ? Shape.a : curve.tangent()};
if (plain && half > available)
{
half = available;
if (half < 2.0)
{
Plan.problem = STR_C("Not enough straight track around the crossing");
return false;
}
}
else if (half > available)
{
curve = curve.scaled(available / half);
half = available;
if (curve.radius < 50.0)
{
Plan.problem = STR_C("Not enough straight track around the crossing");
return false;
}
Plan.note = format(STR_C("R reduced to %d m, the straights end %d m from the crossing"), static_cast<int>(curve.radius), static_cast<int>(available));
}
auto const heightfirst = [&](double const Along) { return Line.start.y + Line.grade * Along; };
auto const heightsecond = [&](double const Along) { return second.start.y + second.grade * Along; };
auto const sign{glm::dot(second.direction, direction) > 0.0 ? 1.0 : -1.0};
auto const at = [&](glm::dvec2 const &Planar, double const Height) { return glm::dvec3{Planar.x, Height, Planar.y}; };
Plan.other = nearest;
Plan.crossing = crossing;
Plan.first = first;
Plan.direction = direction;
Plan.angle = angle;
Plan.alongfirst = alongfirst;
Plan.half = half;
Plan.curve = curve;
Plan.points = {at(crossing - first * half, heightfirst(alongfirst - half)), at(crossing + first * half, heightfirst(alongfirst + half)), at(crossing - direction * half, heightsecond(alongsecond - sign * half)),
at(crossing + direction * half, heightsecond(alongsecond + sign * half))};
return true;
}
bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape)
{
auto &state{m_straights};
state.status.clear();
slip_plan plan;
if (false == plan_double_slip(Line, Point, Shape, plan))
{
state.status = plan.problem;
return false;
}
if (plan.replaces != nullptr)
return replace_double_slip(*plan.replaces, Shape);
state.status = plan.note;
auto *other{plan.other};
auto const crossing{plan.crossing};
auto const first{plan.first};
auto const direction{plan.direction};
auto const angle{plan.angle};
auto const alongfirst{plan.alongfirst};
auto const half{plan.half};
auto const curve{plan.curve};
auto const heightfirst = [&](double const Along) { return Line.start.y + Line.grade * Along; };
auto const &pointA{plan.points[0]};
auto const &pointB{plan.points[1]};
auto const &pointC{plan.points[2]};
auto const &pointD{plan.points[3]};
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> created;
std::vector<TTrack *> removed;
TTrack *style{nullptr};
if (false == cut_straight(Line, alongfirst - half, alongfirst + half, states, created, removed, &style))
return false;
auto const secondline{editor_track::find_straight(*other, m_straights.tolerance)};
auto const secondalong{glm::dot(crossing - plan_of(secondline.start), secondline.direction)};
if (secondline.tracks.empty() || false == cut_straight(secondline, secondalong - half, secondalong + half, states, created, removed, nullptr))
{
for (auto *track : created)
editor_track::retire(*track);
for (auto const &entry : states)
{
editor_track::apply(*entry.first, entry.second);
editor_track::commit({entry.first});
}
for (auto *track : removed)
{
editor_track::revive(*track);
editor_track::commit({track});
}
return false;
}
auto &styled{style != nullptr ? *style : *Line.tracks.front()};
// a single slip joins the pair of the tracks on the side of the click
auto const nearer{glm::distance((plan_of(pointA) + plan_of(pointD)) * 0.5, plan_of(Point)) <= glm::distance((plan_of(pointC) + plan_of(pointB)) * 0.5, plan_of(Point))};
auto const parts{Shape.type == "Rkp" && false == nearer ? build_double_slip(pointC, pointD, pointA, pointB, crossing, heightfirst(alongfirst), direction, first, angle, curve, styled, Shape.type)
: build_double_slip(pointA, pointB, pointC, pointD, crossing, heightfirst(alongfirst), first, direction, angle, curve, styled, Shape.type)};
editor_track::commit(parts);
created.insert(created.end(), parts.begin(), parts.end());
push_track_snapshot(std::move(states), std::move(created), std::move(removed));
straight_refresh();
switch_placed(parts);
return true;
}
bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape)
{
auto &state{m_straights};
auto const base{Part.name().substr(0, Part.name().size() - 2)};
std::array<TTrack *, 4> switches{};
for (int i = 0; i < 4; ++i)
{
switches[i] = simulation::Paths.find(base + "_" + static_cast<char>('a' + i));
if (switches[i] == nullptr || switches[i]->m_editorremoved || switches[i]->eType != tt_Switch || switches[i]->m_paths.size() < 2)
{
state.status = STR_C("The double slip isn't complete, it can't be replaced");
return false;
}
}
struct end
{
TTrack *track;
glm::dvec3 point;
glm::dvec2 inward;
TTrack *outer;
};
std::vector<end> ends;
std::vector<TTrack *> parts(switches.begin(), switches.end());
for (auto *track : switches)
{
segment_data const *straight{nullptr};
for (auto const &path : track->m_paths)
if (path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{})
straight = &path;
if (straight == nullptr)
straight = &track->m_paths.front();
auto const start{straight->points[segment_data::point::start]};
ends.push_back({track, start, glm::normalize(plan_of(straight->points[segment_data::point::end] - start)), track->SwitchExtension->pPrevs[0]});
for (int i = 0; i < 2; ++i)
{
auto *inner{track->SwitchExtension->pNexts[i]};
if (inner != nullptr && inner->eType == tt_Normal && std::find(parts.begin(), parts.end(), inner) == parts.end() && inner->Length() < 5.0)
parts.push_back(inner);
}
}
int partner{-1};
for (int i = 1; i < 4; ++i)
if (glm::dot(ends[0].inward, ends[i].inward) < -0.999)
partner = i;
if (partner < 0)
{
state.status = STR_C("The double slip geometry isn't recognized");
return false;
}
auto const &a{ends[0]};
auto const &b{ends[partner]};
std::vector<int> others;
for (int i = 1; i < 4; ++i)
if (i != partner)
others.push_back(i);
if (glm::dot(ends[0].inward, ends[others[0]].inward) < 0.0)
std::swap(others[0], others[1]);
auto const &c{ends[others[0]]};
auto const &d{ends[others[1]]};
auto const first{a.inward};
auto direction{c.inward};
auto const determinant{cross(first, direction)};
if (std::abs(determinant) < 1e-9)
return false;
auto const t{cross(plan_of(c.point) - plan_of(a.point), direction) / determinant};
glm::dvec2 const crossing{plan_of(a.point) + first * t};
auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))};
auto const oldhalf{glm::distance(plan_of(a.point), crossing)};
auto const blade{switch_design::secant_blade(Shape.rail, Shape.radius, Shape.ratio)};
auto curve{editor_track::slip_of(Shape.radius, Shape.middle, angle, 0.5 * std::abs(switches[0]->fTrackWidth) + switches[0]->rail_dimensions().head + 0.002, blade.first, blade.second)};
curve.tip = switch_design::cut_tip(Shape.rail, curve.radius);
auto const full{curve.tangent()};
auto radius{curve.radius};
auto half{full};
auto const height{(a.point.y + b.point.y) * 0.5};
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> created;
std::vector<TTrack *> removed;
if (half > oldhalf)
{
for (auto const *e : {&a, &b, &c, &d})
{
if (e->outer == nullptr || false == editor_track::is_straight(*e->outer, m_straights.tolerance))
{
half = oldhalf;
radius = curve.radius * half / full;
state.status = format(STR_C("R limited to %d m, a neighbouring track isn't straight"), static_cast<int>(radius));
break;
}
auto const line{editor_track::find_straight(*e->outer, m_straights.tolerance)};
auto const reach{std::abs(glm::dot(plan_of(line.end) - crossing, -e->inward))};
auto const reachstart{std::abs(glm::dot(plan_of(line.start) - crossing, -e->inward))};
if (std::max(reach, reachstart) - 0.5 < half)
{
half = std::min(half, std::max(reach, reachstart) - 0.5);
radius = curve.radius * half / full;
state.status = format(STR_C("R reduced to %d m to fit the straights"), static_cast<int>(radius));
}
}
}
for (auto *track : parts)
editor_track::retire(*track);
removed.insert(removed.end(), parts.begin(), parts.end());
TTrack *style{a.outer != nullptr && a.outer->eType == tt_Normal ? a.outer : switches[0]};
std::array<glm::dvec3, 4> points;
std::array<end const *, 4> order{&a, &b, &c, &d};
for (int i = 0; i < 4; ++i)
{
auto const &e{*order[i]};
auto const outward{-e.inward};
auto const target{crossing + outward * half};
points[i] = {target.x, e.point.y, target.y};
if (half < oldhalf - 0.01)
{
auto *filler{editor_track::create_path(*style, [&] {
segment_data path;
path.points[segment_data::point::start] = points[i];
path.points[segment_data::point::end] = e.point;
return path;
}())};
created.push_back(filler);
}
else if (half > oldhalf + 0.01 && e.outer != nullptr)
{
auto const line{editor_track::find_straight(*e.outer, m_straights.tolerance)};
auto const alongold{glm::dot(plan_of(e.point) - plan_of(line.start), line.direction)};
auto const alongnew{glm::dot(target - plan_of(line.start), line.direction)};
cut_straight(line, std::min(alongold, alongnew), std::max(alongold, alongnew), states, created, removed, nullptr);
}
}
auto const parts2{build_double_slip(points[0], points[1], points[2], points[3], crossing, height, first, direction, angle, curve.scaled(half / full), *style)};
std::vector<TTrack *> commitlist(parts2.begin(), parts2.end());
for (auto *track : created)
if (std::find(commitlist.begin(), commitlist.end(), track) == commitlist.end())
commitlist.push_back(track);
editor_track::commit(commitlist);
created.insert(created.end(), parts2.begin(), parts2.end());
push_track_snapshot(std::move(states), std::move(created), std::move(removed));
straight_refresh();
// the drives of the switches replaced go with them, the new ones get theirs
std::vector<scene::instance_handle> drives;
for (auto const *track : switches)
if (auto const drive{find_switch_drive(*track)})
drives.push_back(drive->standing.instance);
if (false == drives.empty() && remove_templates(drives) > 0)
join_last_steps();
for (auto *track : parts2)
if (track != nullptr && track->eType == tt_Switch && place_switch_drive(*track))
join_last_steps();
return true;
}
std::vector<editor_mode::curve_sample> editor_mode::sample_chain(editor_track::chain const &Chain)
{
std::vector<curve_sample> result;
double station{0.0};
for (std::size_t i = 0; i < Chain.tracks.size(); ++i)
{
auto const &path{Chain.tracks[i]->m_paths.front()};
auto const forward{Chain.forward[i]};
bezier const curve{path};
auto const length{glm::distance(curve.p0, curve.p3)};
int const steps{std::max(8, static_cast<int>(std::ceil(length / 0.5)))};
for (int k = (result.empty() ? 0 : 1); k <= steps; ++k)
{
auto const t{forward ? static_cast<double>(k) / steps : 1.0 - static_cast<double>(k) / steps};
auto const position{curve.point(t)};
auto const derivative{forward ? curve.first(t) : -curve.first(t)};
if (false == result.empty())
station += plan_distance(position, result.back().position);
curve_sample sample;
sample.station = station;
sample.position = position;
sample.tangent = glm::normalize(plan_of(derivative));
auto const roll{path.rolls[0] + (path.rolls[1] - path.rolls[0]) * t};
sample.roll = static_cast<float>(forward ? roll : -roll);
sample.radius = path.radius;
result.push_back(sample);
}
}
return result;
}
editor_mode::curve_sample editor_mode::sample_at(std::vector<curve_sample> const &Frame, double const Station)
{
if (Frame.empty())
return {};
if (Station <= Frame.front().station)
return Frame.front();
if (Station >= Frame.back().station)
return Frame.back();
auto const next{std::lower_bound(Frame.begin(), Frame.end(), Station, [](curve_sample const &Sample, double const Value) { return Sample.station < Value; })};
auto const previous{std::prev(next)};
auto const span{std::max(1e-9, next->station - previous->station)};
auto const f{(Station - previous->station) / span};
curve_sample result;
result.station = Station;
result.position = glm::mix(previous->position, next->position, f);
result.tangent = glm::normalize(glm::mix(previous->tangent, next->tangent, f));
result.roll = static_cast<float>(previous->roll + (next->roll - previous->roll) * f);
result.radius = previous->radius;
return result;
}
double editor_mode::nearest_station(std::vector<curve_sample> const &Frame, glm::dvec3 const &Point)
{
double best{std::numeric_limits<double>::max()};
double station{0.0};
for (std::size_t i = 0; i + 1 < Frame.size(); ++i)
{
glm::dvec2 const a{Frame[i].position.x, Frame[i].position.z};
glm::dvec2 const b{Frame[i + 1].position.x, Frame[i + 1].position.z};
auto const segment{b - a};
auto const t{std::clamp(glm::dot(plan_of(Point) - a, segment) / std::max(1e-12, glm::dot(segment, segment)), 0.0, 1.0)};
auto const distance{glm::distance(plan_of(Point), a + segment * t)};
if (distance < best)
{
best = distance;
station = Frame[i].station + (Frame[i + 1].station - Frame[i].station) * t;
}
}
return station;
}
std::vector<segment_data> editor_mode::curved_switch_paths(editor_track::switch_template const &Shape, std::vector<curve_sample> const &Frame, double const Station, int const Direction, int const Side)
{
auto const map = [&](glm::dvec3 const &Local, glm::dvec2 &Tangent, glm::dvec2 const &Localtangent) {
auto const sample{sample_at(Frame, Station + Direction * Local.z)};
auto const along{sample.tangent * static_cast<double>(Direction)};
glm::dvec2 const across{glm::dvec2{-along.y, along.x} * static_cast<double>(Side)};
Tangent = glm::normalize(along * Localtangent.y + across * Localtangent.x);
auto const planar{plan_of(sample.position) + across * Local.x};
auto const cant{glm::radians(static_cast<double>(sample.roll) * Direction)};
auto const cone{-Local.x * Side * std::tan(cant)};
return glm::dvec3{planar.x, sample.position.y + Local.y + cone, planar.y};
};
auto const hermite = [](glm::dvec3 const &Start, glm::dvec2 const &Startdirection, glm::dvec3 const &End, glm::dvec2 const &Enddirection) {
auto const handle{plan_distance(Start, End) / 3.0};
segment_data path;
path.points[segment_data::point::start] = Start;
path.points[segment_data::point::end] = End;
path.points[segment_data::point::control1] = {Startdirection.x * handle, (End.y - Start.y) / 3.0, Startdirection.y * handle};
path.points[segment_data::point::control2] = {-Enddirection.x * handle, -(End.y - Start.y) / 3.0, -Enddirection.y * handle};
return path;
};
auto const bend = [&](segment_data const &Local) {
auto const &start{Local.points[segment_data::point::start]};
auto const &end{Local.points[segment_data::point::end]};
auto const &control1{Local.points[segment_data::point::control1]};
auto const &control2{Local.points[segment_data::point::control2]};
glm::dvec2 const chord{end.x - start.x, end.z - start.z};
auto const localstart{glm::normalize(control1 != glm::dvec3{} ? plan_of(control1) : chord)};
auto const localend{glm::normalize(control2 != glm::dvec3{} ? -plan_of(control2) : chord)};
glm::dvec2 startdirection;
glm::dvec2 enddirection;
auto const p0{map(start, startdirection, localstart)};
auto const p3{map(end, enddirection, localend)};
auto path{hermite(p0, startdirection, p3, enddirection)};
{
auto const cone = [&](double const T) {
auto const point{bezier{path}.point(T)};
auto const sample{sample_at(Frame, nearest_station(Frame, point))};
auto const lateral{glm::dot(plan_of(point) - plan_of(sample.position), glm::dvec2{-sample.tangent.y, sample.tangent.x})};
return sample.position.y - lateral * std::tan(glm::radians(static_cast<double>(sample.roll)));
};
auto const h1{cone(1.0 / 3.0)};
auto const h2{cone(2.0 / 3.0)};
auto const y1{3.0 * h1 - 1.5 * h2 - 5.0 / 6.0 * p0.y + p3.y / 3.0};
auto const y2{3.0 * h2 - 1.5 * h1 + p0.y / 3.0 - 5.0 / 6.0 * p3.y};
path.points[segment_data::point::control1].y = y1 - p0.y;
path.points[segment_data::point::control2].y = y2 - p3.y;
}
auto const rollstart{sample_at(Frame, Station + Direction * start.z).roll * Direction};
auto const rollend{sample_at(Frame, Station + Direction * end.z).roll * Direction};
path.rolls = {static_cast<float>(rollstart), static_cast<float>(rollend)};
path.radius = Local.radius;
{
bezier const curve{path};
auto const begin{plan_of(curve.first(0.0))};
auto const finish{plan_of(curve.first(1.0))};
auto const turn{glm::length(begin) > 1e-9 && glm::length(finish) > 1e-9 ? geometry::signed_angle(begin, finish) : 0.0};
if (Local.radius != 0.f && std::abs(turn) > 1e-6)
path.radius = static_cast<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);
}
return result;
}
void editor_mode::insert_curved_switch(int const Direction, int const Side)
{
auto &tool{m_switch};
auto const &shape{tool.templates[tool.armed]};
auto const total{tool.frame.back().station};
if (shape.length > total - 0.02)
{
tool.status = format(STR_C("The curve is %.1f m long, too short for the %.1f m switch"), total, shape.length);
return;
}
auto const station{std::clamp(tool.along, Direction > 0 ? 0.0 : shape.length, Direction > 0 ? total - shape.length : total)};
auto const from{Direction > 0 ? station : station - shape.length};
auto const to{from + shape.length};
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> created;
std::vector<TTrack *> removed;
std::vector<TTrack *> candidates{tool.frame_tracks};
for (auto const cut : {from, to})
{
auto const point{sample_at(tool.frame, cut).position};
for (auto *candidate : std::vector<TTrack *>(candidates))
{
if (candidate->m_editorremoved || candidate->eType != tt_Normal)
continue;
auto const t{editor_track::nearest_parameter(*candidate, point)};
auto const found{editor_track::point_at(*candidate, t)};
if (plan_distance(found, point) > 0.05)
continue;
if (t > 0.002 && t < 0.998)
{
if (std::find(created.begin(), created.end(), candidate) == created.end() && std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == candidate; }))
states.emplace_back(candidate, editor_track::capture(*candidate));
if (auto *second{editor_track::split_path(*candidate, t)})
{
created.push_back(second);
candidates.push_back(second);
}
}
break;
}
}
for (auto *candidate : candidates)
{
if (candidate->m_editorremoved)
continue;
auto const middle{editor_track::point_at(*candidate, 0.5)};
auto const where{nearest_station(tool.frame, middle)};
if (where > from + 0.01 && where < to - 0.01)
{
editor_track::retire(*candidate);
removed.push_back(candidate);
}
}
auto const paths{curved_switch_paths(shape, tool.frame, station, Direction, Side)};
auto const made{create_switch_set(shape, paths, *tool.frame_tracks.front(), switch_name_for_new())};
created.insert(created.end(), made.begin(), made.end());
if (made.size() == 1)
tool.status = format(STR_C("Switch %s inserted into the curve, Ctrl+Z takes it back"), made.front()->name().c_str());
auto const placed{created};
push_track_snapshot(std::move(states), std::move(created), std::move(removed));
straight_refresh();
switch_placed(placed);
}