mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-20 13:54:53 +02:00
- sleepers3d / sleepers3dplain / sleepers3dbed in eu07.ini: timbers put together from the begin, main and end parts of a model and stretched to their length, plain sleepers of their own model; tracks whose plain sleepers would touch are laid as one cluster, lines of timbers led by priority and turned to the bisectors of frogs and crossings at a limited rate, whole timbers under crossings, split ones between tracks with room to part them, gaps filled per rail and overlaps shortened or dropped; clusters worked out in a background thread while driving, on demand in the editor - the ballast can be left out under the beams (sleepers3dbed no) - blades get their heel and throw from the switch template or blades <heel> <throw> in the scenery; blades of double switches run on into the track linking them to the next switch - rail cutting at frogs, wing rails, movable noses and switch crossings done by one carving pass (RailCarve) - tint of the rail treads applied to the colour only, without the matte band at the frog - a switch trailed through with a movable nose set the other way derails the vehicle - self-tests: sleepercheck, sharedzones, beamsnear, throw, sleepersbg
900 lines
33 KiB
C++
900 lines
33 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://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
#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 "world/Track.h"
|
|
#include "utilities/translation.h"
|
|
|
|
#include "imgui/imgui.h"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <optional>
|
|
#include <tuple>
|
|
|
|
namespace
|
|
{
|
|
|
|
using geometry::bezier;
|
|
using geometry::has_controls;
|
|
using geometry::cross;
|
|
using geometry::grade_of;
|
|
using geometry::plan_distance;
|
|
using geometry::plan_of;
|
|
|
|
struct switch_end_move
|
|
{
|
|
glm::dvec3 from{0.0};
|
|
glm::dvec3 to{0.0};
|
|
double shift{0.0};
|
|
};
|
|
|
|
std::string track_label(TTrack const &Track)
|
|
{
|
|
return Track.name().empty() ? std::string{"(noname)"} : Track.name();
|
|
}
|
|
|
|
std::optional<std::vector<switch_end_move>> switch_end_moves(TTrack const &Track, segment_data const &Main, bool const Online, std::pair<double, double> const &Room, double const Offset, std::string &Status)
|
|
{
|
|
auto const axis{editor_track::main_axis(Track)};
|
|
std::vector<switch_end_move> ends;
|
|
for (auto const index : {segment_data::point::start, segment_data::point::end})
|
|
{
|
|
auto const from{Track.m_paths.front().points[index]};
|
|
auto const to{Main.points[index]};
|
|
if (false == Online || glm::distance(from, to) < 0.005 || axis->lateral(to) > Offset)
|
|
continue;
|
|
auto const target{axis->along(to)};
|
|
if (target < Room.first - 0.01 || target > Room.second + 0.01)
|
|
{
|
|
Status = STR_C("The switch would run into another switch or past the end of the straight");
|
|
return std::nullopt;
|
|
}
|
|
ends.push_back({from, to, index == segment_data::point::start ? axis->along(from) - target : target - axis->along(from)});
|
|
}
|
|
std::stable_sort(ends.begin(), ends.end(), [](switch_end_move const &A, switch_end_move const &B) { return A.shift > B.shift; });
|
|
return ends;
|
|
}
|
|
|
|
std::string switch_rejoin(TTrack &Track, std::vector<editor_track::joint> const &Joints, std::vector<bool> const &Handled, editor_track::edit &Edit)
|
|
{
|
|
std::vector<editor_track::joint> following;
|
|
std::string loose;
|
|
for (std::size_t i = 0; i < Joints.size(); ++i)
|
|
{
|
|
auto const &joint{Joints[i]};
|
|
if (Handled[i] || joint.other->m_editorremoved)
|
|
continue;
|
|
auto const &theirs{joint.other->m_paths[joint.theirs.path]};
|
|
auto const shift{glm::distance(editor_track::point_position(Track, joint.own), editor_track::point_position(*joint.other, joint.theirs))};
|
|
if (shift > 0.3 * bezier{theirs}.plan_length())
|
|
{
|
|
loose += (loose.empty() ? "" : ", ") + track_label(*joint.other);
|
|
continue;
|
|
}
|
|
following.push_back(joint);
|
|
Edit.capture(*joint.other);
|
|
}
|
|
editor_track::follow(Track, following);
|
|
return loose;
|
|
}
|
|
|
|
double constexpr kBranchStraight{500.0}; // how far the straight past the curve next to a switch is followed
|
|
|
|
// direction out of the path at the end
|
|
glm::dvec3 end_outward(segment_data const &Path, bool const Atend)
|
|
{
|
|
bezier const curve{Path};
|
|
auto direction{Atend ? curve.first(1.0) : -curve.first(0.0)};
|
|
if (glm::length(plan_of(direction)) < 1e-9)
|
|
direction = Atend ? curve.p3 - curve.p0 : curve.p0 - curve.p3;
|
|
return direction;
|
|
}
|
|
|
|
// straight from the start of the line to its first curve
|
|
double opening(alignment::result const &Result)
|
|
{
|
|
return Result.elements.empty() || Result.elements.front().kind != alignment::element_kind::straight ? 0.0 : Result.elements.front().length;
|
|
}
|
|
|
|
// straight from the last curve of the line to its end
|
|
double closing(alignment::result const &Result)
|
|
{
|
|
return Result.elements.empty() || Result.elements.back().kind != alignment::element_kind::straight ? 0.0 : Result.elements.back().length;
|
|
}
|
|
|
|
alignment::vertex scaled(alignment::vertex Vertex, double const Factor)
|
|
{
|
|
Vertex.radius *= Factor;
|
|
Vertex.transition_in *= Factor;
|
|
Vertex.transition_out *= Factor;
|
|
for (auto &arc : Vertex.arcs)
|
|
{
|
|
arc.radius *= Factor;
|
|
arc.transition *= Factor;
|
|
}
|
|
return Vertex;
|
|
}
|
|
|
|
// where the path of the index leaves the chain
|
|
glm::dvec3 exit_of(editor_track::chain const &Chain, std::size_t const Index)
|
|
{
|
|
auto const &path{Chain.tracks[Index]->m_paths.front()};
|
|
return path.points[Chain.forward[Index] ? segment_data::point::end : segment_data::point::start];
|
|
}
|
|
|
|
// raises the pieces by Rise at their start, less and less up to nothing at their end
|
|
void lift(std::vector<segment_data> &Pieces, double const Rise)
|
|
{
|
|
std::vector<double> lengths;
|
|
double total{0.0};
|
|
for (auto const &piece : Pieces)
|
|
{
|
|
lengths.push_back(bezier{piece}.plan_length());
|
|
total += lengths.back();
|
|
}
|
|
if (total <= 1e-6 || std::abs(Rise) <= 1e-6)
|
|
return;
|
|
auto const slope{Rise / total};
|
|
double along{0.0};
|
|
for (std::size_t i = 0; i < Pieces.size(); ++i)
|
|
{
|
|
auto &points{Pieces[i].points};
|
|
auto const controls{has_controls(Pieces[i])};
|
|
points[segment_data::point::start].y += Rise - slope * along;
|
|
points[segment_data::point::end].y += Rise - slope * (along + lengths[i]);
|
|
// the controls are kept from their ends
|
|
if (controls)
|
|
{
|
|
points[segment_data::point::control1].y -= slope * glm::length(plan_of(points[segment_data::point::control1]));
|
|
points[segment_data::point::control2].y += slope * glm::length(plan_of(points[segment_data::point::control2]));
|
|
}
|
|
along += lengths[i];
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool editor_mode::switch_room(TTrack &Track, double &Low, double &High, std::string &Reason)
|
|
{
|
|
Low = 0.0;
|
|
High = 0.0;
|
|
if (Track.eType != tt_Switch || Track.m_paths.size() < 2 || has_controls(Track.m_paths.front()))
|
|
return false;
|
|
auto const axis{editor_track::main_axis(Track)};
|
|
if (false == axis.has_value())
|
|
return false;
|
|
auto const line{editor_track::find_straight(Track, m_straights.tolerance)};
|
|
if (false == tracks_editable(line.tracks, Reason))
|
|
return false;
|
|
std::vector<std::tuple<double, double, bool>> spans;
|
|
for (std::size_t i = 0; i < line.tracks.size(); ++i)
|
|
{
|
|
if (line.tracks[i] == &Track)
|
|
continue;
|
|
auto const &path{line.tracks[i]->m_paths[line.paths[i]]};
|
|
auto const a{axis->along(path.points[segment_data::point::start])};
|
|
auto const b{axis->along(path.points[segment_data::point::end])};
|
|
spans.emplace_back(std::min(a, b), std::max(a, b), line.tracks[i]->eType == tt_Normal);
|
|
}
|
|
std::sort(spans.begin(), spans.end());
|
|
double low{0.0};
|
|
double high{axis->length};
|
|
for (auto const &[from, to, plain] : spans)
|
|
if (from > axis->length - 0.01)
|
|
{
|
|
if (false == plain || from > high + 0.01)
|
|
break;
|
|
high = std::max(high, to);
|
|
}
|
|
for (auto span{spans.rbegin()}; span != spans.rend(); ++span)
|
|
{
|
|
auto const &[from, to, plain] = *span;
|
|
if (to < 0.01)
|
|
{
|
|
if (false == plain || to < low - 0.01)
|
|
break;
|
|
low = std::min(low, from);
|
|
}
|
|
}
|
|
Low = low;
|
|
High = high;
|
|
return true;
|
|
}
|
|
|
|
bool editor_mode::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)
|
|
{
|
|
if (Shift > 0.0)
|
|
{
|
|
auto const a{Line.along(From)};
|
|
auto const b{Line.along(To)};
|
|
if (cut_straight(Line, std::min(a, b), std::max(a, b), Edit.states, Edit.created, Edit.removed, nullptr))
|
|
return true;
|
|
Status = STR_C("The switch would overlap another switch, move it onto plain track");
|
|
return false;
|
|
}
|
|
TTrack const *style{nullptr};
|
|
bool filler{false};
|
|
for (auto const &[other, point] : editor_track::connected_points(Track, From))
|
|
{
|
|
if (other->m_editorremoved)
|
|
continue;
|
|
if (other->eType == tt_Normal && std::find(Line.tracks.begin(), Line.tracks.end(), other) != Line.tracks.end())
|
|
{
|
|
Edit.capture(*other);
|
|
editor_track::move_point(*other, point, To);
|
|
continue;
|
|
}
|
|
filler = true;
|
|
if (style == nullptr && other->eType == tt_Normal)
|
|
style = other;
|
|
}
|
|
if (filler)
|
|
{
|
|
segment_data path;
|
|
path.points[segment_data::point::start] = To;
|
|
path.points[segment_data::point::end] = From;
|
|
auto *piece{editor_track::create_path(style != nullptr ? *style : Track, path)};
|
|
Edit.created.push_back(piece);
|
|
Edit.changed.push_back(piece);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::optional<editor_mode::switch_branch> editor_mode::take_switch_branch(TTrack const &Track, editor_track::joint const &Joint, std::size_t const Index) const
|
|
{
|
|
auto *first{Joint.other};
|
|
if (first == nullptr || first->eType != tt_Normal || first->m_paths.empty())
|
|
return std::nullopt;
|
|
auto const category{first->iCategoryFlag & 15};
|
|
auto const regular = [&](TTrack const *Other) { return Other != nullptr && Other->eType == tt_Normal && false == Other->m_editorremoved && editor_track::is_supported(*Other) && (Other->iCategoryFlag & 15) == category; };
|
|
// from the switch on: a straight up to the curve maybe, the curve, the straight past it
|
|
std::vector<TTrack *> run;
|
|
auto *current{first};
|
|
auto atstart{Joint.theirs.kind == editor_track::point_kind::start};
|
|
int phase{0};
|
|
std::size_t straight{0};
|
|
double past{0.0};
|
|
while (regular(current) && std::find(run.begin(), run.end(), current) == run.end() && past < kBranchStraight)
|
|
{
|
|
auto const plain{editor_track::is_straight(*current, m_straights.tolerance)};
|
|
if (false == plain && phase == 2)
|
|
break;
|
|
if (false == plain)
|
|
phase = 1;
|
|
else if (phase == 1)
|
|
{
|
|
phase = 2;
|
|
straight = run.size();
|
|
}
|
|
if (phase == 2)
|
|
past += current->Length();
|
|
run.push_back(current);
|
|
auto *next{atstart ? current->trNext : current->trPrev};
|
|
if (next == nullptr || (next->trPrev != current && next->trNext != current))
|
|
break;
|
|
atstart = next->trPrev == current;
|
|
current = next;
|
|
}
|
|
if (phase != 2)
|
|
return std::nullopt;
|
|
switch_branch branch;
|
|
branch.joint = Index;
|
|
branch.straight = straight;
|
|
std::string error;
|
|
auto &chain{branch.chain};
|
|
if (false == editor_track::find_chain(run.front(), run.back(), chain, error) || chain.tracks != run || plan_distance(chain.start, editor_track::point_position(Track, Joint.own)) > geometry::same_point)
|
|
return std::nullopt;
|
|
auto &design{branch.design};
|
|
design.norms = m_route.design.norms;
|
|
design.shape = m_route.design.shape;
|
|
design.transition_pieces = m_route.design.transition_pieces;
|
|
design.speed = chain.velocity > 0.0 ? chain.velocity : 100.0;
|
|
if (auto const hit{alignment_store::on_track(*run[straight - 1])})
|
|
{
|
|
auto const &stored{alignment_store::records()[hit->record].design};
|
|
design.norms = stored.norms;
|
|
design.shape = stored.shape;
|
|
design.transition_pieces = stored.transition_pieces;
|
|
design.arc_piece_angle = stored.arc_piece_angle;
|
|
design.speed = stored.speed;
|
|
}
|
|
design.norms.checked = false;
|
|
auto const found{editor_track::recognize_line(chain, m_straights.tolerance, design.norms.gauge, design.shape)};
|
|
if (found.size() != 1 || found.front().kink)
|
|
return std::nullopt;
|
|
design.start = chain.start;
|
|
design.end = chain.end;
|
|
design.start_direction = glm::normalize(plan_of(chain.start_direction));
|
|
design.end_direction = glm::normalize(plan_of(chain.end_direction));
|
|
design.start_grade = grade_of(chain.start_direction);
|
|
design.end_grade = grade_of(chain.end_direction);
|
|
auto vertex{route_vertex_of(found.front().shape)};
|
|
vertex.position = found.front().position;
|
|
design.vertices = {vertex};
|
|
branch.result = alignment::compute(design);
|
|
if (false == branch.result.valid)
|
|
return std::nullopt;
|
|
return branch;
|
|
}
|
|
|
|
bool editor_mode::plan_switch_branch(segment_data const &Path, editor_track::joint const &Joint, switch_branch &Branch) const
|
|
{
|
|
auto const atend{Joint.own.kind == editor_track::point_kind::end};
|
|
auto const start{Path.points[atend ? segment_data::point::end : segment_data::point::start]};
|
|
auto const outward{end_outward(Path, atend)};
|
|
auto laid{Branch.design};
|
|
laid.start = start;
|
|
laid.start_direction = glm::normalize(plan_of(outward));
|
|
laid.start_grade = grade_of(outward);
|
|
auto const original{Branch.design.vertices.front()};
|
|
auto const make = [&](double const Factor) {
|
|
laid.vertices = {scaled(original, Factor)};
|
|
return alignment::compute(laid);
|
|
};
|
|
// compute() makes the curve tighter when it doesn't fit between the ends
|
|
auto const fits = [&](alignment::result const &Result) { return Result.valid && false == Result.curves.empty() && Result.curves.front().radius >= laid.vertices.front().radius * (1.0 - 1e-6); };
|
|
double factor{1.0};
|
|
auto result{make(factor)};
|
|
if (false == result.valid || result.curves.empty())
|
|
return false;
|
|
if (false == fits(result))
|
|
{
|
|
// the design keeps the curve the way it was made to fit
|
|
auto &vertex{laid.vertices.front()};
|
|
auto const &report{result.curves.front()};
|
|
auto const reduced{report.radius / vertex.radius};
|
|
if (vertex.compound)
|
|
vertex = scaled(vertex, reduced);
|
|
else
|
|
{
|
|
vertex.radius = report.radius;
|
|
vertex.transition_in = report.transition_in;
|
|
vertex.transition_out = report.transition_out;
|
|
}
|
|
factor = reduced;
|
|
result = alignment::compute(laid);
|
|
if (false == result.valid)
|
|
return false;
|
|
}
|
|
else if (opening(Branch.result) < 0.1 && opening(result) >= 0.1)
|
|
{
|
|
// the curve which adjoined the switch keeps to it: the largest one which fits
|
|
double low{1.0};
|
|
double high{1.5};
|
|
while (fits(make(high)) && high < 5.0)
|
|
high *= 1.5;
|
|
if (fits(make(high)))
|
|
return false;
|
|
for (int i = 0; i < 40; ++i)
|
|
{
|
|
auto const middle{0.5 * (low + high)};
|
|
(fits(make(middle)) ? low : high) = middle;
|
|
}
|
|
factor = low;
|
|
result = make(factor);
|
|
if (false == result.valid)
|
|
return false;
|
|
}
|
|
// the paths are laid up to the first joint on the straight past the old and the new curve
|
|
auto const &chain{Branch.chain};
|
|
auto const end{plan_of(chain.end)};
|
|
auto const behind = [&](glm::dvec3 const &Point) { return glm::dot(end - plan_of(Point), laid.end_direction); };
|
|
auto const curveend{closing(result)};
|
|
auto last{chain.tracks.size()};
|
|
for (auto k{Branch.straight - 1}; k < chain.tracks.size(); ++k)
|
|
if (behind(exit_of(chain, k)) <= curveend - 0.5)
|
|
{
|
|
last = k;
|
|
break;
|
|
}
|
|
if (last == chain.tracks.size())
|
|
return false;
|
|
auto part{laid};
|
|
auto const joint{exit_of(chain, last)};
|
|
part.end = joint;
|
|
part.end_grade = grade_of(end_outward(chain.tracks[last]->m_paths.front(), chain.forward[last]));
|
|
auto const partresult{alignment::compute(part)};
|
|
if (false == partresult.valid)
|
|
return false;
|
|
Branch.part = {};
|
|
Branch.part.tracks.assign(chain.tracks.begin(), chain.tracks.begin() + last + 1);
|
|
Branch.part.forward.assign(chain.forward.begin(), chain.forward.begin() + last + 1);
|
|
Branch.part.start = chain.start;
|
|
Branch.part.end = joint;
|
|
Branch.pieces = alignment::pieces(partresult, part, last + 1);
|
|
if (Branch.pieces.size() < last + 1)
|
|
return false;
|
|
editor_track::keep_heights(Branch.part, Branch.pieces);
|
|
lift(Branch.pieces, start.y - chain.start.y);
|
|
Branch.pieces.front().points[segment_data::point::start] = start;
|
|
Branch.pieces.back().points[segment_data::point::end] = joint;
|
|
Branch.laid = laid;
|
|
Branch.factor = factor;
|
|
return true;
|
|
}
|
|
|
|
void editor_mode::lay_switch_branch(switch_branch const &Branch, editor_track::edit &Edit)
|
|
{
|
|
for (auto *track : Branch.part.tracks)
|
|
Edit.capture(*track);
|
|
auto const created{editor_track::relay(Branch.part, Branch.pieces)};
|
|
Edit.created.insert(Edit.created.end(), created.begin(), created.end());
|
|
// the design kept for the line starting or ending at the switch follows it
|
|
auto const &records{alignment_store::records()};
|
|
auto const from{Branch.chain.start};
|
|
auto const joint{Branch.part.end};
|
|
for (std::size_t i = 0; i < records.size(); ++i)
|
|
{
|
|
auto const &record{records[i]};
|
|
auto const atstart{plan_distance(record.design.start, from) < geometry::same_point};
|
|
if (false == atstart && plan_distance(record.design.end, from) >= geometry::same_point)
|
|
continue;
|
|
auto design{record.design};
|
|
auto &vertices{design.vertices};
|
|
if (vertices.empty())
|
|
return;
|
|
auto const outward{Branch.laid.start_direction};
|
|
auto const k{atstart ? std::size_t{0} : vertices.size() - 1};
|
|
if (atstart)
|
|
{
|
|
design.start = Branch.laid.start;
|
|
design.start_direction = outward;
|
|
design.start_grade = Branch.laid.start_grade;
|
|
}
|
|
else
|
|
{
|
|
design.end = Branch.laid.start;
|
|
design.end_direction = -outward;
|
|
design.end_grade = -Branch.laid.start_grade;
|
|
}
|
|
auto const &corners{record.result.vertices};
|
|
if (vertices.size() >= 2 && corners.size() == vertices.size())
|
|
{
|
|
// the corner moves along its other leg to the line from the switch
|
|
auto const corner{corners[k]};
|
|
auto const leg{corners[atstart ? 1 : k - 1] - corner};
|
|
auto const determinant{cross(outward, leg)};
|
|
if (std::abs(determinant) < 1e-9)
|
|
return;
|
|
auto const offset{cross(corner - plan_of(Branch.laid.start), leg) / determinant};
|
|
if (offset <= 0.0)
|
|
return;
|
|
vertices[k].offset = offset;
|
|
}
|
|
// the curve next to the switch takes the shape it was laid with
|
|
auto const &shape{Branch.laid.vertices.front()};
|
|
if (shape.compound || vertices[k].compound)
|
|
vertices[k] = scaled(vertices[k], Branch.factor);
|
|
else
|
|
{
|
|
vertices[k].radius = shape.radius;
|
|
vertices[k].transition_in = atstart ? shape.transition_in : shape.transition_out;
|
|
vertices[k].transition_out = atstart ? shape.transition_out : shape.transition_in;
|
|
}
|
|
auto const result{alignment::compute(design)};
|
|
if (false == result.valid || false == alignment_store::follows(result, Branch.part.tracks))
|
|
return;
|
|
// the km of the joint the paths are laid up to stays as it was
|
|
auto origin{record.origin};
|
|
if (atstart)
|
|
{
|
|
auto const before{alignment_store::chainage_on(record.result, joint)};
|
|
auto const after{alignment_store::chainage_on(result, joint)};
|
|
if (before.has_value() && after.has_value())
|
|
origin += *before - *after;
|
|
}
|
|
auto const layer{record.layer};
|
|
auto const source{record.source};
|
|
auto const fit{record.fit};
|
|
alignment_store::forget(static_cast<int>(i));
|
|
alignment_store::store(design, result, origin, layer, source, fit);
|
|
return;
|
|
}
|
|
}
|
|
|
|
bool editor_mode::move_switch_paths(TTrack &Track, std::vector<segment_data> const &Paths, std::string &Status)
|
|
{
|
|
if (Paths.size() < 2 || Track.m_paths.size() < 2 || false == editor_track::main_axis(Track).has_value())
|
|
return false;
|
|
double low{0.0};
|
|
double high{0.0};
|
|
std::string reason;
|
|
bool const online{switch_room(Track, low, high, reason)};
|
|
if (false == online && false == reason.empty())
|
|
{
|
|
Status = reason;
|
|
return false;
|
|
}
|
|
auto const ends{switch_end_moves(Track, Paths.front(), online, {low, high}, m_straights.tolerance.offset, Status)};
|
|
if (false == ends.has_value())
|
|
return false;
|
|
auto const joints{editor_track::joints(Track)};
|
|
std::vector<glm::dvec3> positions;
|
|
for (auto const &joint : joints)
|
|
positions.push_back(editor_track::point_position(Track, joint.own));
|
|
std::vector<bool> handled(joints.size(), false);
|
|
auto branches{switch_branches(Track, joints)};
|
|
editor_track::edit edit;
|
|
edit.capture(Track);
|
|
auto const line{online ? editor_track::find_straight(Track, m_straights.tolerance) : editor_track::straight{}};
|
|
for (auto const &end : *ends)
|
|
{
|
|
for (std::size_t i = 0; i < joints.size(); ++i)
|
|
if (glm::distance(positions[i], end.from) < geometry::same_point)
|
|
handled[i] = true;
|
|
if (false == switch_vacate(Track, end.from, end.to, end.shift, line, edit, Status))
|
|
return false;
|
|
}
|
|
Track.m_paths.assign(Paths.begin(), Paths.begin() + 2);
|
|
auto const notes{plan_switch_branches(Track, joints, branches, handled)};
|
|
auto const loose{switch_rejoin(Track, joints, handled, edit)};
|
|
editor_track::commit(edit.changed);
|
|
for (auto const &branch : branches)
|
|
lay_switch_branch(branch, edit);
|
|
push_track_snapshot(std::move(edit.states), std::move(edit.created), std::move(edit.removed));
|
|
if (false == loose.empty())
|
|
Status = STR_C("Too far from the new ends of the switch, left unjoined: ") + loose;
|
|
if (false == notes.empty())
|
|
Status += (Status.empty() ? "" : "\n") + notes;
|
|
return true;
|
|
}
|
|
|
|
std::vector<editor_mode::switch_branch> editor_mode::switch_branches(TTrack const &Track, std::vector<editor_track::joint> const &Joints, std::vector<TTrack const *> const &Skip) const
|
|
{
|
|
std::vector<switch_branch> branches;
|
|
auto const axis{editor_track::main_axis(Track)};
|
|
if (false == axis.has_value())
|
|
return branches;
|
|
for (std::size_t i = 0; i < Joints.size(); ++i)
|
|
{
|
|
if (std::find(Skip.begin(), Skip.end(), Joints[i].other) != Skip.end() || axis->lateral(editor_track::point_position(Track, Joints[i].own)) <= m_straights.tolerance.offset)
|
|
continue;
|
|
if (auto branch{take_switch_branch(Track, Joints[i], i)})
|
|
branches.push_back(std::move(*branch));
|
|
}
|
|
return branches;
|
|
}
|
|
|
|
std::string editor_mode::plan_switch_branches(TTrack const &Track, std::vector<editor_track::joint> const &Joints, std::vector<switch_branch> &Branches, std::vector<bool> &Handled) const
|
|
{
|
|
std::string notes;
|
|
for (auto branch{Branches.begin()}; branch != Branches.end();)
|
|
{
|
|
auto const &joint{Joints[branch->joint]};
|
|
std::string reason;
|
|
if (false == plan_switch_branch(Track.m_paths[joint.own.path], joint, *branch) || false == tracks_editable(branch->part.tracks, reason))
|
|
{
|
|
notes += (notes.empty() ? "" : "\n") + format(STR_C("The curve past the end of the switch can't keep to it, %s follows the switch on its own"), track_label(*joint.other).c_str());
|
|
branch = Branches.erase(branch);
|
|
continue;
|
|
}
|
|
Handled[branch->joint] = true;
|
|
if (std::abs(branch->factor - 1.0) > 1e-4)
|
|
notes += (notes.empty() ? "" : "\n") + format(STR_C("The curve past the end of the switch keeps to it with the radius of %.1f m instead of %.1f m"), branch->laid.vertices.front().radius, branch->design.vertices.front().radius);
|
|
++branch;
|
|
}
|
|
return notes;
|
|
}
|
|
|
|
bool editor_mode::double_room(double_switch const &Set, double &Low, double &High, double &Length, std::string &Reason)
|
|
{
|
|
double low{0.0};
|
|
double high{0.0};
|
|
double low2{0.0};
|
|
double high2{0.0};
|
|
auto const axis{editor_track::main_axis(*Set.first)};
|
|
if (false == axis.has_value() || false == switch_room(*Set.first, low, high, Reason) || false == switch_room(*Set.second, low2, high2, Reason))
|
|
return false;
|
|
auto const &main{Set.second->m_paths.front()};
|
|
Low = low;
|
|
High = axis->along(main.points[segment_data::point::start]) + high2;
|
|
Length = axis->along(main.points[segment_data::point::end]);
|
|
return true;
|
|
}
|
|
|
|
bool editor_mode::move_double_paths(double_switch const &Set, std::vector<segment_data> const &Paths, std::string &Status)
|
|
{
|
|
auto const axis{editor_track::main_axis(*Set.first)};
|
|
if (Paths.size() < 5 || false == axis.has_value())
|
|
return false;
|
|
double low{0.0};
|
|
double high{0.0};
|
|
double length{0.0};
|
|
std::string reason;
|
|
bool const online{double_room(Set, low, high, length, reason)};
|
|
if (false == online && false == reason.empty())
|
|
{
|
|
Status = reason;
|
|
return false;
|
|
}
|
|
struct outer_end
|
|
{
|
|
TTrack *track{nullptr};
|
|
switch_end_move move;
|
|
};
|
|
std::vector<outer_end> ends;
|
|
auto const outer = [&](TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, bool const Atstart) {
|
|
if (false == online || glm::distance(From, To) < 0.005 || axis->lateral(To) > m_straights.tolerance.offset)
|
|
return true;
|
|
auto const target{axis->along(To)};
|
|
if (target < low - 0.01 || target > high + 0.01)
|
|
{
|
|
Status = STR_C("The switch would run into another switch or past the end of the straight");
|
|
return false;
|
|
}
|
|
ends.push_back({&Track, {From, To, Atstart ? axis->along(From) - target : target - axis->along(From)}});
|
|
return true;
|
|
};
|
|
if (false == outer(*Set.first, Set.first->m_paths.front().points[segment_data::point::start], Paths[0].points[segment_data::point::start], true) ||
|
|
false == outer(*Set.second, Set.second->m_paths.front().points[segment_data::point::end], Paths[2].points[segment_data::point::end], false))
|
|
return false;
|
|
std::stable_sort(ends.begin(), ends.end(), [](outer_end const &A, outer_end const &B) { return A.move.shift > B.move.shift; });
|
|
std::array<TTrack *, 2> const switches{Set.first, Set.second};
|
|
std::array<std::vector<editor_track::joint>, 2> joints;
|
|
std::array<std::vector<glm::dvec3>, 2> positions;
|
|
std::array<std::vector<bool>, 2> handled;
|
|
for (std::size_t k = 0; k < 2; ++k)
|
|
{
|
|
joints[k] = editor_track::joints(*switches[k]);
|
|
for (auto const &joint : joints[k])
|
|
{
|
|
positions[k].push_back(editor_track::point_position(*switches[k], joint.own));
|
|
handled[k].push_back(joint.other == Set.link || joint.other == Set.first || joint.other == Set.second);
|
|
}
|
|
}
|
|
auto const joint{Set.first->m_paths.front().points[segment_data::point::end]};
|
|
auto const &link{Set.link->m_paths.front()};
|
|
bool const reversed{glm::distance(link.points[segment_data::point::start], joint) > glm::distance(link.points[segment_data::point::end], joint)};
|
|
std::array<std::vector<switch_branch>, 2> branches;
|
|
for (std::size_t k = 0; k < 2; ++k)
|
|
branches[k] = switch_branches(*switches[k], joints[k], {Set.first, Set.link, Set.second});
|
|
editor_track::edit edit;
|
|
for (auto *track : {Set.first, Set.link, Set.second})
|
|
edit.capture(*track);
|
|
for (auto const &end : ends)
|
|
{
|
|
for (std::size_t k = 0; k < 2; ++k)
|
|
for (std::size_t i = 0; i < joints[k].size(); ++i)
|
|
if (glm::distance(positions[k][i], end.move.from) < geometry::same_point)
|
|
handled[k][i] = true;
|
|
auto const line{editor_track::find_straight(*end.track, m_straights.tolerance)};
|
|
if (false == switch_vacate(*end.track, end.move.from, end.move.to, end.move.shift, line, edit, Status))
|
|
return false;
|
|
}
|
|
Set.first->m_paths.assign(Paths.begin(), Paths.begin() + 2);
|
|
Set.second->m_paths.assign(Paths.begin() + 2, Paths.begin() + 4);
|
|
Set.link->m_paths = {reversed ? geometry::reversed(Paths[4]) : Paths[4]};
|
|
std::string notes;
|
|
for (std::size_t k = 0; k < 2; ++k)
|
|
{
|
|
auto const note{plan_switch_branches(*switches[k], joints[k], branches[k], handled[k])};
|
|
if (false == note.empty())
|
|
notes += (notes.empty() ? "" : "\n") + note;
|
|
}
|
|
std::string loose;
|
|
for (std::size_t k = 0; k < 2; ++k)
|
|
{
|
|
auto const left{switch_rejoin(*switches[k], joints[k], handled[k], edit)};
|
|
if (false == left.empty())
|
|
loose += (loose.empty() ? "" : ", ") + left;
|
|
}
|
|
editor_track::commit(edit.changed);
|
|
for (auto const &set : branches)
|
|
for (auto const &branch : set)
|
|
lay_switch_branch(branch, edit);
|
|
push_track_snapshot(std::move(edit.states), std::move(edit.created), std::move(edit.removed));
|
|
if (false == loose.empty())
|
|
Status = STR_C("Too far from the new ends of the switch, left unjoined: ") + loose;
|
|
if (false == notes.empty())
|
|
Status += (Status.empty() ? "" : "\n") + notes;
|
|
return true;
|
|
}
|
|
|
|
bool editor_mode::slide_switch(TTrack &Track, double const Distance)
|
|
{
|
|
if (std::abs(Distance) < 0.005)
|
|
return false;
|
|
auto const set{double_of(Track)};
|
|
auto &first{set.has_value() ? *set->first : Track};
|
|
if (first.m_paths.size() < 2)
|
|
return false;
|
|
auto paths{editor_track::slid(first, Distance)};
|
|
if (paths.empty())
|
|
return false;
|
|
std::string status;
|
|
bool moved{false};
|
|
if (set.has_value())
|
|
{
|
|
auto const shift{paths.front().points[segment_data::point::start] - first.m_paths.front().points[segment_data::point::start]};
|
|
paths = double_paths(*set);
|
|
for (auto &path : paths)
|
|
path.translate(shift);
|
|
moved = move_double_paths(*set, paths, status);
|
|
}
|
|
else
|
|
moved = move_switch_paths(Track, paths, status);
|
|
if (false == moved)
|
|
{
|
|
m_switch.status = status;
|
|
ui()->set_status(status, true);
|
|
return false;
|
|
}
|
|
switch_store::follow();
|
|
records_to_history();
|
|
straight_refresh();
|
|
m_switch.status = status.empty() ? format(STR_C("Switch %s moved %.2f m along the straight"), first.name().c_str(), Distance) : status;
|
|
ui()->set_status(m_switch.status, false == status.empty());
|
|
return true;
|
|
}
|
|
|
|
bool editor_mode::switch_slides(TTrack const &Track) const
|
|
{
|
|
if (Track.eType == tt_Normal)
|
|
{
|
|
auto const set{double_of(Track)};
|
|
return set.has_value() && switch_slides(*set->first) && switch_slides(*set->second);
|
|
}
|
|
return Track.eType == tt_Switch && Track.m_paths.size() == 2 && false == Track.m_editorremoved && false == has_controls(Track.m_paths.front());
|
|
}
|
|
|
|
void editor_mode::switch_slide_start(TTrack &Track)
|
|
{
|
|
auto &slide{m_switch.slide};
|
|
slide = {};
|
|
std::string reason;
|
|
auto const set{double_of(Track)};
|
|
auto &first{set.has_value() ? *set->first : Track};
|
|
auto const axis{editor_track::main_axis(first)};
|
|
if (false == axis.has_value())
|
|
return;
|
|
slide.length = axis->length;
|
|
if (false == (set.has_value() ? double_room(*set, slide.low, slide.high, slide.length, reason) : switch_room(first, slide.low, slide.high, reason)))
|
|
{
|
|
if (false == reason.empty())
|
|
ui()->set_status(reason, true);
|
|
return;
|
|
}
|
|
auto const &mouse{ImGui::GetIO().MousePos};
|
|
slide.active = true;
|
|
slide.track = &first;
|
|
slide.pressed = {mouse.x, mouse.y};
|
|
slide.grip = axis->along(cursor_level(axis->origin.y));
|
|
std::vector<TTrack *> members{&first};
|
|
if (set.has_value())
|
|
members = {set->first, set->link, set->second};
|
|
for (auto *member : members)
|
|
{
|
|
if (member->eType != tt_Switch)
|
|
continue;
|
|
auto const line{editor_track::find_straight(*member, m_straights.tolerance)};
|
|
for (std::size_t i = 0; i < line.tracks.size(); ++i)
|
|
{
|
|
if (std::find(members.begin(), members.end(), line.tracks[i]) != members.end())
|
|
continue;
|
|
auto const &path{line.tracks[i]->m_paths[line.paths[i]]};
|
|
for (auto const index : {segment_data::point::start, segment_data::point::end})
|
|
slide.joints.push_back(axis->along(path.points[index]));
|
|
}
|
|
}
|
|
std::vector<TTrack const *> const skip(members.begin(), members.end());
|
|
for (auto *member : members)
|
|
{
|
|
if (member->eType != tt_Switch)
|
|
continue;
|
|
std::size_t const offset{set.has_value() && member == set->second ? 2u : 0u};
|
|
auto const joints{editor_track::joints(*member)};
|
|
for (auto &branch : switch_branches(*member, joints, skip))
|
|
{
|
|
auto const &joint{joints[branch.joint]};
|
|
slide.branches.push_back({joint, offset + static_cast<std::size_t>(joint.own.path), std::move(branch)});
|
|
}
|
|
}
|
|
}
|
|
|
|
void editor_mode::switch_slide_update()
|
|
{
|
|
auto &slide{m_switch.slide};
|
|
if (false == slide.active)
|
|
return;
|
|
auto const axis{slide.track != nullptr && false == slide.track->m_editorremoved && slide.track->m_paths.size() >= 2 ? editor_track::main_axis(*slide.track) : std::nullopt};
|
|
if (false == axis.has_value())
|
|
{
|
|
slide = {};
|
|
return;
|
|
}
|
|
auto const length{slide.length};
|
|
auto const distance{std::clamp(axis->along(cursor_level(axis->origin.y)) - slide.grip, slide.low, slide.high - length)};
|
|
double best{1.0};
|
|
double snapped{distance};
|
|
for (auto const joint : slide.joints)
|
|
for (auto const end : {0.0, length})
|
|
if (auto const gap{std::abs(joint - (end + distance))}; gap < best && std::abs(joint - end) > 0.01)
|
|
{
|
|
best = gap;
|
|
snapped = joint - end;
|
|
}
|
|
slide.distance = std::clamp(snapped, slide.low, slide.high - length);
|
|
}
|
|
|
|
void editor_mode::switch_slide_finish()
|
|
{
|
|
auto &slide{m_switch.slide};
|
|
if (false == slide.active)
|
|
return;
|
|
auto *track{slide.track};
|
|
auto const distance{slide.distance};
|
|
auto const &mouse{ImGui::GetIO().MousePos};
|
|
auto const dragged{glm::length(glm::vec2{mouse.x, mouse.y} - slide.pressed) > kDragPixels};
|
|
slide = {};
|
|
if (track != nullptr && false == track->m_editorremoved && dragged)
|
|
slide_switch(*track, distance);
|
|
}
|
|
|
|
void editor_mode::draw_switch_slide() const
|
|
{
|
|
auto const &slide{m_switch.slide};
|
|
if (false == slide.active || slide.track == nullptr || slide.track->m_paths.size() < 2)
|
|
return;
|
|
auto const &mouse{ImGui::GetIO().MousePos};
|
|
if (glm::length(glm::vec2{mouse.x, mouse.y} - slide.pressed) <= kDragPixels)
|
|
return;
|
|
auto paths{editor_track::slid(*slide.track, slide.distance)};
|
|
if (paths.empty())
|
|
return;
|
|
if (auto const set{double_of(*slide.track)})
|
|
{
|
|
auto const shift{paths.front().points[segment_data::point::start] - slide.track->m_paths.front().points[segment_data::point::start]};
|
|
paths = double_paths(*set);
|
|
for (auto &path : paths)
|
|
path.translate(shift);
|
|
}
|
|
screen_projection const projection;
|
|
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
|
|
auto const draw = [&](segment_data const &Path, ImU32 const Color, float const Width) {
|
|
bezier const curve{Path};
|
|
auto previous{curve.point(0.0)};
|
|
for (int i = 1; i <= 24; ++i)
|
|
{
|
|
auto const next{curve.point(i / 24.0)};
|
|
projection.line(drawlist, previous, next, Color, Width);
|
|
previous = next;
|
|
}
|
|
};
|
|
for (auto const &path : paths)
|
|
draw(path, overlay_color::marked, 3.0f);
|
|
// the curves past the switch, laid again up to the straight beyond them
|
|
for (auto const &line : slide.branches)
|
|
{
|
|
if (line.path >= paths.size())
|
|
continue;
|
|
auto branch{line.branch};
|
|
if (plan_switch_branch(paths[line.path], line.joint, branch))
|
|
for (auto const &piece : branch.pieces)
|
|
draw(piece, overlay_color::grip, 2.0f);
|
|
}
|
|
ImVec2 at;
|
|
auto const start{paths.front().points[segment_data::point::start]};
|
|
if (false == projection.project(start, at))
|
|
return;
|
|
auto const km{alignment_store::chainage_at(start)};
|
|
auto const label{km.has_value() ? format("%+.2f m km %.3f", slide.distance, *km / 1000.0) : format("%+.2f m", slide.distance)};
|
|
auto const size{ImGui::CalcTextSize(label.c_str())};
|
|
drawlist->AddRectFilled(ImVec2(at.x + 10.0f, at.y - 8.0f), ImVec2(at.x + 18.0f + size.x, at.y + 8.0f), IM_COL32(0, 0, 0, 180), 3.0f);
|
|
drawlist->AddText(ImVec2(at.x + 14.0f, at.y - 8.0f), overlay_color::highlight, label.c_str());
|
|
}
|