17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-20 17:24:53 +02:00
Files
maszyna/application/editormodegauge.cpp
maj00r ff1710d9ea feat(editor): labels for a layman, light at the start; whole sceneries fitted to the terrain model
Labels:
- every button, field, tooltip and message of the editor reread for a beginner: short nouns for the fields with the
  sentence in the tooltip, the less used options under More settings, one glossary (field of work, template, track,
  grade), sides told from directions, degrees and per mille as signs, messages built from one format string
- plural forms through STR_P; the last entry of a po file is read too, and the Polish plural rule is in the header
- lang/editor_pl.po: the code pass entries merged, 391 entries no code uses and 3 duplicates dropped
- roads: the notes and errors naming a road translated; the build options and the layout fold the texture length,
  kerb sizes, traffic share, roundabout part, height over the ground and junction speed limit under More settings, the
  lanes start folded

Grade lines fitted to the terrain model (NMT), a line or the whole scenery at once:
- fit_ground: a robust fit on nodes 5 m apart; the ground parting from the line (a bridge, a viaduct, a wall) is gone
  past rather than followed, the bends keep off the switches as far as their curves reach, a run of bends the same way
  is one point of intersection, a curve with no room is as sharp as the room lets it be
- the ends of the line and the other paths of its switches leading into lines fitted already bound the line by what
  the tracks between can climb; ends meeting in the plan, laid apart in height by hand, are joined before the run
- apply_profile: the paths are split where split_path takes the parameter (a straight was cut in the wrong place, into
  pieces of a few centimetres the game takes for zero length), no piece shorter than the margin; the paths off the
  ends of the route are brought to its height
- Bytom: 1600 lines, grade breaks at the joints 89 -> 20; Tarnowskie Gory: 848 lines, 223 -> 3; no step over 2 cm
2026-10-19 15:27:50 +02:00

