17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-28 18:07:29 +01:00

feat(editor): a deleted track leaves its signal reads and its track circuit to the track laid in its place, and the SRK check lists the signals no track reads and the broken lines, with a Repair

A deleted track whose read events or track circuit no other track took says so by the cursor, naming the signals and the circuits, and the editor keeps them for the track laid in its place, within a metre of its axis. The read of each signal goes to the piece by the signal, on the list its facing there gives, so a track laid the other way round keeps the trains reading it; the other events go to the piece at its middle, the circuit to a track lying there for the most part, a new track taking it instead of the one of the track it was made from. It happens in the step which lays the track, so Ctrl+Z takes it back and laying it again gets them again; a track which took a circuit over and is removed by another step leaves it to that step, and the events of a signal taken away stay off the tracks.

The SRK panel of a station lists what it and its lines need: signals no track reads, lines broken off where the tracks go on beyond a gap, tracks of a line of the automatic block in no block section, block sections whose tracks are gone. A row shows the place; Repair has the track by each signal read it again, as moving it does, and sets the line signals again where tracks are in no block section. A warning after an operation shows by the cursor in amber, a refusal in red as before. srkcheck counts these too, srkfix repairs, join takes the height of the ends for sceneries far above zero.
This commit is contained in:
maj00r
2026-10-22 22:40:01 +02:00
parent de61cf05db
commit 2d9be3ae10
11 changed files with 704 additions and 41 deletions

View File

