17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-02 23:16:22 +01:00

Merge remote-tracking branch 'origin/feat/switch-blades' into feat/renderers-cleanup

This commit is contained in:
2026-10-29 23:55:04 +01:00
145 changed files with 36946 additions and 3791 deletions

View File

@@ -12,12 +12,16 @@ http://mozilla.org/MPL/2.0/.
#include "application/editoruilayer.h"
#include "application/editorprojection.h"
#include "application/editormodeshared.h"
#include "application/editormodetrackshared.h"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "editor/editorSettings.hpp"
#include "editor/editorSwitchStore.hpp"
#include "utilities/Globals.h"
#include "rendering/renderer.h"
#include "world/Track.h"
#include "utilities/utilities.h"
#include "scene/scene.h"
#include "scene/scenelayers.h"
#include "simulation/simulation.h"
@@ -29,11 +33,13 @@ http://mozilla.org/MPL/2.0/.
#include <unordered_set>
#include <unordered_map>
#include <queue>
#include <deque>
#include <array>
#include <functional>
#include <cctype>
#include <chrono>
#include <cmath>
#include <limits>
namespace
{
@@ -62,17 +68,6 @@ bool rail(TTrack const &Track)
return (Track.iCategoryFlag & 15) == kRailCategory && false == Track.m_editorremoved && Track.visible();
}
std::string name_of(TTrack const &Track)
{
return Track.name().empty() ? std::string{STR_C("(no name)")} : Track.name();
}
std::string lower(std::string Text)
{
std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast<char>(std::tolower(Character)); });
return Text;
}
char const *type_of(TTrack const &Track)
{
switch (Track.eType)
@@ -92,6 +87,32 @@ double ortho_metres_per_pixel()
return Global.EditorOrtho && height > 0.0f ? 2.0 * static_cast<double>(Global.EditorOrthoExtent) / static_cast<double>(height) : 0.0;
}
// the bands of the colouring of the tracks: up to the value, the colour and its label in the legend
struct colour_band
{
double below;
ImU32 colour;
char const *label;
};
double constexpr kBeyond{std::numeric_limits<double>::max()};
ImU32 const kUncoloured{IM_COL32(200, 200, 200, 170)};
double const kColouredReach{1500.0}; // m around the camera in the 3D view
double const kLabelsTo{1.0}; // m a pixel of the top view, farther out the labels of the tracks are left out
std::vector<colour_band> const kRadiusBands{{300.0, IM_COL32(235, 60, 60, 235), "R < 300 m"}, {600.0, IM_COL32(245, 140, 50, 235), "R 300-600 m"}, {1200.0, IM_COL32(240, 220, 60, 235), "R 600-1200 m"},
{3000.0, IM_COL32(110, 210, 80, 235), "R 1200-3000 m"}, {kBeyond, IM_COL32(70, 190, 235, 235), "R > 3000 m"}};
std::vector<colour_band> const kSpeedBands{{40.5, IM_COL32(235, 60, 60, 235), "<= 40 km/h"}, {60.5, IM_COL32(245, 140, 50, 235), "50-60 km/h"}, {80.5, IM_COL32(240, 220, 60, 235), "70-80 km/h"},
{100.5, IM_COL32(110, 210, 80, 235), "90-100 km/h"}, {120.5, IM_COL32(70, 190, 235, 235), "110-120 km/h"}, {kBeyond, IM_COL32(170, 120, 255, 235), "> 120 km/h"}};
std::vector<colour_band> const kGradeBands{{2.0, kUncoloured, "0-2 \xe2\x80\xb0"}, {5.0, IM_COL32(110, 210, 80, 235), "2-5 \xe2\x80\xb0"}, {10.0, IM_COL32(240, 220, 60, 235), "5-10 \xe2\x80\xb0"},
{20.0, IM_COL32(245, 140, 50, 235), "10-20 \xe2\x80\xb0"}, {kBeyond, IM_COL32(235, 60, 60, 235), "> 20 \xe2\x80\xb0"}};
ImU32 band_colour(std::vector<colour_band> const &Bands, double const Value)
{
for (auto const &band : Bands)
if (Value < band.below)
return band.colour;
return kUncoloured;
}
bool plan_near(bezier const &Curve, glm::dvec2 const &Point, double const Reach)
{
auto const centre{(plan_of(Curve.p0) + plan_of(Curve.p1) + plan_of(Curve.p2) + plan_of(Curve.p3)) * 0.25};
@@ -184,31 +205,358 @@ void editor_mode::update_track_hover()
void editor_mode::draw_track_lines() const
{
auto const metres{ortho_metres_per_pixel()};
if (metres < kLinesFrom)
auto const colouring{ui()->mode() == nodebank_panel::TRACK ? m_track_colouring : track_colouring::plain};
// the top view of the rails alone draws them in a colour of their own, as on a plan: both rails where they're apart on the screen
auto const rails{metres > 0.0 && Global.EditorOrthoTracks > 1 && colouring == track_colouring::plain};
if (metres < kLinesFrom && colouring == track_colouring::plain && false == rails)
return;
auto const &io{ImGui::GetIO()};
screen_projection const projection;
glm::dvec2 const centre{Global.pCamera.Pos.x, Global.pCamera.Pos.z};
auto const reach{Global.EditorOrtho ? 0.5 * metres * std::hypot(static_cast<double>(io.DisplaySize.x), static_cast<double>(io.DisplaySize.y)) : kColouredReach};
auto const width{rails ? std::max(2.0f, static_cast<float>(0.07 / metres)) : colouring == track_colouring::plain ? 1.5f : 3.0f};
auto const railcolour{EditorSettings.top_rails()};
std::vector<glm::dvec3> points;
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || false == rail(*track))
continue;
auto const velocity{editor_track::velocity(*track)};
for (auto const &path : track->m_paths)
{
bezier const curve{path};
if (false == plan_near(curve, centre, reach))
continue;
auto colour{rails ? railcolour : IM_COL32(235, 235, 235, 150)};
if (colouring == track_colouring::radius)
{
auto const radius{::track_mode::piece_plan_radius(path)};
colour = radius >= 0.5 ? band_colour(kRadiusBands, radius) : kUncoloured;
}
else if (colouring == track_colouring::speed)
colour = velocity > 0.0 ? band_colour(kSpeedBands, velocity) : kUncoloured;
auto const length{curve.plan_length(4)};
auto const count{std::clamp(static_cast<int>(length / std::max(metres, 0.5) / 8.0), colouring == track_colouring::grade ? std::clamp(static_cast<int>(length / 10.0), 1, 8) : 1, rails ? 128 : 32)};
if (rails && track->fTrackWidth / metres > 6.0)
{
points.clear();
for (int k = 0; k <= count; ++k)
points.push_back(curve.point(static_cast<double>(k) / count));
auto const half{(track->fTrackWidth + 0.07) * 0.5};
auto const across = [&](std::size_t const K) {
auto const along{points[std::min(K + 1, points.size() - 1)] - points[K > 0 ? K - 1 : 0]};
auto const normal{glm::dvec3{-along.z, 0.0, along.x}};
auto const size{glm::length(normal)};
return size > 1e-9 ? normal * (half / size) : glm::dvec3{0.0};
};
for (auto const side : {-1.0, 1.0})
for (std::size_t k = 1; k < points.size(); ++k)
projection.line(drawlist, points[k - 1] + across(k - 1) * side, points[k] + across(k) * side, colour, width);
continue;
}
auto previous{curve.point(0.0)};
for (int k = 1; k <= count; ++k)
{
auto const next{curve.point(static_cast<double>(k) / count)};
if (colouring == track_colouring::grade)
{
auto const run{geometry::plan_distance(previous, next)};
colour = run > 1e-3 ? band_colour(kGradeBands, std::abs(next.y - previous.y) / run * 1000.0) : kUncoloured;
}
projection.line(drawlist, previous, next, colour, width);
previous = next;
}
}
}
}
void editor_mode::draw_track_labels() const
{
auto const metres{ortho_metres_per_pixel()};
if (false == m_track_labels || metres <= 0.0 || metres > kLabelsTo)
return;
auto const &io{ImGui::GetIO()};
screen_projection const projection;
glm::dvec2 const centre{Global.pCamera.Pos.x, Global.pCamera.Pos.z};
auto const reach{0.5 * metres * std::hypot(static_cast<double>(io.DisplaySize.x), static_cast<double>(io.DisplaySize.y))};
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
auto const low{editor_ui::view_min()};
auto const high{editor_ui::view_max()};
struct track_label
{
int rank;
double order;
glm::dvec3 point;
std::string text;
ImU32 colour;
};
std::vector<track_label> labels;
// the screen in cells, the ones the tracks are drawn over marked: a label keeps off them when it can
float constexpr cell{8.0f};
auto const columns{std::max(1, static_cast<int>((high.x - low.x) / cell) + 1)};
auto const rows{std::max(1, static_cast<int>((high.y - low.y) / cell) + 1)};
std::vector<char> covered(static_cast<std::size_t>(columns) * rows, 0);
auto const cell_of = [&](float const X, float const Y, int &Column, int &Row) {
Column = static_cast<int>((X - low.x) / cell);
Row = static_cast<int>((Y - low.y) / cell);
return Column >= 0 && Row >= 0 && Column < columns && Row < rows;
};
// one label for a curve, from its first transition curve to its last, and one for a row of straights in line, not one for each
// of their paths: the paths of a curve are pieces of it, a piece of a transition curve has no radius of its own to name
std::unordered_set<TTrack const *> labelled;
auto const plain = [&](TTrack const *Other) { return Other != nullptr && Other->eType == tt_Normal && rail(*Other) && false == Other->m_paths.empty(); };
auto const heading = [](TTrack const &Other) { return plan_of(Other.m_paths.front().points[segment_data::point::end] - Other.m_paths.front().points[segment_data::point::start]); };
auto const own_turn = [](TTrack const &Other, bool const Forward) {
bezier const curve{Other.m_paths.front()};
auto const begin{plan_of(curve.first(0.0))};
auto const finish{plan_of(curve.first(1.0))};
if (glm::length(begin) < 1e-9 || glm::length(finish) < 1e-9)
return 0.0;
auto const turn{geometry::signed_angle(glm::normalize(begin), glm::normalize(finish))};
return Forward ? turn : -turn;
};
// the paths joined in a row with the one given, both ways, as long as each next one belongs with it; each with the flag of being
// gone along from its start
auto const row = [&](TTrack &Track, std::function<bool(TTrack const &, bool)> const &Belongs) {
std::deque<std::pair<TTrack *, bool>> run{{&Track, true}};
auto const known = [&](TTrack const *Other) { return std::any_of(run.begin(), run.end(), [&](auto const &Member) { return Member.first == Other; }); };
for (auto const throughend : {true, false})
{
TTrack *previous{&Track};
TTrack *current{throughend ? Track.trNext : Track.trPrev};
while (plain(current) && false == known(current) && false == labelled.contains(current))
{
auto const entersatstart{current->trPrev == previous};
if (false == entersatstart && current->trNext != previous)
break;
if (false == Belongs(*current, throughend == entersatstart))
break;
if (throughend)
run.emplace_back(current, entersatstart);
else
run.emplace_front(current, false == entersatstart);
previous = current;
current = entersatstart ? current->trNext : current->trPrev;
}
}
return std::vector<std::pair<TTrack *, bool>>{run.begin(), run.end()};
};
// the curve as the plan reads it: the radius of the arc, of each arc of a compound one, the transition curves and the cant
auto const curve_text = [](editor_track::curve const &Curve) {
if (Curve.designed.has_value() && Curve.designed->spirals)
return format("2xL %.0f R %.0f", Curve.transition_in, Curve.radius);
std::string text{"R "};
if (Curve.compound && Curve.arcs.size() >= 2)
for (std::size_t i = 0; i < Curve.arcs.size(); ++i)
text += format(i == 0 ? "%.0f" : "/%.0f", Curve.arcs[i].radius);
else
text += format("%.0f", Curve.radius);
if (Curve.transition_in >= 0.5 || Curve.transition_out >= 0.5)
text += std::abs(Curve.transition_in - Curve.transition_out) < 0.5 ? format(" L %.0f", Curve.transition_in) : format(" L %.0f/%.0f", Curve.transition_in, Curve.transition_out);
// the arcs of a compound curve kept with its design have each a cant of their own: all shown where they aren't the same
auto const cants{Curve.designed.has_value() && Curve.compound ? alignment::compound_cants(*Curve.designed) : std::vector<double>{}};
if (std::any_of(cants.begin(), cants.end(), [&](double const Cant) { return std::abs(Cant - cants.front()) >= 0.5; }))
for (std::size_t i = 0; i < cants.size(); ++i)
text += format(i == 0 ? " h %.0f" : "/%.0f", cants[i]);
else if (Curve.cant >= 0.5)
text += format(" h %.0f", Curve.cant);
return text;
};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || false == rail(*track))
if (track == nullptr || false == rail(*track) || track->m_paths.empty())
continue;
bezier const first{track->m_paths.front()};
if (false == plan_near(first, centre, reach))
continue;
for (auto const &path : track->m_paths)
{
bezier const curve{path};
if (false == plan_near(curve, centre, reach))
continue;
auto const count{std::clamp(static_cast<int>(curve.plan_length(4) / metres / 8.0), 1, 32)};
auto previous{curve.point(0.0)};
for (int k = 1; k <= count; ++k)
auto const count{std::clamp(static_cast<int>(curve.plan_length() / (metres * cell)), 2, 256)};
ImVec2 previous;
bool known{false};
for (int k = 0; k <= count; ++k)
{
auto const next{curve.point(static_cast<double>(k) / count)};
projection.line(drawlist, previous, next, IM_COL32(235, 235, 235, 150), 1.5f);
previous = next;
ImVec2 at;
if (false == projection.project(curve.point(static_cast<double>(k) / count), at))
{
known = false;
continue;
}
if (known)
{
auto const steps{std::max(1, static_cast<int>(std::max(std::abs(at.x - previous.x), std::abs(at.y - previous.y)) / cell * 2.0f))};
for (int step = 0; step <= steps; ++step)
{
auto const t{static_cast<float>(step) / steps};
int column, row;
if (cell_of(previous.x + (at.x - previous.x) * t, previous.y + (at.y - previous.y) * t, column, row))
covered[static_cast<std::size_t>(row) * columns + column] = 1;
}
}
previous = at;
known = true;
}
}
if (track->eType == tt_Switch)
{
auto text{name_of(*track)};
if (auto const record{switch_store::of(*track)})
text += " " + switch_store::label(*record);
labels.push_back({0, 0.0, first.point(0.0), text, IM_COL32(255, 220, 120, 255)});
continue;
}
if (labelled.contains(track) || false == plain(track))
continue;
auto const straight{editor_track::is_straight(*track, m_straights.tolerance)};
std::vector<std::pair<TTrack *, bool>> run;
if (straight)
{
// the straights in line: the next one parallel to this one, a bend between them is the end of the row
auto const along{heading(*track)};
run = row(*track, [&](TTrack const &Other, bool) {
auto const other{heading(Other)};
return editor_track::is_straight(Other, m_straights.tolerance) && glm::length(along) > 1e-6 && glm::length(other) > 1e-6 &&
std::abs(geometry::cross(glm::normalize(along), glm::normalize(other))) < 0.002;
});
}
else
{
// the pieces of a curve turning the same way: one turning the other way starts a curve of its own
auto const side{own_turn(*track, true)};
run = row(*track, [&](TTrack const &Other, bool const Forward) { return false == editor_track::is_straight(Other, m_straights.tolerance) && own_turn(Other, Forward) * side >= 0.0; });
}
double length{0.0};
for (auto const &member : run)
{
labelled.insert(member.first);
length += bezier{member.first->m_paths.front()}.plan_length();
}
if (length < 0.5)
continue;
// the label in the middle of the row
glm::dvec3 middle{first.point(0.5)};
auto left{length * 0.5};
for (auto const &[member, forward] : run)
{
bezier const piece{member->m_paths.front()};
auto const span{piece.plan_length()};
if (left <= span || &member == &run.back().first)
{
auto const share{std::clamp(span > 1e-6 ? left / span : 0.5, 0.0, 1.0)};
middle = piece.point(forward ? share : 1.0 - share);
break;
}
left -= span;
}
if (straight)
{
labels.push_back({2, -length, middle, format(STR_C("straight %.0f m"), length), IM_COL32(200, 215, 230, 225)});
continue;
}
// a track bent by hand, as an S between two points dragged, has no radius to read: its length alone
editor_track::curve shape;
if (editor_track::analyse_curve(run, m_route.design.norms.gauge, shape) && shape.radius >= 0.5 && shape.radius <= 50000.0)
labels.push_back({1, shape.radius, middle, curve_text(shape), IM_COL32(235, 235, 235, 240)});
else
labels.push_back({2, -length, middle, format("L %.0f m", length), IM_COL32(235, 235, 235, 240)});
}
std::sort(labels.begin(), labels.end(), [](track_label const &A, track_label const &B) { return A.rank != B.rank ? A.rank < B.rank : A.order < B.order; });
// each label where it reads best: beside its track if it's free there, else moved out to the nearest room, a line leading back
std::vector<std::pair<ImVec2, ImVec2>> taken;
auto const tracks_under = [&](ImVec2 const &From, ImVec2 const &To) {
int count{0};
for (auto y = From.y; y <= To.y + cell * 0.5f; y += cell)
for (auto x = From.x; x <= To.x + cell * 0.5f; x += cell)
{
int column, row;
if (cell_of(std::min(x, To.x), std::min(y, To.y), column, row))
count += covered[static_cast<std::size_t>(row) * columns + column];
}
return count;
};
float constexpr kRings[]{0.0f, 26.0f, 42.0f, 62.0f, 86.0f, 116.0f, 150.0f};
int constexpr kDirections{16};
for (auto const &label : labels)
{
ImVec2 at;
if (false == projection.project(label.point, at) || at.x < low.x || at.y < low.y || at.x > high.x || at.y > high.y)
continue;
auto const size{ImGui::CalcTextSize(label.text.c_str())};
auto const width{size.x + 8.0f};
auto const height{size.y + 4.0f};
ImVec2 const nearest{at.x, at.y - 10.0f - height * 0.5f};
auto best{std::numeric_limits<float>::max()};
ImVec2 from, to;
for (auto const ring : kRings)
for (int k = 0; k < (ring > 0.0f ? kDirections : 1); ++k)
{
auto const angle{static_cast<float>(k) * 6.2831853f / kDirections};
ImVec2 const middle{nearest.x + std::cos(angle) * ring * 1.6f, nearest.y + std::sin(angle) * ring};
ImVec2 const a{middle.x - width * 0.5f, middle.y - height * 0.5f};
ImVec2 const b{middle.x + width * 0.5f, middle.y + height * 0.5f};
if (a.x < low.x || a.y < low.y || b.x > high.x || b.y > high.y)
continue;
if (std::any_of(taken.begin(), taken.end(), [&](auto const &Rect) { return a.x < Rect.second.x + 4.0f && b.x > Rect.first.x - 4.0f && a.y < Rect.second.y + 2.0f && b.y > Rect.first.y - 2.0f; }))
continue;
auto const cost{ring + 5.0f * static_cast<float>(tracks_under(a, b))};
if (cost < best)
{
best = cost;
from = a;
to = b;
}
}
if (best == std::numeric_limits<float>::max())
continue;
taken.emplace_back(from, to);
ImVec2 const middle{(from.x + to.x) * 0.5f, (from.y + to.y) * 0.5f};
if (std::hypot(middle.x - nearest.x, middle.y - nearest.y) > 1.0f)
{
ImVec2 const end{std::clamp(at.x, from.x, to.x), std::clamp(at.y, from.y, to.y)};
auto const lead{(label.colour & 0x00ffffffu) | (170u << 24)};
drawlist->AddLine(at, end, lead, 1.0f);
drawlist->AddCircleFilled(at, 2.5f, lead);
}
drawlist->AddRectFilled(from, to, IM_COL32(0, 0, 0, 165), 3.0f);
drawlist->AddText(ImVec2(from.x + 4.0f, from.y + 2.0f), label.colour, label.text.c_str());
}
}
void editor_mode::draw_track_legend() const
{
if (m_track_colouring == track_colouring::plain)
return;
auto const &bands{m_track_colouring == track_colouring::radius ? kRadiusBands : m_track_colouring == track_colouring::speed ? kSpeedBands : kGradeBands};
std::vector<std::pair<ImU32, std::string>> rows;
for (auto const &band : bands)
rows.emplace_back(band.colour, band.label);
if (m_track_colouring == track_colouring::radius)
rows.emplace_back(kUncoloured, STR("straight"));
else if (m_track_colouring == track_colouring::speed)
rows.emplace_back(kUncoloured, STR("no limit"));
char const *const titles[]{"", "By the radius", "By the speed", "By the grade"};
std::string const title{STR_C(titles[static_cast<int>(m_track_colouring)])};
auto const line{ImGui::GetTextLineHeightWithSpacing()};
auto const swatch{ImGui::GetTextLineHeight()};
float width{ImGui::CalcTextSize(title.c_str()).x};
for (auto const &row : rows)
width = std::max(width, swatch + 6.0f + ImGui::CalcTextSize(row.second.c_str()).x);
auto const pad{8.0f};
// in the bottom right corner, over the profile strip when it's open
auto const bottom{m_profile.strip_top > editor_ui::view_min().y + 4.0f * line ? std::min(editor_ui::view_max().y, m_profile.strip_top) : editor_ui::view_max().y};
ImVec2 const to{editor_ui::view_max().x - 12.0f, bottom - 12.0f};
ImVec2 const from{to.x - width - pad * 2.0f, to.y - pad * 2.0f - line * static_cast<float>(rows.size() + 1)};
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
drawlist->AddRectFilled(from, to, IM_COL32(0, 0, 0, 170), 4.0f);
ImVec2 at{from.x + pad, from.y + pad};
drawlist->AddText(at, IM_COL32(255, 255, 255, 235), title.c_str());
for (auto const &row : rows)
{
at.y += line;
drawlist->AddRectFilled(ImVec2(at.x, at.y + 2.0f), ImVec2(at.x + swatch, at.y + swatch - 2.0f), row.first, 2.0f);
drawlist->AddText(ImVec2(at.x + swatch + 6.0f, at.y), IM_COL32(235, 235, 235, 235), row.second.c_str());
}
}
@@ -262,6 +610,11 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
};
auto *selected{selected_track()};
if (m_tape.active)
{
intent.position = m_tape.mouse;
return make(kind::measure, m_tape.points.size() == 1 ? "Click: the end of the measure" : "Click: the start of the measure. M or Esc: done");
}
if (m_lay.active)
{
auto const &lay{m_lay};
@@ -271,7 +624,7 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
intent.track = lay.mouse_snap.track;
intent.position = lay.mouse_snap.position;
auto const diverging{lay.mouse_snap.track->eType == tt_Switch && lay.mouse_snap.point == editor_track::point_ref{1, editor_track::point_kind::end}};
if (lay.points.empty() && diverging)
if (lay.points.empty() && diverging && m_track_layout != track_layout::straight)
return make(kind::lay_end, "Click: start from this free end, drag: the curve of the switch goes on");
return make(kind::lay_end, lay.points.empty() ? "Click: start from this free end, drag: lead a track out of it" : "Click: join this end and lay");
}
@@ -294,7 +647,17 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
catenary_intent(intent);
return intent;
}
if (m_track_tab == track_tab::signals)
if (m_track_tab == track_tab::srk && m_srk.drawing())
{
bool valid{false};
intent.track = m_hover.track;
intent.path = m_hover.path;
intent.position = m_hover.track != nullptr ? m_hover.point : ground;
intent.action = m_srk.boundary_hover(intent.position, valid);
intent.what = kind::srk_boundary;
return intent;
}
if (m_track_tab == track_tab::srk)
{
if (auto const hit{signal_hit()}; hit >= 0)
{
@@ -305,29 +668,38 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
intent.what = kind::signal_move;
return intent;
}
auto const *armed{signal_armed()};
if (m_hover.track != nullptr && armed != nullptr)
if (auto const *armed{signal_armed()}; armed != nullptr)
{
signal_spot spot;
if (signal_spot_at(*m_hover.track, m_hover.path, m_hover.point, cursor_level(m_hover.point.y), 0, 0, *armed, spot))
if (m_hover.track != nullptr && signal_spot_at(*m_hover.track, m_hover.path, m_hover.point, cursor_level(m_hover.point.y), 0, 0, *armed, spot))
{
intent.track = m_hover.track;
intent.path = m_hover.path;
intent.position = spot.position;
intent.action = signal_intent_label(*armed, spot);
intent.what = kind::signal_place;
return intent;
}
return intent;
}
if (m_hover.track != nullptr)
{
intent.track = m_hover.track;
intent.path = m_hover.path;
intent.position = m_hover.point;
return make(kind::select, "Choose the type of the signal in the Tracks window first");
return make(kind::select, m_hover.track->eType != tt_Normal ? "Click: the station of this switch" : "Click: the station of the switch nearest along this track");
}
return intent;
}
if (m_track_tab == track_tab::kilometrage && m_hover.track != nullptr)
{
intent.track = m_hover.track;
intent.path = m_hover.path;
intent.position = m_hover.point;
make(kind::kilometrage, "Click: a point of the km here");
if (auto const km{m_km.readout(m_hover.point)}; false == km.empty())
intent.action = km + "\n" + intent.action;
return intent;
}
if (m_track_style.picking && (m_track_tab == track_tab::path || m_track_tab == track_tab::turnout))
{
intent.track = m_hover.track;
@@ -368,7 +740,7 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
intent.snap.track = selected;
intent.snap.point = hit;
intent.snap.position = intent.position;
auto const diverging{selected->eType == tt_Switch && hit == editor_track::point_ref{1, editor_track::point_kind::end}};
auto const diverging{selected->eType == tt_Switch && hit == editor_track::point_ref{1, editor_track::point_kind::end} && m_track_layout != track_layout::straight};
return make(kind::extend_end, diverging ? "Drag: the curve of the switch goes on; Ctrl+drag: move the end" : "Drag: a new track out of this end, as in laying; Ctrl+drag: move the end");
}
}
@@ -419,6 +791,18 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
return make(kind::straight_handle, hit == 2 ? "Drag: shift the straight sideways" : "Drag: move this end, the curves at it follow");
}
}
if (m_route.trace.active && route_active())
{
intent.index = route_trace_hit();
if (control && intent.index >= 0)
{
intent.position = m_route.trace.points[intent.index];
return make(kind::route_trace, "Click: take this point out");
}
intent.index = -1;
intent.position = route_trace_cursor();
return make(kind::route_trace, m_route.trace.points.empty() ? "Click: a point in the middle of the track on the photo. Enter: apply, Esc: drop" : "Click: one more point, the curve follows. Ctrl+click: take a point out");
}
if (int vertex, grip; route_hit(vertex, grip))
{
intent.index = vertex >= 0 ? vertex : grip;
@@ -436,11 +820,23 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
return make(kind::straight_tool, m_straights.tool == 1 ? "Press: break the straight here, drag to turn the rest" : "Click: start of the shift");
}
}
// a switch armed, at a free end of a switch, P4 of its diverging track or the end of its main one: the switch goes on from there at
// once, as from the free end of a plain track
if (m_track_tab == track_tab::turnout && m_hover.track != nullptr && m_hover.track->eType == tt_Switch && free_switch_offered())
if (auto const end{free_switch_end(ground)}; end.track == m_hover.track && geometry::plan_distance(end.position, ground) <= 3.0)
{
intent.position = end.position;
return make(kind::switch_free, "Press and drag: a switch going on from this end of the switch, sideways its side");
}
if ((m_track_tab == track_tab::turnout || m_track_tab == track_tab::path) && m_hover.track != nullptr && switch_slides(*m_hover.track))
{
intent.track = m_hover.track;
intent.path = m_hover.path;
intent.position = m_hover.point;
// one bent into a curve slides along the curve
auto const bent{m_hover.track->eType == tt_Switch && m_hover.track->m_paths.size() == 2 && geometry::has_controls(m_hover.track->m_paths.front())};
if (bent)
return make(kind::switch_slide, m_track_tab == track_tab::path ? "Click: select this switch, drag: slide it along the curve" : "Click: edit this switch, drag: slide it along the curve");
return make(kind::switch_slide, m_track_tab == track_tab::path ? "Click: select this switch, drag: slide it along the straight" : "Click: edit this switch, drag: slide it along the straight");
}
if (selected != nullptr && false == selected->m_paths.empty())
@@ -453,12 +849,29 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
return make(kind::switch_at, "Press and drag: a switch here, along sets its direction, sideways its side");
}
}
if (m_track_tab == track_tab::lineside && m_fouling.open)
if (auto const hit{fouling_hit()}; hit.has_value())
// a press on another plain track takes it for the switch at once, with no click to select it first; a click only selects it
if (m_track_tab == track_tab::turnout && m_switch.armed >= 0 && m_hover.track != nullptr && m_hover.track != selected && m_hover.track->eType == tt_Normal &&
false == m_hover.track->m_paths.empty())
{
intent.track = m_hover.track;
intent.path = m_hover.path;
intent.position = m_hover.point;
return make(kind::switch_at, "Press and drag: a switch on this track, along sets its direction, sideways its side. Click: select the track");
}
// a press on the ground lays a switch with no track there, going on from a free end within reach
if (m_hover.track == nullptr && free_switch_offered())
{
auto const end{free_switch_end(ground)};
intent.position = end.track != nullptr ? end.position : ground;
return make(kind::switch_free, end.track != nullptr ? "Press and drag: a switch going on from this free end, sideways its side"
: "Press and drag: a switch with no track, along the direction of its main track, then sideways its side");
}
if (m_track_tab == track_tab::lineside && m_fouling.tab().open)
if (auto const hit{m_fouling.hit()}; hit.has_value())
{
intent.index = hit->first;
intent.path = hit->second ? 1 : 0;
intent.position = hit->second ? m_fouling.found[hit->first].marker.location : m_fouling.points[hit->first].position;
intent.position = m_fouling.position_of(*hit);
return make(kind::fouling, hit->second ? "Click: move this W17 to its place" : "Click: put the W17 here");
}
if (m_hover.track != nullptr)
@@ -512,11 +925,82 @@ void editor_mode::update_track_intent()
}
}
void editor_mode::tape_update()
{
auto &tape{m_tape};
if (false == tape.active || ImGui::GetIO().WantCaptureMouse)
return;
glm::vec2 const cursor{ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y};
auto const ground{cursor_ground()};
if (auto const end{snap_free_end(ground, cursor, nullptr, kRailCategory, {})}; end.track != nullptr)
tape.mouse = end.position;
else if (auto const under{track_under_cursor()}; under.track != nullptr)
tape.mouse = under.point;
else
tape.mouse = ground;
}
std::string editor_mode::tape_readout() const
{
auto const &tape{m_tape};
if (tape.points.empty())
return {};
auto const &from{tape.points.front()};
auto const &to{tape.points.size() >= 2 ? tape.points[1] : tape.mouse};
auto const plan{geometry::plan_distance(from, to)};
auto const rise{to.y - from.y};
auto azimuth{glm::degrees(std::atan2(to.x - from.x, to.z - from.z))};
if (azimuth < 0.0)
azimuth += 360.0;
auto text{format(STR_C("%.2f m in plan, %.2f m in space"), plan, glm::distance(from, to))};
text += "\n" + format(STR_C("height %+.2f m"), rise) + (plan > 0.01 ? format(STR_C(" grade %+.1f per mille"), rise / plan * 1000.0) : std::string{});
text += "\n" + format(STR_C("azimuth %.1f deg"), azimuth);
if (tape.points.size() >= 2)
text += "\n" + STR("Click: a new measure. M or Esc: done");
return text;
}
void editor_mode::draw_tape() const
{
auto const &tape{m_tape};
if (false == tape.active)
return;
screen_projection const projection;
auto *drawlist{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())};
auto const colour{IM_COL32(255, 230, 60, 255)};
std::vector<glm::dvec3> points{tape.points};
if (points.size() < 2)
points.push_back(tape.mouse);
for (std::size_t i = 0; i + 1 < points.size(); ++i)
projection.line(drawlist, points[i], points[i + 1], colour, 2.0f);
for (auto const &point : points)
{
ImVec2 screen;
if (projection.project(point, screen))
{
drawlist->AddCircleFilled(screen, 4.0f, colour);
drawlist->AddCircle(screen, 7.0f, IM_COL32(0, 0, 0, 200), 12, 1.5f);
}
}
}
void editor_mode::track_press(track_intent const &Intent)
{
using kind = track_intent::kind;
switch (Intent.what)
{
case kind::measure:
if (m_tape.points.size() >= 2)
m_tape.points.clear();
m_tape.points.push_back(m_tape.mouse);
return;
case kind::route_trace:
if (Intent.index >= 0)
m_route.trace.points.erase(m_route.trace.points.begin() + Intent.index);
else
m_route.trace.points.push_back(Intent.position);
route_trace_fit();
return;
case kind::lay_end:
if (m_lay.points.empty() && start_extend(Intent.snap))
return;
@@ -575,12 +1059,33 @@ void editor_mode::track_press(track_intent const &Intent)
m_route.grip = Intent.index;
handle_drag_start(Intent.position);
return;
case kind::switch_at: start_switch_placement(); return;
case kind::switch_at:
{
// the press took a track not selected for the switch: let go with no drag it only selects the track
if (Intent.track != nullptr && Intent.track != selected_track())
select_track(Intent.track);
auto const &io{ImGui::GetIO()};
m_switch.pressed_at = {io.MousePos.x, io.MousePos.y};
start_switch_placement();
return;
}
case kind::switch_free:
{
auto const &io{ImGui::GetIO()};
m_switch.pressed_at = {io.MousePos.x, io.MousePos.y};
start_free_switch(Intent.position);
return;
}
case kind::kilometrage:
if (Intent.track != nullptr)
m_km.pick(*Intent.track, Intent.position);
return;
case kind::srk_boundary: m_srk.boundary_click(); return;
case kind::fouling:
if (Intent.path == 1)
fouling_fix(Intent.index);
m_fouling.fix(Intent.index);
else
fouling_place(Intent.index);
m_fouling.place(Intent.index);
return;
case kind::extend_end:
if (false == start_extend(Intent.snap))
@@ -648,6 +1153,7 @@ void editor_mode::draw_track_intent() const
case kind::route_vertex:
case kind::route_grip: drawlist->AddCircle(at, 13.0f, overlay_color::marked, 24, 2.5f); break;
case kind::switch_at:
case kind::switch_free:
case kind::straight_tool:
drawlist->AddCircleFilled(at, 4.0f, overlay_color::grip);
drawlist->AddCircle(at, 11.0f, overlay_color::grip, 20, 2.0f);
@@ -656,7 +1162,8 @@ void editor_mode::draw_track_intent() const
case kind::extend_end:
case kind::turntable_fit: drawlist->AddCircleFilled(at, 5.0f, IM_COL32(255, 60, 255, 255)); break;
case kind::turntable_exit: drawlist->AddCircle(at, 11.0f, overlay_color::grip, 20, 2.0f); break;
case kind::lay: drawlist->AddCircle(at, 6.0f, IM_COL32(255, 255, 255, 200), 12, 1.5f); break;
case kind::lay:
case kind::route_trace: drawlist->AddCircle(at, 6.0f, IM_COL32(255, 255, 255, 200), 12, 1.5f); break;
default: break;
}
}
@@ -671,10 +1178,26 @@ void editor_mode::draw_track_intent() const
details += "\n" + std::string{STR_C(type_of(*track))};
if (track->eType == tt_Switch)
details += m_hover.path == 0 ? STR(" (main)") : STR(" (branch)");
details += format(" L %.1f m", bezier{path}.plan_length());
details += path.radius != 0.0f ? format(" R %.0f m", std::abs(path.radius)) : std::string{" "} + STR_C("straight");
bezier const curve{path};
details += format(" L %.1f m", curve.plan_length());
auto const radius{::track_mode::piece_plan_radius(path)};
details += radius >= 0.5 ? format(" R %.0f m", radius) : std::string{" "} + STR_C("straight");
auto const velocity{editor_track::velocity(*track)};
details += velocity > 0.0 ? format(" V %.0f km/h", velocity) : std::string{};
double nearest{std::numeric_limits<double>::max()};
double at{0.0};
for (int k = 0; k <= 64; ++k)
if (auto const distance{geometry::plan_distance(curve.point(k / 64.0), m_hover.point)}; distance < nearest)
{
nearest = distance;
at = k / 64.0;
}
auto const tangent{curve.first(at)};
if (auto const run{glm::length(plan_of(tangent))}; run > 1e-9)
details += "\n" + format(STR_C("grade %+.1f per mille"), tangent.y / run * 1000.0);
auto const roll{std::max(std::abs(path.rolls[0]), std::abs(path.rolls[1]))};
if (roll > 1e-3f)
details += format(STR_C(" cant %.0f mm"), 1500.0 * std::sin(glm::radians(static_cast<double>(roll))));
}
auto *foreground{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())};
auto const actionsize{ImGui::CalcTextSize(intent.action.c_str())};
@@ -716,6 +1239,22 @@ void editor_mode::split_track_at(TTrack &Track, glm::dvec3 const &Point)
auto const before{editor_track::capture(Track)};
if (auto *created{editor_track::split_path(Track, parameter)})
{
// a break of the grade, as a point of a road: the point between the two takes the height of the ground and the ballast
// there, each of them an even grade from its other end up to it
auto const &path{Track.m_paths.front()};
auto const joint{path.points[segment_data::point::end]};
auto const height{bed_height(joint, geometry::plan_of(geometry::bezier{path}.first(1.0)), &Track)};
auto const none{std::numeric_limits<double>::quiet_NaN()};
auto const unknown = [](std::vector<glm::dvec3> const &Points, std::vector<glm::dvec2> const &) { return std::vector<double>(Points.size(), std::numeric_limits<double>::quiet_NaN()); };
for (auto *part : {&Track, created})
{
std::vector<segment_data> pieces{part->m_paths.front()};
auto &points{pieces.front().points};
points[part == &Track ? segment_data::point::end : segment_data::point::start].y = height;
ground_follow::follow(pieces, {points[segment_data::point::start].y, none, points[segment_data::point::end].y, none}, unknown);
part->m_paths.front() = pieces.front();
}
editor_track::commit({&Track, created});
push_track_snapshot({{&Track, before}}, {created});
straight_refresh();
}
@@ -787,11 +1326,13 @@ void editor_mode::render_track_context()
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::MenuItem(STR_C("Split here"), "K", false, normal))
if (ImGui::MenuItem(STR_C("Split here, a break of the grade"), "K", false, normal))
split_track_at(*track, point);
ImGui::Separator();
if (ImGui::MenuItem(STR_C("Switch"), "T", false, normal || track->eType == tt_Switch))
show_track_tab(track_tab::turnout);
if (track->eType == tt_Switch && track->SwitchExtension != nullptr && ImGui::MenuItem(STR_C("Throw it over"), "H"))
throw_switch(*track);
if (ImGui::MenuItem(STR_C("Plan"), "G", false, normal))
show_track_tab(track_tab::straights);
if (ImGui::MenuItem(STR_C("Parallel track"), "N", false, normal))
@@ -802,19 +1343,17 @@ void editor_mode::render_track_context()
if (ImGui::MenuItem(STR_C("Place a vehicle here")))
{
show_track_tab(track_tab::lineside);
vehicle_start(*track, point);
m_vehicle.start(*track, point);
}
if (ImGui::MenuItem(STR_C("Hectometre posts from here")))
{
show_track_tab(track_tab::lineside);
hekto_start(*track, point);
m_hekto.start(*track, point);
}
if (ImGui::MenuItem(STR_C("Fouling point markers"), nullptr, false, track->eType == tt_Switch))
{
show_track_tab(track_tab::lineside);
m_fouling.scope = 0;
m_fouling.dirty = true;
m_fouling.expand = true;
m_fouling.check_selected_switch();
}
if (ImGui::MenuItem(STR_C("Vertical profile of the line"), "P"))
{
@@ -823,9 +1362,8 @@ void editor_mode::render_track_context()
}
if (ImGui::MenuItem(STR_C("Speed check of the line"), "V"))
{
m_speed.scope = 0;
show_track_tab(track_tab::speed);
speed_start();
m_speed.check_line();
}
ImGui::Separator();
if (ImGui::MenuItem(track_selected(track) ? STR_C("Take out of the selection") : STR_C("Add to the selection"), STR_C("Shift+LMB")))
@@ -855,7 +1393,7 @@ void editor_mode::render_track_search()
}
ImGui::SetNextItemWidth(-1.0f);
ImGui::InputTextWithHint("##findtrack", STR_C("Find a track by its name (Ctrl+F)"), search.text, sizeof(search.text));
std::string const text{lower(search.text)};
std::string const text{ToLower(search.text)};
if (text.empty())
{
search.searched.clear();
@@ -869,7 +1407,7 @@ void editor_mode::render_track_search()
search.total = 0;
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || false == rail(*track) || lower(track->name()).find(text) == std::string::npos)
if (track == nullptr || false == rail(*track) || ToLower(track->name()).find(text) == std::string::npos)
continue;
++search.total;
if (search.found.size() < kSearchLimit)
@@ -915,6 +1453,8 @@ char const *editor_mode::typed_meaning() const
return "type the length of the next leg, Enter adds the point";
if (m_track_tab == track_tab::catenary && catenary_takes_typed())
return "type the span from the last pole, LMB by the track places the pole there";
if (m_extend.active && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc))
return "type the radius of the curve";
return nullptr;
}
@@ -973,7 +1513,7 @@ bool editor_mode::typed_key(int const Key)
glm::dvec3 editor_mode::typed_lay(glm::dvec3 const &Mouse) const
{
auto const values{typed_values()};
if (values.empty() || m_lay.points.empty() || values[0] <= 0.0)
if (values.empty() || m_lay.points.empty() || false == (values[0] > 0.0 && values[0] <= 50000.0))
return Mouse;
auto const &last{m_lay.points.back()};
glm::dvec2 heading{Mouse.x - last.x, Mouse.z - last.z};
@@ -1348,7 +1888,9 @@ void editor_mode::track_set_delete()
if (std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) != m_route.chain.tracks.end())
m_route = {};
}
auto const designs{designs_trimmed(removed)};
push_track_snapshot({}, {}, removed);
drives_deleted(removed);
m_track_set.clear();
m_node = nullptr;
ui()->set_node(nullptr);
@@ -1357,6 +1899,8 @@ void editor_mode::track_set_delete()
auto status{format(STR_P("%zu track deleted, Ctrl+Z brings it back", "%zu tracks deleted, Ctrl+Z brings them back", static_cast<long long>(removed.size())), removed.size())};
if (kept > 0)
status += format(STR_C(", %zu with vehicles on them or in the files which can't be changed are left"), kept);
if (false == designs.empty())
status += "\n" + designs;
ui()->set_status(status, false);
}
@@ -1369,7 +1913,7 @@ void editor_mode::render_track_set_ui()
ImGui::PushTextWrapPos(0.0f);
ImGui::TextDisabled("%s", STR_C("Their properties are in the inspector, the gizmo moves them together"));
ImGui::PopTextWrapPos();
render_parallel_set_ui();
m_parallel.render_set_options();
ImGui::Separator();
}
@@ -1489,9 +2033,9 @@ void editor_mode::render_lineside_ui()
std::function<void()> render;
};
std::array<tab, 3> const tabs{{
{"Hectometre posts", &m_hekto.open, &m_hekto.expand, [this]() { render_hekto_ui(); }},
{"Fouling points", &m_fouling.open, &m_fouling.expand, [this]() { render_fouling_ui(); }},
{"Trainset", &m_vehicle.open, &m_vehicle.expand, [this]() { render_vehicle_ui(); }},
{"Hectometre posts", &m_hekto.tab().open, &m_hekto.tab().expand, [this]() { m_hekto.render_options(); }},
{"Fouling points", &m_fouling.tab().open, &m_fouling.tab().expand, [this]() { m_fouling.render_options(); }},
{"Trainset", &m_vehicle.tab().open, &m_vehicle.tab().expand, [this]() { m_vehicle.render_options(); }},
}};
if (selected_track() == nullptr)
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("LMB on a track: the line the objects go along"));