mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 00:29:19 +02:00
369 lines
15 KiB
C++
369 lines
15 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 "editor/editorFormat.hpp"
|
|
#include "editor/editorGeometry.hpp"
|
|
|
|
#include "scene/scenelayers.h"
|
|
#include "simulation/simulation.h"
|
|
#include "utilities/Logs.h"
|
|
#include "world/Track.h"
|
|
|
|
#include "imgui/imgui.h"
|
|
#include "utilities/translation.h"
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
|
|
namespace
|
|
{
|
|
|
|
double const kSpeedLineReach{5000.0}; // m each way, for the line through the selected path
|
|
double const kSpeedOverlayRange{3000.0};
|
|
|
|
bool rail(TTrack const &Track)
|
|
{
|
|
return (Track.iCategoryFlag & 15) == 1 && false == Track.m_editorremoved && Track.m_road == nullptr && editor_track::is_supported(Track);
|
|
}
|
|
|
|
std::string speed_text(double const Speed)
|
|
{
|
|
return Speed > 0.0 ? format("%.0f", Speed) : std::string{"none"};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void editor_mode::speed_start()
|
|
{
|
|
auto &state{m_speed};
|
|
state.queue.clear();
|
|
state.next = 0;
|
|
state.results.clear();
|
|
state.selected = -1;
|
|
state.status.clear();
|
|
state.options.norms = m_route.design.norms;
|
|
auto const add = [&](TTrack *Track) {
|
|
for (int i = 0; i < static_cast<int>(Track->m_paths.size()); ++i)
|
|
state.queue.emplace_back(Track, i);
|
|
};
|
|
if (state.scope == 0)
|
|
{
|
|
auto *track{selected_track()};
|
|
if (track == nullptr || false == rail(*track))
|
|
{
|
|
state.status = STR_C("Select a track first");
|
|
return;
|
|
}
|
|
editor_track::chain line;
|
|
std::string error;
|
|
if (track->eType == tt_Normal && editor_track::find_run(*track, kSpeedLineReach, line, error))
|
|
{
|
|
for (auto *member : line.tracks)
|
|
add(member);
|
|
// the switches at the ends of the line too
|
|
for (auto *end : {line.tracks.front(), line.tracks.back()})
|
|
for (auto *neighbour : editor_track::neighbours(*end))
|
|
if (neighbour->eType == tt_Switch && rail(*neighbour) && std::none_of(state.queue.begin(), state.queue.end(), [&](auto const &Entry) { return Entry.first == neighbour; }))
|
|
add(neighbour);
|
|
}
|
|
else
|
|
{
|
|
add(track);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (auto *track : simulation::Paths.sequence())
|
|
if (track != nullptr && rail(*track))
|
|
add(track);
|
|
}
|
|
state.history = m_history.size();
|
|
}
|
|
|
|
void editor_mode::speed_step()
|
|
{
|
|
auto &state{m_speed};
|
|
if (state.next >= state.queue.size())
|
|
return;
|
|
auto const deadline{std::chrono::steady_clock::now() + std::chrono::milliseconds(15)};
|
|
while (state.next < state.queue.size() && std::chrono::steady_clock::now() < deadline)
|
|
{
|
|
auto const &entry{state.queue[state.next++]};
|
|
if (entry.first->m_editorremoved)
|
|
continue;
|
|
state.results.push_back(speed_check::check(*entry.first, entry.second, state.options));
|
|
}
|
|
if (state.next < state.queue.size())
|
|
return;
|
|
// the worst first: the violations, then the warnings, each by the largest excess of the speed set over the safe one
|
|
auto const excess = [&](speed_check::verdict const &Verdict) { return Verdict.set > 0.0 ? Verdict.set - Verdict.safe : state.options.top - Verdict.safe; };
|
|
auto const rank = [&](speed_check::verdict const &Verdict) { return Verdict.violated(state.options) ? 0 : Verdict.warned(state.options) ? 1 : 2; };
|
|
std::stable_sort(state.results.begin(), state.results.end(), [&](auto const &A, auto const &B) {
|
|
auto const a{rank(A)};
|
|
auto const b{rank(B)};
|
|
if (a != b)
|
|
return a < b;
|
|
return a < 2 ? excess(A) > excess(B) : A.safe < B.safe;
|
|
});
|
|
auto const count{std::count_if(state.results.begin(), state.results.end(), [&](auto const &Verdict) { return Verdict.violated(state.options); })};
|
|
auto const warnings{std::count_if(state.results.begin(), state.results.end(), [&](auto const &Verdict) { return Verdict.warned(state.options); })};
|
|
state.status = format(STR_C("%zu paths checked, %d with the speed above what the geometry allows, %d switch branch warnings"), state.results.size(), static_cast<int>(count), static_cast<int>(warnings));
|
|
WriteLog("Editor: speed check - " + state.status, logtype::generic);
|
|
}
|
|
|
|
std::vector<int> editor_mode::speed_listed() const
|
|
{
|
|
std::vector<int> listed;
|
|
for (int i = 0; i < static_cast<int>(m_speed.results.size()); ++i)
|
|
{
|
|
auto const &verdict{m_speed.results[i]};
|
|
if ((m_speed.limits == 1 && verdict.set <= 0.0) || (m_speed.limits == 2 && verdict.set > 0.0))
|
|
continue;
|
|
if (m_speed.show_all || verdict.violated(m_speed.options) || verdict.warned(m_speed.options))
|
|
listed.push_back(i);
|
|
}
|
|
return listed;
|
|
}
|
|
|
|
void editor_mode::speed_focus(int const Index)
|
|
{
|
|
auto &state{m_speed};
|
|
if (Index < 0 || Index >= static_cast<int>(state.results.size()))
|
|
return;
|
|
state.selected = Index;
|
|
auto const &verdict{state.results[Index]};
|
|
auto const &paths{simulation::Paths.sequence()};
|
|
if (std::find(paths.begin(), paths.end(), verdict.track) == paths.end() || verdict.track->m_editorremoved)
|
|
return;
|
|
m_node = verdict.track;
|
|
ui()->set_node(m_node);
|
|
focus_track(*verdict.track, verdict.path, verdict.position);
|
|
}
|
|
|
|
void editor_mode::speed_apply(std::vector<int> const &Indices, bool const Branches)
|
|
{
|
|
auto &state{m_speed};
|
|
struct change
|
|
{
|
|
TTrack *track;
|
|
double limits[2]; // km/h for the path, and the diverging track of a switch, -1: none
|
|
};
|
|
std::vector<std::pair<TTrack *, editor_track::state>> states;
|
|
std::vector<change> changes;
|
|
std::size_t locked{0};
|
|
for (auto const index : Indices)
|
|
{
|
|
auto const &verdict{state.results[index]};
|
|
auto const safe{Branches ? verdict.safe : verdict.hard};
|
|
if (safe >= state.options.top || verdict.track->m_editorremoved)
|
|
continue;
|
|
if (false == scene::Layers.editable(verdict.track))
|
|
{
|
|
++locked;
|
|
continue;
|
|
}
|
|
auto existing{std::find_if(changes.begin(), changes.end(), [&](change const &Change) { return Change.track == verdict.track; })};
|
|
if (existing == changes.end())
|
|
{
|
|
changes.push_back({verdict.track, {speed_check::path_velocity(*verdict.track, 0), speed_check::path_velocity(*verdict.track, 1)}});
|
|
states.emplace_back(verdict.track, editor_track::capture(*verdict.track));
|
|
existing = std::prev(changes.end());
|
|
}
|
|
auto &limit{existing->limits[verdict.track->eType == tt_Switch ? std::clamp(verdict.path, 0, 1) : 0]};
|
|
auto const speed{speed_check::suggested(safe)};
|
|
limit = limit > 0.0 ? std::min(limit, speed) : speed;
|
|
}
|
|
auto const skipped{locked > 0 ? format(", %zu in the files which can't be changed are left as they are", locked) : std::string{}};
|
|
if (changes.empty())
|
|
{
|
|
if (locked > 0)
|
|
state.status = STR_C("Nothing changed") + skipped;
|
|
return;
|
|
}
|
|
push_track_snapshot(std::move(states));
|
|
for (auto const &entry : changes)
|
|
{
|
|
auto const velocity{entry.track->eType == tt_Switch ? speed_check::switch_velocity(entry.limits[0], entry.limits[1]) : entry.limits[0]};
|
|
editor_track::velocity(*entry.track, velocity);
|
|
entry.track->mark_dirty();
|
|
}
|
|
for (auto &verdict : state.results)
|
|
verdict.set = speed_check::path_velocity(*verdict.track, verdict.path);
|
|
state.history = m_history.size();
|
|
state.status = format(STR_C("Speed limits of %zu paths lowered to the safe ones, Ctrl+Z brings them back"), changes.size()) + skipped;
|
|
WriteLog("Editor: speed check - " + state.status, logtype::generic);
|
|
}
|
|
|
|
void editor_mode::render_speed_body()
|
|
{
|
|
auto &state{m_speed};
|
|
ImGui::TextDisabled(STR_C("Speed limits of the paths against the speed their geometry allows:\nradius and cant, cant ramps, curves without the transitions, switch branches"));
|
|
ImGui::RadioButton(STR_C("Line through the selected path"), &state.scope, 0);
|
|
ImGui::SameLine();
|
|
ImGui::RadioButton(STR_C("Whole scenery"), &state.scope, 1);
|
|
bool const running{state.next < state.queue.size()};
|
|
if (running)
|
|
{
|
|
auto const progress{static_cast<float>(state.next) / static_cast<float>(state.queue.size())};
|
|
ImGui::ProgressBar(progress, ImVec2(-70.0f, 0.0f), format(STR_C("Checking %zu / %zu"), state.next, state.queue.size()).c_str());
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton(STR_C("Cancel")))
|
|
state.queue.resize(state.next);
|
|
}
|
|
else if (ImGui::Button(STR_C("Check")))
|
|
{
|
|
speed_start();
|
|
}
|
|
if (ImGui::TreeNode(STR_C("Limits")))
|
|
{
|
|
auto &options{state.options};
|
|
ImGui::PushItemWidth(100.0f);
|
|
ImGui::InputDouble(STR_C("Unbalanced acceleration (m/s2)"), &options.norms.unbalanced, 0.0, 0.0, "%.2f");
|
|
ImGui::InputDouble(STR_C("Switch branches (m/s2)"), &options.branch_unbalanced, 0.0, 0.0, "%.2f");
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip(STR_C("Unbalanced acceleration allowed on the diverging tracks of the switches, which have no cant.\n0.65 m/s2 gives 40 km/h for R 190 m and 100 km/h for R 1200 m"));
|
|
ImGui::InputDouble(STR_C("Cant change rate (mm/s)"), &options.norms.cant_rate, 0.0, 0.0, "%.0f");
|
|
ImGui::InputDouble(STR_C("Cant ramp 1:(k*V), k"), &options.norms.ramp_factor, 0.0, 0.0, "%.1f");
|
|
ImGui::InputDouble(STR_C("Unbalanced acc. change rate (m/s3)"), &options.norms.jerk, 0.0, 0.0, "%.2f");
|
|
ImGui::InputDouble(STR_C("Reference length (m)"), &options.reference_length, 0.0, 0.0, "%.0f");
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip(STR_C("Length over which an abrupt change of the curvature or the cant is felt,\nabout the length of a car. Shorter: stricter for the curves without the transitions"));
|
|
ImGui::PopItemWidth();
|
|
options.norms.unbalanced = std::max(0.05, options.norms.unbalanced);
|
|
options.branch_unbalanced = std::max(0.05, options.branch_unbalanced);
|
|
options.norms.cant_rate = std::max(1.0, options.norms.cant_rate);
|
|
options.norms.ramp_factor = std::max(1.0, options.norms.ramp_factor);
|
|
options.norms.jerk = std::max(0.01, options.norms.jerk);
|
|
options.reference_length = std::clamp(options.reference_length, 2.0, 200.0);
|
|
ImGui::TextDisabled(STR_C("Change the limits, then Check again"));
|
|
ImGui::TreePop();
|
|
}
|
|
if (false == state.status.empty())
|
|
ImGui::TextWrapped("%s", state.status.c_str());
|
|
if (state.results.empty())
|
|
return;
|
|
if (state.history != m_history.size())
|
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("The scenery changed since the check"));
|
|
ImGui::SetNextItemWidth(190.0f);
|
|
ImGui::Combo("##speedlimits", &state.limits, "All the paths\0With a speed limit set\0Without a speed limit\0");
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip(STR_C("Paths without a speed limit of their own run at the line speed,\nthose are listed whenever the geometry limits the speed at all"));
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox(STR_C("Within their limits too"), &state.show_all);
|
|
auto const listed{speed_listed()};
|
|
std::vector<int> violated;
|
|
for (auto const index : listed)
|
|
if (state.results[index].violated(state.options))
|
|
violated.push_back(index);
|
|
if (false == violated.empty())
|
|
{
|
|
ImGui::SameLine();
|
|
if (ImGui::Button(format(STR_C("Set the safe speed on %zu paths"), violated.size()).c_str()))
|
|
speed_apply(violated, false);
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip(STR_C("Lowers the speed limit of each listed path to its safe speed, rounded down to 10 km/h (5 km/h below 40).\n"
|
|
"The limits of the switch branches are only warnings and aren't applied here, the button of a single path does.\n"
|
|
"A switch has one velocity for both its tracks: negative limits only the diverging one, the main one keeping no limit.\n"
|
|
"When the main track needs a limit of its own, the lower of the two applies to both"));
|
|
}
|
|
auto const namewidth{std::max(120.0f, ImGui::GetContentRegionAvail().x * 0.3f)};
|
|
ImGui::Columns(6, STR_C("speedcolumns"), false);
|
|
ImGui::SetColumnWidth(0, namewidth + 8.0f);
|
|
for (auto const *header : {"Path", "Set", "Safe", "Suggested", "Cause", "R / cant"})
|
|
{
|
|
ImGui::TextDisabled("%s", header);
|
|
ImGui::NextColumn();
|
|
}
|
|
ImGui::Columns(1);
|
|
bool const chosen{state.selected >= 0 && state.selected < static_cast<int>(state.results.size())};
|
|
auto const details{chosen ? ImGui::GetFrameHeightWithSpacing() + ImGui::GetTextLineHeightWithSpacing() * 5.0f : 0.0f};
|
|
ImGui::BeginChild("speedresults", ImVec2(0.0f, -details), true);
|
|
ImGui::Columns(6, STR_C("speedrows"), false);
|
|
ImGui::SetColumnWidth(0, namewidth);
|
|
ImGuiListClipper clipper;
|
|
clipper.Begin(static_cast<int>(listed.size()));
|
|
while (clipper.Step())
|
|
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row)
|
|
{
|
|
auto const index{listed[row]};
|
|
auto const &verdict{state.results[index]};
|
|
bool const violated{verdict.violated(state.options)};
|
|
bool const bad{violated || verdict.warned(state.options)};
|
|
auto const colour{violated ? ImVec4(1.0f, 0.45f, 0.4f, 1.0f) : ImVec4(1.0f, 0.75f, 0.3f, 1.0f)};
|
|
auto name{verdict.track->name().empty() ? std::string{"(noname)"} : verdict.track->name()};
|
|
if (verdict.track->eType == tt_Switch)
|
|
name += verdict.path == 0 ? " (main)" : " (branch)";
|
|
if (bad)
|
|
ImGui::PushStyleColor(ImGuiCol_Text, colour);
|
|
if (ImGui::Selectable((name + "##speed" + std::to_string(index)).c_str(), state.selected == index, ImGuiSelectableFlags_SpanAllColumns))
|
|
speed_focus(index);
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
if (bad)
|
|
ImGui::PopStyleColor();
|
|
ImGui::SetTooltip("%s", speed_check::explain(verdict, state.options).c_str());
|
|
if (bad)
|
|
ImGui::PushStyleColor(ImGuiCol_Text, colour);
|
|
}
|
|
ImGui::NextColumn();
|
|
ImGui::TextUnformatted(speed_text(verdict.set).c_str());
|
|
ImGui::NextColumn();
|
|
ImGui::TextUnformatted(verdict.safe >= state.options.top ? STR_C("any") : format("%.0f", verdict.safe).c_str());
|
|
ImGui::NextColumn();
|
|
ImGui::TextUnformatted(verdict.safe >= state.options.top ? "-" : format("%.0f", speed_check::suggested(verdict.safe)).c_str());
|
|
ImGui::NextColumn();
|
|
ImGui::TextUnformatted(speed_check::describe(verdict.why));
|
|
ImGui::NextColumn();
|
|
if (verdict.radius > 0.0)
|
|
ImGui::Text(STR_C("%.0f m / %.0f mm"), verdict.radius, verdict.cant);
|
|
else
|
|
ImGui::TextUnformatted("-");
|
|
ImGui::NextColumn();
|
|
if (bad)
|
|
ImGui::PopStyleColor();
|
|
}
|
|
ImGui::Columns(1);
|
|
ImGui::EndChild();
|
|
if (chosen)
|
|
{
|
|
auto const &verdict{state.results[state.selected]};
|
|
ImGui::TextWrapped("%s", speed_check::explain(verdict, state.options).c_str());
|
|
if ((verdict.violated(state.options) || verdict.warned(state.options)) && ImGui::Button(STR_C("Set the safe speed on this path")))
|
|
speed_apply({state.selected}, true);
|
|
}
|
|
}
|
|
|
|
void editor_mode::draw_speed_overlay() const
|
|
{
|
|
auto const &state{m_speed};
|
|
if (false == state.open || state.results.empty())
|
|
return;
|
|
screen_projection const projection;
|
|
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
|
|
glm::dvec3 const camera{Global.pCamera.Pos};
|
|
for (int i = 0; i < static_cast<int>(state.results.size()); ++i)
|
|
{
|
|
auto const &verdict{state.results[i]};
|
|
bool const violated{verdict.violated(state.options)};
|
|
if ((false == violated && false == verdict.warned(state.options)) || glm::distance(verdict.position, camera) > kSpeedOverlayRange)
|
|
continue;
|
|
ImVec2 screen;
|
|
if (false == projection.project(verdict.position, screen))
|
|
continue;
|
|
auto const colour{i == state.selected ? IM_COL32(255, 255, 80, 255) : violated ? IM_COL32(255, 70, 60, 230) : IM_COL32(255, 180, 60, 200)};
|
|
drawlist->AddCircle(screen, 12.0f, colour, 16, 2.5f);
|
|
auto const text{format("%s > %.0f", speed_text(verdict.set).c_str(), verdict.safe)};
|
|
drawlist->AddText(ImVec2(screen.x + 14.0f, screen.y - 7.0f), colour, text.c_str());
|
|
}
|
|
}
|