@@ -1333,6 +1333,13 @@ bool editor_mode::update()
{
Global.editor_tracks = (current_work_area() == work_area::tracks);
selftest_step();
// after the tool has told its own status
if (false == m_orphan_note.empty())
{
ui()->set_notice(m_orphan_note);
WriteLog("Editor: " + m_orphan_note);
m_orphan_note.clear();
}
if (m_vehicle.take_leave())
{
Application.pop_mode();
@@ -6202,6 +6209,7 @@ void editor_mode::exit()
g_redo.clear();
m_history.clear();
m_orphans.clear();
m_fill_last.clear();
m_orthophoto.cancel_pending();
m_orthophoto.detach_scene(); // the layer is an editor aid, keep it out of the other modes

View File

@@ -1905,6 +1905,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri
bool standing_signals(std::vector<standing_signal> &Signals, bool const Wait) override;
std::size_t signal_batch(signal_changes const &Changes, std::vector<std::pair<TTrack *, editor_track::state>> States, std::vector<TTrack *> Created, std::string &Error) override;
bool signal_heads(std::vector<signal_head> &Heads, bool const Wait) override;
std::size_t signal_reattach(std::vector<scene::instance_handle> const &Instances) override;
struct signal_template
{
@@ -1978,6 +1979,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri
signal_template const *signal_template_of(std::string const &File) const;
bool signal_spot_at(TTrack &Track, int const Path, glm::dvec3 const &Point, glm::dvec3 const &Cursor, int const Facing, int const Side, signal_template const &Template, signal_spot &Spot) const;
void signal_refresh(bool const Force = false);
// the signals standing, read by the tracks as they are now, the templates as found so far
std::vector<signal_standing> signal_standing_now() const;
int signal_hit() const;
void signal_press(track_intent const &Intent);
void signal_release();
@@ -2001,6 +2004,22 @@ class editor_mode : public application_mode, private editor_track::observer, pri
void draw_signal_overlay() const;
std::string signal_intent_label(signal_template const &Template, signal_spot const &Spot) const;
std::string signal_variant(signal_template const &Template, signal_spot const &Spot) const;
// what a deleted track carried and no other one took, its read events and track circuits, for a track laid in its place
struct orphan_record
{
TTrack *track{nullptr}; // retired; the record waits while it stays so
std::vector<std::pair<std::string, int>> events; // the name, the list it was on
std::vector<std::string> circuits;
// the tracks given the circuits: while one stays the circuits don't wait, once a step other than an undo removes one they
// are its own
std::vector<TTrack *> given;
bool fresh{false}; // made by the step being taken
};
std::vector<orphan_record> m_orphans;
std::string m_orphan_note; // shown by the cursor after the step which made it
void orphans_register(std::vector<TTrack *> const &Removed, std::vector<TTrack *> const &Kept);
// Created: made by the step, their circuits the ones of the tracks they were made from; Changed: the others it changed
void orphans_take_over(std::vector<TTrack *> const &Created, std::vector<TTrack *> const &Changed);
// a model laid along the track: bent to follow it, or repeated along it
struct sweep_tool

View File

@@ -55,6 +55,17 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma
glm::dvec3 a{0.0};
glm::dvec3 b{0.0};
words >> a.x >> a.z >> b.x >> b.z;
double radius{0.0};
int variant{0};
int transitions{0};
double height{0.0};
if (command == "join")
{
words >> radius >> variant >> transitions;
// of the ends, for sceneries far above zero
if (words >> height)
a.y = b.y = height;
}
auto const from{editor_track::find_free_end(nullptr, 1, a, 3.0, {})};
extend_tool tool;
if (from.track == nullptr || false == free_end(*from.track, from.point.path, from.point.kind == editor_track::point_kind::end, tool))
@@ -67,10 +78,6 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma
double length{0.0};
if (command == "join")
{
double radius{0.0};
int variant{0};
int transitions{0};
words >> radius >> variant >> transitions;
auto const to{editor_track::find_free_end(nullptr, 1, b, 3.0, {from.track})};
if (to.track == nullptr)
{

View File

@@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/.
#include "simulation/simulation.h"
#include "utilities/Globals.h"
#include "utilities/utilities.h"
#include "world/Event.h"
#include "world/Track.h"
#include "imgui/imgui.h"
@@ -48,6 +49,9 @@ float const kDragStart{6.0f};
double const kDragTravel{1.0};
double const kRefreshPeriod{2.0};
double const kSignalReach{10.0};
// a track laid in the place of a deleted one runs within it of its axis
double const kOrphanReach{1.0};
std::size_t const kOrphanRecords{64};
char const *const kKinds[] = {"main", "block", "warning", "shunt", "stop", "substitute", "repeater"};
char const *const kKindLabels[] = {"Semaphore", "Automatic block", "Warning signal", "Shunting signal", "Stop signal", "Substitute signal", "Repeater"};
@@ -222,6 +226,54 @@ void draw_signal_icon(ImDrawList *Draw, ImVec2 const Origin, ImVec2 const Size,
Draw->AddCircleFilled(ImVec2(centre, headtop + 2.0f + lamp * 1.1f + i * lamp * 2.2f), lamp, lamp_colour(Lamps[i]));
}
// the point of an axis of the track nearest in the plan to the one given: how far, on which path, where on it
double plan_gap(TTrack const &Track, glm::dvec3 const &Point, int &Path, double &Parameter)
{
auto best{std::numeric_limits<double>::max()};
for (int path = 0; path < static_cast<int>(Track.m_paths.size()); ++path)
{
auto const parameter{nearest_on(Track, path, Point)};
auto const distance{geometry::plan_distance(bezier{Track.m_paths[path]}.point(parameter), Point)};
if (distance < best)
{
best = distance;
Path = path;
Parameter = parameter;
}
}
return best;
}
bool plan_overlaps(TTrack const &A, TTrack const &B, double const Margin)
{
auto const box = [](TTrack const &Track, glm::dvec2 &Low, glm::dvec2 &High) {
Low = glm::dvec2{std::numeric_limits<double>::max()};
High = -Low;
for (auto const &path : Track.m_paths)
{
bezier const curve{path};
for (auto const &point : {curve.p0, curve.p1, curve.p2, curve.p3})
{
Low = glm::min(Low, plan_of(point));
High = glm::max(High, plan_of(point));
}
}
};
glm::dvec2 low_a, high_a, low_b, high_b;
box(A, low_a, high_a);
box(B, low_b, high_b);
return low_a.x - Margin <= high_b.x && low_b.x - Margin <= high_a.x && low_a.y - Margin <= high_b.y && low_b.y - Margin <= high_a.y;
}
std::string listed(std::vector<std::string> const &Names)
{
std::string text;
for (auto const &name : Names)
text += (text.empty() ? "" : ", ") + name;
return text;
}
} // namespace
bool editor_mode::signal_scan(bool const Wait)
@@ -369,38 +421,7 @@ void editor_mode::signal_refresh(bool const Force)
tool.standing_history = m_history.size();
tool.standing_redo = g_redo.size();
tool.standing_time = now;
tool.standing.clear();
if (scene::Layers.empty())
return;
auto const found{standing_templates([this](std::string const &File) { return signal_template_of(File) != nullptr; })};
std::unordered_map<std::string, std::pair<TTrack *, int>> readers;
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved)
continue;
for (int list = 0; list < 6; ++list)
for (auto const &event : editor_track::events(*track, list))
if (false == event.first.empty())
readers.emplace(ToLower(event.first), std::make_pair(track, list));
}
for (auto const &include : found)
{
auto const *item{signal_template_of(include.file)};
signal_standing standing;
standing.include = include;
standing.kind = kind_index(item->kind);
standing.name = include.values.empty() ? std::string{} : include.values.front();
if (false == item->read.empty())
{
standing.read = editor_includes::substitute(item->read, include.values);
if (auto const reader{readers.find(ToLower(standing.read))}; reader != readers.end())
{
standing.track = reader->second.first;
standing.list = reader->second.second;
}
}
tool.standing.push_back(std::move(standing));
}
tool.standing = signal_standing_now();
if (tool.selected != 0 && std::none_of(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; }))
tool.selected = 0;
}
@@ -513,6 +534,45 @@ void editor_mode::signal_move_update()
tool.moved_valid = track != nullptr && signal_drop_at(*found, *track, path, held, 0, tool.moved);
}
std::vector<editor_mode::signal_standing> editor_mode::signal_standing_now() const
{
std::vector<signal_standing> standing;
if (scene::Layers.empty() || m_signal.templates.empty())
return standing;
auto const found{standing_templates([this](std::string const &File) { return signal_template_of(File) != nullptr; })};
std::unordered_map<std::string, std::pair<TTrack *, int>> readers;
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved)
continue;
for (int list = 0; list < 6; ++list)
for (auto const &event : editor_track::events(*track, list))
if (false == event.first.empty())
readers.emplace(ToLower(event.first), std::make_pair(track, list));
}
for (auto const &include : found)
{
auto const *item{signal_template_of(include.file)};
if (item == nullptr)
continue;
signal_standing signal;
signal.include = include;
signal.kind = kind_index(item->kind);
signal.name = include.values.empty() ? std::string{} : include.values.front();
if (false == item->read.empty())
{
signal.read = editor_includes::substitute(item->read, include.values);
if (auto const reader{readers.find(ToLower(signal.read))}; reader != readers.end())
{
signal.track = reader->second.first;
signal.list = reader->second.second;
}
}
standing.push_back(std::move(signal));
}
return standing;
}
editor_mode::signal_standing const *editor_mode::signal_standing_of(scene::instance_handle const Instance) const
{
auto const &standing{m_signal.standing};
@@ -841,6 +901,287 @@ void editor_mode::signal_detach_events(std::string const &Read, std::vector<std:
}
}
void editor_mode::orphans_register(std::vector<TTrack *> const &Removed, std::vector<TTrack *> const &Kept)
{
auto const carries = [](TTrack &Track) {
for (int list = 0; list < 6; ++list)
for (auto const &event : editor_track::events(Track, list))
if (false == event.first.empty())
return true;
return false == Track.Isolated.empty();
};
// a record of a track removed again, after an undo brought it back, is the step's to make anew
m_orphans.erase(std::remove_if(m_orphans.begin(), m_orphans.end(), [&](orphan_record const &Record) { return std::find(Removed.begin(), Removed.end(), Record.track) != Removed.end(); }), m_orphans.end());
// a track which took the circuits over, removed by the step, leaves them to what the step made
for (auto &record : m_orphans)
if (std::any_of(record.given.begin(), record.given.end(), [&](TTrack const *Track) { return std::find(Removed.begin(), Removed.end(), Track) != Removed.end(); }))
{
record.circuits.clear();
record.given.clear();
}
std::vector<TTrack *> gone;
for (auto *track : Removed)
if (track != nullptr && track->m_editorremoved && false == track->m_paths.empty() && carries(*track))
gone.push_back(track);
if (gone.empty())
return;
// what the step moved onto the tracks left isn't lost
std::unordered_set<std::string> carried;
for (auto *track : simulation::Paths.sequence())
if (track != nullptr && false == track->m_editorremoved)
for (int list = 0; list < 6; ++list)
for (auto const &event : editor_track::events(*track, list))
carried.insert(ToLower(event.first));
std::unordered_set<std::string> circuits;
for (auto *track : Kept)
if (track != nullptr && false == track->m_editorremoved)
for (auto const *isolated : track->Isolated)
if (isolated != nullptr)
circuits.insert(ToLower(isolated->asName));
for (auto *track : gone)
{
orphan_record record;
record.track = track;
record.fresh = true;
for (int list = 0; list < 6; ++list)
for (auto const &event : editor_track::events(*track, list))
{
auto const name{ToLower(event.first)};
if (false == name.empty() && carried.count(name) == 0 && std::none_of(record.events.begin(), record.events.end(), [&](auto const &Event) { return Event.first == name; }))
record.events.emplace_back(name, list);
}
for (auto const *isolated : track->Isolated)
if (isolated != nullptr && circuits.count(ToLower(isolated->asName)) == 0)
record.circuits.push_back(isolated->asName);
if (record.events.empty() && record.circuits.empty())
continue;
m_orphans.push_back(std::move(record));
}
if (m_orphans.size() > kOrphanRecords)
m_orphans.erase(m_orphans.begin(), m_orphans.begin() + static_cast<std::ptrdiff_t>(m_orphans.size() - kOrphanRecords));
}
void editor_mode::orphans_take_over(std::vector<TTrack *> const &Created, std::vector<TTrack *> const &Changed)
{
// a record waits while its track stays deleted, so the step laying another one in its place, undone and laid again, gets it again
if (std::none_of(m_orphans.begin(), m_orphans.end(), [](orphan_record const &Record) { return Record.track->m_editorremoved; }))
return;
std::vector<TTrack *> candidates;
for (auto const *tracks : {&Created, &Changed})
for (auto *track : *tracks)
if (track != nullptr && false == track->m_editorremoved && false == track->m_paths.empty() && track->Dynamics.empty() && editor_track::is_supported(*track) &&
std::find(candidates.begin(), candidates.end(), track) == candidates.end())
candidates.push_back(track);
if (candidates.empty() && std::none_of(m_orphans.begin(), m_orphans.end(), [](orphan_record const &Record) { return Record.fresh; }))
return;
std::unordered_set<std::string> carried;
for (auto *track : simulation::Paths.sequence())
if (track != nullptr && false == track->m_editorremoved)
for (int list = 0; list < 6; ++list)
for (auto const &event : editor_track::events(*track, list))
carried.insert(ToLower(event.first));
// not the list of the Signals tool, nor its templates once found: the step taken may hold one of them; looked for once an event
// waits
std::vector<signal_standing> signals;
std::unordered_map<std::string, signal_standing const *> readers;
auto known{false};
auto const reader_of = [&](std::string const &Name) -> signal_standing const * {
if (false == known)
{
known = true;
if (m_signal.templates.empty())
signal_scan(false);
signals = signal_standing_now();
for (auto const &standing : signals)
if (false == standing.read.empty())
readers.emplace(ToLower(standing.read), &standing);
}
auto const found{readers.find(Name)};
return found != readers.end() ? found->second : nullptr;
};
std::vector<std::string> taken;
std::vector<std::string> left;
std::size_t taken_events{0};
std::size_t left_events{0};
std::vector<std::string> taken_circuits;
std::vector<std::string> left_circuits;
std::size_t deleted{0};
for (auto &record : m_orphans)
{
if (false == record.track->m_editorremoved)
{
record.fresh = false;
continue;
}
if (candidates.empty() && false == record.fresh)
continue;
auto const &old{*record.track};
deleted += record.fresh ? 1 : 0;
for (auto const &[name, list] : record.events)
{
if (carried.count(name) > 0)
continue;
auto const *standing{reader_of(name)};
auto got{false};
// an event no longer there, of a signal taken away, stays off the tracks
if (false == candidates.empty() && simulation::Events.FindEvent(name) != nullptr)
{
// the place on the old axis: by the signal reading it, else the middle of the track
int old_path{0};
double old_parameter{0.5};
auto const by_signal{standing != nullptr && plan_gap(old, standing->include.location, old_path, old_parameter) <= kSignalReach};
if (false == by_signal)
{
old_path = 0;
old_parameter = 0.5;
}
auto const anchor{bezier{old.m_paths[old_path]}.point(old_parameter)};
auto const old_forward{forward_at(old, old_path, old_parameter)};
TTrack *best{nullptr};
int best_path{0};
double best_parameter{0.0};
auto nearest{kOrphanReach};
for (auto *track : candidates)
{
int path{0};
double parameter{0.0};
auto const gap{plan_gap(*track, anchor, path, parameter)};
if (gap <= nearest && std::abs(glm::dot(forward_at(*track, path, parameter), old_forward)) > 0.9)
{
nearest = gap;
best = track;
best_path = path;
best_parameter = parameter;
}
}
if (best != nullptr)
{
// laid the other way round, the trains running towards its end read it at its start
auto const reversed{glm::dot(forward_at(*best, best_path, best_parameter), old_forward) < 0.0};
auto target{reversed && list % 3 != 0 ? list + (list % 3 == 1 ? 1 : -1) : list};
if (by_signal)
{
auto const &location{standing->include.location};
auto const axis{bezier{best->m_paths[best_path]}.point(nearest_on(*best, best_path, location))};
auto const side{glm::dot(plan_of(location) - plan_of(axis), right_of(travel_of(standing->include.yaw))) >= 0.0 ? 1 : -1};
signal_spot spot;
if (signal_drop_at(*standing, *best, best_path, location, side, spot))
target = spot.list + (list >= 3 ? 3 : 0);
}
editor_track::events(*best, target).emplace_back(name, nullptr);
std::string missing;
editor_track::bind_events(*best, missing);
best->mark_dirty();
carried.insert(name);
got = true;
}
}
if (got)
{
if (standing != nullptr)
taken.push_back(standing->name);
else
++taken_events;
}
else if (record.fresh)
{
if (standing != nullptr)
left.push_back(standing->name);
else
++left_events;
}
}
if (false == record.circuits.empty() && std::all_of(record.given.begin(), record.given.end(), [](TTrack const *Track) { return Track->m_editorremoved; }))
{
auto got{false};
// a track lying in the place of the old one for the most part: a new one, in the circuit of the track it was made from,
// or one without a circuit
for (auto *track : candidates)
{
auto const made{std::find(Created.begin(), Created.end(), track) != Created.end()};
if ((false == made && false == track->Isolated.empty()) || false == plan_overlaps(*track, old, 2.0))
continue;
auto const same{track->Isolated.size() == record.circuits.size() && std::equal(record.circuits.begin(), record.circuits.end(), track->Isolated.begin(), [](std::string const &Name, TIsolated const *Isolated) { return Isolated != nullptr && ToLower(Isolated->asName) == ToLower(Name); })};
if (same)
continue;
std::size_t on{0};
std::size_t all{0};
for (auto const &path : track->m_paths)
{
bezier const curve{path};
for (int k = 0; k <= 32; ++k)
{
int old_path{0};
double old_parameter{0.0};
++all;
if (plan_gap(old, curve.point(k / 32.0), old_path, old_parameter) <= kOrphanReach)
++on;
}
}
if (all == 0 || on * 2 < all)
continue;
editor_track::isolated(*track, record.circuits);
if (std::find(record.given.begin(), record.given.end(), track) == record.given.end())
record.given.push_back(track);
got = true;
}
if (got)
taken_circuits.insert(taken_circuits.end(), record.circuits.begin(), record.circuits.end());
else if (record.fresh)
left_circuits.insert(left_circuits.end(), record.circuits.begin(), record.circuits.end());
}
record.fresh = false;
}
auto const parts = [](std::vector<std::string> const &Signals, std::size_t const Events, std::vector<std::string> const &Circuits) {
std::vector<std::string> part;
if (false == Signals.empty())
part.push_back(format(STR_P("the read of signal %s", "the reads of signals %s", static_cast<long long>(Signals.size())), listed(Signals).c_str()));
if (Events > 0)
part.push_back(format(STR_P("%zu event", "%zu events", static_cast<long long>(Events)), Events));
if (false == Circuits.empty())
part.push_back(format(STR_P("the track circuit %s", "the track circuits %s", static_cast<long long>(Circuits.size())), listed(Circuits).c_str()));
return listed(part);
};
std::string note;
if (auto const got{parts(taken, taken_events, taken_circuits)}; false == got.empty())
note = format(STR_C("Taken over from the deleted track: %s"), got.c_str());
if (auto const lost{parts(left, left_events, left_circuits)}; false == lost.empty())
note += (note.empty() ? "" : "\n") +
format(STR_P("The deleted track carried %s: a track laid in its place takes it over, Ctrl+Z brings it back", "The deleted tracks carried %s: tracks laid in their place take it over, Ctrl+Z brings them back", static_cast<long long>(std::max<std::size_t>(deleted, 1))), lost.c_str());
if (false == note.empty())
m_orphan_note += (m_orphan_note.empty() ? "" : "\n") + note;
}
std::size_t editor_mode::signal_reattach(std::vector<scene::instance_handle> const &Instances)
{
signal_refresh(true);
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::size_t attached{0};
for (auto const instance : Instances)
{
auto const *standing{signal_standing_of(instance)};
if (standing == nullptr || standing->read.empty() || standing->track != nullptr || simulation::Events.FindEvent(ToLower(standing->read)) == nullptr)
continue;
signal_spot spot;
if (false == signal_standing_spot(*standing, spot) || spot.track == nullptr || false == spot.track->Dynamics.empty())
continue;
if (std::none_of(states.begin(), states.end(), [&](auto const &State) { return State.first == spot.track; }))
states.emplace_back(spot.track, editor_track::capture(*spot.track));
auto &events{editor_track::events(*spot.track, spot.list)};
if (std::none_of(events.begin(), events.end(), [&](auto const &Event) { return ToLower(Event.first) == ToLower(standing->read); }))
events.emplace_back(ToLower(standing->read), nullptr);
std::string missing;
editor_track::bind_events(*spot.track, missing);
spot.track->mark_dirty();
++attached;
}
if (states.empty())
return 0;
push_track_snapshot(std::move(states));
signal_refresh(true);
return attached;
}
bool editor_mode::signal_delete(scene::instance_handle const Instance)
{
if (false == scene::Layers.tracked(Instance) || false == scene::Layers.removable(Instance))
@@ -922,6 +1263,7 @@ bool editor_mode::standing_signals(std::vector<standing_signal> &Signals, bool c
signal.file = standing.include.file;
signal.track = standing.track;
signal.facing = standing.track == nullptr ? 0 : standing.list % 3 == 2 ? 1 : standing.list % 3 == 1 ? -1 : 0;
signal.read = standing.read;
}
return true;
}

