mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-20 16:14:52 +02:00
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
420 lines
16 KiB
C++
420 lines
16 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/editormodeshared.h"
|
|
#include "editor/editorFormat.hpp"
|
|
#include "editor/editorAlignmentStore.hpp"
|
|
#include "editor/editorSwitchStore.hpp"
|
|
#include "editor/editorGeometry.hpp"
|
|
|
|
#include "world/Track.h"
|
|
#include "scene/scenelayers.h"
|
|
#include "simulation/simulation.h"
|
|
#include "utilities/Logs.h"
|
|
#include "utilities/translation.h"
|
|
|
|
#include "imgui/imgui.h"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
|
|
namespace
|
|
{
|
|
|
|
using geometry::bezier;
|
|
using geometry::plan_of;
|
|
using geometry::signed_angle;
|
|
using geometry::turned;
|
|
using path_ends = std::pair<glm::dvec3, glm::dvec3>;
|
|
|
|
struct record_drift
|
|
{
|
|
int followed{0};
|
|
int departed{0};
|
|
double departure{0.0};
|
|
double cant{0.0};
|
|
std::string layer;
|
|
};
|
|
|
|
glm::dvec2 chord_direction(glm::dvec3 const &Start, glm::dvec3 const &End)
|
|
{
|
|
auto const direction{plan_of(End - Start)};
|
|
return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0};
|
|
}
|
|
|
|
path_ends const &ends_before(std::vector<TTrack *> const &Moving, std::vector<path_ends> const &Before, TTrack const *Track)
|
|
{
|
|
return Before[static_cast<std::size_t>(std::find(Moving.begin(), Moving.end(), Track) - Moving.begin())];
|
|
}
|
|
|
|
bool lies_on(int const Record, TTrack const &Track)
|
|
{
|
|
bezier const curve{Track.m_paths.front()};
|
|
std::array<double, 3> constexpr samples{0.0, 0.5, 1.0};
|
|
return std::ranges::all_of(samples, [&](double const T) { return alignment_store::on_record(Record, curve.point(T), alignment_store::tolerance).has_value(); });
|
|
}
|
|
|
|
bool covers_record(int const Record, std::vector<TTrack *> const &Moving)
|
|
{
|
|
for (auto *track : simulation::Paths.sequence())
|
|
if (track != nullptr && false == track->m_editorremoved && track->eType == tt_Normal && false == track->m_paths.empty() && std::find(Moving.begin(), Moving.end(), track) == Moving.end() && lies_on(Record, *track))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool carried_rigidly(int const Record, std::vector<TTrack *> const &Tracks, std::vector<TTrack *> const &Moving, std::vector<path_ends> const &Before, record_drift &Drift)
|
|
{
|
|
auto const &first{ends_before(Moving, Before, Tracks.front())};
|
|
auto const &path{Tracks.front()->m_paths.front()};
|
|
auto const &start{path.points[segment_data::point::start]};
|
|
auto const from{chord_direction(first.first, first.second)};
|
|
auto const to{chord_direction(start, path.points[segment_data::point::end])};
|
|
auto const angle{signed_angle(from, to)};
|
|
auto const carried = [&](glm::dvec3 const &Old, glm::dvec3 const &Current) { return glm::distance(plan_of(start) + turned(plan_of(Old) - plan_of(first.first), angle), plan_of(Current)) <= 0.01; };
|
|
for (auto const *track : Tracks)
|
|
{
|
|
auto const &was{ends_before(Moving, Before, track)};
|
|
auto const &now{track->m_paths.front()};
|
|
if (false == carried(was.first, now.points[segment_data::point::start]) || false == carried(was.second, now.points[segment_data::point::end]))
|
|
return false;
|
|
}
|
|
if (glm::distance(first.first, start) > 1e-4 || std::abs(angle) > 1e-7)
|
|
{
|
|
alignment_store::transform(Record, first.first, from, start, to);
|
|
++Drift.followed;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void measure_departure(int const Record, std::vector<TTrack *> const &Tracks, record_drift &Drift)
|
|
{
|
|
auto const &records{alignment_store::records()};
|
|
auto const gauge{records[Record].design.norms.gauge};
|
|
for (auto const *track : Tracks)
|
|
{
|
|
if (track->m_editorremoved)
|
|
continue;
|
|
auto const &path{track->m_paths.front()};
|
|
bezier const curve{path};
|
|
double largest{0.0};
|
|
for (auto const t : {0.0, 0.5, 1.0})
|
|
{
|
|
auto const found{alignment_store::on_record(Record, curve.point(t), 1000.0)};
|
|
largest = std::max(largest, found.has_value() ? found->offset : 1000.0);
|
|
}
|
|
if (largest > alignment_store::tolerance)
|
|
{
|
|
++Drift.departed;
|
|
Drift.departure = std::max(Drift.departure, largest);
|
|
Drift.layer = scene::Layers.layer(records[Record].layer).name;
|
|
continue;
|
|
}
|
|
for (auto const end : {0, 1})
|
|
if (auto const found{alignment_store::on_record(Record, curve.point(end), alignment_store::tolerance)})
|
|
{
|
|
auto const actual{gauge * std::sin(std::abs(glm::radians(static_cast<double>(path.rolls[end]))))};
|
|
Drift.cant = std::max(Drift.cant, std::abs(actual - alignment_store::cant_at(Record, found->chainage)));
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> drift_notes(record_drift const &Drift)
|
|
{
|
|
std::vector<std::string> notes;
|
|
if (Drift.followed > 0)
|
|
notes.push_back(STR("The kept design of the line went along with its paths"));
|
|
if (Drift.departed > 0)
|
|
notes.push_back(format(STR_P("%d path left the design kept in %s, by up to %.2f m: Ctrl+Z takes it back, the Plan tool (G) lays it by the design again or forgets the design",
|
|
"%d paths left the design kept in %s, by up to %.2f m: Ctrl+Z takes it back, the Plan tool (G) lays them by the design again or forgets it", Drift.departed),
|
|
Drift.departed, Drift.layer.c_str(), Drift.departure));
|
|
if (Drift.cant > 5.0)
|
|
notes.push_back(format(STR_C("The cant differs from the kept design by up to %.0f mm"), Drift.cant));
|
|
return notes;
|
|
}
|
|
|
|
switch_store::record switch_entry(editor_track::switch_template const &Shape)
|
|
{
|
|
switch_store::record entry;
|
|
if (Shape.source == nullptr)
|
|
{
|
|
entry.type = Shape.type;
|
|
entry.rail = Shape.rail;
|
|
entry.radius = Shape.radius;
|
|
entry.ratio = Shape.ratio;
|
|
entry.a = Shape.a;
|
|
entry.b = Shape.b;
|
|
entry.source = "designed";
|
|
}
|
|
else if (auto const known{switch_store::of(*Shape.source)}; known.has_value() && known->kind == "switch")
|
|
{
|
|
entry = *known;
|
|
entry.number = "-";
|
|
}
|
|
else
|
|
{
|
|
// Rz, Rlj or Rld, as classify finds it by the shape
|
|
entry.type = "Rz";
|
|
entry.radius = Shape.radius;
|
|
entry.ratio = Shape.ratio;
|
|
entry.source = "measured";
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
template <std::size_t Size>
|
|
bool edit_dashed(char const *Label, std::string &Value)
|
|
{
|
|
std::array<char, Size> text{};
|
|
std::snprintf(text.data(), text.size(), "%s", Value == "-" ? "" : Value.c_str());
|
|
if (false == ImGui::InputText(Label, text.data(), text.size(), ImGuiInputTextFlags_EnterReturnsTrue) && false == ImGui::IsItemDeactivatedAfterEdit())
|
|
return false;
|
|
Value = text[0] != '\0' ? text.data() : "-";
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void editor_mode::records_watch_begin()
|
|
{
|
|
auto &watch{m_record_watch};
|
|
watch = {};
|
|
watch.active = true;
|
|
watch.moving = m_track_drag;
|
|
for (auto const &link : m_track_gizmo_drag.switches)
|
|
for (auto const &member : link.cluster)
|
|
if (std::find(watch.moving.begin(), watch.moving.end(), member.first) == watch.moving.end())
|
|
watch.moving.push_back(member.first);
|
|
for (auto *track : watch.moving)
|
|
{
|
|
auto const &path{track->m_paths.front()};
|
|
watch.before.emplace_back(path.points[segment_data::point::start], path.points[segment_data::point::end]);
|
|
if (track->eType != tt_Normal)
|
|
continue;
|
|
if (auto const found{alignment_store::on_track(*track)})
|
|
watch.on[found->record].push_back(track);
|
|
}
|
|
for (auto const &[record, tracks] : watch.on)
|
|
if (covers_record(record, watch.moving))
|
|
watch.whole.insert(record);
|
|
}
|
|
|
|
// what the drag of the gizmo did to the kept alignments and switches: an alignment all of whose paths were moved or turned as a
|
|
// whole goes along with them, paths pulled out of an alignment or given another cant are told about, as are switches bent out
|
|
// of the shape of their type; switches moved as a whole take their records along
|
|
void editor_mode::records_watch_end()
|
|
{
|
|
auto &watch{m_record_watch};
|
|
if (false == watch.active)
|
|
return;
|
|
watch.active = false;
|
|
record_drift drift;
|
|
for (auto const &[record, tracks] : watch.on)
|
|
if (watch.whole.count(record) == 0 || false == carried_rigidly(record, tracks, watch.moving, watch.before, drift))
|
|
measure_departure(record, tracks, drift);
|
|
auto notes{drift_notes(drift)};
|
|
switch_store::follow();
|
|
for (auto const *track : watch.moving)
|
|
if (track->eType == tt_Switch && false == track->m_editorremoved)
|
|
if (auto const known{switch_store::named(*track)}; known.has_value() && false == switch_store::of(*track).has_value())
|
|
notes.push_back(format(STR_C("Switch %s lost the shape of its type %s: Ctrl+Z takes it back"), track->name().c_str(), switch_store::label(*known).c_str()));
|
|
records_to_history();
|
|
if (false == notes.empty())
|
|
{
|
|
std::string text;
|
|
for (auto const ¬e : notes)
|
|
text += (text.empty() ? "" : "\n") + note;
|
|
ui()->set_status(text, drift.departed > 0 || drift.cant > 5.0 || notes.size() > static_cast<std::size_t>(drift.followed > 0 ? 1 : 0));
|
|
WriteLog("Editor: " + text, logtype::generic);
|
|
}
|
|
watch = {};
|
|
}
|
|
|
|
void editor_mode::records_to_history()
|
|
{
|
|
auto alignments{alignment_store::take_changes()};
|
|
auto switches{switch_store::take_changes()};
|
|
if (m_history.empty() || m_history.back().action != EditorSnapshot::Action::TrackEdit)
|
|
return;
|
|
auto &last{m_history.back()};
|
|
if (alignments.has_value() && false == last.alignments.has_value())
|
|
last.alignments = std::move(alignments);
|
|
if (switches.has_value() && false == last.switch_records.has_value())
|
|
last.switch_records = std::move(switches);
|
|
}
|
|
|
|
// the switch is kept in the scenery with its type: of the catalogue template, of the switch of the scenery it was copied
|
|
// from, or measured; on a curve it is an arc switch, one-sided when it diverges to the side the curve turns to
|
|
void editor_mode::record_switch(TTrack &Track, editor_track::switch_template const &Shape)
|
|
{
|
|
if (Track.eType != tt_Switch || Track.m_paths.size() < 2 || Track.name().empty() || scene::Layers.empty())
|
|
return;
|
|
auto entry{switch_entry(Shape)};
|
|
entry.kind = "switch";
|
|
entry.name = Track.name();
|
|
entry.parts = {entry.name};
|
|
switch_store::place(entry, Track);
|
|
switch_store::classify(entry, Track);
|
|
switch_store::put(entry, alignment_store::layer_of(Track));
|
|
}
|
|
|
|
// a double switch is kept as one record of its two switches, with the type, the size and the side of the first one
|
|
void editor_mode::record_double(TTrack &First, TTrack &Second, editor_track::switch_template const &Shape)
|
|
{
|
|
if (First.eType != tt_Switch || Second.eType != tt_Switch || First.m_paths.size() < 2 || First.name().empty() || Second.name().empty() || scene::Layers.empty())
|
|
return;
|
|
auto entry{switch_entry(Shape)};
|
|
entry.kind = "double";
|
|
entry.name = First.name();
|
|
entry.parts = {First.name(), Second.name()};
|
|
switch_store::place(entry, First);
|
|
switch_store::classify(entry, First);
|
|
switch_store::put(entry, alignment_store::layer_of(First));
|
|
}
|
|
|
|
void editor_mode::record_connection(connection::kind const Kind, std::vector<TTrack *> const &Switches, connection::result const &Plan, editor_track::switch_template const &Shape)
|
|
{
|
|
if (Switches.empty() || Switches.front()->eType != tt_Switch || Switches.front()->m_paths.size() < 2 || scene::Layers.empty())
|
|
return;
|
|
auto entry{switch_entry(Shape)};
|
|
entry.kind = "connection";
|
|
entry.name = Switches.front()->name();
|
|
for (auto const *track : Switches)
|
|
if (track->name().empty())
|
|
return;
|
|
else
|
|
entry.parts.push_back(track->name());
|
|
switch_store::place(entry, *Switches.front());
|
|
switch_store::classify(entry, *Switches.front());
|
|
entry.type = connection::word(Kind);
|
|
entry.a = Plan.spacing;
|
|
entry.b = Plan.straights.empty() ? 0.0 : Plan.straights.front();
|
|
switch_store::put(entry, alignment_store::layer_of(*Switches.front()));
|
|
}
|
|
|
|
std::optional<editor_mode::double_switch> editor_mode::double_of(TTrack const &Track) const
|
|
{
|
|
auto const of_switch = [](TTrack const &Switch) -> std::optional<double_switch> {
|
|
if (Switch.eType != tt_Switch || Switch.m_editorremoved)
|
|
return std::nullopt;
|
|
auto const known{switch_store::named(Switch)};
|
|
if (false == known.has_value() || known->kind != "double" || known->parts.size() != 2)
|
|
return std::nullopt;
|
|
auto *first{simulation::Paths.find(known->parts[0])};
|
|
auto *second{simulation::Paths.find(known->parts[1])};
|
|
for (auto const *part : {first, second})
|
|
if (part == nullptr || part->m_editorremoved || part->eType != tt_Switch || part->m_paths.size() != 2)
|
|
return std::nullopt;
|
|
if (first == second)
|
|
return std::nullopt;
|
|
auto const start{second->m_paths.front().points[segment_data::point::start]};
|
|
for (auto const &[other, point] : editor_track::connected_points(*first, first->m_paths.front().points[segment_data::point::end]))
|
|
{
|
|
if (other->m_editorremoved || other->eType != tt_Normal || other->m_paths.size() != 1)
|
|
continue;
|
|
auto const &path{other->m_paths.front()};
|
|
auto const opposite{path.points[point.kind == editor_track::point_kind::start ? segment_data::point::end : segment_data::point::start]};
|
|
if (glm::distance(opposite, start) < geometry::same_point)
|
|
return double_switch{first, other, second};
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
if (Track.eType == tt_Switch)
|
|
return of_switch(Track);
|
|
if (Track.eType != tt_Normal || Track.m_editorremoved || Track.m_paths.size() != 1)
|
|
return std::nullopt;
|
|
for (auto const index : {segment_data::point::start, segment_data::point::end})
|
|
for (auto const &[other, point] : editor_track::connected_points(Track, Track.m_paths.front().points[index]))
|
|
if (other->eType == tt_Switch)
|
|
if (auto const found{of_switch(*other)}; found.has_value() && found->link == &Track)
|
|
return found;
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::vector<segment_data> editor_mode::double_paths(double_switch const &Set)
|
|
{
|
|
auto link{Set.link->m_paths.front()};
|
|
auto const joint{Set.first->m_paths.front().points[segment_data::point::end]};
|
|
if (glm::distance(link.points[segment_data::point::start], joint) > glm::distance(link.points[segment_data::point::end], joint))
|
|
link = geometry::reversed(link);
|
|
return {Set.first->m_paths[0], Set.first->m_paths[1], Set.second->m_paths[0], Set.second->m_paths[1], link};
|
|
}
|
|
|
|
void editor_mode::record_slip(std::vector<TTrack *> const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base, double const Middle)
|
|
{
|
|
if (Parts.empty() || scene::Layers.empty())
|
|
return;
|
|
switch_store::record entry;
|
|
entry.kind = Kind;
|
|
entry.name = Base;
|
|
entry.type = Type;
|
|
entry.radius = Radius;
|
|
entry.middle = Middle;
|
|
entry.ratio = Angle > 1e-9 ? 1.0 / std::tan(Angle) : 0.0;
|
|
for (auto const *part : Parts)
|
|
if (part != nullptr && false == part->name().empty() && (Kind == "crossing" || part->eType == tt_Switch))
|
|
entry.parts.push_back(part->name());
|
|
if (entry.parts.empty())
|
|
return;
|
|
auto const *first{simulation::Paths.find(entry.parts.front())};
|
|
if (first == nullptr || first->m_paths.empty())
|
|
return;
|
|
switch_store::place(entry, *first);
|
|
switch_store::put(entry, alignment_store::layer_of(*first));
|
|
}
|
|
|
|
void editor_mode::render_switch_record(TTrack const &Track) const
|
|
{
|
|
auto const known{switch_store::of(Track)};
|
|
if (false == known.has_value())
|
|
{
|
|
ImGui::TextDisabled("%s", STR_C("The scenery keeps no type for this switch, it's read from its shape"));
|
|
return;
|
|
}
|
|
auto entry{*known};
|
|
ImGui::Text(STR_C("Type kept in the scenery: %s"), switch_store::label(entry).c_str());
|
|
if (ImGui::IsItemHovered())
|
|
{
|
|
std::string text;
|
|
if (entry.kind == "switch" && entry.main_radius != 0.0)
|
|
text = format(STR_C("Diverging track R %.0f m: the R %.0f m of the type bent with the main track"), switch_store::branch_radius(entry), entry.radius);
|
|
if (entry.source == "measured")
|
|
text += (text.empty() ? "" : "\n") + std::string{STR_C("The size is measured from the shape of the switch, it's of no template of the catalogue")};
|
|
if (false == text.empty())
|
|
ImGui::SetTooltip("%s", text.c_str());
|
|
}
|
|
bool changed{false};
|
|
if (begin_fields("##switchrecord", 0.4f))
|
|
{
|
|
field_row("Number##switchnumber");
|
|
changed |= edit_dashed<32>("##switchnumber", entry.number);
|
|
item_tooltip("Number of the switch in the station, as the plans of the station give it");
|
|
field_row("Rail##switchrail");
|
|
changed |= edit_dashed<16>("##switchrail", entry.rail);
|
|
item_tooltip("Rail of the switch: 60E1, 49E1...");
|
|
auto km{entry.km.has_value() ? *entry.km / 1000.0 : 0.0};
|
|
field_row("Km of the start##switchkm");
|
|
if (ImGui::InputDouble("##switchkm", &km, 0.0, 0.0, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue))
|
|
{
|
|
entry.km = km * 1000.0;
|
|
changed = true;
|
|
}
|
|
item_tooltip("Chainage of the start of the switch; taken from the horizontal alignment kept in the scenery when the switch lies on one");
|
|
ImGui::EndTable();
|
|
}
|
|
if (changed)
|
|
{
|
|
switch_store::put(entry, entry.layer);
|
|
switch_store::settle();
|
|
}
|
|
}
|