17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-20 11:34:53 +02:00
Files
maszyna/application/editormoderoute.cpp
maj00r 16348c655a refactor(editor): the track mode split by tool instead of one file of 6900 lines
- editormoderoute.cpp: the route design, its kept records and digitizing
- editormodestraights.cpp: the plan tool: straights, the detour, the curve fit
- editormodelay.cpp: laying track, free ends and extending, the build tools
- editormodeswitchplace.cpp: placing switches, crossovers and links, double slips, curved switches
- editormodetrack.cpp keeps the selection, the snapshots, the tabs, the inspector and the path panel, the hints
- the sizes of the handles and reaches and the small helpers they all use in editormodetrackshared.h

The code is moved as it was.
2026-10-18 02:24:04 +02:00

1226 lines
48 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;
bool editor_mode::route_active() const
{
return ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::route && false == m_route.chain.tracks.empty();
}
void editor_mode::route_recommend(alignment::vertex &Vertex) const
{
auto const recommended{alignment::recommend(m_route.design.speed, Vertex.radius, m_route.design.norms)};
Vertex.cant = recommended.cant;
Vertex.transition_in = recommended.transition;
Vertex.transition_out = recommended.transition;
}
void editor_mode::route_bind_ends()
{
auto const &chain{m_route.chain};
auto &design{m_route.design};
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);
design.start_radius = chain.start_radius;
design.end_radius = chain.end_radius;
design.start_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, false, m_straights.tolerance));
design.end_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, true, m_straights.tolerance));
}
void editor_mode::route_reset()
{
auto &route{m_route};
route.chain = {};
route.error.clear();
route.status.clear();
route.vertex = -1;
route.grip = -1;
route.result = {};
if (route.from == nullptr || route.to == nullptr)
return;
if (false == editor_track::find_chain(route.from, route.to, route.chain, route.error, true))
return;
if (false == route.chain.switches.empty())
route.status = format(STR_C("The fragment runs through %zu switches along their main tracks. They move as a whole with the line, "
"so they have to fall on its straights"),
route.chain.switches.size());
auto const &chain{route.chain};
alignment::design design;
design.norms = route.design.norms;
design.shape = route.design.shape;
design.transition_pieces = route.design.transition_pieces;
design.speed = chain.velocity > 0.0 ? chain.velocity : 100.0;
route.design = design;
route_bind_ends();
design = route.design;
glm::dvec2 intersection;
if (false == alignment::collinear(design))
{
alignment::vertex vertex;
vertex.radius = chain.radius > 0.0 ? std::max(10.0, std::round(chain.radius / 10.0) * 10.0) : 1000.0;
route_recommend(vertex);
if (alignment::tangent_intersection(design, intersection))
{
route.design.vertices.push_back(vertex);
}
else
{
auto const distance{plan_distance(design.start, design.end)};
vertex.offset = distance / 3.0;
route.design.vertices.push_back(vertex);
route.design.vertices.push_back(vertex);
}
}
route_update();
}
bool editor_mode::route_from_curve(TTrack &Track)
{
editor_track::curve curve;
if (false == editor_track::find_curve(Track, m_straights.tolerance, m_route.design.norms.gauge, curve))
{
m_route.chain = {};
m_route.result = {};
m_route.vertex = -1;
m_route.grip = -1;
m_route.error = STR_C("No curve here: LMB on a curve");
return false;
}
m_route.from = curve.from;
m_route.to = curve.to;
route_reset();
if (m_route.chain.tracks.empty())
return false;
auto &design{m_route.design};
auto vertex{route_vertex_of(curve)};
m_route.designed = curve.designed.has_value() ? 1 : 0;
if (curve.reversals == 0)
{
design.vertices = {vertex};
}
else
{
auto const distance{plan_distance(design.start, design.end)};
vertex.offset = distance / 3.0;
vertex.compound = false;
vertex.reverse_turn = false;
design.vertices = {vertex, vertex};
}
route_update();
m_route.status = STR_C("Curve of ") + std::to_string(m_route.chain.tracks.size()) + STR_C(" paths, turning ") + std::to_string(static_cast<int>(std::round(glm::degrees(curve.turn)))) + STR_C(" deg");
return true;
}
alignment::vertex editor_mode::route_vertex_of(editor_track::curve const &Curve) const
{
if (Curve.designed.has_value())
return *Curve.designed;
alignment::vertex vertex;
vertex.radius = Curve.radius > 0.0 ? std::max(1.0, Curve.radius) : 1000.0;
vertex.transition_in = std::max(0.0, Curve.transition_in);
vertex.transition_out = std::max(0.0, Curve.transition_out);
vertex.cant = std::max(0.0, Curve.cant);
vertex.reverse_turn = std::abs(Curve.turn) > glm::pi<double>();
compound_from(Curve, vertex);
return vertex;
}
bool editor_mode::route_load(TTrack &Track)
{
m_route.designed = 0;
if (false == (m_route.single ? route_from_curve(Track) : route_from_line(Track)))
return false;
route_read_stored(Track);
return true;
}
// the design kept in the scenery for the fragment takes the place of the one read from the paths; its chainage is carried on
void editor_mode::route_read_stored(TTrack &Track)
{
auto &route{m_route};
route.departed = -1;
route.departure = 0.0;
route.origin = 0.0;
if (route.chain.tracks.empty())
return;
auto const chain{route.chain};
bool reversed;
auto index{alignment_store::find(chain.start, chain.end, reversed)};
route.departed = alignment_store::departed(chain.tracks, route.departure);
auto const loose{std::any_of(chain.tracks.begin(), chain.tracks.end(), [](TTrack const *Track) { return false == alignment_store::on_track(*Track).has_value(); })};
bool digitized{false};
int parts{0};
int skipped{0};
std::string failure;
if (index < 0 && route.departed < 0 && loose && false == route.single)
{
parts = digitize_parts(chain, &failure, skipped);
if (parts == 1 && skipped == 0)
{
alignment_store::settle();
index = alignment_store::find(chain.start, chain.end, reversed);
digitized = true;
}
else if (parts > 0)
{
alignment_store::settle();
route_from_line(Track);
route.departed = alignment_store::departed(route.chain.tracks, route.departure);
}
}
if (index >= 0 && false == reversed && false == route.single)
{
route_take_stored(index);
}
else if (auto const km{alignment_store::chainage_at(chain.start)})
{
auto const ahead{chain.start + glm::dvec3{route.design.start_direction.x, 0.0, route.design.start_direction.y}};
auto const further{alignment_store::chainage_at(ahead)};
if (further.has_value() && *further > *km)
route.origin = *km;
}
if (digitized && route.designed == 0)
route.status += STR_C("\nThe line is digitized now, a straight");
else if (digitized)
route.status += format(STR_C("\nThe line is digitized now, curves kept in the scenery: %d"), route.designed);
else if (parts > 0)
route.status += format(STR_C("\nThe line is digitized in parts: %d, paths left out: %d"), parts, skipped) + (failure.empty() ? std::string{} : " - " + failure);
else if (skipped > 0 && loose && skipped < static_cast<int>(chain.tracks.size()))
route.status += format(STR_C("\nPaths left out of the digitized line: %d"), skipped) + (failure.empty() ? std::string{} : " - " + failure);
else if (false == failure.empty())
route.status += STR_C("\nThe line can't be digitized: ") + failure;
else if (route.designed > 0)
route.status += format(STR_C("\nCurves read from the design kept in the scenery: %d"), route.designed);
else if (false == route.design.vertices.empty())
route.status += STR_C("\nCurves recognized from the paths, the scenery keeps no design for them");
}
void editor_mode::route_take_stored(int const Index)
{
auto &route{m_route};
auto const &records{alignment_store::records()};
if (Index < 0 || Index >= static_cast<int>(records.size()))
return;
auto const &stored{records[Index]};
auto &design{route.design};
design.vertices = stored.design.vertices;
design.shape = stored.design.shape;
design.speed = stored.design.speed;
design.norms = stored.design.norms;
design.transition_pieces = stored.design.transition_pieces;
design.arc_piece_angle = stored.design.arc_piece_angle;
route.designed = static_cast<int>(design.vertices.size());
route.origin = stored.origin;
route.vertex = -1;
route.grip = -1;
route_update();
}
void editor_mode::render_route_stored()
{
auto &route{m_route};
ImGui::PushItemWidth(120.0f);
auto km{route.origin / 1000.0};
if (ImGui::InputDouble(STR_C("Km of the start"), &km, 0.0, 0.0, "%.3f"))
route.origin = km * 1000.0;
ImGui::PopItemWidth();
item_tooltip("Chainage of the start of the fragment, kept with the design in the scenery; the vertical profile of these paths counts from it");
if (ImGui::SmallButton(STR_C("Digitize all the tracks")))
{
digitize_all();
return;
}
item_tooltip("Every line of the scenery not digitized yet is kept in it with its straights, arcs and transition curves, the way the drawn tracks are kept; the paths stay as they are");
auto const &records{alignment_store::records()};
if (route.departed < 0 || route.departed >= static_cast<int>(records.size()))
return;
auto const &layer{scene::Layers.layer(records[route.departed].layer).name};
ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), STR_C("The design kept in %s for this fragment departs from its paths by up to %.2f m"), layer.c_str(), route.departure);
if (ImGui::SmallButton(STR_C("Take the kept design")))
{
route_take_stored(route.departed);
route.status = STR_C("The kept design is loaded, applying it lays the paths by it again");
}
item_tooltip("Loads the design kept in the scenery in place of the one read from the paths; nothing changes until it's applied");
ImGui::SameLine();
if (ImGui::SmallButton(STR_C("Forget the kept design")))
{
alignment_store::forget(route.departed);
alignment_store::settle();
route.departed = -1;
route.status = STR_C("The kept design will be removed from ") + layer + STR_C(" on save");
}
item_tooltip("The paths stay as they are, the scenery no longer keeps a design for them");
}
void editor_mode::route_store()
{
auto &route{m_route};
if (false == route.result.valid || route.chain.tracks.empty() || scene::Layers.empty())
return;
alignment_store::store(route.design, route.result, route.origin, alignment_store::layer_of(*route.chain.tracks.front()));
route.departed = -1;
route.departure = 0.0;
}
bool editor_mode::digitize_drawn(std::vector<TTrack *> const &Created, bool const Alone)
{
std::vector<TTrack *> drawn;
for (auto *track : Created)
if (track != nullptr && track->eType == tt_Normal && false == track->m_editorremoved && false == track->m_paths.empty())
drawn.push_back(track);
if (drawn.empty() || scene::Layers.empty())
return false;
editor_track::chain chain;
std::string error;
if (editor_track::find_run(*drawn.front(), kDrawnReach, chain, error) && digitize_chain(chain))
return true;
if (Alone && editor_track::find_chain(drawn.front(), drawn.back(), chain, error) && chain.tracks.size() == drawn.size() && digitize_chain(chain))
return true;
WriteLog("Editor: the drawn paths don't make a line of straights, arcs and transition curves, no design kept for them", logtype::generic);
return false;
}
void editor_mode::digitize_all()
{
std::set<TTrack const *> visited;
int kept{0};
int failed{0};
std::string first;
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty() || false == editor_track::is_supported(*track) || visited.count(track) > 0)
continue;
if (alignment_store::on_track(*track).has_value())
continue;
editor_track::chain chain;
std::string error;
if (false == editor_track::find_run(*track, kDrawnReach, chain, error) || chain.tracks.empty())
{
visited.insert(track);
continue;
}
visited.insert(chain.tracks.begin(), chain.tracks.end());
std::string reason;
kept += digitize_parts(chain, &reason, failed);
if (first.empty())
first = reason;
}
alignment_store::settle();
auto text{format(STR_C("Lines digitized: %d, paths left out: %d"), kept, failed)};
if (false == first.empty())
text += STR_C(", the first: ") + first;
WriteLog("Editor: " + text, logtype::generic);
ui()->set_status(text, failed > 0);
if (false == m_route.chain.tracks.empty())
route_load(*m_route.chain.tracks[m_route.chain.tracks.size() / 2]);
}
int editor_mode::digitize_parts(editor_track::chain const &Chain, std::string *Reason, int &Skipped)
{
auto const count{Chain.tracks.size()};
if (count == 0 || std::all_of(Chain.tracks.begin(), Chain.tracks.end(), [](TTrack const *Track) { return alignment_store::on_track(*Track).has_value(); }))
return 0;
TTrack *culprit{nullptr};
std::string reason;
if (digitize_chain(Chain, &reason, &culprit))
return 1;
if (count == 1)
{
++Skipped;
reason = (Chain.tracks.front()->name().empty() ? std::string{"(noname)"} : Chain.tracks.front()->name()) + ": " + reason;
WriteLog("Editor: digitizing - " + reason, logtype::generic);
if (Reason != nullptr && Reason->empty())
*Reason = reason;
return 0;
}
auto const found{static_cast<std::size_t>(std::find(Chain.tracks.begin(), Chain.tracks.end(), culprit) - Chain.tracks.begin())};
auto const split{found < count ? std::max<std::size_t>(1, found) : count / 2};
int parts{0};
for (auto const &[first, last] : {std::pair<std::size_t, std::size_t>{0, split - 1}, std::pair<std::size_t, std::size_t>{split, count - 1}})
{
editor_track::chain part;
std::string error;
if (editor_track::find_chain(Chain.tracks[first], Chain.tracks[last], part, error) && part.tracks.size() == last - first + 1)
parts += digitize_parts(part, Reason, Skipped);
else
Skipped += static_cast<int>(last - first + 1);
}
return parts;
}
bool editor_mode::digitize_chain(editor_track::chain const &Chain, std::string *Reason, TTrack **Culprit)
{
if (Chain.tracks.empty())
return false;
auto const name = [](TTrack const *Track) { return Track == nullptr || Track->name().empty() ? std::string{"(noname)"} : Track->name(); };
auto const nearest = [&](glm::dvec2 const &Point) {
TTrack *found{nullptr};
double distance{std::numeric_limits<double>::max()};
for (auto *track : Chain.tracks)
for (auto const end : {segment_data::point::start, segment_data::point::end})
if (auto const d{glm::distance(plan_of(track->m_paths.front().points[end]), Point)}; d < distance)
{
distance = d;
found = track;
}
return found;
};
alignment::design design;
design.norms = m_route.design.norms;
design.transition_pieces = m_route.design.transition_pieces;
design.arc_piece_angle = m_route.design.arc_piece_angle;
design.speed = Chain.velocity > 0.0 ? Chain.velocity : 100.0;
design.start = Chain.start;
design.end = Chain.end;
if (glm::length(plan_of(Chain.start_direction)) < 1e-9 || glm::length(plan_of(Chain.end_direction)) < 1e-9)
return false;
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);
design.start_radius = Chain.start_radius;
design.end_radius = Chain.end_radius;
auto const preferred{m_route.design.shape};
auto const other{preferred == alignment::transition_shape::clothoid ? alignment::transition_shape::cubic_parabola : alignment::transition_shape::clothoid};
alignment::result result;
alignment::design best;
alignment::result bestresult;
double closest{std::numeric_limits<double>::max()};
TTrack *farthest{nullptr};
bool fits{false};
for (auto const attempt : {0, 1, 2, 3})
{
auto const shape{attempt % 2 == 0 ? preferred : other};
design.shape = shape;
if (attempt == 2)
{
design.norms.arc_min = 0.0;
design.norms.arc_min_factor = 0.0;
}
design.vertices.clear();
for (auto const &entry : editor_track::recognize_line(Chain, m_straights.tolerance, design.norms.gauge, shape))
{
if (entry.kink)
{
if (Reason != nullptr)
*Reason = format(STR_C("the straights meet with no curve at path %s"), name(nearest(entry.position)).c_str());
if (Culprit != nullptr)
*Culprit = nearest(entry.position);
return false;
}
auto vertex{route_vertex_of(entry.shape)};
vertex.position = entry.position;
design.vertices.push_back(vertex);
}
if (design.vertices.size() >= 2)
{
design.vertices.front().offset = std::max(0.0, glm::dot(design.vertices.front().position - plan_of(design.start), design.start_direction));
design.vertices.back().offset = std::max(0.0, glm::dot(plan_of(design.end) - design.vertices.back().position, design.end_direction));
}
result = alignment::compute(design);
if (alignment_store::follows(result, Chain.tracks))
{
fits = true;
break;
}
TTrack *worst{nullptr};
if (auto const away{alignment_store::deviation(result, Chain.tracks, worst)}; away < closest)
{
closest = away;
farthest = worst;
best = design;
bestresult = result;
}
}
double fit{0.0};
if (false == fits && closest <= kDigitizeFit)
{
design = best;
result = bestresult;
fit = closest;
fits = true;
}
if (false == fits)
{
if (Culprit != nullptr)
*Culprit = farthest;
if (Reason != nullptr)
*Reason = closest < std::numeric_limits<double>::max() ? format(STR_C("the curves read from the paths leave path %s by %.2f m"), name(farthest).c_str(), closest) : std::string{STR_C("the paths don't make a line of straights, arcs and transition curves")};
return false;
}
auto const &records{alignment_store::records()};
std::set<int> covered;
for (auto *track : Chain.tracks)
if (auto const found{alignment_store::on_track(*track)})
covered.insert(found->record);
double origin{0.0};
for (auto const index : covered)
{
auto const &old{records[index]};
auto const start{alignment_store::chainage_on(result, old.design.start)};
auto const end{alignment_store::chainage_on(result, old.design.end)};
if (false == start.has_value() || false == end.has_value())
return false;
if (index == *covered.begin() && *end > *start)
origin = old.origin - *start;
}
auto const layer{covered.empty() ? alignment_store::layer_of(*Chain.tracks.front()) : records[*covered.begin()].layer};
for (auto index{covered.rbegin()}; index != covered.rend(); ++index)
alignment_store::forget(*index);
alignment_store::store(design, result, origin, layer, "drawn", fit);
return true;
}
bool editor_mode::route_from_line(TTrack &Track)
{
auto &route{m_route};
editor_track::chain run;
if (false == editor_track::find_run(Track, kLineReach, run, route.error))
return false;
route.from = run.tracks.front();
route.to = run.tracks.back();
route_reset();
if (route.chain.tracks.empty())
return false;
auto &design{route.design};
auto const found{editor_track::recognize_line(route.chain, m_straights.tolerance, design.norms.gauge, design.shape)};
route.designed = 0;
if (false == found.empty())
{
design.vertices.clear();
for (auto const &entry : found)
{
alignment::vertex vertex;
if (entry.kink)
{
vertex.radius = std::max(10.0, std::round(alignment::recommend(design.speed, 1000.0, design.norms).radius_min / 10.0) * 10.0);
vertex.transition_in = 0.0;
vertex.transition_out = 0.0;
vertex.cant = 0.0;
}
else
{
vertex = route_vertex_of(entry.shape);
if (entry.shape.designed.has_value())
++route.designed;
}
vertex.position = entry.position;
design.vertices.push_back(vertex);
}
if (design.vertices.size() >= 2)
{
design.vertices.front().offset = std::max(0.0, glm::dot(design.vertices.front().position - plan_of(design.start), design.start_direction));
design.vertices.back().offset = std::max(0.0, glm::dot(plan_of(design.end) - design.vertices.back().position, design.end_direction));
}
route_update();
}
route.status = format(STR_C("Line of %zu paths, %.0f m, %zu vertices"), route.chain.tracks.size(), route.chain.length, design.vertices.size());
return true;
}
void editor_mode::route_apply()
{
auto &route{m_route};
route.status.clear();
if (false == route.result.valid || route.chain.tracks.empty())
return;
editor_track::chain chain;
if (false == editor_track::find_chain(route.from, route.to, chain, route.error, true))
return;
if (glm::distance(chain.start, route.chain.start) > 1e-3 || glm::distance(chain.end, route.chain.end) > 1e-3)
{
route.error = STR_C("The fragment changed since it was selected, select it again");
return;
}
auto involved{chain.tracks};
for (auto const &passage : chain.switches)
involved.push_back(passage.track);
if (false == tracks_editable(involved, route.error))
return;
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> created;
// takes back what was done before the paths were re-laid
auto const undo = [&]() {
for (auto *track : created)
editor_track::retire(*track);
for (auto entry = states.rbegin(); entry != states.rend(); ++entry)
{
editor_track::apply(*entry->first, entry->second);
editor_track::commit({entry->first});
}
};
auto *from{route.from};
auto *to{route.to};
auto const startextension{route.result.start_extension};
auto const endextension{route.result.end_extension};
if (startextension > 0.0 || endextension > 0.0)
{
auto const before{chain};
if (startextension > 0.0)
from = editor_track::take_straight(editor_track::straight_beyond(before, false, m_straights.tolerance), from, before.start, route.result.start, states, created);
if (endextension > 0.0)
to = editor_track::take_straight(editor_track::straight_beyond(before, true, m_straights.tolerance), to, before.end, route.result.end, states, created);
std::string error;
auto const found{editor_track::find_chain(from, to, chain, error, true)};
auto const startoff{found ? plan_distance(chain.start, route.result.start) : 0.0};
auto const endoff{found ? plan_distance(chain.end, route.result.end) : 0.0};
if (false == found || startoff > 5.0 || endoff > 5.0)
{
auto const startrun{editor_track::straight_beyond(before, false, m_straights.tolerance)};
auto const endrun{editor_track::straight_beyond(before, true, m_straights.tolerance)};
WriteLog(format("editor: cutting the straights failed: found %d, start %.2f m off (extension %.2f, run %zu paths %.2f m), end %.2f m off (extension %.2f, run %zu paths %.2f m), chain %zu paths, from %s, to %s, error '%s'", found ? 1 : 0, startoff,
startextension, startrun.tracks.size(), startrun.length, endoff, endextension, endrun.tracks.size(), endrun.length, chain.tracks.size(), from != nullptr ? from->name().c_str() : "-", to != nullptr ? to->name().c_str() : "-", error.c_str()));
undo();
route.error = error.empty() ? format(STR_C("The adjoining straights can't be cut at the ends of the curve: the start misses by %.1f m, the end by %.1f m"), startoff, endoff) : error;
return;
}
if (false == tracks_editable(chain.tracks, route.error))
{
undo();
return;
}
}
for (auto *track : chain.tracks)
if (std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == track; }))
states.emplace_back(track, editor_track::capture(*track));
std::vector<TTrack *> relaid;
if (chain.switches.empty())
{
auto pieces{alignment::pieces(route.result, route.design, chain.tracks.size())};
editor_track::keep_heights(chain, pieces);
relaid = editor_track::relay(chain, pieces, &to);
}
else if (false == route_relay_through_switches(chain, states, relaid, to))
{
undo();
return;
}
// the straights were cut along their own direction, the curve ends where the design says: the ends are brought together
auto const added{relaid.size()};
created.insert(created.end(), relaid.begin(), relaid.end());
push_track_snapshot(std::move(states), std::move(created));
route.from = from;
route.to = to;
editor_track::find_chain(route.from, route.to, route.chain, route.error, true);
if (startextension > 0.0 || endextension > 0.0)
{
route_bind_ends();
auto &vertices{route.design.vertices};
if (vertices.size() >= 2)
{
vertices.front().offset += startextension;
vertices.back().offset += endextension;
}
route_update();
}
route_store();
records_to_history();
route.status = STR_C("Re-laid ") + std::to_string(chain.tracks.size()) + STR_C(" paths") + (added > 0 ? STR_C(", added ") + std::to_string(added) : std::string{});
if (startextension > 0.0 || endextension > 0.0)
route.status += format(", the curve took over %.2f / %.2f m of the adjoining straights", startextension, endextension);
WriteLog("Editor: route design - " + route.status, logtype::generic);
}
// the switches of the chain are moved as a whole onto the designed line, the regular paths between them re-laid
bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Created, TTrack *&Last)
{
auto &route{m_route};
auto const &result{route.result};
auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{"(noname)"} : Track->name(); };
auto const plan = [](glm::dvec3 const &Vector) { return glm::normalize(plan_of(Vector)); };
struct placement
{
TTrack *track;
double from, length; // chainage along the new line
glm::dvec3 entry; // of the main track, where the chain comes in
double angle;
};
// chainages along the old line, the switches keep their distance from the nearer end of the chain
double before{0.0};
std::vector<double> starts;
std::size_t next{0};
for (std::size_t i = 0; i <= Chain.tracks.size(); ++i)
{
for (; next < Chain.switches.size() && Chain.switches[next].before == i; ++next)
{
starts.push_back(before);
before += bezier{Chain.switches[next].track->m_paths.front()}.plan_length();
}
if (i < Chain.tracks.size())
before += bezier{Chain.tracks[i]->m_paths.front()}.plan_length();
}
auto const oldlength{before};
std::vector<placement> placements;
std::vector<double> breaks;
for (std::size_t k = 0; k < Chain.switches.size(); ++k)
{
auto const &passage{Chain.switches[k]};
auto const &main{passage.track->m_paths.front()};
auto const length{bezier{main}.plan_length()};
auto const from{starts[k] + length * 0.5 < oldlength * 0.5 ? starts[k] : result.length - (oldlength - starts[k])};
if (from < 0.0 || from + length > result.length || (false == breaks.empty() && from < breaks.back() - 0.01))
{
route.error = STR_C("The switch ") + name(passage.track) + STR_C(" doesn't fit on the designed line");
return false;
}
auto const entry{main.points[passage.forward ? segment_data::point::start : segment_data::point::end]};
auto const exit{main.points[passage.forward ? segment_data::point::end : segment_data::point::start]};
bezier const curve{main};
auto const direction{passage.forward ? plan(curve.first(0.0)) : -plan(curve.first(1.0))};
auto const first{alignment::evaluate(result, from)};
auto const last{alignment::evaluate(result, from + length)};
auto const angle{signed_angle(direction, plan(first.direction))};
// where the other end of the main track goes, set against the line
auto const moved{plan_of(first.position) + turned(plan_of(exit - entry), angle)};
auto const departure{glm::distance(moved, plan_of(last.position))};
if (departure > 0.02)
{
route.error = format(STR_C("The switch %s would leave the designed line by %.2f m at its other end: it lies in a curve of the design. "
"Move the vertices so that the switch falls on a straight"),
name(passage.track).c_str(), departure);
return false;
}
placements.push_back({passage.track, from, length, entry, angle});
breaks.push_back(from);
breaks.push_back(from + length);
}
// the regular parts between the switches, one interval each, the switches taking the ones between
auto const parts{editor_track::chain_parts(Chain)};
std::vector<std::size_t> counts;
std::vector<int> partof;
std::size_t part{0};
for (std::size_t k = 0; k <= Chain.switches.size(); ++k)
{
auto const first{k == 0 ? std::size_t{0} : Chain.switches[k - 1].before};
auto const last{k == Chain.switches.size() ? Chain.tracks.size() : Chain.switches[k].before};
counts.push_back(last - first);
partof.push_back(last > first ? static_cast<int>(part++) : -1);
if (k < Chain.switches.size())
counts.push_back(1);
}
if (part != parts.size())
{
route.error = STR_C("The paths between the switches can't be read as a chain");
return false;
}
std::vector<std::size_t> intervals;
auto const pieces{alignment::pieces(result, route.design, 0, breaks, counts, &intervals)};
std::vector<std::vector<segment_data>> groups(parts.size());
for (std::size_t i = 0; i < pieces.size(); ++i)
if (intervals[i] % 2 == 0 && partof[intervals[i] / 2] >= 0)
groups[partof[intervals[i] / 2]].push_back(pieces[i]);
for (std::size_t i = 0; i < parts.size(); ++i)
if (groups[i].size() < parts[i].tracks.size())
{
route.error = STR_C("The designed line between the switches is too short for the paths there");
return false;
}
// all checked, the changes go in
auto const remember = [&](TTrack *Track) {
if (std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == Track; }))
States.emplace_back(Track, editor_track::capture(*Track));
};
for (auto const &place : placements)
{
auto joints{editor_track::joints(*place.track)};
joints.erase(std::remove_if(joints.begin(), joints.end(), [&](editor_track::joint const &Joint) { return std::find(Chain.tracks.begin(), Chain.tracks.end(), Joint.other) != Chain.tracks.end(); }), joints.end());
remember(place.track);
std::vector<TTrack *> changed{place.track};
for (auto const &joint : joints)
{
remember(joint.other);
if (std::find(changed.begin(), changed.end(), joint.other) == changed.end())
changed.push_back(joint.other);
}
// the heights stay, the paths between the switches keep theirs too
auto target{alignment::evaluate(result, place.from).position};
target.y = place.entry.y;
editor_track::place(*place.track, place.entry, target, place.angle);
editor_track::follow(*place.track, joints);
editor_track::commit(changed);
}
for (std::size_t i = 0; i < parts.size(); ++i)
{
auto group{groups[i]};
editor_track::keep_heights(parts[i], group);
TTrack *last{nullptr};
auto const added{editor_track::relay(parts[i], group, &last)};
if (last != nullptr && parts[i].tracks.back() == Chain.tracks.back())
Last = last;
Created.insert(Created.end(), added.begin(), added.end());
}
return true;
}
glm::dvec3 editor_mode::route_vertex_position(int const Vertex) const
{
auto const &design{m_route.design};
auto const &result{m_route.result};
auto const count{static_cast<int>(design.vertices.size())};
glm::dvec2 const start{design.start.x, design.start.z};
glm::dvec2 const end{design.end.x, design.end.z};
glm::dvec2 position;
if (Vertex < static_cast<int>(result.vertices.size()) && false == result.vertices.empty())
position = result.vertices[Vertex];
else if (count == 1)
{
if (false == alignment::tangent_intersection(design, position))
position = (start + end) * 0.5;
}
else if (Vertex == 0)
position = start + glm::normalize(design.start_direction) * design.vertices.front().offset;
else if (Vertex == count - 1)
position = end - glm::normalize(design.end_direction) * design.vertices.back().offset;
else
position = design.vertices[Vertex].position;
auto const height{Vertex < static_cast<int>(result.vertex_elevations.size()) ? result.vertex_elevations[Vertex] : design.start.y + (design.end.y - design.start.y) * (Vertex + 1) / (count + 1.0)};
return {position.x, height, position.y};
}
bool editor_mode::route_hit(int &Vertex, int &Grip) const
{
if (false == route_active())
return false;
screen_projection const projection;
float best = kHandleRadius * kHandleRadius;
int hit{-1};
for (int i = 0; i < static_cast<int>(m_route.design.vertices.size()); ++i)
{
if (auto const distance{projection.mouse_distance2(route_vertex_position(i))}; distance < best)
{
best = distance;
hit = i;
}
}
int griphit{-1};
for (int i = 0; i < static_cast<int>(m_route.result.vertex_chainages.size()) && m_route.result.length > 0.0; ++i)
{
if (auto const distance{projection.mouse_distance2(alignment::evaluate(m_route.result, m_route.result.vertex_chainages[i]).position)}; distance < best)
{
best = distance;
griphit = i;
hit = -1;
}
}
Vertex = hit;
Grip = griphit;
return hit >= 0 || griphit >= 0;
}
void editor_mode::draw_route_overlay() const
{
auto const &route{m_route};
auto const &design{route.design};
auto const &result{route.result};
screen_projection const projection;
ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport());
auto const &start{result.valid ? result.start : design.start};
auto const &end{result.valid ? result.end : design.end};
std::vector<glm::dvec3> polygon{start};
for (int i = 0; i < static_cast<int>(design.vertices.size()); ++i)
polygon.push_back(route_vertex_position(i));
polygon.push_back(end);
for (std::size_t i = 0; i + 1 < polygon.size(); ++i)
projection.line(drawlist, polygon[i], polygon[i + 1], IM_COL32(255, 210, 60, 150), 1.5f);
if (result.length > 0.0 && result.profile.size() >= 2)
{
for (auto const &e : result.elements)
{
ImU32 const color = e.kind == alignment::element_kind::straight ? IM_COL32(255, 255, 255, 230) : e.kind == alignment::element_kind::arc ? overlay_color::grip : IM_COL32(255, 140, 30, 255);
int const steps = std::clamp(static_cast<int>(e.length / 2.0), 1, 400);
auto previous{alignment::evaluate(result, e.chainage).position};
for (int j = 1; j <= steps; ++j)
{
auto const next{alignment::evaluate(result, e.chainage + e.length * j / steps).position};
projection.line(drawlist, previous, next, color, 3.0f);
previous = next;
}
ImVec2 screen;
if (projection.project(alignment::evaluate(result, e.chainage).position, screen))
drawlist->AddCircleFilled(screen, 3.5f, overlay_color::highlight);
}
}
for (auto const atend : {false, true})
{
auto const &point{atend ? end : start};
auto const direction{atend ? design.end_direction : design.start_direction};
ImVec2 screen;
if (projection.project(point, screen))
drawlist->AddRectFilled(ImVec2(screen.x - 5.0f, screen.y - 5.0f), ImVec2(screen.x + 5.0f, screen.y + 5.0f), IM_COL32(70, 140, 255, 255));
auto const arrow{glm::dvec3{direction.x, 0.0, direction.y} * (atend ? 15.0 : -15.0)};
projection.line(drawlist, point, point + arrow, IM_COL32(70, 140, 255, 255), 2.0f);
}
for (int i = 0; i < static_cast<int>(design.vertices.size()); ++i)
{
ImVec2 screen;
if (false == projection.project(route_vertex_position(i), screen))
continue;
drawlist->AddCircleFilled(screen, 6.0f, overlay_color::marked);
if (i == route.vertex)
drawlist->AddCircle(screen, 11.0f, overlay_color::highlight, 16, 2.5f);
if (i < static_cast<int>(result.vertex_chainages.size()) && result.length > 0.0 && projection.project(alignment::evaluate(result, result.vertex_chainages[i]).position, screen))
{
drawlist->AddCircleFilled(screen, 6.0f, overlay_color::grip, 4);
if (i == route.grip)
drawlist->AddCircle(screen, 11.0f, overlay_color::highlight, 16, 2.5f);
auto const *report{i < static_cast<int>(result.curves.size()) ? &result.curves[i] : nullptr};
auto radius{format("R %.0f", report != nullptr ? report->radius : design.vertices[i].radius)};
if (report != nullptr && report->radii.size() >= 2)
{
radius = "R";
for (std::size_t k = 0; k < report->radii.size(); ++k)
radius += format(k == 0 ? " %.0f" : " / %.0f", report->radii[k]);
}
drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y + 4.0f), overlay_color::grip, radius.c_str());
}
drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y - 18.0f), overlay_color::marked, format("W%d", i + 1).c_str());
}
}
void editor_mode::render_route_gizmo()
{
auto &route{m_route};
auto const count{static_cast<int>(route.design.vertices.size())};
bool const grip{route.grip >= 0 && route.grip < count && route.grip < static_cast<int>(route.result.vertex_chainages.size())};
if (false == grip && (route.vertex < 0 || route.vertex >= count))
{
m_route_dragging = false;
return;
}
auto const position{grip ? alignment::evaluate(route.result, route.result.vertex_chainages[route.grip]).position : route_vertex_position(route.vertex)};
glm::dvec3 moved{position};
if (false == handle_drag_step(moved))
{
if (m_route_dragging)
{
m_route_dragging = false;
route_apply();
}
return;
}
m_route_dragging = true;
if (grip)
{
auto &vertex{route.design.vertices[route.grip]};
auto const corner{route_vertex_position(route.grip)};
auto const distance{plan_distance(moved, corner)};
for (auto const &curve : route.result.curves)
{
if (curve.vertex != route.grip || vertex.reverse_turn)
continue;
auto const secant{1.0 / std::cos(curve.deflection * 0.5) - 1.0};
if (secant > 1e-6)
vertex.radius = std::max(10.0, std::round(distance / secant));
}
route_update();
return;
}
auto &vertex{route.design.vertices[route.vertex]};
glm::dvec2 const point{moved.x, moved.z};
if (count >= 2)
{
if (route.vertex == 0)
vertex.offset = std::max(0.1, glm::dot(point - plan_of(route.design.start), glm::normalize(route.design.start_direction)));
else if (route.vertex == count - 1)
vertex.offset = std::max(0.1, glm::dot(plan_of(route.design.end) - point, glm::normalize(route.design.end_direction)));
else
vertex.position = point;
}
route_update();
}
void editor_mode::render_route_ui()
{
auto &route{m_route};
render_norms_choice();
ImGui::Separator();
if (false == route.error.empty())
ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", route.error.c_str());
if (route.chain.tracks.empty())
return;
ImGui::TextDisabled(STR_C("%zu paths, %.2f m"), route.chain.tracks.size(), route.chain.length);
ImGui::SameLine();
if (ImGui::SmallButton(STR_C("Reset design")))
{
auto *track{route.chain.tracks[route.chain.tracks.size() / 2]};
route_load(*track);
return;
}
item_tooltip("Reads the curves of the line from the scenery again, dropping the changes not applied");
render_route_stored();
auto changed{render_route_vertices()};
if (ImGui::CollapsingHeader(STR_C("Design speed and limits")))
changed |= render_route_parameters();
if (changed)
route_update();
ImGui::Separator();
render_route_result();
}
bool editor_mode::render_route_parameters()
{
auto &design{m_route.design};
bool changed{false};
ImGui::PushItemWidth(120.0f);
changed |= ImGui::InputDouble(STR_C("Design speed (km/h)"), &design.speed, 0.0, 0.0, "%.0f");
item_tooltip("Speed the cant, the transitions and the recommended radius are designed for");
design.speed = std::max(1.0, design.speed);
int shape = static_cast<int>(design.shape);
char const *shapes[] = {STR_C("Cubic parabola"), STR_C("Clothoid")};
if (ImGui::Combo(STR_C("Transition curve"), &shape, shapes, IM_ARRAYSIZE(shapes)))
{
design.shape = static_cast<alignment::transition_shape>(shape);
changed = true;
}
item_tooltip("Cubic parabola: the usual one on the Polish lines; clothoid: curvature growing linearly with the length");
ImGui::PopItemWidth();
if (ImGui::TreeNode(STR_C("Limits")))
{
auto &norms{design.norms};
ImGui::PushItemWidth(100.0f);
changed |= ImGui::InputDouble(STR_C("Rail axes distance (mm)"), &norms.gauge, 0.0, 0.0, "%.0f");
changed |= ImGui::InputDouble(STR_C("Unbalanced acceleration (m/s2)"), &norms.unbalanced, 0.0, 0.0, "%.2f");
changed |= ImGui::InputDouble(STR_C("Maximum cant (mm)"), &norms.cant_max, 0.0, 0.0, "%.0f");
changed |= ImGui::InputDouble(STR_C("Cant change rate (mm/s)"), &norms.cant_rate, 0.0, 0.0, "%.0f");
changed |= ImGui::InputDouble(STR_C("Cant ramp 1:(k*V), k"), &norms.ramp_factor, 0.0, 0.0, "%.1f");
changed |= ImGui::InputDouble(STR_C("Unbalanced acc. change rate (m/s3)"), &norms.jerk, 0.0, 0.0, "%.2f");
changed |= ImGui::InputDouble(STR_C("Straight between curves (s of travel)"), &norms.tangent_min_time, 0.0, 0.0, "%.1f");
ImGui::PopItemWidth();
norms.gauge = std::max(100.0, norms.gauge);
norms.cant_rate = std::max(1.0, norms.cant_rate);
norms.jerk = std::max(0.01, norms.jerk);
ImGui::TreePop();
}
return changed;
}
bool editor_mode::render_route_vertices()
{
bool changed{false};
auto const count{static_cast<int>(m_route.design.vertices.size())};
for (int i = 0; i < count; ++i)
{
ImGui::PushID(i);
changed |= render_route_vertex(i);
ImGui::PopID();
}
return changed;
}
bool editor_mode::render_route_vertex(int const Index)
{
auto &route{m_route};
auto &design{route.design};
auto &vertex{design.vertices[Index]};
auto const count{static_cast<int>(design.vertices.size())};
bool changed{false};
if (count > 1)
ImGui::TextDisabled(STR_C("Curve %d of the S-curve"), Index + 1);
ImGui::PushItemWidth(110.0f);
changed |= ImGui::DragScalar(vertex.compound ? STR_C("Radius R1 (m)") : STR_C("Radius R (m)"), ImGuiDataType_Double, &vertex.radius, 5.0f, nullptr, nullptr, "%.0f");
if (ImGui::Checkbox(STR_C("Compound curve (several radii)"), &vertex.compound))
{
changed = true;
if (vertex.compound && vertex.arcs.empty())
vertex.arcs.push_back({std::max(10.0, std::round(vertex.radius * 0.6 / 10.0) * 10.0), 0.0, 1.0});
}
item_tooltip("Curve made of arcs of different radii, each one its own path, joined with transitions");
if (vertex.compound)
{
auto total{std::max(0.0, vertex.share)};
for (auto const &arc : vertex.arcs)
total += std::max(0.0, arc.share);
auto const percent = [&](double const Share) { return total > 0.0 ? 100.0 * std::max(0.0, Share) / total : 0.0; };
changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), 1).c_str(), ImGuiDataType_Double, &vertex.share, 0.01f, nullptr, nullptr, "%.2f");
ImGui::SameLine();
ImGui::TextDisabled("%.0f%%", percent(vertex.share));
int removed{-1};
for (int k = 0; k < static_cast<int>(vertex.arcs.size()); ++k)
{
auto &arc{vertex.arcs[k]};
ImGui::PushID(k);
ImGui::Separator();
changed |= ImGui::DragScalar(format(STR_C("Radius R%d (m)"), k + 2).c_str(), ImGuiDataType_Double, &arc.radius, 5.0f, nullptr, nullptr, "%.0f");
ImGui::SameLine();
if (ImGui::SmallButton("x"))
removed = k;
item_tooltip("Takes this radius out of the curve");
changed |= ImGui::DragScalar(format(STR_C("Transition R%d-R%d (m)"), k + 1, k + 2).c_str(), ImGuiDataType_Double, &arc.transition, 1.0f, nullptr, nullptr, "%.0f");
changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), k + 2).c_str(), ImGuiDataType_Double, &arc.share, 0.01f, nullptr, nullptr, "%.2f");
ImGui::SameLine();
ImGui::TextDisabled("%.0f%%", percent(arc.share));
arc.radius = std::max(1.0, arc.radius);
arc.transition = std::max(0.0, arc.transition);
arc.share = std::max(0.0, arc.share);
ImGui::PopID();
}
vertex.share = std::max(0.0, vertex.share);
if (removed >= 0)
{
vertex.arcs.erase(vertex.arcs.begin() + removed);
if (vertex.arcs.empty())
vertex.compound = false;
changed = true;
}
if (ImGui::SmallButton(STR_C("Add a radius")))
{
auto const last{vertex.arcs.empty() ? vertex.radius : vertex.arcs.back().radius};
vertex.arcs.push_back({std::max(10.0, std::round(last * 0.8 / 10.0) * 10.0), 0.0, 1.0});
changed = true;
}
item_tooltip("Another arc after the last one, the angle of the curve is shared out by the shares");
ImGui::Separator();
}
changed |= ImGui::DragScalar(STR_C("Transition in (m)"), ImGuiDataType_Double, &vertex.transition_in, 1.0f, nullptr, nullptr, "%.0f");
changed |= ImGui::DragScalar(STR_C("Transition out (m)"), ImGuiDataType_Double, &vertex.transition_out, 1.0f, nullptr, nullptr, "%.0f");
changed |= ImGui::DragScalar(STR_C("Cant (mm)"), ImGuiDataType_Double, &vertex.cant, 1.0f, nullptr, nullptr, "%.0f");
item_tooltip("Height of the outer rail above the inner one");
ImGui::PopItemWidth();
vertex.radius = std::max(1.0, vertex.radius);
vertex.transition_in = std::max(0.0, vertex.transition_in);
vertex.transition_out = std::max(0.0, vertex.transition_out);
vertex.cant = std::max(0.0, vertex.cant);
if (false == m_norms_free)
{
if (ImGui::SmallButton(STR_C("Apply recommended for V")))
{
route_recommend(vertex);
changed = true;
}
item_tooltip("Cant and transitions recommended for the design speed and the radius");
auto const recommended{alignment::recommend(design.speed, vertex.radius, design.norms)};
ImGui::TextDisabled(STR_C("For %.0f km/h: R min %.0f m, cant %.0f mm (equilibrium %.0f, min %.0f), transition %.0f m"), design.speed, recommended.radius_min, recommended.cant, recommended.cant_equilibrium, recommended.cant_min, recommended.transition);
}
if (ImGui::TreeNode(STR_C("Details")))
{
for (auto const &curve : route.result.curves)
{
if (curve.vertex != Index)
continue;
ImGui::Text(STR_C("Deflection %.4f deg, tangents %.2f / %.2f m, arc %.2f m"), glm::degrees(curve.deflection), curve.tangent_in, curve.tangent_out, curve.arc_length);
ImGui::Text(STR_C("Unbalanced acc. %.3f m/s2, cant rate %.1f / %.1f mm/s, acc. rate %.3f / %.3f m/s3"), curve.unbalanced, curve.cant_rate_in, curve.cant_rate_out, curve.jerk_in, curve.jerk_out);
}
changed |= ImGui::Checkbox(STR_C("Turn the longer way (over 180 deg)"), &vertex.reverse_turn);
item_tooltip("For the loops: the curve turns by more than a half circle");
ImGui::TreePop();
}
return changed;
}
void editor_mode::render_route_result()
{
auto const &route{m_route};
auto const &design{route.design};
auto const &result{route.result};
if (result.length > 0.0)
{
auto const pieces{alignment::minimum_pieces(result, design)};
ImGui::Text(STR_C("Length %.2f m (now %.2f m), %zu elements"), result.length, route.chain.length, result.elements.size());
ImGui::TextDisabled(STR_C("Paths: %zu in the fragment, %zu needed%s"), route.chain.tracks.size(), pieces, pieces > route.chain.tracks.size() ? STR_C(" (copies of the neighbours will be added)") : "");
if (result.start_extension > 0.0 || result.end_extension > 0.0)
ImGui::Text(STR_C("Takes over %.2f m of the straight at the start, %.2f m at the end"), result.start_extension, result.end_extension);
}
for (auto const &error : result.errors)
ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", error.c_str());
for (auto const &warning : result.warnings)
ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", warning.c_str());
if (ImGui::TreeNode(STR_C("Elements")))
{
ImGui::TextDisabled(STR_C("Straight available beyond the ends: %.2f / %.2f m"), design.start_reserve, design.end_reserve);
char const *kinds[] = {"straight", "transition", "curve", "transition", "spiral"};
for (auto const &e : result.elements)
ImGui::Text("km %s %-10s L %8.2f m%s", profile::format_chainage(m_route.origin + e.chainage).c_str(), kinds[static_cast<int>(e.kind)], e.length, e.kind == alignment::element_kind::straight ? "" : (" R " + std::to_string(static_cast<int>(std::round(e.radius))) + " m").c_str());
ImGui::TreePop();
}
if (result.valid)
{
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f));
if (ImGui::Button(STR_C("Apply to the scenery"), ImVec2(-1.0f, 28.0f)))
route_apply();
ImGui::PopStyleColor(2);
}
else
{
ImGui::TextDisabled(STR_C("Correct the errors to apply the design"));
}
if (false == route.status.empty())
ImGui::TextUnformatted(route.status.c_str());
}
editor_track::straight const &editor_mode::current_straight()
{
auto *track{selected_track()};
if (track != m_straights.current_for)
{
m_straights.current_for = track;
m_straights.handle = -1;
m_straights.current = track != nullptr ? editor_track::find_straight(*track, m_straights.tolerance) : editor_track::straight{};
find_neighbour_straights();
}
return m_straights.current;
}