View File

@@ -50,6 +50,7 @@ double constexpr kLabelRange{700.0}; // m, of the names shown there, farther one
double constexpr kRailHead{0.18}; // m, above the axis of a path
double constexpr kLineLimit{30000.0}; // m, of a line followed out to the next station
double constexpr kGapReach{1500.0}; // m, the tracks going on beyond the end of a line found within it
double constexpr kBlockShare{1.1}; // of the braking distance, the block sections towards the end of the tracks
double constexpr kPredicted{305.0}; // m, out from the next station to its entry signal, while it has none
// the events of a block section, switching the block signal guarding it
@@ -1151,6 +1152,7 @@ void srk_tool::render_options()
metric_tiles({{format("%zu", layout.switches.size()), STR_C("switches"), kSwitch},
{format("%zu", tracks.size() - layout.switches.size()), STR_C("tracks"), kPlain},
{format("%.0f m", layout.to - layout.from), STR_C("length"), ImGui::GetColorU32(ImGuiCol_Text)}});
render_problems();
if (begin_fields("##srkstation"))
{
auto const text = [&](char const *Label, char const *Id, std::string const &Value, char const *Hint, auto const &Set) {
@@ -2784,6 +2786,10 @@ void srk_tool::survey_lines(std::vector<standing_signal> const &Standing)
if (k > 0 && row.sections.size() > 1 && row.sections.back().from == line.distances[k])
row.sections[row.sections.size() - 2].to = line.distances[k];
}
std::vector<std::pair<double, double>> bare;
for (std::size_t k = 1; k + 1 < row.sections.size(); ++k)
if (row.sections[k].name.empty() && row.sections[k].from >= 0.0 && row.sections[k].to - row.sections[k].from >= 1.0)
bare.emplace_back(row.sections[k].from, row.sections[k].to);
row.sections.erase(std::remove_if(row.sections.begin(), row.sections.end(), [](line_row::run const &Run) { return Run.name.empty() || Run.to - std::max(0.0, Run.from) < 1.0; }), row.sections.end());
for (auto &run : row.sections)
{
@@ -2791,6 +2797,8 @@ void srk_tool::survey_lines(std::vector<standing_signal> const &Standing)
run.km_to = km_along(line, row.km_number, run.to);
}
row.legacy = false == row.stops.empty() && false == switched;
if (false == row.legacy && row.block != block_kind::semi)
row.bare = std::move(bare);
auto const next{next_entry(line, Standing, row.length)};
row.next = next >= 0 ? signal_shown(Standing[next].name, stem) : std::string{};
m_lines.push_back(std::move(row));
@@ -2820,6 +2828,125 @@ void srk_tool::survey_lines(std::vector<standing_signal> const &Standing)
}
}
void srk_tool::survey_problems(std::vector<standing_signal> const &Standing)
{
m_problems.clear();
if (false == m_layout.has_value())
return;
auto const stem{stem_of_open()};
auto const plan = [](glm::dvec3 const &Point) { return glm::dvec2{Point.x, Point.z}; };
auto const by_open = [&](glm::dvec3 const &Point) {
if (gap_to(*m_layout, Point) <= 8.0)
return true;
return std::any_of(m_line_ways.begin(), m_line_ways.end(), [&](station::line const &Line) {
double gap{0.0};
auto const distance{station::distance_on(Line, Point, gap)};
return gap <= 8.0 && distance > -50.0 && distance < Line.length() + 1.0;
});
};
for (auto const &signal : Standing)
if (false == signal.read.empty() && signal.track == nullptr && by_open(signal.position))
m_problems.push_back({format(STR_C("Signal %s: no track reads it"), signal_shown(signal.name, stem).c_str()), signal.position, signal.instance});
for (std::size_t index = 0; index < m_lines.size() && index < m_line_ways.size(); ++index)
{
auto const &row{m_lines[index]};
auto const &line{m_line_ways[index]};
if (false == row.station && line.points.size() > 1 && line.length() < kLineLimit - 50.0)
{
// the tracks go on beyond a gap: one missing, deleted maybe
auto const end{line.points.back()};
auto const step{plan(line.points.back() - line.points[line.points.size() - 2])};
if (glm::length(step) > 1e-9)
{
auto const way{glm::normalize(step)};
std::vector<TTrack const *> own(line.tracks.begin(), line.tracks.end());
for (auto const &target : editor_track::free_ends(nullptr, 1, end, kGapReach, own))
{
auto const ahead{plan(target.position - end)};
auto const along{glm::dot(ahead, way)};
auto const direction{plan(target.direction)};
if (along < 5.0 || along < glm::length(ahead) * 0.8 || glm::length(direction) < 1e-9 || std::abs(glm::dot(glm::normalize(direction), way)) < 0.7)
continue;
m_problems.push_back({format(STR_C("The line out of %s breaks off %.0f m beyond it, the tracks go on %.0f m farther"), row.entry.c_str(), line.length(), glm::length(ahead)), end});
break;
}
}
}
for (auto const &bare : row.bare)
m_problems.push_back({format(STR_C("The line out of %s: %.0f m of tracks %.0f m beyond the signal are in no block section"), row.entry.c_str(), bare.second - bare.first, bare.first), station::point_on(line, (bare.first + bare.second) * 0.5).point, 0, true});
}
// the block sections their controls switch, the tracks gone
std::unordered_set<std::string> circuits;
for (auto *track : simulation::Paths.sequence())
if (track != nullptr && false == track->m_editorremoved)
for (auto const *isolated : track->Isolated)
if (isolated != nullptr)
circuits.insert(ToLower(isolated->asName));
for (auto const &control : m_templates.standing_templates([](std::string const &File) { return File == kControl; }))
if (false == control.values.empty() && circuits.count(ToLower(control.values[0])) == 0 && by_open(control.location))
m_problems.push_back({format(STR_C("Block section %s has no track"), control.values[0].c_str()), control.location});
}
void srk_tool::render_problems()
{
if (m_problems.empty())
return;
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, kWrong);
auto const open{ImGui::CollapsingHeader(format("%s (%zu)###srkproblems", STR_C("To repair"), m_problems.size()).c_str(), ImGuiTreeNodeFlags_DefaultOpen)};
ImGui::PopStyleColor();
item_tooltip("What the station and its lines need yet: signals no track reads, lines broken off where a track is missing, tracks of a line of the automatic block in no block section, block sections whose tracks are gone. Click a row to see the place");
if (false == open)
return;
for (std::size_t index = 0; index < m_problems.size(); ++index)
{
auto const &problem{m_problems[index]};
ImGui::PushStyleColor(ImGuiCol_Text, problem.signal != 0 || problem.lines ? kDoubtful : kWrong);
if (ImGui::Selectable((problem.text + "##srkproblem" + std::to_string(index)).c_str()))
m_view.fly_to(problem.at + glm::dvec3{0.0, 4.0, 0.0}, glm::normalize(glm::dvec3{0.3, -0.5, 0.8}), 60.0);
ImGui::PopStyleColor();
}
auto const fixable{static_cast<std::size_t>(std::count_if(m_problems.begin(), m_problems.end(), [](problem const &Problem) { return Problem.signal != 0 || Problem.lines; }))};
if (fixable == 0)
{
ImGui::TextDisabled("%s", STR_C("A track missing is laid in the Track tool: laid in the place of a deleted one it takes over its events and its track circuit"));
return;
}
if (main_button(format("%s (%zu)###srkfix", STR_C("Repair"), fixable).c_str(), ImVec2(-1.0f, 0.0f)))
fix_problems();
item_tooltip("The signals no track reads are read by the track they stand by again, as moving them does, the lines with tracks in no block section get their line signals set again. A track missing is laid in the Track tool. A step of undo each");
}
void srk_tool::fix_problems()
{
std::vector<scene::instance_handle> signals;
auto lines{false};
for (auto const &problem : m_problems)
{
if (problem.signal != 0)
signals.push_back(problem.signal);
lines = lines || problem.lines;
}
std::vector<std::string> done;
if (false == signals.empty())
{
auto const read{m_signals.signal_reattach(signals)};
done.push_back(format(STR_P("%zu of %zu signal read again", "%zu of %zu signals read again", static_cast<long long>(signals.size())), read, signals.size()));
if (read < signals.size())
done.back() += STR(": the rest stand farther than 10 m from a track, by vehicles or their events are gone");
}
auto const status{done.empty() ? std::string{} : done.front()};
if (lines)
{
m_status.clear();
set_lines();
if (false == status.empty())
m_status = status + (m_status.empty() ? "" : "\n" + m_status);
}
else
m_status = status;
}
void srk_tool::set_lines(block_edit const *Edit)
{
m_status.clear();
@@ -3506,6 +3633,7 @@ void srk_tool::survey_signals()
m_heads_known = false;
m_lines.clear();
m_line_ways.clear();
m_problems.clear();
m_lines_problem = STR("Looking for the signal templates...");
if (false == m_measured)
measure({});
@@ -3513,6 +3641,7 @@ void srk_tool::survey_signals()
}
check_heads(standing);
survey_lines(standing);
survey_problems(standing);
auto const stem{stem_of_open()};
std::vector<station::signal> stops;
for (auto const &signal : standing)
@@ -4881,7 +5010,48 @@ editor_selftest_result srk_tool::selftest(std::string const &Command, std::istri
WriteLog("SELFTEST srkcheck: block section " + control.values[0] + " switched by " + control.values[index] + ", which doesn't stand");
}
}
WriteLog(format("SELFTEST srkcheck: %zu signals, %zu block sections, %zu bad", standing.size(), controls.size(), bad));
std::size_t unread{0};
for (auto const &signal : standing)
if (false == signal.read.empty() && signal.track == nullptr)
{
++unread;
WriteLog("SELFTEST srkcheck: signal " + signal.name + " read by no track, its event " + signal.read);
}
std::unordered_set<std::string> circuits;
for (auto *track : simulation::Paths.sequence())
if (track != nullptr && false == track->m_editorremoved)
for (auto const *isolated : track->Isolated)
if (isolated != nullptr)
circuits.insert(ToLower(isolated->asName));
std::size_t bare{0};
for (auto const &control : controls)
if (false == control.values.empty() && circuits.count(ToLower(control.values[0])) == 0)
{
++bare;
WriteLog("SELFTEST srkcheck: block section " + control.values[0] + " without a track");
}
bad += unread + bare;
if (m_layout.has_value())
{
survey_signals();
for (auto const &problem : m_problems)
WriteLog(format("SELFTEST srkcheck: open station: %s at %.1f %.1f%s%s", problem.text.c_str(), problem.at.x, problem.at.z, problem.signal != 0 ? " (signal)" : "", problem.lines ? " (lines)" : ""));
}
WriteLog(format("SELFTEST srkcheck: %zu signals, %zu block sections, %zu unread, %zu without a track, %zu problems of the open station, %zu bad", standing.size(), controls.size(), unread, bare, m_problems.size(), bad));
return editor_selftest_result::handled;
}
if (Command == "srkfix")
{
// srkfix: the Repair of the check of the open station
if (false == m_layout.has_value())
{
WriteLog("SELFTEST srkfix: no station open");
return editor_selftest_result::handled;
}
survey_signals();
auto const before{m_problems.size()};
fix_problems();
WriteLog(format("SELFTEST srkfix: %zu problems before, status %s", before, m_status.c_str()));
return editor_selftest_result::handled;
}
if (Command == "srkstrip")

