17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 00:29:19 +02:00
Files
maszyna/application/editormodelineside.cpp

984 lines
36 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 "editor/editorIncludeInfo.hpp"
#include "scene/scene.h"
#include "scene/scenelayers.h"
#include "simulation/simulation.h"
#include "utilities/Logs.h"
#include "utilities/utilities.h"
#include "world/Track.h"
#include "imgui/imgui.h"
#include "utilities/translation.h"
#include <algorithm>
#include <cctype>
#include <cmath>
namespace
{
using geometry::bezier;
using geometry::plan_distance;
using geometry::plan_of;
double const kSampleStep{2.0}; // m between the samples of the line
double const kLineCorridor{15.0}; // m from the axis, posts farther away belong to another line
double const kOverlayRange{3000.0};
double const kFoulingTrace{250.0}; // m from the start of a switch the tracks are followed for
double const kRailHeight{0.18}; // m from the points of a track to the heads of its rails
int const kRailCategory{1};
char const *const kHektoStyles[] = {"nowy", "stary", "zgnity"};
char const *const kFoulingModels[] = {"ip/pkp/w17.inc", "ip/pkp/w17_new.inc", "ip/pkp/w17_old.inc", "ip/pkp/w17_dwuteo.inc"};
std::string normalized(std::string Text)
{
std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast<char>(Character == '\\' ? '/' : std::tolower(Character)); });
return Text;
}
bool numeric(std::string const &Text, double &Value)
{
char *end{nullptr};
Value = std::strtod(Text.c_str(), &end);
return false == Text.empty() && *end == '\0';
}
std::string number(double const Value, int const Decimals)
{
auto text{format("%.*f", Decimals, Value)};
if (text == "-0" || text.find_first_not_of("-0.") == std::string::npos)
text.erase(0, text.front() == '-' ? 1 : 0);
return text;
}
double heading_of(glm::dvec2 const &Direction)
{
auto const degrees{glm::degrees(std::atan2(Direction.x, Direction.y))};
return degrees < 0.0 ? degrees + 360.0 : degrees;
}
// index of the sample at or before the chainage
std::size_t sample_index(std::vector<editor_track::route_sample> const &Samples, double const Chainage)
{
auto const found{std::upper_bound(Samples.begin(), Samples.end(), Chainage, [](double const Value, editor_track::route_sample const &Sample) { return Value < Sample.chainage; })};
return found == Samples.begin() ? 0 : std::min<std::size_t>(std::distance(Samples.begin(), found) - 1, Samples.size() - 2);
}
glm::dvec2 sampled_direction(std::vector<editor_track::route_sample> const &Samples, double const Chainage)
{
auto const index{sample_index(Samples, Chainage)};
auto const &a{Samples[index]};
auto const &b{Samples[index + 1]};
auto const span{b.chainage - a.chainage};
auto const f{span > 1e-9 ? std::clamp((Chainage - a.chainage) / span, 0.0, 1.0) : 0.0};
auto const direction{a.direction * (1.0 - f) + b.direction * f};
return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0};
}
// chainage of the point of the line nearest to the point, and how far from the line it is
double project(std::vector<editor_track::route_sample> const &Samples, glm::dvec3 const &Point, double &Distance)
{
Distance = std::numeric_limits<double>::max();
double chainage{0.0};
auto const point{plan_of(Point)};
for (std::size_t i = 1; i < Samples.size(); ++i)
{
auto const a{plan_of(Samples[i - 1].position)};
auto const b{plan_of(Samples[i].position)};
auto const ab{b - a};
auto const length2{glm::dot(ab, ab)};
auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0) : 0.0};
auto const distance{glm::length(point - (a + ab * t))};
if (distance < Distance)
{
Distance = distance;
chainage = Samples[i - 1].chainage + (Samples[i].chainage - Samples[i - 1].chainage) * t;
}
}
return chainage;
}
// the digits after the rotation make the number of the post, the last one the hectometre
bool hectometres_of(std::vector<std::string> const &Values, int &Hectometres)
{
if (Values.size() < 6)
return false;
Hectometres = 0;
for (std::size_t i = 5; i < Values.size(); ++i)
{
double digit;
if (false == numeric(Values[i], digit) || digit < 0.0 || digit > 9.0)
return false;
Hectometres = Hectometres * 10 + static_cast<int>(digit);
}
return true;
}
std::string hekto_file(int const Hectometres, int const Style)
{
auto style{Style};
if (style > 2)
{
auto const pick{LocalRandom(0.0, 1.0)};
style = pick < 0.6 ? 0 : pick < 0.9 ? 1 : 2;
}
auto const size{Hectometres < 100 ? "1" : Hectometres < 1000 ? "10" : "100"};
return std::string{"przytorowe/slupek_hekto_"} + kHektoStyles[style] + "_" + size + ".inc";
}
std::vector<std::string> hekto_digits(int const Hectometres)
{
return {std::to_string((Hectometres / 1000) % 10), std::to_string((Hectometres / 100) % 10), std::to_string((Hectometres / 10) % 10), std::to_string(Hectometres % 10)};
}
std::string km_text(int const Hectometres)
{
return format("%d.%d", Hectometres / 10, Hectometres % 10);
}
// points along the track from the end of a path on, the way a train goes: through a switch along its main track
std::vector<glm::dvec3> trace(TTrack &Track, int const Path, bool const Forward, double const Limit)
{
std::vector<glm::dvec3> points;
auto *track{&Track};
auto path{Path};
auto forward{Forward};
double length{0.0};
for (int guard = 0; guard < 64 && track != nullptr && length < Limit; ++guard)
{
bezier const curve{track->m_paths[path]};
auto const count{std::clamp(static_cast<int>(curve.plan_length() / 1.0), 4, 400)};
for (int k = points.empty() ? 0 : 1; k <= count; ++k)
{
auto const point{curve.point(forward ? static_cast<double>(k) / count : 1.0 - static_cast<double>(k) / count)};
if (false == points.empty())
length += plan_distance(points.back(), point);
points.push_back(point);
}
auto const exit{points.back()};
TTrack *next{nullptr};
int nextpath{0};
bool nextforward{true};
for (auto const &connection : editor_track::connected_points(*track, exit))
{
auto *other{connection.first};
if (other == track || (other->iCategoryFlag & 15) != kRailCategory)
continue;
bool const start{connection.second.kind == editor_track::point_kind::start};
// entering a switch at its start the train takes the main track
if (next == nullptr || (other == next && start && connection.second.path == 0))
{
next = other;
nextpath = connection.second.path;
nextforward = start;
}
}
track = next;
path = nextpath;
forward = nextforward;
}
return points;
}
// nearest point of the polyline
glm::dvec3 nearest_on(std::vector<glm::dvec3> const &Line, glm::dvec3 const &Point, double &Distance)
{
Distance = std::numeric_limits<double>::max();
glm::dvec3 result{Point};
auto const point{plan_of(Point)};
for (std::size_t i = 1; i < Line.size(); ++i)
{
auto const a{plan_of(Line[i - 1])};
auto const ab{plan_of(Line[i]) - a};
auto const length2{glm::dot(ab, ab)};
auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0) : 0.0};
auto const distance{glm::length(point - (a + ab * t))};
if (distance < Distance)
{
Distance = distance;
result = glm::mix(Line[i - 1], Line[i], t);
}
}
return result;
}
void draw_label(ImDrawList *Draw, ImVec2 const At, ImU32 const Colour, std::string const &Text)
{
auto const size{ImGui::CalcTextSize(Text.c_str())};
Draw->AddRectFilled(ImVec2(At.x + 8.0f, At.y - 8.0f), ImVec2(At.x + 14.0f + size.x, At.y - 6.0f + size.y), IM_COL32(0, 0, 0, 150), 3.0f);
Draw->AddText(ImVec2(At.x + 11.0f, At.y - 7.0f), Colour, Text.c_str());
}
} // namespace
std::vector<editor_mode::standing_template> editor_mode::standing_templates(std::function<bool(std::string const &)> const &Accept) const
{
std::vector<standing_template> result;
for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle)
{
if (false == scene::Layers.tracked(handle))
continue;
auto const &included{scene::Layers.instance(handle)};
if (included.removed || included.dead || included.file == nullptr)
continue;
auto const file{normalized(*included.file)};
if (false == Accept(file) || included.context.rotation != glm::vec3{0.f} || included.context.scale != glm::vec3{1.f})
continue;
std::string target;
std::vector<std::string> values;
if (false == editor_includes::parse_directive(scene::Layers.directive(handle), target, values) || values.size() < 5)
continue;
glm::dvec3 location;
double yaw;
if (false == numeric(values[1], location.x) || false == numeric(values[2], location.y) || false == numeric(values[3], location.z) || false == numeric(values[4], yaw))
continue;
standing_template standing;
standing.instance = handle;
standing.file = file;
standing.location = included.context.offset + location;
standing.yaw = yaw;
standing.values = std::move(values);
result.push_back(std::move(standing));
}
return result;
}
std::size_t editor_mode::place_templates(std::vector<template_item> const &Items, std::string &Error)
{
if (Items.empty())
return 0;
if (scene::Layers.empty())
{
Error = STR_C("Templates can be placed only in a scenery opened for editing (-edit)");
return 0;
}
auto const layer{scene::Layers.resolve(scene::Layers.active())};
if (std::string reason; false == scene::Layers.accepts(layer, &reason))
{
Error = STR_C("The active layer can't take them: ") + (reason.empty() ? std::string{STR_C("there's no active layer")} : reason);
return 0;
}
auto const context{scene::Layers.layer(layer).context_insert()};
if (context.rotation != glm::vec3{0.f} || context.scale != glm::vec3{1.f})
{
Error = STR_C("The file of the active layer would receive them with a rotation or a scale in effect, pick another layer");
return 0;
}
EditorSnapshot snap;
for (auto const &item : Items)
{
auto const local{context.to_local(item.location)};
std::string directive;
if (item.described)
{
include_info info;
if (false == editor_includes::load(item.file, info, Error))
continue;
directive = editor_includes::directive(item.file, info, editor_includes::parameter_count(item.file), local, static_cast<float>(item.yaw), item.track, item.tilt, item.values);
}
else
{
std::vector<std::string> values{"none", number(local.x, 3), number(local.y, 3), number(local.z, 3), number(item.yaw, 2)};
values.insert(values.end(), item.rest.begin(), item.rest.end());
directive = editor_includes::compose_directive(item.file, values);
}
auto const instance{scene::Layers.place(layer, item.file, directive)};
if (instance == 0)
continue;
simulation::State.preview_include(directive, context, layer, instance);
snap.instances.push_back(instance);
}
if (snap.instances.empty())
{
Error = STR_C("The active layer can't take them");
return 0;
}
auto const count{snap.instances.size()};
snap.node_name = Items.front().file;
trim_history();
m_history.push_back(std::move(snap));
g_redo.clear();
return count;
}
std::size_t editor_mode::move_templates(std::vector<std::pair<standing_template, template_item>> const &Moves)
{
EditorSnapshot snap;
for (auto const &[standing, item] : Moves)
{
if (false == scene::Layers.tracked(standing.instance) || false == scene::Layers.removable(standing.instance))
continue;
auto const &included{scene::Layers.instance(standing.instance)};
auto values{standing.values};
auto const local{item.location - included.context.offset};
values[1] = number(local.x, 3);
values[2] = number(local.y, 3);
values[3] = number(local.z, 3);
values[4] = number(item.yaw, 2);
auto before{scene::Layers.directive(standing.instance)};
set_include_directive(standing.instance, editor_includes::compose_directive(*included.file, values));
snap.directives.emplace_back(standing.instance, std::move(before));
}
if (snap.directives.empty())
return 0;
auto const count{snap.directives.size()};
snap.node_name = Moves.front().first.file;
trim_history();
m_history.push_back(std::move(snap));
g_redo.clear();
return count;
}
std::size_t editor_mode::remove_templates(std::vector<scene::instance_handle> const &Instances)
{
EditorSnapshot snap;
for (auto const instance : Instances)
{
if (false == scene::Layers.tracked(instance) || false == scene::Layers.removable(instance))
continue;
scene::Layers.removed(instance, true);
snap.instances.push_back(instance);
}
if (snap.instances.empty())
return 0;
auto const count{snap.instances.size()};
trim_history();
m_history.push_back(std::move(snap));
g_redo.clear();
select_include(0);
return count;
}
void editor_mode::hekto_start(TTrack &Track, glm::dvec3 const &Point)
{
auto &tool{m_hekto};
tool.track = &Track;
tool.point = Point;
tool.km_read = false;
tool.dirty = true;
tool.expand = true;
tool.status.clear();
}
void editor_mode::hekto_read_kilometrage()
{
auto &tool{m_hekto};
auto const &found{tool.found};
if (found.empty())
return;
if (found.size() == 1)
{
// one post doesn't tell the way the kilometrage grows: it's taken to grow along the longer part of the line past the post
auto const length{tool.samples.empty() ? 0.0 : tool.samples.back().chainage};
tool.growth = found.front().chainage > 0.5 * length ? -1 : 1;
tool.km = found.front().hectometres / 10.0 + tool.growth * (tool.origin - found.front().chainage) / 1000.0;
tool.status = format(STR_C("Kilometrage read from the post %s, check which way it grows"), km_text(found.front().hectometres).c_str());
return;
}
// a straight line through the numbers of the posts against where they stand
double sc{0.0}, sk{0.0}, scc{0.0}, sck{0.0};
for (auto const &post : found)
{
auto const km{post.hectometres / 10.0};
sc += post.chainage;
sk += km;
scc += post.chainage * post.chainage;
sck += post.chainage * km;
}
auto const n{static_cast<double>(found.size())};
auto const denominator{n * scc - sc * sc};
if (std::abs(denominator) < 1e-9)
return;
auto const slope{(n * sck - sc * sk) / denominator};
auto const intercept{(sk - slope * sc) / n};
if (std::abs(std::abs(slope) * 1000.0 - 1.0) > 0.2)
{
tool.status = format(STR_C("The numbers of the %zu posts along the line don't go by 100 m, the kilometrage wasn't read from them"), found.size());
return;
}
tool.growth = slope > 0.0 ? 1 : -1;
tool.km = std::round((intercept + slope * tool.origin) * 1000.0) / 1000.0;
tool.status = format(STR_C("Kilometrage read from %zu posts standing along the line"), found.size());
}
void editor_mode::hekto_update()
{
auto &tool{m_hekto};
if (false == tool.dirty && tool.history == m_history.size())
return;
tool.dirty = false;
tool.history = m_history.size();
tool.posts.clear();
tool.found.clear();
tool.error.clear();
tool.samples.clear();
if (tool.track == nullptr || tool.track->m_editorremoved)
{
tool.track = nullptr;
return;
}
tool.route = editor_track::run_route(*tool.track, tool.reach);
if (tool.route.spans.empty())
{
tool.error = STR_C("No line through this path");
return;
}
tool.samples = editor_track::sample_route(tool.route, kSampleStep);
if (tool.samples.size() < 2)
{
tool.error = STR_C("No line through this path");
return;
}
double distance;
tool.origin = project(tool.samples, tool.point, distance);
auto const length{tool.samples.back().chainage};
for (auto const &standing : standing_templates([](std::string const &File) { return File.find("hekto") != std::string::npos; }))
{
hekto_found post;
post.post = standing;
if (false == hectometres_of(standing.values, post.hectometres))
continue;
post.chainage = project(tool.samples, standing.location, distance);
if (distance > kLineCorridor || post.chainage <= 0.0 || post.chainage >= length)
continue;
tool.found.push_back(post);
}
if (false == tool.km_read)
{
tool.km_read = true;
hekto_read_kilometrage();
}
auto const chainage_of = [&](int const Hectometres) { return tool.origin + tool.growth * (Hectometres / 10.0 - tool.km) * 1000.0; };
for (auto &post : tool.found)
{
post.expected = chainage_of(post.hectometres);
post.error = post.chainage - post.expected;
post.outside = true;
}
auto const km_at = [&](double const Chainage) { return tool.km + tool.growth * (Chainage - tool.origin) / 1000.0; };
auto const low{std::max(0, static_cast<int>(std::ceil(std::min(km_at(0.0), km_at(length)) * 10.0 - 1e-6)))};
auto const high{static_cast<int>(std::floor(std::max(km_at(0.0), km_at(length)) * 10.0 + 1e-6))};
for (int hectometres = low; hectometres <= high && tool.posts.size() < 5000; ++hectometres)
{
hekto_post post;
post.hectometres = hectometres;
post.chainage = chainage_of(hectometres);
if (post.chainage < 0.0 || post.chainage > length)
continue;
auto const axis{editor_track::sampled_position(tool.samples, post.chainage)};
auto const growing{sampled_direction(tool.samples, post.chainage) * static_cast<double>(tool.growth)};
glm::dvec2 const left{-growing.y, growing.x};
// alternately: the even hectometres on the right, the odd ones on the left of the growing kilometrage
auto const side{tool.side != 0 ? tool.side : (hectometres % 2 == 0 ? 1 : -1)};
auto const planar{plan_of(axis) + left * (side * tool.offset)};
post.position = placement_on_ground({planar.x, axis.y, planar.y});
post.yaw = std::fmod(heading_of(growing) + tool.turn, 360.0);
// of the posts with this number the one nearest to the place stands for it, the others are duplicates. it stands wrong
// when it's farther from the place than allowed: along the line, across it, or on the other side
auto best{std::numeric_limits<double>::max()};
for (int i = 0; i < static_cast<int>(tool.found.size()); ++i)
{
auto &found{tool.found[i]};
if (found.hectometres != hectometres)
continue;
found.outside = false;
found.distance = plan_distance(found.post.location, post.position);
if (found.distance < best)
{
best = found.distance;
post.standing = i;
}
}
for (int i = 0; i < static_cast<int>(tool.found.size()); ++i)
{
auto &found{tool.found[i]};
if (found.hectometres != hectometres)
continue;
found.duplicate = i != post.standing;
found.wrong = false == found.duplicate && found.distance > tool.tolerance;
}
tool.posts.push_back(post);
}
}
void editor_mode::hekto_remove_duplicates()
{
auto &tool{m_hekto};
hekto_update();
std::vector<scene::instance_handle> instances;
for (auto const &found : tool.found)
if (found.duplicate)
instances.push_back(found.post.instance);
auto const removed{remove_templates(instances)};
tool.status = format(STR_C("%zu duplicate hectometre posts removed, Ctrl+Z brings them back"), removed);
WriteLog("Editor: " + tool.status);
tool.dirty = true;
}
void editor_mode::hekto_place()
{
auto &tool{m_hekto};
hekto_update();
std::vector<template_item> items;
for (auto const &post : tool.posts)
{
if (post.standing >= 0)
continue;
template_item item;
item.file = hekto_file(post.hectometres, tool.style);
item.location = post.position;
item.yaw = post.yaw;
item.rest = hekto_digits(post.hectometres);
items.push_back(std::move(item));
}
std::string error;
auto const placed{place_templates(items, error)};
tool.status = placed > 0 ? format(STR_C("%zu hectometre posts placed, Ctrl+Z takes them back"), placed) : error;
WriteLog("Editor: " + tool.status, logtype::generic);
tool.dirty = true;
}
void editor_mode::hekto_fix()
{
auto &tool{m_hekto};
hekto_update();
std::vector<std::pair<standing_template, template_item>> moves;
for (auto const &post : tool.posts)
{
if (post.standing < 0 || false == tool.found[post.standing].wrong)
continue;
template_item item;
item.location = post.position;
item.yaw = post.yaw;
moves.emplace_back(tool.found[post.standing].post, item);
}
auto const moved{move_templates(moves)};
tool.status = format(STR_C("%zu hectometre posts moved to their places, Ctrl+Z takes it back"), moved);
WriteLog("Editor: " + tool.status, logtype::generic);
tool.dirty = true;
}
void editor_mode::render_hekto_ui()
{
auto &tool{m_hekto};
auto *selected{selected_track()};
if (tool.track == nullptr || (selected != nullptr && selected != tool.track && ImGui::SmallButton(STR_C("Along the line of the selected path"))))
{
if (selected == nullptr)
{
ImGui::TextDisabled("%s", STR_C("Select a path of the line, or RMB on it: Hectometre posts"));
return;
}
hekto_start(*selected, bezier{selected->m_paths.front()}.point(0.0));
}
auto const changed = [&](bool const Changed) {
if (Changed)
tool.dirty = true;
};
ImGui::PushItemWidth(110.0f);
changed(ImGui::InputDouble(STR_C("Kilometre at the point (km)"), &tool.km, 0.0, 0.0, "%.3f"));
ImGui::SameLine();
if (ImGui::SmallButton(STR_C("Read from the posts")))
{
tool.km_read = false;
tool.dirty = true;
}
ImGui::TextDisabled("%s", STR_C("The kilometrage grows"));
ImGui::SameLine();
changed(ImGui::RadioButton(STR_C("along the line"), &tool.growth, 1));
ImGui::SameLine();
changed(ImGui::RadioButton(STR_C("against it"), &tool.growth, -1));
ImGui::TextDisabled("%s", STR_C("Side, looking where it grows"));
ImGui::SameLine();
changed(ImGui::RadioButton(STR_C("right"), &tool.side, 1));
ImGui::SameLine();
changed(ImGui::RadioButton(STR_C("left"), &tool.side, -1));
ImGui::SameLine();
changed(ImGui::RadioButton(STR_C("alternately"), &tool.side, 0));
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("The even hectometres on the right, the odd ones on the left"));
changed(ImGui::InputDouble(STR_C("From the axis (m)"), &tool.offset, 0.0, 0.0, "%.2f"));
tool.offset = std::clamp(tool.offset, 1.5, 30.0);
char const *styles[] = {"nowy", "stary", "zgnity", "mixed"};
ImGui::Combo(STR_C("Posts"), &tool.style, styles, IM_ARRAYSIZE(styles));
char const *turns[] = {"0", "90", "180", "270"};
int turn{tool.turn / 90};
if (ImGui::Combo(STR_C("Turned by (deg)"), &turn, turns, IM_ARRAYSIZE(turns)))
{
tool.turn = turn * 90;
tool.dirty = true;
}
changed(ImGui::InputDouble(STR_C("Along the line each way (m)"), &tool.reach, 0.0, 0.0, "%.0f"));
tool.reach = std::clamp(tool.reach, 100.0, 100000.0);
changed(ImGui::InputDouble(STR_C("A post standing off by more (m) is wrong"), &tool.tolerance, 0.0, 0.0, "%.1f"));
tool.tolerance = std::clamp(tool.tolerance, 0.5, 50.0);
ImGui::PopItemWidth();
hekto_update();
if (false == tool.error.empty())
{
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str());
return;
}
auto const missing{std::count_if(tool.posts.begin(), tool.posts.end(), [](hekto_post const &Post) { return Post.standing < 0; })};
auto const wrong{std::count_if(tool.found.begin(), tool.found.end(), [](hekto_found const &Post) { return Post.wrong; })};
auto const duplicates{std::count_if(tool.found.begin(), tool.found.end(), [](hekto_found const &Post) { return Post.duplicate; })};
ImGui::Text(STR_C("Line %.0f m, km %s - %s: %d posts to place, %zu standing, %d of them wrong"), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, tool.posts.empty() ? "-" : km_text(std::min(tool.posts.front().hectometres, tool.posts.back().hectometres)).c_str(),
tool.posts.empty() ? "-" : km_text(std::max(tool.posts.front().hectometres, tool.posts.back().hectometres)).c_str(), static_cast<int>(missing), tool.found.size(), static_cast<int>(wrong));
if (missing > 0 && ImGui::Button(format(STR_C("Place %d posts"), static_cast<int>(missing)).c_str()))
hekto_place();
if (duplicates > 0)
{
if (missing > 0)
ImGui::SameLine();
if (ImGui::Button(format(STR_C("Remove %d duplicate posts"), static_cast<int>(duplicates)).c_str()))
hekto_remove_duplicates();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Posts with a number another post nearer to its place already has"));
}
if (wrong > 0 || duplicates > 0)
{
if (wrong > 0)
{
if (missing > 0 || duplicates > 0)
ImGui::SameLine();
if (ImGui::Button(format(STR_C("Move %d wrong posts to their places"), static_cast<int>(wrong)).c_str()))
hekto_fix();
}
ImGui::BeginChild("##hektowrong", ImVec2(0.0f, std::min(6, static_cast<int>(wrong + duplicates)) * ImGui::GetTextLineHeightWithSpacing() + 6.0f), true);
for (auto const &post : tool.found)
{
if (false == post.wrong && false == post.duplicate)
continue;
auto const label{post.duplicate ? format(STR_C("km %s duplicate##%u"), km_text(post.hectometres).c_str(), post.post.instance) : format(STR_C("km %s stands %.1f m from its place##%u"), km_text(post.hectometres).c_str(), post.distance, post.post.instance)};
if (ImGui::Selectable(label.c_str()))
{
glm::dvec3 const look{glm::normalize(glm::dvec3{-1.0, -0.8, -1.0})};
m_focus_start_pos = Camera.Pos;
m_focus_start_angle = Camera.Angle;
m_focus_target_pos = post.post.location - look * 20.0;
m_focus_target_angle = glm::vec3(static_cast<float>(std::asin(look.y)), static_cast<float>(std::atan2(-look.x, -look.z)), 0.0f);
m_focus_active = true;
m_focus_time = 0.0;
m_focus_duration = 0.6;
}
}
ImGui::EndChild();
}
if (false == tool.status.empty())
ImGui::TextWrapped("%s", tool.status.c_str());
}
void editor_mode::draw_hekto_overlay() const
{
auto const &tool{m_hekto};
if (false == tool.open || tool.track == nullptr)
return;
screen_projection const projection;
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
glm::dvec3 const camera{Global.pCamera.Pos};
for (auto const &post : tool.posts)
{
if (glm::distance(post.position, camera) > kOverlayRange)
continue;
ImVec2 screen;
if (false == projection.project(post.position, screen))
continue;
bool const standing{post.standing >= 0 && false == tool.found[post.standing].wrong};
auto const colour{standing ? IM_COL32(180, 180, 180, 200) : IM_COL32(90, 255, 120, 235)};
drawlist->AddCircleFilled(screen, 4.0f, colour);
draw_label(drawlist, screen, colour, km_text(post.hectometres));
}
for (auto const &post : tool.found)
{
if ((false == post.wrong && false == post.duplicate) || glm::distance(post.post.location, camera) > kOverlayRange)
continue;
ImVec2 screen;
if (false == projection.project(post.post.location, screen))
continue;
auto const colour{post.duplicate ? IM_COL32(255, 160, 40, 235) : IM_COL32(255, 70, 60, 235)};
drawlist->AddCircle(screen, 9.0f, colour, 16, 2.5f);
draw_label(drawlist, screen, colour, post.duplicate ? format(STR_C("km %s duplicate"), km_text(post.hectometres).c_str()) : format(STR_C("km %s, %.1f m from its place"), km_text(post.hectometres).c_str(), post.distance));
if (post.duplicate)
continue;
for (auto const &target : tool.posts)
if (target.hectometres == post.hectometres)
projection.line(drawlist, post.post.location, target.position, IM_COL32(255, 70, 60, 160), 1.5f);
}
ImVec2 screen;
if (projection.project(tool.point, screen))
{
drawlist->AddCircle(screen, 7.0f, IM_COL32(90, 200, 255, 235), 16, 2.5f);
draw_label(drawlist, screen, IM_COL32(90, 200, 255, 235), format("km %.3f", tool.km));
}
}
void editor_mode::fouling_update()
{
auto &tool{m_fouling};
auto *selected{selected_track()};
if (tool.scope == 0 && selected != tool.selected)
tool.dirty = true;
if (false == tool.dirty && tool.history == m_history.size())
return;
tool.dirty = false;
tool.history = m_history.size();
tool.selected = selected;
tool.points.clear();
tool.found.clear();
tool.error.clear();
std::vector<TTrack *> switches;
glm::dvec3 const camera{Global.pCamera.Pos};
if (tool.scope == 0)
{
if (selected == nullptr || selected->eType != tt_Switch)
{
tool.error = STR_C("Select a switch, or check around the camera");
return;
}
switches.push_back(selected);
}
else
{
for (auto *track : simulation::Paths.sequence())
if (track != nullptr && track->eType == tt_Switch && false == track->m_editorremoved && (track->iCategoryFlag & 15) == kRailCategory && track->m_paths.size() >= 2 &&
(tool.scope == 2 || glm::distance(track->location(), camera) <= tool.reach))
switches.push_back(track);
}
for (auto *track : switches)
{
if (track->m_paths.size() < 2 || false == editor_track::is_supported(*track))
continue;
auto const main{trace(*track, 0, true, kFoulingTrace)};
auto const branch{trace(*track, 1, true, kFoulingTrace)};
if (main.size() < 2 || branch.size() < 2)
continue;
glm::dvec3 previous{branch.front()};
double previousdistance{0.0};
for (std::size_t i = 1; i < branch.size(); ++i)
{
double distance;
nearest_on(main, branch[i], distance);
if (distance < tool.spacing)
{
previous = branch[i];
previousdistance = distance;
continue;
}
auto const f{distance - previousdistance > 1e-9 ? (tool.spacing - previousdistance) / (distance - previousdistance) : 1.0};
auto const onbranch{glm::mix(previous, branch[i], std::clamp(f, 0.0, 1.0))};
double unused;
auto const onmain{nearest_on(main, onbranch, unused)};
fouling_point point;
point.track = track;
auto const middle{(onbranch + onmain) * 0.5};
// the points of the paths are at the rail foot, the heads of the rails are the height of the rail over them
point.position = tool.height_mode == 1 ? glm::dvec3{middle.x, 0.5 * (onbranch.y + onmain.y) + kRailHeight + tool.height, middle.z} : placement_on_ground({middle.x, std::max(onbranch.y, onmain.y), middle.z});
auto const back{plan_of(previous - branch[i])};
point.yaw = std::fmod(heading_of(glm::length(back) > 1e-9 ? glm::normalize(back) : glm::dvec2{0.0, 1.0}) + tool.turn, 360.0);
tool.points.push_back(point);
break;
}
}
// markers standing: each goes to the nearest fouling point, the others are left alone
auto const markers{standing_templates([](std::string const &File) { return File.find("w17") != std::string::npos || File.find("ukres") != std::string::npos; })};
for (auto const &marker : markers)
{
int nearest{-1};
double best{std::numeric_limits<double>::max()};
for (int i = 0; i < static_cast<int>(tool.points.size()); ++i)
{
auto const distance{plan_distance(marker.location, tool.points[i].position)};
if (distance < best)
{
best = distance;
nearest = i;
}
}
if (nearest < 0 || best > 40.0)
continue;
auto &point{tool.points[nearest]};
if (point.standing >= 0 && tool.found[point.standing].error <= best)
continue;
fouling_found found;
found.marker = marker;
found.point = nearest;
found.error = best;
found.height_error = marker.location.y - point.position.y;
found.wrong = best > tool.tolerance || (tool.height_mode == 1 && std::abs(found.height_error) > tool.height_tolerance);
if (point.standing >= 0)
tool.found[point.standing] = found;
else
{
point.standing = static_cast<int>(tool.found.size());
tool.found.push_back(found);
}
}
auto const missing{std::count_if(tool.points.begin(), tool.points.end(), [](fouling_point const &Point) { return Point.standing < 0; })};
auto const wrong{std::count_if(tool.found.begin(), tool.found.end(), [](fouling_found const &Found) { return Found.wrong; })};
tool.status = format(STR_C("%zu switches, %zu fouling points: %d without a marker, %d markers standing wrong"), switches.size(), tool.points.size(), static_cast<int>(missing), static_cast<int>(wrong));
}
void editor_mode::fouling_place()
{
auto &tool{m_fouling};
fouling_update();
std::vector<template_item> items;
for (auto const &point : tool.points)
{
if (point.standing >= 0)
continue;
template_item item;
item.file = kFoulingModels[std::clamp(tool.model, 0, 3)];
item.location = point.position;
item.yaw = point.yaw;
if (tool.model != 2)
item.rest = {"0"};
items.push_back(std::move(item));
}
std::string error;
auto const placed{place_templates(items, error)};
tool.status = placed > 0 ? format(STR_C("%zu fouling point markers placed, Ctrl+Z takes them back"), placed) : error;
WriteLog("Editor: " + tool.status, logtype::generic);
tool.dirty = true;
}
void editor_mode::fouling_fix()
{
auto &tool{m_fouling};
fouling_update();
std::vector<std::pair<standing_template, template_item>> moves;
for (auto const &found : tool.found)
{
if (false == found.wrong)
continue;
template_item item;
auto const &point{tool.points[found.point]};
if (found.error <= tool.tolerance)
{
// in its place, only too high or too low
item.location = {found.marker.location.x, point.position.y, found.marker.location.z};
item.yaw = found.marker.yaw;
}
else
{
item.location = point.position;
item.yaw = point.yaw;
}
moves.emplace_back(found.marker, item);
}
auto const moved{move_templates(moves)};
tool.status = format(STR_C("%zu fouling point markers moved to their places, Ctrl+Z takes it back"), moved);
WriteLog("Editor: " + tool.status, logtype::generic);
tool.dirty = true;
}
void editor_mode::render_fouling_ui()
{
auto &tool{m_fouling};
ImGui::TextDisabled("%s", STR_C("W17 where the axes of the tracks meeting at a switch are the spacing apart,\nhalf way between them"));
auto const changed = [&](bool const Changed) {
if (Changed)
tool.dirty = true;
};
changed(ImGui::RadioButton(STR_C("The selected switch"), &tool.scope, 0));
ImGui::SameLine();
changed(ImGui::RadioButton(STR_C("Around the camera"), &tool.scope, 1));
ImGui::SameLine();
changed(ImGui::RadioButton(STR_C("Whole scenery"), &tool.scope, 2));
ImGui::PushItemWidth(110.0f);
if (tool.scope == 1)
changed(ImGui::InputDouble(STR_C("Around the camera (m)"), &tool.reach, 0.0, 0.0, "%.0f"));
changed(ImGui::InputDouble(STR_C("Spacing of the axes (m)"), &tool.spacing, 0.0, 0.0, "%.2f"));
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("3.5 m on the straight track; more on the curves, where the cars lean out"));
changed(ImGui::InputDouble(STR_C("A marker off by more (m) is wrong"), &tool.tolerance, 0.0, 0.0, "%.1f"));
changed(ImGui::RadioButton(STR_C("On the ground"), &tool.height_mode, 0));
ImGui::SameLine();
changed(ImGui::RadioButton(STR_C("Over the rail heads"), &tool.height_mode, 1));
if (tool.height_mode == 1)
{
ImGui::SameLine();
changed(ImGui::InputDouble(STR_C("by (m)"), &tool.height, 0.0, 0.0, "%.3f"));
changed(ImGui::InputDouble(STR_C("A marker higher or lower by more (m) is wrong"), &tool.height_tolerance, 0.0, 0.0, "%.3f"));
tool.height_tolerance = std::clamp(tool.height_tolerance, 0.005, 1.0);
}
char const *models[] = {"w17", "w17_new", "w17_old", "w17_dwuteo"};
ImGui::Combo(STR_C("Marker"), &tool.model, models, IM_ARRAYSIZE(models));
char const *turns[] = {"0", "90", "180", "270"};
int turn{tool.turn / 90};
if (ImGui::Combo(STR_C("Turned by (deg)"), &turn, turns, IM_ARRAYSIZE(turns)))
{
tool.turn = turn * 90;
tool.dirty = true;
}
ImGui::PopItemWidth();
tool.reach = std::clamp(tool.reach, 50.0, 50000.0);
tool.spacing = std::clamp(tool.spacing, 2.0, 10.0);
tool.tolerance = std::clamp(tool.tolerance, 0.2, 20.0);
fouling_update();
if (false == tool.error.empty())
{
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str());
return;
}
ImGui::TextWrapped("%s", tool.status.c_str());
auto const missing{std::count_if(tool.points.begin(), tool.points.end(), [](fouling_point const &Point) { return Point.standing < 0; })};
auto const wrong{std::count_if(tool.found.begin(), tool.found.end(), [](fouling_found const &Found) { return Found.wrong; })};
if (missing > 0 && ImGui::Button(format(STR_C("Place %d markers"), static_cast<int>(missing)).c_str()))
fouling_place();
if (wrong > 0)
{
if (missing > 0)
ImGui::SameLine();
if (ImGui::Button(format(STR_C("Move %d wrong markers to their places"), static_cast<int>(wrong)).c_str()))
fouling_fix();
}
}
void editor_mode::draw_fouling_overlay() const
{
auto const &tool{m_fouling};
if (false == tool.open)
return;
screen_projection const projection;
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
glm::dvec3 const camera{Global.pCamera.Pos};
for (auto const &point : tool.points)
{
if (glm::distance(point.position, camera) > kOverlayRange)
continue;
ImVec2 screen;
if (false == projection.project(point.position, screen))
continue;
bool const standing{point.standing >= 0 && false == tool.found[point.standing].wrong};
auto const colour{standing ? IM_COL32(180, 180, 180, 200) : IM_COL32(90, 255, 120, 235)};
drawlist->AddCircleFilled(screen, 4.0f, colour);
draw_label(drawlist, screen, colour, "W17");
}
for (auto const &found : tool.found)
{
if (false == found.wrong || glm::distance(found.marker.location, camera) > kOverlayRange)
continue;
ImVec2 screen;
if (false == projection.project(found.marker.location, screen))
continue;
drawlist->AddCircle(screen, 9.0f, IM_COL32(255, 70, 60, 235), 16, 2.5f);
draw_label(drawlist, screen, IM_COL32(255, 70, 60, 235), format(STR_C("W17 %.1f m off, %+.2f m high"), found.error, found.height_error));
projection.line(drawlist, found.marker.location, tool.points[found.point].position, IM_COL32(255, 70, 60, 160), 1.5f);
}
}