17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-26 23:59:48 +01:00
Files
maszyna/application/editormodejoints.cpp
maj00r f361bc406b feat(editor): the whole-scenery grade fit keeps the tracks a station shares at one height, lets the switches lie on the vertical curves the regulation allows, runs straight through the double slips and leaves the bridges out of the faults
A switch may lie on a concave vertical curve, and on a convex one no sharper than 2000 m (Dz.U. 2026 poz. 1225 § 58 ust. 16): the fit bends over the switches and their shared timbers where the ground does, a curve on a switch is a warning, and only a sharper convex one an error. A sharp crest of a hump keeps its curve off the switch zones. The zone of a crossing reaches only as far as the path crossed has rails, so the middle of a double slip keeps to itself.

A line runs through a switch along its main path, and through the quarters of a double slip straight on, so the long lines go through the slips and the connections between them are fitted after. Rails of the lines fitted already nearer than a sleeper is long are taken at their height, as both lie on the same timbers. A short line between two lines whose end grades can't be joined within the norms keeps only the heights of its ends, and the grade breaks at the joints. The norms speed of a line is the one most of its length is laid for, so a few tracks set much faster don't stiffen the curves of all of it.

The fault finder leaves out the places over the terrain model on a bridge or a viaduct, the ground falling away under them by the abutments. A formation modifier may be open at its start or end.
2026-10-22 01:48:04 +02:00