887 lines
34 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 "editor/editorFormat.hpp"
#include "editor/editorGauge.hpp"
#include "editor/editorGeometry.hpp"
#include "model/AnimModel.h"
#include "model/Model3d.h"
#include "rendering/editoroverlay.h"
#include "rendering/renderer.h"
#include "simulation/simulation.h"
#include "utilities/Logs.h"
#include "world/Sweep.h"
#include "world/Track.h"
#include "imgui/imgui.h"
#include "utilities/translation.h"
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <algorithm>
#include <chrono>
#include <cctype>
#include <cmath>
#include <cstdint>
#include <optional>
#include <unordered_map>
#include <unordered_set>
using geometry::plan_of;
namespace
{
char const *const gauge_file{"editor_structure_gauges.txt"};
// past this distance of the camera from the origin of the overlay it's rebuilt, to keep its float precision
double const gauge_origin_range{2000.0};
// length of the line shown each way from the selected path, and around a hit
double const gauge_reach{500.0};
double const gauge_spot_reach{60.0};
// time spent on the scan of the whole map in a frame
auto const gauge_slice{std::chrono::milliseconds(8)};
// distance of the camera from the place of a hit, along the track
double const gauge_view_distance{4.0};
// sampled line of the tracks with the widening of the gauge along it
struct gauge_line
{
gauge::profile const *profile{nullptr};
editor_track::route route;
std::vector<editor_track::route_sample> samples;
std::vector<gauge::section> sections;
};
// half of the carriageway of a road at the sample, it narrows linearly to the width at the end
double carriageway(editor_track::route_span const &Span, double const Chainage)
{
auto const &track{*Span.track};
auto const end{track.fTrackWidth2 > 0.f ? track.fTrackWidth2 : track.fTrackWidth};
auto t{Span.to > Span.from ? std::clamp((Chainage - Span.from) / (Span.to - Span.from), 0.0, 1.0) : 0.0};
if (false == Span.forward)
t = 1.0 - t;
return 0.5 * std::abs(track.fTrackWidth + (end - track.fTrackWidth) * t);
}
gauge_line make_line(editor_track::route &Route, gauge::profile const &Profile)
{
gauge_line line;
line.profile = &Profile;
line.samples = editor_track::sample_route(Route, 1.0);
line.route = Route;
bool const road{Profile.kind == gauge::kind::road};
std::vector<double> chainage, curvature, cant;
for (auto &sample : line.samples)
{
if (false == road)
sample.position.y += gauge::rail_height;
chainage.push_back(sample.chainage);
curvature.push_back(sample.curvature);
cant.push_back(sample.cant);
}
line.sections = gauge::sections(Profile.kind, chainage, curvature, cant);
if (road)
for (std::size_t i = 0; i < line.samples.size(); ++i)
line.sections[i].base = carriageway(line.route.spans[line.samples[i].span], line.samples[i].chainage);
return line;
}
// outline checked along the path, none on the pavements, which are the roads with the pavement texture
gauge::profile const *outline_of(gauge::choice const &Choice, TTrack const &Track)
{
auto const *profile{Choice.of(Track.iCategoryFlag)};
if (profile == nullptr || profile->outline.size() < 2)
return nullptr;
if (profile == Choice.road)
{
auto name{editor_track::material_name(Track.m_material1)};
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const C) { return static_cast<char>(std::tolower(C)); });
if (name.find("chodnik") != std::string::npos)
return nullptr;
}
return profile;
}
// the line along the route from the path with the outline of its kind, none without the outline or the length
std::optional<gauge_line> track_line(gauge::choice const &Choice, TTrack const &Track, editor_track::route &Route)
{
auto const *profile{outline_of(Choice, Track)};
if (profile == nullptr)
return std::nullopt;
auto line{make_line(Route, *profile)};
if (line.samples.size() < 2)
return std::nullopt;
return line;
}
// lines through all paths of the scenery, each path in one of them at least
std::vector<gauge_line> scenery_lines(gauge::choice const &Choice)
{
std::vector<gauge_line> lines;
std::unordered_set<TTrack const *> visited;
auto const seen{[&](editor_track::route_span const &Span) { return visited.count(Span.track) > 0; }};
auto const checked{[&](TTrack const *Track) { return Track != nullptr && editor_track::is_supported(*Track) && outline_of(Choice, *Track) != nullptr; }};
for (auto *track : simulation::Paths.sequence())
{
if (false == checked(track) || visited.count(track) > 0)
continue;
auto route{editor_track::run_route(*track, 1e7)};
// the line may run on over the paths already covered, one of them is enough for the widening ahead of a curve
auto &spans{route.spans};
while (spans.size() > 2 && seen(spans[0]) && seen(spans[1]))
spans.erase(spans.begin());
while (spans.size() > 2 && seen(spans[spans.size() - 1]) && seen(spans[spans.size() - 2]))
spans.pop_back();
for (auto const &span : spans)
visited.insert(span.track);
lines.push_back(make_line(route, *outline_of(Choice, *track)));
}
// diverging paths of the switches
for (auto *track : simulation::Paths.sequence())
if (checked(track) && track->eType == tt_Switch)
{
editor_track::route route;
route.spans.push_back({track, 1, true});
lines.push_back(make_line(route, *outline_of(Choice, *track)));
}
return lines;
}
// samples of the lines, found by the position in the plan
class corridor
{
public:
explicit corridor(std::vector<gauge_line> const &Lines) : m_lines{Lines}
{
for (std::uint32_t line = 0; line < Lines.size(); ++line)
for (std::uint32_t i = 0; i < Lines[line].samples.size(); ++i)
{
// with the room for the widening in the sharpest curves on the largest cant
m_margin = std::max(m_margin, gauge::widest(*Lines[line].profile) + Lines[line].sections[i].base + 1.2);
auto const point{plan_of(Lines[line].samples[i].position)};
m_cells[key(point, cell)].push_back({line, i});
m_regions.insert(key(point, region));
}
}
static glm::dvec2 normal(editor_track::route_sample const &Sample)
{
return {-Sample.direction.y, Sample.direction.x};
}
// whether the sphere may reach the gauge of a line sampled in the cells around it
bool close(glm::dvec3 const &Center, double const Radius) const
{
auto const center{plan_of(Center)};
auto const reach{Radius + m_margin};
auto const from{index(center - reach, cell)};
auto const to{index(center + reach, cell)};
for (auto x = from.x; x <= to.x; ++x)
for (auto y = from.y; y <= to.y; ++y)
if (m_cells.count(key(x, y)) > 0)
return true;
return false;
}
// whether the sphere may reach the gauge, roughly
bool nearby(glm::dvec3 const &Center, double const Radius) const
{
auto const center{plan_of(Center)};
auto const reach{Radius + m_margin + region};
auto const from{index(center - reach, region)};
auto const to{index(center + reach, region)};
for (auto x = from.x; x <= to.x; ++x)
for (auto y = from.y; y <= to.y; ++y)
if (m_regions.count(key(x, y)) > 0)
return true;
return false;
}
struct hit
{
double depth{0.0}; // not positive outside of the gauge
editor_track::route_span const *span{nullptr}; // of the line the point enters
glm::dvec2 direction{0.0}; // of the line there, in the plan
};
// deepest intrusion of the point into the gauge of the lines nearby
hit intrusion(glm::dvec3 const &Point) const
{
auto const point{plan_of(Point)};
auto const center{index(point, cell)};
// nearest sample of every line around
std::vector<std::pair<entry, double>> nearest;
for (auto x = center.x - 1; x <= center.x + 1; ++x)
for (auto y = center.y - 1; y <= center.y + 1; ++y)
{
auto const found{m_cells.find(key(x, y))};
if (found == m_cells.end())
continue;
for (auto const &candidate : found->second)
{
auto const distance{glm::length2(plan_of(sample(candidate).position) - point)};
if (distance > m_margin * m_margin)
continue;
auto const same{std::find_if(nearest.begin(), nearest.end(), [&](auto const &Other) { return Other.first.line == candidate.line; })};
if (same == nearest.end())
nearest.push_back({candidate, distance});
else if (distance < same->second)
*same = {candidate, distance};
}
}
hit deepest;
for (auto const &[found, distance] : nearest)
{
auto const &line{m_lines[found.line]};
auto const &sample{line.samples[found.index]};
auto const offset{point - plan_of(sample.position)};
auto const along{glm::dot(offset, sample.direction)};
if ((found.index == 0 && along < -0.6) || (found.index + 1 == line.samples.size() && along > 0.6))
continue;
gauge::ring(*line.profile, line.sections[found.index], m_ring);
auto const depth{gauge::intrusion(m_ring, {glm::dot(offset, normal(sample)), Point.y - (sample.position.y + sample.grade * along)})};
if (depth > deepest.depth)
deepest = {depth, &line.route.spans[sample.span], sample.direction};
}
return deepest;
}
private:
static constexpr double cell{8.0};
static constexpr double region{64.0};
struct entry
{
std::uint32_t line;
std::uint32_t index;
};
static glm::i64vec2 index(glm::dvec2 const &Point, double const Size)
{
return {static_cast<std::int64_t>(std::floor(Point.x / Size)), static_cast<std::int64_t>(std::floor(Point.y / Size))};
}
static std::uint64_t key(std::int64_t const X, std::int64_t const Y)
{
return (static_cast<std::uint64_t>(X) << 32) ^ static_cast<std::uint32_t>(Y);
}
static std::uint64_t key(glm::dvec2 const &Point, double const Size)
{
auto const cell{index(Point, Size)};
return key(cell.x, cell.y);
}
editor_track::route_sample const &sample(entry const &Entry) const
{
return m_lines[Entry.line].samples[Entry.index];
}
std::vector<gauge_line> const &m_lines;
double m_margin{0.0};
mutable std::vector<gauge::place> m_ring; // reused by the checks
std::unordered_map<std::uint64_t, std::vector<entry>> m_cells;
std::unordered_set<std::uint64_t> m_regions;
};
glm::dmat4 placement(TAnimModel const &Instance)
{
auto const angles{Instance.Angles()};
auto matrix{glm::translate(glm::dmat4(1.0), Instance.location())};
matrix = glm::rotate(matrix, glm::radians(static_cast<double>(angles.y)), glm::dvec3(0.0, 1.0, 0.0));
matrix = glm::rotate(matrix, glm::radians(static_cast<double>(angles.x)), glm::dvec3(1.0, 0.0, 0.0));
matrix = glm::rotate(matrix, glm::radians(static_cast<double>(angles.z)), glm::dvec3(0.0, 0.0, 1.0));
return glm::scale(matrix, glm::dvec3(Instance.Scale()));
}
// calls the visitor with the world corners of the visible triangles of the submodel, its siblings and children
template <typename Visitor> void visit_triangles(TSubModel *Submodel, glm::dmat4 const &Matrix, Visitor const &Visit)
{
for (auto *submodel = Submodel; submodel != nullptr; submodel = submodel->NextGet())
{
if (submodel->iVisible == 0)
continue;
auto matrix{Matrix};
if ((submodel->iFlags & 0xC000) && submodel->GetMatrix() != nullptr)
matrix *= glm::dmat4(glm::make_mat4(submodel->GetMatrix()->readArray()));
if (submodel->eType == GL_TRIANGLES && submodel->m_geometry.handle != null_handle)
{
auto const &vertices{GfxRenderer->Vertices(submodel->m_geometry.handle)};
auto const &indices{GfxRenderer->Indices(submodel->m_geometry.handle)};
auto const corner{[&](std::size_t const Index) { return glm::dvec3(matrix * glm::dvec4(glm::dvec3(vertices[Index].position), 1.0)); }};
auto const count{indices.empty() ? vertices.size() : indices.size()};
for (std::size_t i = 0; i + 2 < count; i += 3)
{
if (indices.empty())
Visit(corner(i), corner(i + 1), corner(i + 2));
else
Visit(corner(indices[i]), corner(indices[i + 1]), corner(indices[i + 2]));
}
}
visit_triangles(submodel->ChildGet(), matrix, Visit);
}
}
// the triangle enters the gauge when any point of a grid spread over it does; the deepest such point goes to the hit
void scan_triangle(corridor const &Space, glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C, editor_mode::gauge_hit &Hit)
{
auto const middle{(A + B + C) / 3.0};
if (false == Space.close(middle, std::max({glm::distance(middle, A), glm::distance(middle, B), glm::distance(middle, C)})))
return;
auto const edge{std::max({glm::distance(A, B), glm::distance(B, C), glm::distance(C, A)})};
auto const steps{std::clamp(static_cast<int>(std::ceil(edge / 0.5)), 1, 24)};
for (int u = 0; u <= steps; ++u)
for (int v = 0; u + v <= steps; ++v)
{
auto const point{A + (B - A) * (static_cast<double>(u) / steps) + (C - A) * (static_cast<double>(v) / steps)};
auto const found{Space.intrusion(point)};
if (found.depth <= 0.0)
continue;
if (found.depth > Hit.depth)
{
Hit.depth = found.depth;
Hit.point = point;
Hit.track = found.span->track; // NOSONAR
Hit.path = found.span->path;
Hit.direction = found.direction;
}
return;
}
}
// the deepest place of the model in the gauge, added to the hits when it enters it
void scan_model(corridor const &Space, TAnimModel &Instance, std::vector<editor_mode::gauge_hit> &Hits)
{
if (false == Instance.visible() || Instance.Model() == nullptr || Instance.Model()->GetSMRoot() == nullptr)
return;
if (false == Space.nearby(Instance.location(), Instance.radius()))
return;
editor_mode::gauge_hit hit{Instance.name(), Instance.location()};
visit_triangles(Instance.Model()->GetSMRoot(), placement(Instance), [&](glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C) { scan_triangle(Space, A, B, C, hit); });
if (hit.depth > 0.0)
Hits.push_back(std::move(hit));
}
// the same for the copies of a model laid along a curve
void scan_sweep(corridor const &Space, sweep_node &Sweep, std::vector<editor_mode::gauge_hit> &Hits)
{
if (Sweep.m_editorremoved || false == Space.nearby(Sweep.location(), Sweep.radius()))
return;
editor_mode::gauge_hit hit{Sweep.name(), Sweep.location()};
for (auto const &shape : Sweep.create_shapes())
{
auto const &vertices{shape.data().vertices};
for (std::size_t i = 0; i + 2 < vertices.size(); i += 3)
scan_triangle(Space, vertices[i].position, vertices[i + 1].position, vertices[i + 2].position, hit);
}
if (hit.depth > 0.0)
Hits.push_back(std::move(hit));
}
void sort_hits(std::vector<editor_mode::gauge_hit> &Hits)
{
std::sort(Hits.begin(), Hits.end(), [](auto const &Left, auto const &Right) { return Left.depth > Right.depth; });
}
// the models entering the gauge, the deepest first
void scan_models(corridor const &Space, std::vector<editor_mode::gauge_hit> &Hits)
{
for (auto *instance : simulation::Instances.sequence())
if (instance != nullptr)
scan_model(Space, *instance, Hits);
for (auto *sweep : simulation::Sweeps.sequence())
if (sweep != nullptr)
scan_sweep(Space, *sweep, Hits);
sort_hits(Hits);
}
// translucent tunnel of the gauge along the line, with the rings every 10 m and the lines along its corners
void build_tunnel(gauge_line const &Line, std::vector<glm::dvec3> &Surface, std::vector<glm::dvec3> &Edges)
{
std::vector<gauge::place> places;
auto const ring{[&](std::size_t const Index) {
gauge::ring(*Line.profile, Line.sections[Index], places);
auto const &sample{Line.samples[Index]};
auto const normal{corridor::normal(sample)};
std::vector<glm::dvec3> points;
for (auto const &place : places)
points.push_back(sample.position + glm::dvec3(normal.x * place.lateral, place.height, normal.y * place.lateral));
return points;
}};
std::vector<glm::dvec3> previous;
double lastring{-1e9};
for (std::size_t i = 0; i < Line.samples.size(); i += 2)
{
auto const current{ring(i)};
if (false == previous.empty())
for (std::size_t j = 1; j < current.size(); ++j)
{
Surface.insert(Surface.end(), {previous[j - 1], previous[j], current[j], previous[j - 1], current[j], current[j - 1]});
Edges.insert(Edges.end(), {previous[j], current[j]});
}
if (Line.samples[i].chainage - lastring >= 10.0 || i + 2 >= Line.samples.size())
{
lastring = Line.samples[i].chainage;
for (std::size_t j = 1; j < current.size(); ++j)
Edges.insert(Edges.end(), {current[j - 1], current[j]});
}
previous = current;
}
}
// table of the points of the outline with the inserting and removing of the points, whether it changed
bool edit_points(std::vector<gauge::profile::point> &Points)
{
bool edited{false};
bool inserted{false};
int remove{-1};
ImGui::Columns(3, "gaugeoutline", false);
ImGui::SetColumnWidth(0, 110.0f);
ImGui::SetColumnWidth(1, 110.0f);
ImGui::TextDisabled(STR_C("Half-width"));
ImGui::NextColumn();
ImGui::TextDisabled(STR_C("Height"));
ImGui::NextColumn();
ImGui::NextColumn();
for (int i = 0; i < static_cast<int>(Points.size()); ++i)
{
ImGui::PushID(i);
auto &point{Points[i]};
ImGui::SetNextItemWidth(-1.0f);
ImGui::InputDouble("##width", &point.half_width, 0.0, 0.0, "%.3f");
edited |= ImGui::IsItemDeactivatedAfterEdit();
ImGui::NextColumn();
ImGui::SetNextItemWidth(-1.0f);
ImGui::InputDouble("##height", &point.height, 0.0, 0.0, "%.3f");
edited |= ImGui::IsItemDeactivatedAfterEdit();
ImGui::NextColumn();
if (ImGui::SmallButton("+"))
{
auto const &next{Points[std::min<std::size_t>(i + 1, Points.size() - 1)]};
Points.insert(Points.begin() + i + 1, {0.5 * (point.half_width + next.half_width), 0.5 * (point.height + next.height)});
inserted = edited = true;
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip(STR_C("Insert a point after this one"));
ImGui::SameLine();
if (Points.size() > 2 && ImGui::SmallButton("x"))
remove = i;
ImGui::NextColumn();
ImGui::PopID();
if (inserted)
break;
}
ImGui::Columns(1);
if (remove >= 0)
{
Points.erase(Points.begin() + remove);
edited = true;
}
return edited;
}
// the railway outline drawn across the track at the size of the panel, so the name in the list has a shape
void gauge_sketch(gauge::profile const &Profile)
{
auto const width{ImGui::GetContentRegionAvail().x};
auto const height{std::round(ImGui::GetFontSize() * 9.0f)};
auto const low{ImGui::GetCursorScreenPos()};
ImGui::Dummy(ImVec2(width, height));
if (Profile.outline.size() < 2)
return;
double top{0.0};
double half{0.0};
for (auto const &point : Profile.outline)
{
top = std::max(top, point.height);
half = std::max(half, point.half_width);
}
if (top <= 0.0 || half <= 0.0)
return;
auto const pad{ImGui::GetFontSize() * 1.2f};
auto const scale{static_cast<float>(std::min((height - pad * 2.0f) / top, (width - pad * 2.0f) / (half * 2.0)))};
ImVec2 const base{std::round(low.x + width * 0.5f), std::round(low.y + height - pad)};
auto const at = [&](double const X, double const Y) { return ImVec2(base.x + static_cast<float>(X) * scale, base.y - static_cast<float>(Y) * scale); };
auto *draw{ImGui::GetWindowDrawList()};
draw->AddRectFilled(low, ImVec2(low.x + width, low.y + height), ImGui::GetColorU32(ImGuiCol_FrameBg), 4.0f);
// ballast, a sleeper and the two rails
ImVec2 const ballast[]{at(-2.1, -0.6), at(2.1, -0.6), at(1.55, -0.2), at(-1.55, -0.2)};
draw->AddConvexPolyFilled(ballast, 4, IM_COL32(110, 100, 90, 255));
draw->AddRectFilled(at(-1.3, -0.2), at(1.3, -0.13), IM_COL32(150, 150, 150, 255));
for (double const side : {-0.7175, 0.7175})
draw->AddRectFilled(at(side - 0.04, -0.13), at(side + 0.04, 0.0), IM_COL32(210, 210, 215, 255));
// the outline, the right half from the bottom up and its mirror back down
std::vector<ImVec2> points;
for (auto const &point : Profile.outline)
points.push_back(at(point.half_width, point.height));
for (auto point{Profile.outline.rbegin()}; point != Profile.outline.rend(); ++point)
points.push_back(at(-point->half_width, point->height));
auto const accent{IM_COL32(120, 200, 255, 255)};
auto const flags{draw->Flags};
draw->Flags &= ~ImDrawListFlags_AntiAliasedFill;
draw->AddConcavePolyFilled(points.data(), static_cast<int>(points.size()), IM_COL32(120, 200, 255, 40));
draw->Flags = flags;
draw->AddPolyline(points.data(), static_cast<int>(points.size()), accent, ImDrawFlags_Closed, 2.0f);
// the height over the top and the widest half at its height
auto const widest{std::max_element(Profile.outline.begin(), Profile.outline.end(), [](auto const &A, auto const &B) { return A.half_width < B.half_width; })};
auto const tall{format("%.2f m", top)};
auto const size{ImGui::CalcTextSize(tall.c_str())};
draw->AddText(ImVec2(std::round(base.x - size.x * 0.5f), std::round(at(0.0, top).y - size.y - 2.0f)), accent, tall.c_str());
auto const wide{format("%.2f m", widest->half_width * 2.0)};
auto const y{at(0.0, widest->height).y};
draw->AddLine(ImVec2(at(-widest->half_width, 0.0).x, y), ImVec2(at(widest->half_width, 0.0).x, y), IM_COL32(255, 220, 120, 160), 1.0f);
auto const wsize{ImGui::CalcTextSize(wide.c_str())};
draw->AddText(ImVec2(std::round(base.x - wsize.x * 0.5f), std::round(y - wsize.y - 1.0f)), IM_COL32(255, 220, 120, 255), wide.c_str());
}
} // namespace
struct gauge_scan
{
std::optional<gauge::profile> railway;
std::optional<gauge::profile> road;
std::vector<gauge_line> lines;
std::optional<corridor> space; // of the lines
std::size_t next{0}; // instance to check
float progress{0.f};
std::chrono::steady_clock::time_point started;
};
void editor_mode::gauge_load()
{
if (m_gauge.profiles.empty())
m_gauge.profiles = gauge::load_profiles(gauge_file);
// the chosen railway and road outlines, the first ones of the kind when there's none
auto const fits{[&](int const Index, bool const Road) {
return Index >= 0 && Index < static_cast<int>(m_gauge.profiles.size()) && (m_gauge.profiles[Index].kind == gauge::kind::road) == Road;
}};
for (auto [index, road] : {std::pair<int *, bool>{&m_gauge.profile, false}, std::pair<int *, bool>{&m_gauge.road, true}})
if (false == fits(*index, road))
{
*index = -1;
for (int i = 0; i < static_cast<int>(m_gauge.profiles.size()) && *index < 0; ++i)
if (fits(i, road))
*index = i;
}
}
gauge::choice editor_mode::gauge_choice() const
{
auto const pick{[&](int const Index) { return Index >= 0 ? &m_gauge.profiles[Index] : nullptr; }};
return {pick(m_gauge.profile), pick(m_gauge.road)};
}
std::vector<editor_mode::gauge_hit> const &editor_mode::gauge_hits() const
{
return m_gauge.map.scanned ? m_gauge.map.hits : m_gauge.line.hits;
}
void editor_mode::update_gauge()
{
if (m_gauge.scan)
step_gauge_map();
if (false == m_gauge.open)
{
if (false == EditorOverlay.batches.empty())
EditorOverlay.clear();
m_gauge.published = false;
return;
}
auto *track{m_gauge.enabled ? selected_track() : nullptr};
// the geometry is rescanned once the edit ends
if (ImGui::IsMouseDown(0) || m_track_gizmo_using || m_dragging)
m_gauge.pending = true;
else if (track == nullptr)
{
if (m_gauge.track != nullptr)
{
m_gauge.line = {};
m_gauge.track = nullptr;
m_gauge.published = false;
}
}
else if (track != m_gauge.track || m_history.size() != m_gauge.history || m_gauge.pending)
{
m_gauge.track = track;
m_gauge.history = m_history.size();
m_gauge.pending = false;
scan_gauge(*track);
m_gauge.published = false;
}
if (false == m_gauge.published || glm::distance(Camera.Pos, EditorOverlay.origin) > gauge_origin_range)
gauge_publish();
}
void editor_mode::scan_gauge(TTrack &Track)
{
gauge_load();
m_gauge.line = {};
auto route{editor_track::run_route(Track, gauge_reach)};
auto line{track_line(gauge_choice(), Track, route)};
if (false == line.has_value())
return;
std::vector<gauge_line> const lines{std::move(*line)};
corridor const space{lines};
build_tunnel(lines.front(), m_gauge.line.surface, m_gauge.line.edges);
scan_models(space, m_gauge.line.hits);
if (false == m_gauge.map.scanned)
m_gauge.current = -1;
}
void editor_mode::scan_gauge_map()
{
gauge_load();
auto scan{std::make_shared<gauge_scan>()};
scan->started = std::chrono::steady_clock::now();
auto const chosen{gauge_choice()};
// copies, the outlines may be edited while it runs
scan->railway = chosen.railway != nullptr ? std::optional<gauge::profile>{*chosen.railway} : std::nullopt;
scan->road = chosen.road != nullptr ? std::optional<gauge::profile>{*chosen.road} : std::nullopt;
scan->lines = scenery_lines({scan->railway ? &*scan->railway : nullptr, scan->road ? &*scan->road : nullptr});
scan->space.emplace(scan->lines);
m_gauge.map = {};
m_gauge.map.scanned = true;
m_gauge.map.history = m_history.size();
auto const name{[](gauge::profile const *Profile) { return Profile != nullptr ? Profile->name : std::string{"-"}; }};
m_gauge.map.profile = name(chosen.railway) + ", " + name(chosen.road);
m_gauge.current = -1;
m_gauge.published = false;
m_gauge.scan = std::move(scan);
}
void editor_mode::step_gauge_map()
{
auto &scan{*m_gauge.scan};
auto const &instances{simulation::Instances.sequence()};
auto const &sweeps{simulation::Sweeps.sequence()};
auto const total{instances.size() + sweeps.size()};
auto const until{std::chrono::steady_clock::now() + gauge_slice};
while (scan.next < total && std::chrono::steady_clock::now() < until)
{
auto const next{scan.next++};
if (next < instances.size())
{
if (auto *instance{instances[next]})
scan_model(*scan.space, *instance, m_gauge.map.hits);
}
else if (auto *sweep{sweeps[next - instances.size()]})
scan_sweep(*scan.space, *sweep, m_gauge.map.hits);
}
scan.progress = total == 0 ? 1.f : static_cast<float>(scan.next) / total;
if (scan.next >= total)
finish_gauge_map();
}
void editor_mode::finish_gauge_map()
{
auto const scan{std::move(m_gauge.scan)};
auto &hits{m_gauge.map.hits};
// the hit the camera went to stays chosen
auto const chosen{m_gauge.current >= 0 && m_gauge.current < static_cast<int>(hits.size()) ? std::optional<glm::dvec3>{hits[m_gauge.current].point} : std::nullopt};
sort_hits(hits);
if (chosen)
m_gauge.current = static_cast<int>(std::find_if(hits.begin(), hits.end(), [&](auto const &Hit) { return Hit.point == *chosen; }) - hits.begin());
WriteLog("Structure gauge (" + m_gauge.map.profile + "): " + std::to_string(hits.size()) + " models enter it along " + std::to_string(scan->lines.size()) + " lines, scanned in " +
std::to_string(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - scan->started).count()) + " ms");
if (false == chosen.has_value() && false == hits.empty())
gauge_focus(0);
}
void editor_mode::gauge_publish()
{
m_gauge.published = true;
EditorOverlay.clear();
EditorOverlay.origin = Camera.Pos;
auto const batch{[](glm::vec4 const &Color, unsigned int const Type, std::vector<glm::dvec3> const &Points) {
gfx::editor_overlay::batch result{Color, Type};
result.points.reserve(Points.size());
for (auto const &point : Points)
result.points.emplace_back(point - EditorOverlay.origin);
return result;
}};
std::vector<glm::dvec3> marker;
auto const &hits{gauge_hits()};
if (m_gauge.current >= 0 && m_gauge.current < static_cast<int>(hits.size()))
{
auto const point{hits[m_gauge.current].point};
marker = {point - glm::dvec3(0.0, 3.0, 0.0), point + glm::dvec3(0.0, 3.0, 0.0), point - glm::dvec3(1.0, 0.0, 0.0), point + glm::dvec3(1.0, 0.0, 0.0),
point - glm::dvec3(0.0, 0.0, 1.0), point + glm::dvec3(0.0, 0.0, 1.0)};
}
EditorOverlay.batches = {batch({0.2f, 0.9f, 0.3f, 0.12f}, GL_TRIANGLES, m_gauge.line.surface),
batch({0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES, m_gauge.line.edges),
batch({1.0f, 0.9f, 0.1f, 1.0f}, GL_LINES, marker),
batch({0.2f, 0.9f, 0.3f, 0.12f}, GL_TRIANGLES, m_gauge.spot.surface),
batch({0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES, m_gauge.spot.edges)};
}
void editor_mode::gauge_focus(int const Index)
{
auto const &hits{gauge_hits()};
if (hits.empty())
return;
m_gauge.current = (Index % static_cast<int>(hits.size()) + static_cast<int>(hits.size())) % static_cast<int>(hits.size());
m_gauge.published = false;
gauge_spot(hits[m_gauge.current]);
auto const &hit{hits[m_gauge.current]};
// along the track, tangent to the gauge at the height of the place, from the end nearer to the camera
glm::dvec3 look{hit.direction.x, 0.0, hit.direction.y};
look = glm::length(look) > 1e-6 ? glm::normalize(look) : glm::dvec3(0.0, 0.0, 1.0);
if (glm::dot(Camera.Pos - hit.point, look) > 0.0)
look = -look;
fly_to(hit.point, look, gauge_view_distance);
}
void editor_mode::gauge_spot(gauge_hit const &Hit)
{
m_gauge.spot = {};
auto const &paths{simulation::Paths.sequence()};
// the hit outlives the scan, the path may be gone since
if (Hit.track == nullptr || std::find(paths.begin(), paths.end(), Hit.track) == paths.end())
return;
auto *track{const_cast<TTrack *>(Hit.track)};
editor_track::route route;
if (Hit.path == 0)
route = editor_track::run_route(*track, gauge_spot_reach);
else
route.spans.push_back({track, Hit.path, true});
if (auto const line{track_line(gauge_choice(), *track, route)})
build_tunnel(*line, m_gauge.spot.surface, m_gauge.spot.edges);
}
void editor_mode::render_gauge_window()
{
if (false == m_gauge.open)
return;
ImGui::SetNextWindowSize(ImVec2(380.0f, 420.0f), ImGuiCond_FirstUseEver);
if (false == ImGui::Begin((std::string(STR_C("Structure gauge")) + "###structuregauge").c_str(), &m_gauge.open))
{
ImGui::End();
return;
}
if (ImGui::Checkbox(STR_C("Show along the selected path"), &m_gauge.enabled))
m_gauge.pending = true;
if (ImGui::IsItemHovered())
ImGui::SetTooltip(STR_C("Translucent tunnel of the gauge 500 m each way along the line of the selected path.\nWidened in the curves under 250 m and tilted on the cant, from 20 m (inner side)\nand 26 m (outer side) ahead of the curve, as in the PKP PLK standard, volume II."));
gauge_load();
bool changed{render_gauge_choice("Railway", m_gauge.profile, false)};
if (ImGui::IsItemHovered())
ImGui::SetTooltip(STR_C("GPL: unified structure gauge, widened only in the curves under 250 m.\nG1, G2, GA, GB, GC: limit (GSZ) and nominal (NSZ) installation gauges, widened by 3750/R in all curves."));
if (m_gauge.profile >= 0)
gauge_sketch(m_gauge.profiles[m_gauge.profile]);
changed |= render_gauge_choice("Road", m_gauge.road, true);
if (ImGui::IsItemHovered())
ImGui::SetTooltip(STR_C("Clearance over the roads, from 0.5 m beyond the edges of the carriageway"));
if (m_gauge.scan)
{
auto const label{format(STR_C("Scanning the scenery %.0f%%, %zu found"), m_gauge.scan->progress * 100.f, m_gauge.map.hits.size())};
ImGui::ProgressBar(m_gauge.scan->progress, ImVec2(-70.0f, 0.0f), label.c_str());
ImGui::SameLine();
if (ImGui::SmallButton(STR_C("Cancel")))
finish_gauge_map();
if (ImGui::IsItemHovered())
ImGui::SetTooltip(STR_C("Stops the scan and keeps the models found so far"));
}
else if (ImGui::Button(STR_C("Find the violations in the whole scenery")))
scan_gauge_map();
else if (ImGui::IsItemHovered())
ImGui::SetTooltip(STR_C("Checks the models along all tracks of the scenery against the chosen gauge\nand lists them, the deepest first.\nRuns in the background, the list fills up as it goes"));
if (m_gauge.map.scanned && false == static_cast<bool>(m_gauge.scan))
{
ImGui::SameLine();
if (ImGui::SmallButton(STR_C("Clear")))
{
m_gauge.map = {};
m_gauge.current = -1;
m_gauge.published = false;
}
else if (m_gauge.map.history != m_history.size())
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("The scenery changed since the scan (%s)"), m_gauge.map.profile.c_str());
}
render_gauge_hits();
changed |= render_gauge_outline(m_gauge.profile);
changed |= render_gauge_outline(m_gauge.road);
if (changed)
m_gauge.pending = true;
ImGui::End();
}
void editor_mode::render_gauge_hits()
{
auto const &hits{gauge_hits()};
if (false == m_gauge.map.scanned && (false == m_gauge.enabled || m_gauge.track == nullptr))
return;
if (hits.empty())
{
ImGui::TextDisabled("%s", m_gauge.map.scanned ? STR_C("No model in the scenery enters the gauge") : STR_C("The gauge is clear along this line"));
return;
}
ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), STR_C("%zu models enter the gauge%s"), hits.size(), m_gauge.map.scanned ? STR_C(" in the whole scenery") : "");
if (ImGui::ArrowButton("##gaugeprevious", ImGuiDir_Left))
gauge_focus(m_gauge.current - 1);
ImGui::SameLine();
if (ImGui::ArrowButton("##gaugenext", ImGuiDir_Right))
gauge_focus(m_gauge.current + 1);
ImGui::SameLine();
ImGui::Text("%d / %zu", m_gauge.current + 1, hits.size());
ImGui::BeginChild("gaugehits", ImVec2(0.0f, std::min(160.0f, ImGui::GetTextLineHeightWithSpacing() * hits.size() + 8.0f)), true);
ImGuiListClipper clipper;
clipper.Begin(static_cast<int>(hits.size()));
while (clipper.Step())
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i)
{
auto const &hit{hits[i]};
auto const label{(hit.model.empty() ? STR("(no name)") : hit.model) + " " + std::to_string(static_cast<int>(std::lround(hit.depth * 100.0))) + " cm##" + std::to_string(i)};
if (ImGui::Selectable(label.c_str(), m_gauge.current == i))
gauge_focus(i);
}
ImGui::EndChild();
}
bool editor_mode::render_gauge_choice(char const *Label, int &Index, bool const Road)
{
bool changed{false};
ImGui::PushID(Label);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(STR_C(Label));
ImGui::SameLine(std::round(ImGui::GetCursorStartPos().x + std::max(ImGui::CalcTextSize(STR_C("Railway")).x, ImGui::CalcTextSize(STR_C("Road")).x) + ImGui::GetStyle().ItemSpacing.x));
ImGui::SetNextItemWidth(-FLT_MIN);
if (ImGui::BeginCombo("##profile", Index >= 0 ? m_gauge.profiles[Index].name.c_str() : STR_C("(none)")))
{
for (int i = 0; i < static_cast<int>(m_gauge.profiles.size()); ++i)
if ((m_gauge.profiles[i].kind == gauge::kind::road) == Road && ImGui::Selectable(m_gauge.profiles[i].name.c_str(), i == Index))
{
changed = i != Index;
Index = i;
}
ImGui::EndCombo();
}
ImGui::PopID();
return changed;
}
bool editor_mode::render_gauge_outline(int const Index)
{
if (Index < 0)
return false;
auto &profile{m_gauge.profiles[Index]};
if (false == ImGui::TreeNode(&profile, STR_C("Outline of %s (right half)"), profile.name.c_str()))
return false;
ImGui::TextDisabled(profile.kind == gauge::kind::road ? STR_C("Metres from the edge of the carriageway and above the road, bottom up.")
: STR_C("PKP PLK standard, volume II; below 1170 mm the limit installation gauge.\nMetres from the track axis and above the rail top, bottom up."));
bool edited{edit_points(profile.outline)};
if (ImGui::SmallButton(STR_C("Restore the starting outlines")))
{
m_gauge.profiles = gauge::default_profiles();
gauge_load();
edited = true;
}
if (edited)
gauge::save_profiles(gauge_file, m_gauge.profiles);
ImGui::TreePop();
return edited;
}