View File

@@ -226,6 +226,7 @@ class srk_tool final : public editor_tool
double km_to{0.0};
};
std::vector<run> sections; // the track circuits along it, out from the station
std::vector<std::pair<double, double>> bare; // out along it, tracks between its block sections in none, of the automatic block
struct post
{
double distance{0.0}; // out along the line
@@ -244,6 +245,16 @@ class srk_tool final : public editor_tool
std::vector<station::line> m_line_ways; // of the lines listed
std::unordered_set<std::string> m_signal_names; // of the signals of the scenery, lowercase: a block signal drawn on one's number takes a letter
std::string m_lines_problem; // why the station has no lines listed
// what the check of the open station finds wrong: signals no track reads, lines broken off, tracks between the block sections of
// a line in none, block sections without a track
struct problem
{
std::string text;
glm::dvec3 at{0.0};
scene::instance_handle signal{0}; // Repair has a track read it again
bool lines{false}; // Repair sets the line signals again
};
std::vector<problem> m_problems;
bool m_drawing{false}; // the block boundaries drawn by hand
// a block boundary under the mouse while they are drawn: block signals across the line there, or the ones standing taken away
struct boundary
@@ -334,6 +345,11 @@ class srk_tool final : public editor_tool
// the lines out of the open station, for the panel
void survey_lines(std::vector<standing_signal> const &Standing);
void render_lines();
void survey_problems(std::vector<standing_signal> const &Standing);
void render_problems();
// the signals no track reads read by the tracks they stand by, the lines with tracks in no block section set again; a step of
// the history each
void fix_problems();
// the SHP track resonators placed where the signals checked need them and have none: 200 m before an entry signal, a block
// signal and a warning disc, at an exit signal onto a line of the automatic block, the track cut where none of its joints lies
// within 5 m, its event put on the track the trains run into there; one step of the history

View File

@@ -277,6 +277,14 @@ void editor_mode::push_track_snapshot(std::vector<std::pair<TTrack *, editor_tra
if (States.empty() && Created.empty() && Removed.empty())
return;
// what the tracks removed carried goes to the ones laid in their place, now or later, in the step which lays them
std::vector<TTrack *> changed;
for (auto const &entry : States)
changed.push_back(entry.first);
auto kept{Created};
kept.insert(kept.end(), changed.begin(), changed.end());
orphans_register(Removed, kept);
orphans_take_over(Created, changed);
trim_history();
auto *track = false == States.empty() ? States.front().first : false == Created.empty() ? Created.front() : Removed.front();
EditorSnapshot snap;

View File

@@ -105,6 +105,7 @@ struct standing_signal
std::string file; // of its template
TTrack *track{nullptr}; // the trains read it on, nullptr while none does
int facing{0}; // the trains it faces run 1: towards the end of the path of the track, -1: towards its start, 0 unknown
std::string read; // its event the trains read, empty for a template without one
};
// a signal template as the signalling picks one: the lights it has and the speeds its events show
struct signal_head
@@ -216,6 +217,8 @@ class editor_signals
virtual std::size_t signal_batch(signal_changes const &Changes, std::vector<std::pair<TTrack *, editor_track::state>> States, std::vector<TTrack *> Created, std::string &Error) = 0;
// the signal templates with lights, false while they are still being looked for
virtual bool signal_heads(std::vector<signal_head> &Heads, bool const Wait) = 0;
// the signals given which no track reads, read again by the track they stand by: one step of the history. returns: how many
virtual std::size_t signal_reattach(std::vector<scene::instance_handle> const &Instances) = 0;
protected:
~editor_signals() = default;

View File

@@ -1027,28 +1027,51 @@ void editor_ui::set_status(std::string const &Status, bool const Error)
{
m_layerspanel.status = Status;
m_layerspanel.status_error = Error;
if (ImGui::GetCurrentContext() != nullptr)
m_statustime = ImGui::GetTime();
if (Error && false == Status.empty())
{
m_refusal = Status;
m_refusaltime = ImGui::GetTime();
m_refusalnotice = false;
}
}
void editor_ui::set_notice(std::string const &Notice)
{
if (Notice.empty() || ImGui::GetCurrentContext() == nullptr)
return;
auto &status{m_layerspanel.status};
// the status of the operation it follows, told a moment ago, stays above it
if (ImGui::GetTime() - m_statustime < 1.0 && false == status.empty())
status += "\n" + Notice;
else
{
status = Notice;
m_layerspanel.status_error = false;
}
m_statustime = ImGui::GetTime();
m_refusal = Notice;
m_refusaltime = ImGui::GetTime();
m_refusalnotice = true;
}
void editor_ui::render_refusal()
{
auto const age{ImGui::GetTime() - m_refusaltime};
if (m_refusal.empty() || age > 4.0)
auto const shown{m_refusalnotice ? 7.0 : 4.0};
if (m_refusal.empty() || age > shown)
return;
auto const &io{ImGui::GetIO()};
auto const alpha{static_cast<float>(std::clamp(4.0 - age, 0.0, 1.0))};
auto const alpha{static_cast<float>(std::clamp(shown - age, 0.0, 1.0))};
auto const wrap{std::round(ImGui::GetFontSize() * 30.0f)};
auto const size{ImGui::CalcTextSize(m_refusal.c_str(), nullptr, false, wrap)};
ImVec2 at{io.MousePos.x + 20.0f, io.MousePos.y - 24.0f - size.y};
at.x = std::clamp(at.x, 10.0f, std::max(10.0f, io.DisplaySize.x - size.x - 10.0f));
at.y = std::clamp(at.y, 10.0f, std::max(10.0f, io.DisplaySize.y - size.y - 10.0f));
auto *draw{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())};
draw->AddRectFilled(ImVec2(at.x - 6.0f, at.y - 4.0f), ImVec2(at.x + size.x + 6.0f, at.y + size.y + 4.0f), IM_COL32(40, 0, 0, static_cast<int>(210 * alpha)), 4.0f);
draw->AddText(ImGui::GetFont(), ImGui::GetFontSize(), at, IM_COL32(255, 150, 130, static_cast<int>(255 * alpha)), m_refusal.c_str(), nullptr, wrap);
draw->AddRectFilled(ImVec2(at.x - 6.0f, at.y - 4.0f), ImVec2(at.x + size.x + 6.0f, at.y + size.y + 4.0f), m_refusalnotice ? IM_COL32(45, 32, 0, static_cast<int>(215 * alpha)) : IM_COL32(40, 0, 0, static_cast<int>(210 * alpha)), 4.0f);
draw->AddText(ImGui::GetFont(), ImGui::GetFontSize(), at, m_refusalnotice ? IM_COL32(255, 205, 110, static_cast<int>(255 * alpha)) : IM_COL32(255, 150, 130, static_cast<int>(255 * alpha)), m_refusal.c_str(), nullptr, wrap);
}
void editor_ui::render_rotation_controls()