823 lines
32 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/editormodejoints.h"
#include "editor/editorNmt.hpp"
#include "application/editormodeshared.h"
#include "application/editorprojection.h"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "editor/editorProfile.hpp"
#include "scene/scene.h"
#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>
#include <limits>
#include <map>
#include <set>
#include <tuple>
namespace
{
using geometry::grade_of;
using geometry::plan_of;
double const kLinked{0.02}; // m, ends this close are joined by the simulation
double const kOverlayRange{3000.0};
double const kGauge{1.5}; // m between the rails, for the cant
int const kRailCategory{1};
double const kModelStep{5.0}; // m between the places of the tracks compared with the terrain model
std::size_t const kModelRun{200}; // places of a path whose model is asked for at once
double const kModelPoll{0.25}; // s between the looks whether the model is in
double const kUnfitted{30.0}; // m; a scenery whose tracks lie this far from the model as a whole isn't fitted to it
// a track over the model by more than the depth goes on a bridge or a viaduct the model leaves out where the model falls away by
// the abutments, steeper than the banks, within the reach along the track both ways; as the grade lines fitted to the model
// tell them. By the abutments the track is over the model by less
double const kBridgeDepth{1.0}; // m
double const kAbutment{100.0}; // per mille
double const kBridgeReach{200.0}; // m
using kind = joints_tool::joint_issue::kind;
bool rail(TTrack const &Track)
{
return (Track.iCategoryFlag & 15) == kRailCategory && false == Track.m_editorremoved && editor_track::is_supported(Track);
}
bool at_start(editor_track::point_ref const &Point)
{
return Point.kind == editor_track::point_kind::start;
}
// direction of the path at its end, into the path
glm::dvec3 into(TTrack const &Track, editor_track::point_ref const &Point)
{
auto const &path{Track.m_paths[Point.path]};
bool const start{at_start(Point)};
auto const &point{path.points[start ? segment_data::point::start : segment_data::point::end]};
auto const &control{path.points[start ? segment_data::point::control1 : segment_data::point::control2]};
glm::dvec3 const direction{control != glm::dvec3{} ? control : path.points[start ? segment_data::point::end : segment_data::point::start] - point};
return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec3{0.0, 0.0, 1.0};
}
// roll of the path at its end, for the travel into the path, in degrees
double roll_into(TTrack const &Track, editor_track::point_ref const &Point)
{
auto const &path{Track.m_paths[Point.path]};
return at_start(Point) ? path.rolls[0] : -path.rolls[1];
}
double plan_angle(glm::dvec3 const &A, glm::dvec3 const &B)
{
auto const a{plan_of(A)};
auto const b{plan_of(B)};
if (glm::length(a) < 1e-9 || glm::length(b) < 1e-9)
return 0.0;
return glm::degrees(std::acos(std::clamp(glm::dot(glm::normalize(a), glm::normalize(b)), -1.0, 1.0)));
}
std::tuple<TTrack const *, int, int> key_of(TTrack const *Track, editor_track::point_ref const &Point)
{
return {Track, Point.path, static_cast<int>(Point.kind)};
}
char const *kind_name(kind const Kind)
{
switch (Kind)
{
case kind::gap: return "gap";
case kind::step: return "step";
case kind::kink: return "kink";
case kind::cant: return "cant jump";
case kind::grade: return "grade break";
case kind::model: return "off the terrain model";
default: return "free end";
}
}
std::string value_text(joints_tool::joint_issue const &Issue)
{
switch (Issue.type)
{
case kind::gap: return format("%.3f m", Issue.value);
case kind::step: return format("%.1f mm", Issue.value * 1000.0);
case kind::kink: return format("%.3f\xc2\xb0", Issue.value);
case kind::cant: return format("%.1f mm", Issue.value);
case kind::grade: return format("%.1f \xe2\x80\xb0", Issue.value);
case kind::model: return format("%+.2f m", Issue.value);
default: return "-";
}
}
ImU32 kind_colour(kind const Kind)
{
switch (Kind)
{
case kind::gap: return IM_COL32(255, 70, 60, 235);
case kind::step: return IM_COL32(255, 140, 50, 230);
case kind::kink: return IM_COL32(255, 190, 60, 225);
case kind::cant: return IM_COL32(200, 120, 255, 225);
case kind::grade: return IM_COL32(90, 200, 255, 220);
case kind::model: return IM_COL32(70, 200, 190, 235);
default: return IM_COL32(200, 200, 200, 200);
}
}
std::string label_of(TTrack const *Track)
{
return Track == nullptr ? std::string{"-"} : ::name_of(*Track);
}
} // namespace
void joints_tool::joints_start()
{
auto &state{m_joints};
state.queue.clear();
state.next = 0;
state.issues.clear();
state.selected = -1;
state.status.clear();
m_reported.clear();
glm::dvec3 const camera{Global.pCamera.Pos};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || false == rail(*track))
continue;
if (state.scope == 0 && glm::distance(track->location(), camera) > state.reach)
continue;
state.queue.push_back(track);
}
state.history = m_history.history_size();
state.places.clear();
state.runs.clear();
state.run_next = 0;
state.model_waiting = false;
state.model_asked = false;
state.model_polled = 0.0;
state.model_unplaced = false;
if (false == state.model_check || state.queue.empty())
return;
m_ground.nmt_locate();
if (false == m_ground.terrain_model().placed())
{
state.model_unplaced = true;
return;
}
std::set<TTrack const *> const queued(state.queue.begin(), state.queue.end());
for (auto const &place : m_ground.nmt_track_places(kModelStep))
if (place.shapes && queued.count(place.track) > 0)
{
if (state.places.empty() || state.places.back().track != place.track || state.places.back().path != place.path || state.places.size() - state.runs.back().first >= kModelRun)
state.runs.emplace_back(state.places.size(), state.places.size());
state.places.push_back(place);
state.runs.back().second = state.places.size();
}
state.model_waiting = false == state.places.empty();
}
bool joints_tool::joints_model()
{
auto &state{m_joints};
auto const now{ImGui::GetTime()};
if (state.model_asked && now - state.model_polled < kModelPoll)
return false;
state.model_polled = now;
auto const box = [&](std::pair<std::size_t, std::size_t> const &Run, glm::dvec2 &Min, glm::dvec2 &Max) {
Min = glm::dvec2{std::numeric_limits<double>::max()};
Max = glm::dvec2{std::numeric_limits<double>::lowest()};
for (auto i = Run.first; i < Run.second; ++i)
{
glm::dvec2 const at{state.places[i].position.x, state.places[i].position.z};
Min = glm::min(Min, at - kBridgeReach);
Max = glm::max(Max, at + kBridgeReach);
}
};
glm::dvec2 low, high;
// all of it asked for once, then the runs looked over in turn as their squares come
if (false == state.model_asked)
{
for (auto const &run : state.runs)
{
box(run, low, high);
m_ground.terrain_model().request(low, high);
}
state.model_asked = true;
}
while (state.run_next < state.runs.size())
{
box(state.runs[state.run_next], low, high);
if (false == m_ground.terrain_model().request(low, high))
return false;
++state.run_next;
}
std::vector<double> differences(state.places.size(), std::numeric_limits<double>::quiet_NaN());
std::vector<double> known;
for (std::size_t i = 0; i < state.places.size(); ++i)
if (double height; m_ground.terrain_model().height(state.places[i].position.x, state.places[i].position.z, height))
{
differences[i] = state.places[i].crown - (height + m_ground.terrain_model_offset());
known.push_back(differences[i]);
}
auto shift{0.0};
if (false == m_ground.terrain_model_fitted() && false == known.empty())
{
std::nth_element(known.begin(), known.begin() + known.size() / 2, known.end());
if (std::abs(known[known.size() / 2]) > kUnfitted)
shift = known[known.size() / 2];
}
// a place on a bridge or a viaduct the model leaves out isn't off it: the route through its track sampled both ways, the
// stretches over the model as on a bridge found along it
std::map<TTrack const *, std::pair<std::vector<editor_track::route_sample>, std::vector<profile::departure>>> routes;
auto const on_bridge = [&](nmt_place const &Place) {
auto found{routes.find(Place.track)};
if (found == routes.end())
{
auto route{editor_track::run_route(*Place.track, kBridgeReach)};
auto samples{editor_track::sample_route(route, 1.0)};
std::vector<double> chainages, heights, ground;
for (auto const &sample : samples)
{
chainages.push_back(sample.chainage);
heights.push_back(sample.position.y - route.spans[sample.span].track->rail_height());
double height;
ground.push_back(m_ground.terrain_model().height(sample.position.x, sample.position.z, height) ? height + m_ground.terrain_model_offset() + shift : std::numeric_limits<double>::quiet_NaN());
}
auto spans{profile::bridged(chainages, heights, ground, kBridgeDepth, kAbutment)};
found = routes.emplace(Place.track, std::make_pair(std::move(samples), std::move(spans))).first;
}
auto const &[samples, spans]{found->second};
auto nearest{std::numeric_limits<double>::max()};
double at{0.0};
for (auto const &sample : samples)
if (auto const distance{geometry::plan_distance(sample.position, Place.position)}; distance < nearest)
{
nearest = distance;
at = sample.chainage;
}
return nearest <= 1.0 && std::any_of(spans.begin(), spans.end(), [&](profile::departure const &Span) { return at >= Span.from - kModelStep && at <= Span.to + kModelStep; });
};
// the place of each path the farthest from the model
std::map<std::pair<TTrack *, int>, std::size_t> worst;
for (std::size_t i = 0; i < state.places.size(); ++i)
{
if (false == std::isfinite(differences[i]))
continue;
differences[i] -= shift;
if (std::abs(differences[i]) <= state.model || (differences[i] > 0.0 && on_bridge(state.places[i])))
continue;
auto const key{std::make_pair(state.places[i].track, state.places[i].path)};
auto const found{worst.find(key)};
if (found == worst.end())
worst.emplace(key, i);
else if (std::abs(differences[i]) > std::abs(differences[found->second]))
found->second = i;
}
for (auto const &entry : worst)
{
auto const &place{state.places[entry.second]};
joint_issue issue;
issue.type = kind::model;
issue.track = place.track;
issue.point = {place.path, editor_track::point_kind::start};
issue.position = place.position;
issue.value = differences[entry.second];
state.issues.push_back(issue);
}
state.places.clear();
state.runs.clear();
state.model_waiting = false;
return true;
}
void joints_tool::joints_check(TTrack &Track, std::vector<joint_issue> &Issues, std::set<std::pair<end_key, end_key>> &Seen, bool const Open) const
{
auto const &state{m_joints};
auto const once = [&](end_key const &A, end_key const &B) { return Seen.insert(A < B ? std::make_pair(A, B) : std::make_pair(B, A)).second; };
for (int i = 0; i < static_cast<int>(Track.m_paths.size()); ++i)
{
for (auto const kind_of_end : {editor_track::point_kind::start, editor_track::point_kind::end})
{
editor_track::point_ref const point{i, kind_of_end};
auto const position{editor_track::point_position(Track, point)};
auto const connections{editor_track::connected_points(Track, position)};
if (connections.empty())
{
joint_issue nearest;
nearest.value = std::numeric_limits<double>::max();
for (auto *section : simulation::Region->sections(position, static_cast<float>(state.gap)))
for (auto const &cell : section->m_cells)
for (auto *other : cell.m_directories.paths)
{
if (other == nullptr || other == &Track || false == rail(*other))
continue;
for (int k = 0; k < static_cast<int>(other->m_paths.size()); ++k)
for (auto const theirkind : {editor_track::point_kind::start, editor_track::point_kind::end})
{
editor_track::point_ref const theirs{k, theirkind};
auto const distance{glm::distance(position, editor_track::point_position(*other, theirs))};
if (distance > state.gap || distance >= nearest.value)
continue;
nearest.track = &Track;
nearest.point = point;
nearest.other = other;
nearest.theirs = theirs;
nearest.value = distance;
}
}
if (nearest.other != nullptr)
{
if (once(key_of(&Track, point), key_of(nearest.other, nearest.theirs)))
{
nearest.type = kind::gap;
nearest.position = position;
Issues.push_back(nearest);
}
}
else if (Open)
{
joint_issue open;
open.type = kind::open;
open.track = &Track;
open.point = point;
open.position = position;
Issues.push_back(open);
}
continue;
}
for (auto const &connection : connections)
{
auto *other{connection.first};
auto const &theirs{connection.second};
if (false == rail(*other) || false == once(key_of(&Track, point), key_of(other, theirs)))
continue;
joint_issue issue;
issue.track = &Track;
issue.point = point;
issue.other = other;
issue.theirs = theirs;
issue.position = position;
auto const step{glm::distance(position, editor_track::point_position(*other, theirs))};
auto const ours{into(Track, point)};
auto const their{into(*other, theirs)};
auto const angle{plan_angle(ours, -their)};
auto const cant{kGauge * 1000.0 * std::abs(std::sin(glm::radians(roll_into(Track, point) + roll_into(*other, theirs))))};
auto const grade{std::abs(grade_of(ours) + grade_of(their)) * 1000.0};
auto const add = [&](kind const Kind, double const Value) {
issue.type = Kind;
issue.value = Value;
Issues.push_back(issue);
};
if (step > state.step)
add(kind::step, step);
if (angle > state.angle)
add(kind::kink, angle);
if (cant > state.cant)
add(kind::cant, cant);
if (grade > state.grade)
add(kind::grade, grade);
}
}
}
}
void joints_tool::tracks_committed(std::vector<TTrack *> const &Tracks)
{
for (auto *track : Tracks)
if (track != nullptr)
m_live.pending.insert(track);
}
void joints_tool::live_update()
{
auto &live{m_live};
auto const gone = [](TTrack const *Track) { return Track != nullptr && Track->m_editorremoved; };
std::erase_if(live.issues, [&](joint_issue const &Issue) { return gone(Issue.track) || gone(Issue.other); });
if (live.pending.empty() || ImGui::IsMouseDown(ImGuiMouseButton_Left) || ImGui::IsMouseDown(ImGuiMouseButton_Right))
return;
// the joints of a track changed are looked at again, from its side and from the side of the tracks it joins
std::set<TTrack *> checked;
for (auto *track : live.pending)
{
if (track->m_editorremoved || false == rail(*track))
continue;
checked.insert(track);
for (auto const &path : track->m_paths)
for (auto const index : {segment_data::point::start, segment_data::point::end})
for (auto const &connection : editor_track::connected_points(*track, path.points[index]))
if (rail(*connection.first))
checked.insert(connection.first);
}
live.pending.clear();
std::erase_if(live.issues, [&](joint_issue const &Issue) { return checked.contains(Issue.track) || checked.contains(Issue.other); });
std::set<std::pair<end_key, end_key>> seen;
for (auto *track : checked)
joints_check(*track, live.issues, seen, false);
}
void joints_tool::update()
{
auto &state{m_joints};
live_update();
if (state.next >= state.queue.size() && false == state.model_waiting)
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 *track{state.queue[state.next++]};
if (track->m_editorremoved)
continue;
joints_check(*track, state.issues, m_reported, state.free_ends);
}
if (state.next < state.queue.size())
return;
if (state.model_waiting && false == joints_model())
return;
std::stable_sort(state.issues.begin(), state.issues.end(), [](joint_issue const &A, joint_issue const &B) {
if (A.type != B.type)
return A.type < B.type;
return std::abs(A.value) > std::abs(B.value);
});
std::array<int, 7> counts{};
for (auto const &issue : state.issues)
++counts[static_cast<int>(issue.type)];
state.status = format(STR_C("%zu paths checked: %d gaps, %d steps, %d kinks, %d cant jumps, %d grade breaks"), state.queue.size(), counts[0], counts[1], counts[2], counts[3], counts[4]);
if (state.model_check && state.model_unplaced)
state.status += STR_C(", the terrain model left out: the scenery isn't placed on the map (the orthophoto settings)");
else if (state.model_check)
state.status += format(STR_C(", %d paths off the terrain model"), counts[5]);
if (state.free_ends)
state.status += format(STR_C(", %d free ends"), counts[6]);
WriteLog("Editor: joints check - " + state.status, logtype::generic);
}
void joints_tool::joints_focus(int const Index)
{
auto &state{m_joints};
if (Index < 0 || Index >= static_cast<int>(state.issues.size()))
return;
state.selected = Index;
auto const &issue{state.issues[Index]};
auto const &paths{simulation::Paths.sequence()};
if (std::find(paths.begin(), paths.end(), issue.track) == paths.end() || issue.track->m_editorremoved)
return;
m_selection.set_current_node(issue.track);
m_selection.clear_track_point();
m_view.focus_track(*issue.track, issue.point.path, issue.position);
}
bool joints_tool::joint_fix(joint_issue &Issue, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Changed)
{
if (Issue.fixed || Issue.type == kind::open || Issue.type == kind::model || Issue.track == nullptr || Issue.other == nullptr || Issue.track->m_editorremoved || Issue.other->m_editorremoved)
return false;
auto const movable = [](TTrack const *Track) {
std::string reason;
return Track->eType == tt_Normal && editor_track::can_edit_geometry(*Track, reason) && scene::Layers.editable(Track);
};
TTrack *mover{nullptr};
editor_track::point_ref moved;
TTrack *fixed{nullptr};
editor_track::point_ref held;
if (movable(Issue.track))
{
mover = Issue.track;
moved = Issue.point;
fixed = Issue.other;
held = Issue.theirs;
}
else if (movable(Issue.other) && (Issue.type != kind::gap || false == editor_track::is_connected(*Issue.other, Issue.theirs)))
{
mover = Issue.other;
moved = Issue.theirs;
fixed = Issue.track;
held = Issue.point;
}
if (mover == nullptr)
return false;
if (std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == mover; }))
States.emplace_back(mover, editor_track::capture(*mover));
if (std::find(Changed.begin(), Changed.end(), mover) == Changed.end())
Changed.push_back(mover);
editor_track::snap_target target;
target.track = fixed;
target.point = held;
target.position = editor_track::point_position(*fixed, held);
target.direction = into(*fixed, held);
switch (Issue.type)
{
case kind::gap:
case kind::kink:
case kind::grade:
editor_track::snap_point(*mover, moved, target, true);
break;
case kind::step:
editor_track::snap_point(*mover, moved, target, false);
break;
case kind::cant:
{
auto &path{mover->m_paths[moved.path]};
auto const roll{static_cast<float>(roll_into(*fixed, held))};
if (at_start(moved))
path.rolls[0] = -roll;
else
path.rolls[1] = roll;
break;
}
default:
break;
}
Issue.fixed = true;
return true;
}
void joints_tool::joints_fix(std::vector<int> const &Indices)
{
auto &state{m_joints};
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> changed;
std::vector<joint_issue *> chosen;
for (auto const index : Indices)
if (index >= 0 && index < static_cast<int>(state.issues.size()))
chosen.push_back(&state.issues[index]);
std::size_t count{0};
for (auto *issue : chosen)
if (joint_fix(*issue, states, changed))
++count;
if (count == 0)
{
state.status = STR_C("Nothing could be fixed: the paths are switches or in the files which can't be changed");
return;
}
m_history.push_track_snapshot(std::move(states));
editor_track::commit(changed);
for (auto *track : changed)
track->mark_dirty();
state.history = m_history.history_size();
state.status = format(STR_C("%zu joints fixed, Ctrl+Z takes them back"), count);
WriteLog("Editor: joints check - " + state.status, logtype::generic);
}
void joints_tool::render_options()
{
auto &state{m_joints};
ImGui::PushTextWrapPos(0.0f);
ImGui::TextDisabled("%s", STR_C("Looks for faults at the joints of the tracks"));
ImGui::PopTextWrapPos();
segmented("jointscope", state.scope, {{0, STR_C("Around the camera")}, {1, STR_C("Whole scenery")}});
bool const running{state.next < state.queue.size() || state.model_waiting};
if (running)
{
if (state.next < state.queue.size())
{
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());
}
else
{
auto const progress{static_cast<float>(state.run_next) / static_cast<float>(std::max<std::size_t>(1, state.runs.size()))};
ImGui::ProgressBar(progress, ImVec2(-70.0f, 0.0f), format(STR_C("Terrain model %zu / %zu"), state.run_next, state.runs.size()).c_str());
}
ImGui::SameLine();
if (ImGui::SmallButton(STR_C("Cancel")))
{
state.queue.resize(state.next);
state.model_waiting = false;
}
}
else
{
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(family_colour(2, 0.55f)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(family_colour(2, 0.75f)));
if (ImGui::Button(STR_C("Check"), ImVec2(-1.0f, tall_button_height())))
joints_start();
ImGui::PopStyleColor(2);
item_tooltip("Finds the gaps between the ends of the tracks, the steps, the kinks and the jumps of the cant and of the grade");
}
if (ImGui::TreeNode(STR_C("Thresholds")))
{
if (begin_fields("##jointlimits", 0.55f))
{
auto const limit = [](char const *Label, char const *Id, double &Value, char const *Format) {
field_row(Label);
ImGui::InputDouble(Id, &Value, 0.0, 0.0, Format);
};
if (state.scope == 0)
limit("Around the camera (m)", "##jointreach", state.reach, "%.0f");
limit("Gap up to (m)", "##jointgap", state.gap, "%.2f");
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("A free end closer than this to the end of another path is a gap: the trains don't go across it.\nThe simulation joins the ends no more than 2 cm apart"));
limit("Step (m)", "##jointstep", state.step, "%.3f");
limit("Kink (deg)", "##jointkink", state.angle, "%.2f");
limit("Cant jump (mm)", "##jointcant", state.cant, "%.1f");
limit("Grade break (per mille)", "##jointgrade", state.grade, "%.1f");
limit("Off the terrain model (m)", "##jointmodel", state.model, "%.2f");
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("The track lies higher or lower than the terrain model (NMT) says the ground is, by more than this.\nEach path is listed once, at its place the farthest off; bridges and tunnels are left out"));
ImGui::EndTable();
}
toggle_chips("jointflags", {{STR_C("Free ends too"), &state.free_ends}, {STR_C("Against the terrain model"), &state.model_check}});
state.reach = std::clamp(state.reach, 50.0, 50000.0);
state.gap = std::clamp(state.gap, 0.03, 20.0);
state.step = std::max(0.001, state.step);
state.angle = std::max(0.01, state.angle);
state.cant = std::max(0.5, state.cant);
state.grade = std::max(0.5, state.grade);
state.model = std::max(0.05, state.model);
ImGui::TextDisabled("%s", STR_C("Change the thresholds, then Check again"));
ImGui::TreePop();
}
if (false == state.status.empty())
ImGui::TextWrapped("%s", state.status.c_str());
if (state.issues.empty())
return;
if (state.history != m_history.history_size())
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", STR_C("The scenery changed since the check"));
int filter{state.filter + 1};
ImGui::SetNextItemWidth(150.0f);
auto const filters{std::string{STR_C("All")} + '\0' + STR_C("gap") + '\0' + STR_C("step") + '\0' + STR_C("kink") + '\0' + STR_C("cant jump") + '\0' + STR_C("grade break") + '\0' + STR_C("off the terrain model") + '\0' +
STR_C("free end") + '\0' + '\0'};
if (ImGui::Combo("##jointfilter", &filter, filters.c_str()))
state.filter = filter - 1;
std::vector<int> listed;
std::vector<int> fixable;
for (int i = 0; i < static_cast<int>(state.issues.size()); ++i)
{
auto const &issue{state.issues[i]};
if (state.filter >= 0 && static_cast<int>(issue.type) != state.filter)
continue;
listed.push_back(i);
if (false == issue.fixed && issue.type != kind::open && issue.type != kind::model)
fixable.push_back(i);
}
if (false == fixable.empty())
{
ImGui::SameLine();
if (ImGui::Button(format(STR_C("Fix %zu listed"), fixable.size()).c_str()))
joints_fix(fixable);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Gaps: the free end goes to the other end, along it. Steps: the end goes to the other one.\n"
"Kinks and grade breaks: the end is turned in line with the other path. Cant: the cant of the other path.\n"
"The plain path moves, a switch stays as it is. One Ctrl+Z takes it all back"));
}
auto const namewidth{std::max(110.0f, ImGui::GetContentRegionAvail().x * 0.32f)};
bool const chosen{state.selected >= 0 && state.selected < static_cast<int>(state.issues.size())};
auto const details{chosen ? ImGui::GetFrameHeightWithSpacing() + ImGui::GetTextLineHeightWithSpacing() * 2.0f : 0.0f};
ImGui::BeginChild("jointresults", ImVec2(0.0f, -details), true);
ImGui::Columns(4, "jointrows", false);
ImGui::SetColumnWidth(0, 90.0f);
ImGui::SetColumnWidth(1, 80.0f);
ImGui::SetColumnWidth(2, 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 &issue{state.issues[index]};
auto const colour{ImGui::ColorConvertU32ToFloat4(kind_colour(issue.type))};
ImGui::PushStyleColor(ImGuiCol_Text, issue.fixed ? ImVec4(0.5f, 0.8f, 0.5f, 1.0f) : colour);
if (ImGui::Selectable(format("%s##joint%d", STR_C(kind_name(issue.type)), index).c_str(), state.selected == index, ImGuiSelectableFlags_SpanAllColumns))
joints_focus(index);
ImGui::PopStyleColor();
ImGui::NextColumn();
ImGui::TextUnformatted(issue.fixed ? STR_C("fixed") : value_text(issue).c_str());
ImGui::NextColumn();
ImGui::TextUnformatted(label_of(issue.track).c_str());
ImGui::NextColumn();
ImGui::TextUnformatted(label_of(issue.other).c_str());
ImGui::NextColumn();
}
ImGui::Columns(1);
ImGui::EndChild();
if (chosen)
{
auto &issue{state.issues[state.selected]};
ImGui::Text("%s %s: %s / %s", STR_C(kind_name(issue.type)), value_text(issue).c_str(), label_of(issue.track).c_str(), label_of(issue.other).c_str());
if (false == issue.fixed && issue.type != kind::open && issue.type != kind::model && ImGui::Button(STR_C("Fix this one")))
joints_fix({state.selected});
}
}
void joints_tool::draw_overlay() const
{
auto const &state{m_joints};
screen_projection const projection;
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
glm::dvec3 const camera{Global.pCamera.Pos};
if (false == state.open)
{
for (auto const &issue : m_live.issues)
{
ImVec2 screen;
if ((false == Global.EditorOrtho && glm::distance(issue.position, camera) > kOverlayRange) || false == projection.project(issue.position, screen))
continue;
auto const colour{kind_colour(issue.type)};
drawlist->AddCircle(screen, 9.0f, colour, 16, 2.0f);
auto const text{std::string{STR_C(kind_name(issue.type))} + " " + value_text(issue)};
drawlist->AddRectFilled(ImVec2(screen.x + 11.0f, screen.y - 9.0f), ImVec2(screen.x + 17.0f + ImGui::CalcTextSize(text.c_str()).x, screen.y + 9.0f), IM_COL32(0, 0, 0, 150), 3.0f);
drawlist->AddText(ImVec2(screen.x + 14.0f, screen.y - 7.0f), colour, text.c_str());
}
return;
}
if (state.issues.empty())
return;
for (int i = 0; i < static_cast<int>(state.issues.size()); ++i)
{
auto const &issue{state.issues[i]};
if (issue.fixed || (state.filter >= 0 && static_cast<int>(issue.type) != state.filter) || (false == Global.EditorOrtho && glm::distance(issue.position, camera) > kOverlayRange))
continue;
ImVec2 screen;
if (false == projection.project(issue.position, screen))
continue;
auto const colour{i == state.selected ? IM_COL32(255, 255, 80, 255) : kind_colour(issue.type)};
drawlist->AddCircle(screen, 11.0f, colour, 16, 2.5f);
auto const text{std::string{STR_C(kind_name(issue.type))} + " " + value_text(issue)};
drawlist->AddText(ImVec2(screen.x + 13.0f, screen.y - 7.0f), colour, text.c_str());
}
}
editor_selftest_result joints_tool::selftest(std::string const &Command, std::istringstream &Words)
{
if (Command == "jointsmodel")
{
// the whole scenery against the terrain model, as the check of the panel does it, waiting for the model
static bool started{false};
if (false == started)
{
double threshold{0.5};
Words >> threshold;
m_joints.scope = 1;
m_joints.model_check = true;
m_joints.model = threshold;
joints_start();
started = true;
}
update();
if (m_joints.next < m_joints.queue.size() || m_joints.model_waiting)
return editor_selftest_result::again;
started = false;
std::size_t count{0};
for (auto const &issue : m_joints.issues)
if (issue.type == joint_issue::kind::model && count++ < 40)
WriteLog(format("SELFTEST jointsmodel: %+.2f m at %.1f %.1f, %s path %d", issue.value, issue.position.x, issue.position.z, issue.track != nullptr ? issue.track->name().c_str() : "-", issue.point.path));
WriteLog("SELFTEST jointsmodel: " + m_joints.status);
return editor_selftest_result::handled;
}
if (Command == "jointslive")
{
live_update();
std::string text;
for (auto const &issue : m_live.issues)
text += format(" [%s %s at %.1f %.1f]", kind_name(issue.type), value_text(issue).c_str(), issue.position.x, issue.position.z);
WriteLog(format("SELFTEST jointslive: %zu pending, %zu marked", m_live.pending.size(), m_live.issues.size()) + text);
return editor_selftest_result::handled;
}
if (Command == "joints")
{
std::vector<glm::dvec3> points;
for (glm::dvec3 point{0.0}; Words >> point.x >> point.z;)
points.push_back(point);
auto const scope{m_joints.scope};
auto const model{m_joints.model_check};
m_joints.scope = 1;
m_joints.model_check = false;
joints_start();
while (m_joints.next < m_joints.queue.size())
update();
m_joints.scope = scope;
m_joints.model_check = model;
char const *const kinds[]{"gap", "step", "kink", "cant jump", "grade break", "off the terrain model", "free end"};
int around{0};
for (auto const &issue : m_joints.issues)
{
if (false == points.empty() && issue.type == joint_issue::kind::open)
continue;
if (false == points.empty() && std::none_of(points.begin(), points.end(), [&](glm::dvec3 const &Point) { return geometry::plan_distance(Point, issue.position) < 5.0; }))
continue;
++around;
auto const name = [](TTrack const *Track) { return Track == nullptr ? std::string{"-"} : Track->name() + (Track->eType == tt_Switch ? "(switch)" : ""); };
auto const rise{issue.other != nullptr ? editor_track::point_position(*issue.other, issue.theirs).y - issue.position.y : 0.0};
WriteLog(format("SELFTEST joints: %s %.3f at %.1f %.1f, %s / %s, rise %.3f", kinds[static_cast<int>(issue.type)], issue.value, issue.position.x, issue.position.z, name(issue.track).c_str(), name(issue.other).c_str(), rise));
}
WriteLog(format("SELFTEST joints: %d issues around the points, %zu in all", around, m_joints.issues.size()));
return editor_selftest_result::handled;
}
return editor_selftest_result::unhandled;
}