View File

@@ -162,6 +162,8 @@ class editor_ui : public ui_layer
static void register_settings();
// shows outcome of an operation in the layers window
void set_status(std::string const &Status, bool const Error = false);
// a warning after an operation done: added to its status, shown by the cursor for a while
void set_notice(std::string const &Notice);
private:
// methods
@@ -211,6 +213,8 @@ class editor_ui : public ui_layer
bool m_layoutreset{false}; // the default layout is to be built again on the next frame
std::string m_refusal;
double m_refusaltime{-100.0};
bool m_refusalnotice{false}; // a warning rather than a refusal
double m_statustime{-100.0};
bool m_tooloptionsopen{true};
bool m_inspectoropen{true};
bool m_sceneopen{true};

View File

@@ -11394,3 +11394,66 @@ msgstr[2] "%zu dostało szablony o stawności swoich szlaków"
msgid "Ctrl+Z takes it back"
msgstr "Ctrl+Z cofa"
msgid "the read of signal %s"
msgid_plural "the reads of signals %s"
msgstr[0] "odczyt semafora %s"
msgstr[1] "odczyty semaforów %s"
msgstr[2] "odczyty semaforów %s"
msgid "%zu event"
msgid_plural "%zu events"
msgstr[0] "%zu zdarzenie"
msgstr[1] "%zu zdarzenia"
msgstr[2] "%zu zdarzeń"
msgid "the track circuit %s"
msgid_plural "the track circuits %s"
msgstr[0] "odcinek izolowany %s"
msgstr[1] "odcinki izolowane %s"
msgstr[2] "odcinki izolowane %s"
msgid "Taken over from the deleted track: %s"
msgstr "Przejęte po usuniętym torze: %s"
msgid "The deleted track carried %s: a track laid in its place takes it over, Ctrl+Z brings it back"
msgid_plural "The deleted tracks carried %s: tracks laid in their place take it over, Ctrl+Z brings them back"
msgstr[0] "Usunięty tor niósł %s: tor ułożony w tym miejscu przejmie to sam, Ctrl+Z cofa"
msgstr[1] "Usunięte tory niosły %s: tory ułożone w tym miejscu przejmą to same, Ctrl+Z cofa"
msgstr[2] "Usunięte tory niosły %s: tory ułożone w tym miejscu przejmą to same, Ctrl+Z cofa"
msgid "Signal %s: no track reads it"
msgstr "Semafor %s: żaden tor go nie odczytuje"
msgid "The line out of %s breaks off %.0f m beyond it, the tracks go on %.0f m farther"
msgstr "Szlak za semaforem %s urywa się po %.0f m, tory biegną dalej %.0f m za przerwą"
msgid "The line out of %s: %.0f m of tracks %.0f m beyond the signal are in no block section"
msgstr "Szlak za semaforem %s: %.0f m torów %.0f m za semaforem nie należy do żadnego odstępu"
msgid "Block section %s has no track"
msgstr "Odstęp %s nie ma już toru"
msgid "To repair"
msgstr "Do naprawy"
msgid "What the station and its lines need yet: signals no track reads, lines broken off where a track is missing, tracks of a line of the automatic block in no block section, block sections whose tracks are gone. Click a row to see the place"
msgstr "Czego stacji i jej szlakom jeszcze brakuje: semafory, których żaden tor nie odczytuje, szlaki urwane tam, gdzie brakuje toru, tory szlaku z SBL poza odstępami, odstępy, których tory zniknęły. Kliknij wiersz, żeby zobaczyć miejsce"
msgid "A track missing is laid in the Track tool: laid in the place of a deleted one it takes over its events and its track circuit"
msgstr "Brakujący tor układa się narzędziem Tor: ułożony w miejscu usuniętego przejmuje jego zdarzenia i odcinek izolowany"
msgid "Repair"
msgstr "Napraw"
msgid "The signals no track reads are read by the track they stand by again, as moving them does, the lines with tracks in no block section get their line signals set again. A track missing is laid in the Track tool. A step of undo each"
msgstr "Semafory, których żaden tor nie odczytuje, znów odczyta tor, przy którym stoją, tak jak po ich przesunięciu; szlakom z torami poza odstępami sygnalizacja zostanie ustawiona jeszcze raz. Brakujący tor układa się narzędziem Tor. Każda poprawka to jeden krok cofania"
msgid "%zu of %zu signal read again"
msgid_plural "%zu of %zu signals read again"
msgstr[0] "Odczyt przywrócony: %zu z %zu semafora"
msgstr[1] "Odczyt przywrócony: %zu z %zu semaforów"
msgstr[2] "Odczyt przywrócony: %zu z %zu semaforów"
msgid ": the rest stand farther than 10 m from a track, by vehicles or their events are gone"
msgstr ": pozostałe stoją dalej niż 10 m od toru, przy torze z pojazdami albo ich zdarzenia już nie istnieją"