17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-27 14:07:28 +01:00
Files
maszyna/application/editormodesignal.cpp
maj00r 2d9be3ae10 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.
2026-10-22 22:40:01 +02:00

1822 lines
73 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "application/editormode.h"
#include "application/editoruilayer.h"
#include "application/editorprojection.h"
#include "application/editormodeshared.h"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "editor/editorIncludeInfo.hpp"
#include "editor/editorSettings.hpp"
#include "rendering/renderer.h"
#include "scene/scenelayers.h"
#include "simulation/simulation.h"
#include "utilities/Globals.h"
#include "utilities/utilities.h"
#include "world/Event.h"
#include "world/Track.h"
#include "imgui/imgui.h"
#include "utilities/translation.h"
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <limits>
#include <unordered_map>
#include <unordered_set>
namespace
{
using geometry::bezier;
using geometry::heading_of;
using geometry::plan_of;
double const kRailTop{0.18};
double const kSignalRange{1500.0};
double const kSignalHead{4.0};
float const kSignalPick{12.0f};
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"};
int const kKindCount{7};
int kind_index(std::string const &Kind)
{
for (int i = 0; i < kKindCount; ++i)
if (Kind == kKinds[i])
return i;
return -1;
}
double nearest_on(TTrack const &Track, int const Path, glm::dvec3 const &Point)
{
bezier const curve{Track.m_paths[Path]};
auto parameter{0.0};
auto best{std::numeric_limits<double>::max()};
for (int k = 0; k <= 256; ++k)
{
auto const distance{geometry::plan_distance(curve.point(k / 256.0), Point)};
if (distance < best)
{
best = distance;
parameter = k / 256.0;
}
}
return parameter;
}
glm::dvec2 forward_at(TTrack const &Track, int const Path, double const Parameter)
{
auto const tangent{bezier{Track.m_paths[Path]}.first(Parameter)};
glm::dvec2 const planar{tangent.x, tangent.z};
return glm::length(planar) > 1e-9 ? glm::normalize(planar) : glm::dvec2{0.0, 1.0};
}
glm::dvec2 right_of(glm::dvec2 const &Direction)
{
return {-Direction.y, Direction.x};
}
glm::dvec2 travel_of(double const Yaw)
{
auto const yaw{glm::radians(Yaw + 180.0)};
return {std::sin(yaw), std::cos(yaw)};
}
int travel_side(int const Side, int const List)
{
return Side * (List == 2 ? 1 : -1);
}
int lean_wanted(int const Side)
{
return Side > 0 ? 1 : 2;
}
int lean_of(std::string const &Lean)
{
return Lean == "left" ? 1 : Lean == "right" ? 2 : 0;
}
std::size_t edit_distance(std::string const &A, std::string const &B)
{
std::vector<std::size_t> row(B.size() + 1);
for (std::size_t j = 0; j <= B.size(); ++j)
row[j] = j;
for (std::size_t i = 1; i <= A.size(); ++i)
{
auto diagonal{row[0]};
row[0] = i;
for (std::size_t j = 1; j <= B.size(); ++j)
{
auto const above{row[j]};
row[j] = std::min({row[j] + 1, row[j - 1] + 1, diagonal + (A[i - 1] == B[j - 1] ? 0 : 1)});
diagonal = above;
}
}
return row[B.size()];
}
ImU32 lamp_colour(char const Lamp)
{
switch (Lamp)
{
case 'z': return IM_COL32(40, 210, 80, 255);
case 'c': return IM_COL32(230, 40, 40, 255);
case 'p': return IM_COL32(255, 160, 30, 255);
case 'b': return IM_COL32(235, 235, 235, 255);
case 'n': return IM_COL32(60, 110, 255, 255);
default: return IM_COL32(70, 70, 70, 255);
}
}
std::string group_of(std::string const &Mount, std::string const &Lamps)
{
return Mount + "|" + Lamps;
}
std::vector<std::string> const &plate_textures()
{
static std::vector<std::string> plates;
static bool scanned{false};
if (scanned)
return plates;
scanned = true;
std::error_code error;
for (std::filesystem::directory_iterator file{"textures/tabl", error}, end; file != end; file.increment(error))
{
std::string stem;
try
{
stem = ToLower(file->path().stem().string());
}
catch (std::exception const &)
{
continue;
}
if (std::find(plates.begin(), plates.end(), stem) == plates.end())
plates.push_back(stem);
}
std::sort(plates.begin(), plates.end());
return plates;
}
std::string plate_for(std::string const &Plate, std::string const &Name)
{
if (false == Plate.empty())
return Plate;
auto const &plates{plate_textures()};
auto const name{ToLower(Name)};
return std::binary_search(plates.begin(), plates.end(), name) ? name : std::string{"0_nothing"};
}
void draw_signal_icon(ImDrawList *Draw, ImVec2 const Origin, ImVec2 const Size, std::string const &Mount, std::string const &Lamps, ImU32 const Frame)
{
auto const lamps{std::max<int>(1, static_cast<int>(Lamps.size()))};
auto const centre{Origin.x + Size.x * 0.5f};
auto const bottom{Origin.y + Size.y - 4.0f};
auto const lamp{std::min(7.0f, (Size.y - 26.0f) / (lamps * 2.2f))};
auto const headwidth{lamp * 2.0f + 6.0f};
auto const headheight{lamps * lamp * 2.2f + 4.0f};
ImU32 const pole{IM_COL32(150, 150, 150, 255)};
float headtop;
if (Mount == "dwarf")
{
headtop = bottom - headheight;
Draw->AddRectFilled(ImVec2(centre - headwidth * 0.7f, bottom - 3.0f), ImVec2(centre + headwidth * 0.7f, bottom), pole);
}
else if (Mount == "gantry")
{
headtop = Origin.y + 10.0f;
Draw->AddLine(ImVec2(Origin.x + 4.0f, Origin.y + 6.0f), ImVec2(Origin.x + Size.x - 4.0f, Origin.y + 6.0f), pole, 3.0f);
Draw->AddLine(ImVec2(centre, Origin.y + 6.0f), ImVec2(centre, headtop), pole, 2.0f);
}
else
{
headtop = Origin.y + 6.0f;
Draw->AddLine(ImVec2(centre, headtop + headheight), ImVec2(centre, bottom), pole, 3.0f);
Draw->AddLine(ImVec2(centre - 6.0f, bottom), ImVec2(centre + 6.0f, bottom), pole, 2.0f);
}
if (Lamps.empty())
{
Draw->AddLine(ImVec2(centre, headtop), ImVec2(centre, headtop + headheight), pole, 3.0f);
Draw->AddLine(ImVec2(centre, headtop + 6.0f), ImVec2(centre + 14.0f, headtop - 2.0f), IM_COL32(230, 60, 60, 255), 4.0f);
return;
}
Draw->AddRectFilled(ImVec2(centre - headwidth * 0.5f, headtop), ImVec2(centre + headwidth * 0.5f, headtop + headheight), IM_COL32(25, 25, 25, 255), 3.0f);
Draw->AddRect(ImVec2(centre - headwidth * 0.5f, headtop), ImVec2(centre + headwidth * 0.5f, headtop + headheight), Frame, 3.0f);
for (int i = 0; i < static_cast<int>(Lamps.size()); ++i)
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)
{
auto &tool{m_signal};
if (Wait)
EditorIncludes.wait();
else
EditorIncludes.start();
if (false == EditorIncludes.scanned() || tool.revision == EditorIncludes.revision())
return false;
tool.revision = EditorIncludes.revision();
std::string chosen[kKindCount];
for (int i = 0; i < kKindCount; ++i)
if (tool.chosen[i] >= 0 && tool.chosen[i] < static_cast<int>(tool.templates.size()))
chosen[i] = tool.templates[tool.chosen[i]].file;
tool.templates.clear();
for (auto const &entry : EditorIncludes.entries())
{
if (entry.category != "signal" || false == entry.complete)
continue;
include_info info;
std::string error;
if (false == editor_includes::load(entry.file, info, error) || kind_index(info.signal_kind) < 0)
continue;
signal_template item;
item.file = entry.file;
item.name = info.name.empty() ? entry.file : info.name;
item.description = info.description;
item.kind = info.signal_kind;
item.mount = info.signal_mount.empty() ? std::string{"mast"} : info.signal_mount;
item.lamps = info.signal_lamps;
item.lean = info.signal_lean;
item.read = info.signal_read;
for (auto const &parameter : info.parameters)
{
item.parameters = std::max(item.parameters, parameter.id);
if (parameter.id == 6 && parameter.role == "texture")
item.plate = true;
if (parameter.id == 7)
item.linked = true;
}
editor_includes::signal_speeds(entry.file, item.speeds);
if (item.kind == "block")
item.aspects = editor_includes::block_aspects(entry.file);
tool.templates.push_back(std::move(item));
}
std::sort(tool.templates.begin(), tool.templates.end(), [](signal_template const &A, signal_template const &B) {
return std::make_tuple(kind_index(A.kind), A.lamps.empty(), A.mount != "mast", A.lamps.size(), A.lamps, A.name.size(), A.name) < std::make_tuple(kind_index(B.kind), B.lamps.empty(), B.mount != "mast", B.lamps.size(), B.lamps, B.name.size(), B.name);
});
for (int i = 0; i < kKindCount; ++i)
{
tool.chosen[i] = -1;
std::unordered_map<std::string, int> sizes;
for (auto const &item : tool.templates)
if (item.kind == kKinds[i])
++sizes[group_of(item.mount, item.lamps)];
auto best{-1};
for (int k = 0; k < static_cast<int>(tool.templates.size()); ++k)
{
auto const &item{tool.templates[k]};
if (item.kind != kKinds[i])
continue;
if (item.file == chosen[i])
{
best = k;
break;
}
if (best < 0 || sizes[group_of(item.mount, item.lamps)] > sizes[group_of(tool.templates[best].mount, tool.templates[best].lamps)])
best = k;
}
tool.chosen[i] = best;
}
return true;
}
editor_mode::signal_template const *editor_mode::signal_armed() const
{
auto const &tool{m_signal};
if (tool.kind < 0 || tool.kind >= kKindCount)
return nullptr;
auto const index{tool.chosen[tool.kind]};
return index >= 0 && index < static_cast<int>(tool.templates.size()) ? &tool.templates[index] : nullptr;
}
editor_mode::signal_template const *editor_mode::signal_template_of(std::string const &File) const
{
auto const file{ToLower(File)};
for (auto const &item : m_signal.templates)
if (item.file == file)
return &item;
return nullptr;
}
bool editor_mode::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
{
if (Track.m_paths.empty() || Track.m_editorremoved)
return false;
auto const path{std::clamp(Path, 0, static_cast<int>(Track.m_paths.size()) - 1)};
auto const parameter{nearest_on(Track, path, Point)};
auto const axis{bezier{Track.m_paths[path]}.point(parameter)};
auto const forward{forward_at(Track, path, parameter)};
auto const right{right_of(forward)};
auto side{Side};
if (side == 0)
side = glm::dot(plan_of(Cursor) - plan_of(axis), right) >= 0.0 ? 1 : -1;
auto facing{Facing};
if (facing == 0)
facing = side;
Spot.track = &Track;
Spot.path = path;
Spot.axis = axis;
Spot.travel = forward * static_cast<double>(facing);
Spot.side = side;
Spot.list = facing > 0 ? 2 : 1;
Spot.offset = Template.mount == "gantry" ? 0.0 : EditorSettings.signal_offset();
auto const across{right * static_cast<double>(side) * Spot.offset};
Spot.position = {axis.x + across.x, axis.y + kRailTop + (Template.mount == "gantry" ? static_cast<double>(m_signal.height) : 0.0), axis.z + across.y};
auto const yaw{heading_of(Spot.travel) + 180.0};
Spot.yaw = yaw >= 360.0 ? yaw - 360.0 : yaw;
return true;
}
std::string editor_mode::signal_intent_label(signal_template const &Template, signal_spot const &Spot) const
{
auto const name{m_signal.name[0] != '\0' ? std::string{m_signal.name} : std::string{"?"}};
auto const right{glm::dot(right_of(Spot.travel), plan_of(Spot.position) - plan_of(Spot.axis)) >= 0.0};
auto const where{right ? STR(" on the right of the trains") : STR(" on the left of the trains")};
auto variant{signal_variant(Template, Spot)};
variant = variant.substr(0, variant.rfind('.'));
if (Template.read.empty())
return format(STR_C("Click: %s %s (%s)%s, the train doesn't read it"), STR(kKindLabels[kind_index(Template.kind)]).c_str(), name.c_str(), variant.c_str(), where.c_str()) + "\n" + STR("Drag along: the trains it faces run the way of the drag");
auto const event{editor_includes::substitute(Template.read, {name})};
auto const path{name_of(*Spot.track)};
return format(STR_C("Click: %s %s (%s)%s, read by %s %s of %s"), STR(kKindLabels[kind_index(Template.kind)]).c_str(), name.c_str(), variant.c_str(), where.c_str(), editor_track::event_keyword(Spot.list), event.c_str(), path.c_str()) + "\n" + STR("Drag along: the trains it faces run the way of the drag");
}
void editor_mode::signal_refresh(bool const Force)
{
auto &tool{m_signal};
auto const scanned{signal_scan(false)};
auto const now{ImGui::GetTime()};
if (false == Force && false == scanned && tool.standing_history == m_history.size() && tool.standing_redo == g_redo.size() && now - tool.standing_time < kRefreshPeriod)
return;
tool.standing_history = m_history.size();
tool.standing_redo = g_redo.size();
tool.standing_time = now;
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;
}
int editor_mode::signal_hit() const
{
screen_projection const projection;
auto const &mouse{ImGui::GetIO().MousePos};
auto best{kSignalPick * kSignalPick};
int result{-1};
glm::dvec3 const camera{Global.pCamera.Pos};
for (int i = 0; i < static_cast<int>(m_signal.standing.size()); ++i)
{
auto const &location{m_signal.standing[i].include.location};
if (glm::distance(location, camera) > kSignalRange)
continue;
ImVec2 foot, head;
if (false == projection.project(location, foot) || false == projection.project(location + glm::dvec3{0.0, kSignalHead, 0.0}, head))
continue;
ImVec2 const span{head.x - foot.x, head.y - foot.y};
auto const length2{span.x * span.x + span.y * span.y};
auto const t{length2 > 1e-6f ? std::clamp(((mouse.x - foot.x) * span.x + (mouse.y - foot.y) * span.y) / length2, 0.0f, 1.0f) : 0.0f};
auto const dx{foot.x + span.x * t - mouse.x};
auto const dy{foot.y + span.y * t - mouse.y};
auto const distance2{dx * dx + dy * dy};
if (distance2 < best)
{
best = distance2;
result = i;
}
}
return result;
}
void editor_mode::signal_press(track_intent const &Intent)
{
auto &tool{m_signal};
auto const &mouse{ImGui::GetIO().MousePos};
tool.pressed = {mouse.x, mouse.y};
tool.facing = 0;
tool.status.clear();
if (Intent.what == track_intent::kind::signal_move)
{
if (Intent.index < 0 || Intent.index >= static_cast<int>(tool.standing.size()))
return;
auto const &standing{tool.standing[Intent.index]};
tool.grab = plan_of(standing.include.location) - plan_of(cursor_level(standing.include.location.y));
tool.home_valid = signal_standing_spot(standing, tool.home);
tool.moving = true;
tool.moved_valid = false;
signal_select(standing.include.instance);
return;
}
auto const *armed{signal_armed()};
if (armed == nullptr || Intent.track == nullptr)
return;
if (false == signal_spot_at(*Intent.track, Intent.path, m_hover.point, cursor_level(m_hover.point.y), 0, 0, *armed, tool.press))
return;
tool.placing = true;
}
void editor_mode::signal_move_update()
{
auto &tool{m_signal};
auto const &mouse{ImGui::GetIO().MousePos};
auto const dragged{glm::length(glm::vec2{mouse.x, mouse.y} - tool.pressed) > kDragStart};
if (tool.placing)
{
auto const *armed{signal_armed()};
if (armed == nullptr || tool.press.track == nullptr || tool.press.track->m_editorremoved)
{
tool.placing = false;
return;
}
if (false == dragged)
return;
auto const cursor{cursor_level(tool.press.axis.y)};
auto const forward{tool.press.travel * static_cast<double>(tool.press.list == 2 ? 1 : -1)};
auto const along{glm::dot(plan_of(cursor) - plan_of(tool.press.axis), forward)};
if (std::abs(along) < kDragTravel)
return;
tool.facing = along > 0.0 ? 1 : -1;
signal_spot spot;
if (signal_spot_at(*tool.press.track, tool.press.path, tool.press.axis, cursor, tool.facing, tool.press.side, *armed, spot))
tool.press = spot;
return;
}
if (false == tool.moving)
return;
if (false == dragged)
return;
auto const *found{signal_standing_of(tool.selected)};
if (found == nullptr)
{
tool.moved_valid = false;
return;
}
auto const &location{found->include.location};
auto const cursor{cursor_level(location.y)};
glm::dvec3 const held{cursor.x + tool.grab.x, location.y, cursor.z + tool.grab.y};
// the track under the cursor; off every track, as it is when the signal is dragged beside its track, the track the
// signal stands by
auto *track{m_hover.track};
auto path{m_hover.path};
if (track == nullptr && tool.home_valid)
{
track = tool.home.track;
path = tool.home.path;
}
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};
auto const found{std::find_if(standing.begin(), standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == Instance; })};
return found != standing.end() ? &*found : nullptr;
}
bool editor_mode::signal_drop_at(signal_standing const &Standing, TTrack &Track, int const Path, glm::dvec3 const &Point, int const Side, signal_spot &Spot) const
{
auto const *item{signal_template_of(Standing.include.file)};
if (item == nullptr || Track.m_paths.empty() || Track.m_editorremoved)
return false;
auto const path{std::clamp(Path, 0, static_cast<int>(Track.m_paths.size()) - 1)};
auto const forward{forward_at(Track, path, nearest_on(Track, path, Point))};
auto const facing{glm::dot(forward, travel_of(Standing.include.yaw)) >= 0.0 ? 1 : -1};
return signal_spot_at(Track, path, Point, Point, facing, Side * facing, *item, Spot);
}
bool editor_mode::signal_standing_spot(signal_standing const &Standing, signal_spot &Spot) const
{
auto const &location{Standing.include.location};
TTrack *track{nullptr};
int path{0};
auto best{kSignalReach};
auto const consider = [&](TTrack *Track) {
if (Track == nullptr || Track->m_editorremoved)
return;
for (int p = 0; p < static_cast<int>(Track->m_paths.size()); ++p)
{
bezier const curve{Track->m_paths[p]};
auto const low{glm::min(glm::min(plan_of(curve.p0), plan_of(curve.p1)), glm::min(plan_of(curve.p2), plan_of(curve.p3)))};
auto const high{glm::max(glm::max(plan_of(curve.p0), plan_of(curve.p1)), glm::max(plan_of(curve.p2), plan_of(curve.p3)))};
if (glm::length(glm::max(glm::max(low - plan_of(location), plan_of(location) - high), glm::dvec2{0.0})) >= best)
continue;
auto const distance{geometry::plan_distance(curve.point(nearest_on(*Track, p, location)), location)};
if (distance < best)
{
best = distance;
track = Track;
path = p;
}
}
};
consider(Standing.track);
if (track == nullptr)
for (auto *candidate : simulation::Paths.sequence())
consider(candidate);
if (track == nullptr)
return false;
auto const axis{bezier{track->m_paths[path]}.point(nearest_on(*track, path, location))};
auto const side{glm::dot(plan_of(location) - plan_of(axis), right_of(travel_of(Standing.include.yaw))) >= 0.0 ? 1 : -1};
if (false == signal_drop_at(Standing, *track, path, location, side, Spot))
return false;
Spot.offset = geometry::plan_distance(location, axis);
Spot.position = location;
Spot.yaw = Standing.include.yaw;
return true;
}
void editor_mode::signal_release()
{
auto &tool{m_signal};
if (tool.placing)
{
tool.placing = false;
if (auto const *armed{signal_armed()}; armed != nullptr && tool.press.track != nullptr && false == tool.press.track->m_editorremoved)
signal_place(tool.press, *armed);
return;
}
if (false == tool.moving)
return;
tool.moving = false;
auto const &mouse{ImGui::GetIO().MousePos};
if (glm::length(glm::vec2{mouse.x, mouse.y} - tool.pressed) <= kDragStart)
return;
if (false == tool.moved_valid)
{
tool.status = refused(STR_C("Drop the signal by a track"));
return;
}
tool.moved_valid = false;
if (auto const *found{signal_standing_of(tool.selected)})
signal_move_to(*found, tool.moved, STR_C("Signal %s moved, read by %s %s of %s, Ctrl+Z takes it back"), STR_C("Signal %s moved, Ctrl+Z takes it back"));
}
bool editor_mode::signal_move_to(signal_standing const &Standing, signal_spot const &Spot, char const *Read, char const *Unread)
{
auto &tool{m_signal};
auto const instance{Standing.include.instance};
if (false == scene::Layers.tracked(instance) || false == scene::Layers.removable(instance))
{
tool.status = refused(STR_C("This signal is in a file the editor doesn't change, it can't be moved"));
return false;
}
auto const &included{scene::Layers.instance(instance)};
auto file{*included.file};
signal_spot before;
auto const *item{signal_template_of(Standing.include.file)};
if (item != nullptr && signal_standing_spot(Standing, before) && travel_side(before.side, before.list) != travel_side(Spot.side, Spot.list))
file = signal_variant(*item, Spot);
auto values{Standing.include.values};
auto const local{Spot.position - included.context.offset};
values[1] = editor_includes::number(local.x);
values[2] = editor_includes::number(local.y);
values[3] = editor_includes::number(local.z);
values[4] = editor_includes::number(Spot.yaw);
auto const *next{signal_template_of(file)};
auto const read{next != nullptr && false == next->read.empty() ? editor_includes::substitute(next->read, values) : Standing.read};
auto directive{scene::Layers.directive(instance)};
std::vector<std::pair<TTrack *, editor_track::state>> states;
if (false == Standing.read.empty() && Spot.track->Dynamics.empty())
{
signal_detach_events(Standing.read, states);
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));
editor_track::events(*Spot.track, Spot.list).emplace_back(ToLower(read), nullptr);
std::string missing;
editor_track::bind_events(*Spot.track, missing);
Spot.track->mark_dirty();
}
auto const name{Standing.name};
auto const node{Standing.include.file};
set_include_directive(instance, editor_includes::compose_directive(file, values));
if (states.empty())
{
EditorSnapshot snap;
snap.directives.emplace_back(instance, std::move(directive));
snap.node_name = node;
trim_history();
m_history.push_back(std::move(snap));
g_redo.clear();
}
else
{
push_track_snapshot(std::move(states));
m_history.back().directives.emplace_back(instance, std::move(directive));
}
signal_refresh(true);
signal_select(instance);
auto const *moved{signal_standing_of(instance)};
tool.status = moved != nullptr && moved->track != nullptr ? format(Read, name.c_str(), editor_track::event_keyword(moved->list), moved->read.c_str(), name_of(*moved->track).c_str()) : format(Unread, name.c_str());
return true;
}
bool editor_mode::signal_flip(scene::instance_handle const Instance)
{
auto const *standing{signal_standing_of(Instance)};
signal_spot spot;
if (standing == nullptr || false == signal_standing_spot(*standing, spot))
{
m_signal.status = refused(STR_C("No track by the signal to take it across"));
return false;
}
auto const across{plan_of(spot.position) - plan_of(spot.axis)};
auto const mirrored{plan_of(spot.axis) + spot.travel * glm::dot(across, spot.travel) - right_of(spot.travel) * glm::dot(across, right_of(spot.travel))};
spot.side = -spot.side;
spot.position = {mirrored.x, spot.position.y, mirrored.y};
return signal_move_to(*standing, spot, STR_C("Signal %s moved to the other side of the track, read by %s %s of %s, Ctrl+Z takes it back"), STR_C("Signal %s moved to the other side of the track, Ctrl+Z takes it back"));
}
bool editor_mode::signal_turn(scene::instance_handle const Instance)
{
auto const *standing{signal_standing_of(Instance)};
signal_spot spot;
if (standing == nullptr || false == signal_standing_spot(*standing, spot))
{
m_signal.status = refused(STR_C("No track by the signal to turn it to"));
return false;
}
auto const *item{signal_template_of(standing->include.file)};
auto const position{spot.position};
if (item == nullptr || false == signal_spot_at(*spot.track, spot.path, spot.axis, spot.axis, spot.list == 2 ? -1 : 1, spot.side, *item, spot))
return false;
spot.position = position;
spot.yaw = std::fmod(standing->include.yaw + 180.0, 360.0);
return signal_move_to(*standing, spot, STR_C("Signal %s turned to the trains of the other direction, read by %s %s of %s, Ctrl+Z takes it back"), STR_C("Signal %s turned to the trains of the other direction, Ctrl+Z takes it back"));
}
bool editor_mode::signal_swap(scene::instance_handle const Instance, std::string const &File)
{
auto &tool{m_signal};
auto const *standing{signal_standing_of(Instance)};
if (standing == nullptr)
return false;
auto const name{standing->name};
auto const before{standing->include.file};
signal_changes changes;
changes.files.emplace_back(Instance, File);
std::string error;
signal_batch(changes, {}, {}, error);
signal_refresh(true);
auto const *swapped{signal_standing_of(Instance)};
if (false == error.empty() || swapped == nullptr || swapped->include.file == before)
{
tool.status = error.empty() ? format(STR_C("Signal %s can't take the template %s"), name.c_str(), File.c_str()) : error;
return false;
}
signal_select(Instance);
tool.status = format(STR_C("Signal %s got the template %s, Ctrl+Z takes it back"), name.c_str(), swapped->include.file.c_str());
return true;
}
std::string editor_mode::signal_variant(signal_template const &Template, signal_spot const &Spot) const
{
auto file{Template.file};
auto const wanted{lean_wanted(glm::dot(right_of(Spot.travel), plan_of(Spot.position) - plan_of(Spot.axis)) >= 0.0 ? 1 : -1)};
if (lean_of(Template.lean) == 0 || lean_of(Template.lean) == wanted)
return file;
auto const group{group_of(Template.mount, Template.lamps)};
auto best{std::numeric_limits<std::size_t>::max()};
for (auto const &other : m_signal.templates)
{
if (other.kind != Template.kind || group_of(other.mount, other.lamps) != group || lean_of(other.lean) != wanted || other.speeds != Template.speeds)
continue;
auto const distance{edit_distance(other.name, Template.name)};
if (distance < best)
{
best = distance;
file = other.file;
}
}
return file;
}
std::string editor_mode::signal_next_name(std::string const &Name) const
{
if (Name.empty())
return Name;
auto digits{Name.size()};
while (digits > 0 && std::isdigit(static_cast<unsigned char>(Name[digits - 1])))
--digits;
if (digits < Name.size() && Name.size() - digits < 9)
return Name.substr(0, digits) + std::to_string(std::stoi(Name.substr(digits)) + 1);
auto const last{static_cast<unsigned char>(Name.back())};
if (std::isalpha(last) && last != 'z' && last != 'Z')
return Name.substr(0, Name.size() - 1) + static_cast<char>(last + 1);
return Name + "2";
}
bool editor_mode::signal_place(signal_spot const &Spot, signal_template const &Template)
{
auto &tool{m_signal};
std::string name{tool.name};
if (name.empty() || name.find_first_of(" \t\";") != std::string::npos)
{
tool.status = refused(STR_C("Give the signal a name without spaces first"));
return false;
}
signal_refresh(true);
auto const taken = [&](std::string const &Name) {
return std::any_of(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return ToLower(Standing.name) == ToLower(Name); });
};
for (int guard = 0; taken(name) && guard < 1000; ++guard)
name = signal_next_name(name);
auto const file{signal_variant(Template, Spot)};
template_item item;
item.file = file;
item.location = Spot.position;
item.yaw = Spot.yaw;
item.described = true;
item.values["name"] = name;
if (Template.plate)
item.values["#6"] = plate_for(tool.plate, name);
if (Template.linked)
item.values["#7"] = tool.linked[0] != '\0' ? std::string{tool.linked} : std::string{"none"};
std::string error;
if (place_templates({item}, error) == 0)
{
tool.status = refused(STR_C("The signal couldn't be placed: ") + error);
return false;
}
auto const instance{m_history.back().instances.front()};
auto const *placed{signal_template_of(file)};
auto const read{placed != nullptr && false == placed->read.empty() ? editor_includes::substitute(placed->read, {name}) : std::string{}};
auto const path{name_of(*Spot.track)};
std::string reason;
if (read.empty())
tool.status = format(STR_C("Signal %s placed (%s), the train doesn't read it, Ctrl+Z takes it back"), name.c_str(), file.c_str());
else if (false == Spot.track->Dynamics.empty())
tool.status = format(STR_C("Signal %s placed (%s), but vehicles stand on the path: add %s to its %s by hand, Ctrl+Z takes it back"), name.c_str(), file.c_str(), read.c_str(), editor_track::event_keyword(Spot.list));
else if (false == scene::Layers.editable(Spot.track, &reason))
tool.status = format(STR_C("Signal %s placed (%s), but the path can't take its event: %s, Ctrl+Z takes it back"), name.c_str(), file.c_str(), reason.c_str());
else
{
auto snap{std::move(m_history.back())};
m_history.pop_back();
auto const before{editor_track::capture(*Spot.track)};
editor_track::events(*Spot.track, Spot.list).emplace_back(ToLower(read), nullptr);
std::string missing;
editor_track::bind_events(*Spot.track, missing);
Spot.track->mark_dirty();
push_track_snapshot({{Spot.track, before}});
m_history.back().instances = std::move(snap.instances);
tool.status = format(STR_C("Signal %s placed (%s), read by %s %s of %s, Ctrl+Z takes it back. Its logic works after the scenery is saved and opened again"), name.c_str(), file.c_str(), editor_track::event_keyword(Spot.list), read.c_str(), path.c_str());
}
std::snprintf(tool.name, sizeof(tool.name), "%s", signal_next_name(name).c_str());
signal_refresh(true);
signal_select(instance);
first_step_done(first_step::signal);
return true;
}
void editor_mode::signal_detach_events(std::string const &Read, std::vector<std::pair<TTrack *, editor_track::state>> &States)
{
auto const name{ToLower(Read)};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved)
continue;
bool carries{false};
for (int list = 0; list < 6 && false == carries; ++list)
for (auto const &event : editor_track::events(*track, list))
carries = carries || ToLower(event.first) == name;
if (false == carries)
continue;
if (std::none_of(States.begin(), States.end(), [&](auto const &State) { return State.first == track; }))
States.emplace_back(track, editor_track::capture(*track));
for (int list = 0; list < 6; ++list)
{
auto &events{editor_track::events(*track, list)};
events.erase(std::remove_if(events.begin(), events.end(), [&](auto const &Event) { return ToLower(Event.first) == name; }), events.end());
}
std::string missing;
editor_track::bind_events(*track, missing);
track->mark_dirty();
}
}
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))
return false;
signal_scan();
auto const &included{scene::Layers.instance(Instance)};
auto const *item{signal_template_of(*included.file)};
if (item == nullptr || item->read.empty())
return false;
std::string file;
std::vector<std::string> values;
if (false == editor_includes::parse_directive(scene::Layers.directive(Instance), file, values))
return false;
auto const read{editor_includes::substitute(item->read, values)};
std::vector<std::pair<TTrack *, editor_track::state>> states;
signal_detach_events(read, states);
if (states.empty())
return false;
scene::Layers.removed(Instance, true);
push_track_snapshot(std::move(states));
m_history.back().instances.push_back(Instance);
ui()->set_status(format(STR_C("Signal %s removed with its event %s, Ctrl+Z brings both back"), values.empty() ? "" : values.front().c_str(), read.c_str()));
select_include(0);
m_signal.selected = 0;
signal_refresh(true);
return true;
}
void editor_mode::signal_renamed(std::string const &Before, std::string const &After, EditorSnapshot &Snapshot)
{
auto const name{ToLower(Before)};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved)
continue;
bool carries{false};
for (int list = 0; list < 6 && false == carries; ++list)
for (auto const &event : editor_track::events(*track, list))
carries = carries || ToLower(event.first) == name;
if (false == carries)
continue;
if (std::none_of(Snapshot.tracks.begin(), Snapshot.tracks.end(), [&](auto const &State) { return State.first == track; }))
Snapshot.tracks.emplace_back(track, editor_track::capture(*track));
for (int list = 0; list < 6; ++list)
for (auto &event : editor_track::events(*track, list))
if (ToLower(event.first) == name)
event = {ToLower(After), nullptr};
std::string missing;
editor_track::bind_events(*track, missing);
track->mark_dirty();
}
if (Snapshot.tracks.empty() || Snapshot.instance == 0)
return;
Snapshot.directives.emplace_back(Snapshot.instance, std::move(Snapshot.serialized));
Snapshot.instance = 0;
Snapshot.serialized.clear();
Snapshot.action = EditorSnapshot::Action::TrackEdit;
Snapshot.node_ptr = Snapshot.tracks.front().first;
Snapshot.node_name = Snapshot.tracks.front().first->name();
}
bool editor_mode::standing_signals(std::vector<standing_signal> &Signals, bool const Wait)
{
Signals.clear();
signal_scan(Wait);
if (m_signal.templates.empty())
return false;
signal_refresh(true);
for (auto const &standing : m_signal.standing)
{
if (standing.kind < 0)
continue;
std::string file;
std::vector<std::string> values;
auto const *item{signal_template_of(standing.include.file)};
auto const linked{item != nullptr && item->linked && editor_includes::parse_directive(scene::Layers.directive(standing.include.instance), file, values) && values.size() > 6};
Signals.push_back({standing.include.instance, standing.name, kKinds[standing.kind], standing.include.location, standing.include.yaw, linked ? values[6] : std::string{}});
auto &signal{Signals.back()};
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;
}
bool editor_mode::signal_heads(std::vector<signal_head> &Heads, bool const Wait)
{
Heads.clear();
signal_scan(Wait);
if (m_signal.templates.empty())
return false;
for (int index = 0; index < static_cast<int>(m_signal.templates.size()); ++index)
{
auto const &item{m_signal.templates[index]};
if (item.lamps.empty())
continue;
auto const kind{kind_index(item.kind)};
Heads.push_back({item.file, item.name, item.kind, item.mount, item.lamps, item.speeds, kind >= 0 && m_signal.chosen[kind] == index, item.aspects, item.lean});
}
return true;
}
std::size_t editor_mode::signal_batch(signal_changes const &Changes, std::vector<std::pair<TTrack *, editor_track::state>> States, std::vector<TTrack *> Created, std::string &Error)
{
auto &tool{m_signal};
signal_scan();
signal_refresh(true);
std::vector<template_item> items;
std::vector<std::pair<signal_spot, std::string>> reads;
for (auto const &order : Changes.placed)
{
auto const kind{kind_index(order.kind)};
auto const *item{kind >= 0 && tool.chosen[kind] >= 0 && tool.chosen[kind] < static_cast<int>(tool.templates.size()) ? &tool.templates[tool.chosen[kind]] : nullptr};
if (auto const *wanted{order.file.empty() ? nullptr : signal_template_of(order.file)}; wanted != nullptr && wanted->kind == order.kind)
item = wanted;
if (item == nullptr)
{
Error = format(STR_C("no template of the kind %s"), STR(kKindLabels[std::max(0, kind)]).c_str());
break;
}
signal_spot spot;
if (order.track == nullptr || false == signal_spot_at(*order.track, order.path, order.point, order.point, order.facing, order.side, *item, spot))
continue;
template_item placing;
placing.file = signal_variant(*item, spot);
placing.location = spot.position;
placing.yaw = spot.yaw;
placing.described = true;
placing.values["name"] = order.name;
auto const *variant{signal_template_of(placing.file)};
if (variant == nullptr)
variant = item;
if (variant->plate)
placing.values["#6"] = plate_for(std::string{}, order.plate);
if (variant->linked)
placing.values["#7"] = order.linked.empty() ? std::string{"none"} : order.linked;
items.push_back(std::move(placing));
reads.emplace_back(spot, variant->read.empty() ? std::string{} : editor_includes::substitute(variant->read, {order.name}));
}
if (false == Error.empty())
{
// nothing changed but the tracks cut for them, a step of undo
if (false == States.empty() || false == Created.empty())
push_track_snapshot(std::move(States), std::move(Created));
return 0;
}
// the names passed round among the signals go through ones of their own first, or a read event renamed would be renamed again
items.insert(items.end(), Changes.others.begin(), Changes.others.end());
EditorSnapshot step;
step.tracks = std::move(States);
std::vector<std::pair<scene::instance_handle, std::string>> directives;
std::unordered_set<scene::instance_handle> changed;
auto const record = [&](scene::instance_handle const Instance, std::string const &Before) {
if (changed.insert(Instance).second)
directives.emplace_back(Instance, Before);
};
for (auto const temporary : {true, false})
for (auto const &rename : Changes.renamed)
{
auto const *standing{signal_standing_of(rename.instance)};
auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr};
auto const before{scene::Layers.directive(rename.instance)};
std::string file;
std::vector<std::string> values;
if (item == nullptr || false == editor_includes::parse_directive(before, file, values) || values.empty())
continue;
if (temporary && ToLower(values[0]) == ToLower(rename.name))
continue;
auto const read_before{item->read.empty() ? std::string{} : editor_includes::substitute(item->read, values)};
values[0] = rename.name + (temporary ? "~" : "");
if (item->plate && values.size() > 5 && false == rename.plate.empty())
values[5] = plate_for(std::string{}, rename.plate);
if (item->linked && values.size() > 6 && false == rename.linked.empty())
values[6] = rename.linked;
auto const read_after{item->read.empty() ? std::string{} : editor_includes::substitute(item->read, values)};
auto const directive{editor_includes::compose_directive(file, values)};
if (directive == before)
continue;
record(rename.instance, before);
set_include_directive(rename.instance, directive);
if (read_before != read_after)
signal_renamed(read_before, read_after, step);
}
for (auto const &[instance, values] : Changes.values)
{
auto const before{scene::Layers.directive(instance)};
std::string file;
std::vector<std::string> standing;
if (false == editor_includes::parse_directive(before, file, standing))
continue;
if (auto const directive{editor_includes::compose_directive(file, values)}; directive != before)
{
record(instance, before);
set_include_directive(instance, directive);
}
}
for (auto const &[instance, wanted] : Changes.files)
{
auto const *standing{signal_standing_of(instance)};
auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr};
auto const *chosen{signal_template_of(wanted)};
auto const before{scene::Layers.directive(instance)};
std::string file;
std::vector<std::string> values;
if (item == nullptr || chosen == nullptr || chosen->kind != item->kind || false == editor_includes::parse_directive(before, file, values) || values.empty())
continue;
signal_spot spot;
auto const *variant{signal_standing_spot(*standing, spot) ? signal_template_of(signal_variant(*chosen, spot)) : chosen};
if (variant == nullptr)
variant = chosen;
if (variant->file == item->file)
continue;
include_info had;
include_info info;
std::string missing;
editor_includes::load(item->file, had, missing);
editor_includes::load(variant->file, info, missing);
auto const role_of = [](include_info &Info, int const Id) { return Info.parameter(Id).role; };
std::vector<std::string> after(std::max(variant->parameters, 1));
for (int id = 1; id <= static_cast<int>(after.size()); ++id)
{
auto &value{after[id - 1]};
auto const role{role_of(info, id)};
for (int old = 1; old <= static_cast<int>(values.size()) && value.empty(); ++old)
if (role != "free" && role != "text" && role_of(had, old) == role)
value = values[old - 1];
if (id == 7 && variant->linked)
value = item->linked && values.size() > 6 ? values[6] : std::string{"none"};
if (value.empty() && id == 6 && variant->plate)
value = plate_for(std::string{}, values[0].substr(values[0].rfind('_') + 1));
if (value.empty())
value = info.parameter(id).value.empty() ? std::string{"none"} : info.parameter(id).value;
}
auto const read_before{item->read.empty() ? std::string{} : editor_includes::substitute(item->read, values)};
auto const read_after{variant->read.empty() ? std::string{} : editor_includes::substitute(variant->read, after)};
record(instance, before);
set_include_directive(instance, editor_includes::compose_directive(variant->file, after));
if (false == read_before.empty() && false == read_after.empty() && ToLower(read_before) != ToLower(read_after))
signal_renamed(read_before, read_after, step);
}
std::unordered_set<TTrack const *> const created(Created.begin(), Created.end());
std::vector<scene::instance_handle> removed;
for (auto const instance : Changes.removed)
{
if (false == scene::Layers.tracked(instance) || false == scene::Layers.removable(instance) || scene::Layers.instance(instance).removed)
continue;
auto const *standing{signal_standing_of(instance)};
auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr};
std::string file;
std::vector<std::string> values;
if (item != nullptr && false == item->read.empty() && editor_includes::parse_directive(scene::Layers.directive(instance), file, values))
{
std::vector<std::pair<TTrack *, editor_track::state>> states;
signal_detach_events(editor_includes::substitute(item->read, values), states);
for (auto &state : states)
if (created.count(state.first) == 0 && std::none_of(step.tracks.begin(), step.tracks.end(), [&](auto const &Taken) { return Taken.first == state.first; }))
step.tracks.push_back(std::move(state));
}
scene::Layers.removed(instance, true);
removed.push_back(instance);
}
std::vector<scene::instance_handle> instances;
if (Error.empty() && false == items.empty())
{
if (place_templates(items, Error) == 0)
reads.clear();
else
{
auto placed{std::move(m_history.back())};
m_history.pop_back();
instances = std::move(placed.instances);
}
}
else
reads.clear();
for (auto const &[spot, read] : reads)
{
if (read.empty() || false == spot.track->Dynamics.empty() || false == scene::Layers.editable(spot.track))
continue;
if (created.count(spot.track) == 0 && std::none_of(step.tracks.begin(), step.tracks.end(), [&](auto const &State) { return State.first == spot.track; }))
step.tracks.emplace_back(spot.track, editor_track::capture(*spot.track));
editor_track::events(*spot.track, spot.list).emplace_back(ToLower(read), nullptr);
std::string missing;
editor_track::bind_events(*spot.track, missing);
spot.track->mark_dirty();
}
auto const count{instances.size()};
instances.insert(instances.end(), removed.begin(), removed.end());
if (step.tracks.empty() && Created.empty() && instances.empty() && directives.empty())
return 0;
if (step.tracks.empty() && Created.empty())
{
EditorSnapshot only;
auto const *file{scene::Layers.instance(directives.empty() ? instances.front() : directives.front().first).file};
only.node_name = file != nullptr ? *file : std::string{};
trim_history();
m_history.push_back(std::move(only));
g_redo.clear();
}
else
push_track_snapshot(std::move(step.tracks), std::move(Created));
auto &pushed{m_history.back()};
pushed.instances = std::move(instances);
pushed.directives.insert(pushed.directives.end(), directives.begin(), directives.end());
signal_refresh(true);
return count;
}
void editor_mode::signal_select(scene::instance_handle const Instance)
{
m_signal.selected = Instance;
if (Instance != 0)
select_include(Instance);
}
void editor_mode::render_signal_palette()
{
auto &tool{m_signal};
ImGui::SetNextItemWidth(-1.0f);
ImGui::InputTextWithHint("##signalfilter", STR_C("filter: name, w24, sbl..."), tool.filter, sizeof(tool.filter));
auto const filter{ToLower(tool.filter)};
auto const *armed{signal_armed()};
std::vector<std::vector<int>> groups;
std::vector<std::string> keys;
for (int i = 0; i < static_cast<int>(tool.templates.size()); ++i)
{
auto const &item{tool.templates[i]};
if (item.kind != kKinds[tool.kind])
continue;
if (false == filter.empty() && item.name.find(filter) == std::string::npos && ToLower(item.description).find(filter) == std::string::npos)
continue;
auto const key{group_of(item.mount, item.lamps)};
auto const found{std::find(keys.begin(), keys.end(), key)};
if (found == keys.end())
{
keys.push_back(key);
groups.push_back({i});
}
else
groups[found - keys.begin()].push_back(i);
}
if (groups.empty())
{
ImGui::TextDisabled("%s", STR_C("No template of this kind matches the filter"));
return;
}
ImVec2 const tile{52.0f, 86.0f};
auto const spacing{ImGui::GetStyle().ItemSpacing.x};
ImGui::BeginChild("##signaltiles", ImVec2(0.0f, std::min(2.0f * (tile.y + 8.0f) + 20.0f, ImGui::GetContentRegionAvail().y * 0.45f)), true);
auto const columns{std::max(1, static_cast<int>((ImGui::GetContentRegionAvail().x + spacing) / (tile.x + spacing)))};
auto *draw{ImGui::GetWindowDrawList()};
for (int g = 0; g < static_cast<int>(groups.size()); ++g)
{
auto const &group{groups[g]};
auto const &first{tool.templates[group.front()]};
auto const chosen{armed != nullptr && group_of(armed->mount, armed->lamps) == keys[g] && armed->kind == first.kind};
if (g % columns != 0)
ImGui::SameLine();
ImGui::PushID(g);
auto const origin{ImGui::GetCursorScreenPos()};
if (ImGui::InvisibleButton("##tile", tile))
{
auto pick{group.front()};
if (armed != nullptr && chosen)
pick = tool.chosen[tool.kind];
tool.chosen[tool.kind] = pick;
}
auto const hovered{ImGui::IsItemHovered()};
draw->AddRectFilled(origin, ImVec2(origin.x + tile.x, origin.y + tile.y), chosen ? IM_COL32(60, 90, 140, 255) : hovered ? IM_COL32(60, 60, 60, 255) : IM_COL32(40, 40, 40, 255), 4.0f);
draw_signal_icon(draw, origin, ImVec2(tile.x, tile.y - 14.0f), first.mount, first.lamps, chosen ? overlay_color::selected : IM_COL32(120, 120, 120, 255));
auto const caption{first.lamps.empty() ? first.name : first.lamps};
auto const captionsize{ImGui::CalcTextSize(caption.c_str())};
draw->AddText(ImVec2(origin.x + std::max(2.0f, (tile.x - captionsize.x) * 0.5f), origin.y + tile.y - 15.0f), IM_COL32(220, 220, 220, 255), caption.c_str());
if (group.size() > 1)
draw->AddText(ImVec2(origin.x + 3.0f, origin.y + 2.0f), IM_COL32(170, 170, 170, 255), std::to_string(group.size()).c_str());
if (hovered)
{
ImGui::BeginTooltip();
ImGui::TextUnformatted(first.description.empty() ? first.name.c_str() : first.description.c_str());
ImGui::TextDisabled(STR_C("%zu variants, e.g. %s"), group.size(), first.file.c_str());
ImGui::TextDisabled("%s", first.read.empty() ? STR_C("The train doesn't read it") : STR_C("The train reads it by an event of the path"));
ImGui::EndTooltip();
}
ImGui::PopID();
}
ImGui::EndChild();
if (armed == nullptr)
return;
auto const key{group_of(armed->mount, armed->lamps)};
ImGui::SetNextItemWidth(-1.0f);
if (ImGui::BeginCombo("##signalvariant", armed->name.c_str()))
{
for (int i = 0; i < static_cast<int>(tool.templates.size()); ++i)
{
auto const &item{tool.templates[i]};
if (item.kind != armed->kind || group_of(item.mount, item.lamps) != key)
continue;
auto const label{item.name + (item.lean == "left" ? std::string{" \xE2\x86\x96"} : item.lean == "right" ? std::string{" \xE2\x86\x97"} : std::string{}) + "##" + item.file};
if (ImGui::Selectable(label.c_str(), &item == armed))
tool.chosen[tool.kind] = i;
if (ImGui::IsItemHovered() && false == item.description.empty())
ImGui::SetTooltip("%s\n%s", item.description.c_str(), item.file.c_str());
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
{
auto const read{armed->read.empty() ? std::string{armed->kind == "warning" || armed->kind == "repeater" ? STR_C("The train doesn't read it: it repeats what the signal ahead of it shows") : STR_C("The train doesn't read it")}
: format(STR_C("The train reads it by %s on the path it stands by"), editor_includes::substitute(armed->read, {tool.name[0] != '\0' ? std::string{tool.name} : std::string{"?"}}).c_str())};
ImGui::SetTooltip("%s\n%s", STR_C("Variant of the chosen type. A head leaning to one side is swapped for its counterpart leaning towards the track on the other side"), read.c_str());
}
if (false == armed->description.empty())
ImGui::TextWrapped("%s", armed->description.c_str());
}
void editor_mode::render_signal_selected()
{
auto const *found{signal_standing_of(m_signal.selected)};
if (found == nullptr)
return;
ImGui::Separator();
ImGui::TextUnformatted(format(STR_C("Selected: %s (%s)"), found->name.c_str(), found->include.file.c_str()).c_str());
item_tooltip("Name, plate and the linked signal: in the node properties. Del removes it with its event");
if (found->read.empty())
ImGui::TextDisabled("%s", STR_C("The train doesn't read it"));
else if (found->track != nullptr)
ImGui::TextDisabled(STR_C("Read by %s %s of %s"), editor_track::event_keyword(found->list), found->read.c_str(), name_of(*found->track).c_str());
else
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), STR_C("No path has %s among its events: the train doesn't see it"), found->read.c_str());
auto const instance{found->include.instance};
if (auto const *item{signal_template_of(found->include.file)}; item != nullptr)
{
auto const shown = [](signal_template const &Template) { return Template.lamps.empty() ? Template.name : Template.name + " " + Template.lamps; };
std::string swap;
ImGui::SetNextItemWidth(-1.0f);
if (ImGui::BeginCombo("##signalswap", format(STR_C("Template: %s"), shown(*item).c_str()).c_str(), ImGuiComboFlags_HeightLarge))
{
for (auto const &other : m_signal.templates)
if (other.kind == item->kind && other.mount == item->mount && ImGui::Selectable((shown(other) + "##" + other.file).c_str(), other.file == item->file) && other.file != item->file)
swap = other.file;
ImGui::EndCombo();
}
item_tooltip("Another template of its kind and mount for the signal: the name, the plate, the place and the event the train reads it by stay. A head leaning to one side is swapped for its counterpart leaning towards the track. Ctrl+Z takes it back");
if (false == swap.empty())
{
signal_swap(instance, swap);
return;
}
}
auto const width{(ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f};
if (ImGui::Button(STR_C("To the other side"), ImVec2(width, 0.0f)))
signal_flip(instance);
item_tooltip("Takes the signal across the track, it keeps facing the same trains. Dragging it over the track does the same");
ImGui::SameLine();
if (ImGui::Button(STR_C("Turn around"), ImVec2(width, 0.0f)))
signal_turn(instance);
item_tooltip("Turns the signal to the trains of the other direction, they read it by the other event of the path");
}
void editor_mode::render_signal_ui()
{
auto &tool{m_signal};
signal_scan(false);
if (tool.templates.empty() && EditorIncludes.searching())
{
ImGui::TextDisabled("%s", STR_C("Looking for the templates..."));
return;
}
if (tool.templates.empty())
{
ImGui::TextWrapped("%s", STR_C("No signal templates: no template of the scenery directory is described with category: signal. tools/editor/signal_headers.py writes the descriptions into the signal templates."));
return;
}
std::vector<std::string> labels;
labels.reserve(kKindCount);
std::vector<segment_option> kinds;
for (int k = 0; k < kKindCount; ++k)
if (std::any_of(tool.templates.begin(), tool.templates.end(), [&](signal_template const &Item) { return Item.kind == kKinds[k]; }))
{
labels.push_back(STR(kKindLabels[k]));
kinds.push_back({k, labels.back().c_str()});
}
if (false == kinds.empty() && std::none_of(kinds.begin(), kinds.end(), [&](segment_option const &Option) { return Option.value == tool.kind; }))
tool.kind = kinds.front().value;
choice_chips("signalkinds", tool.kind, kinds);
render_signal_palette();
auto const *armed{signal_armed()};
ImGui::Separator();
if (begin_fields("##signalnames", 0.3f))
{
field_row("Name");
ImGui::InputText("##signalname", tool.name, sizeof(tool.name));
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Name of the next signal; it counts up after each one placed. Its events and memory cell are named after it"));
ImGui::EndTable();
}
ImGui::PushID("signalmore");
if (ImGui::TreeNode(STR_C("More settings")))
{
bool missing{false};
if (begin_fields("##signalplace", 0.45f))
{
if (armed != nullptr && armed->plate)
{
auto const automatic{format(STR_C("auto: %s"), plate_for({}, tool.name).c_str())};
field_row("Plate");
ImGui::InputTextWithHint("##signalplate", automatic.c_str(), tool.plate, sizeof(tool.plate));
auto const typing{ImGui::IsItemActive()};
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Texture of the plate from textures/tabl, e.g. a-12 or tob. Left empty: the one named like the signal, if there is one"));
auto const &plates{plate_textures()};
auto const typed{ToLower(tool.plate)};
auto const exists{typed.empty() || std::binary_search(plates.begin(), plates.end(), typed)};
if (false == exists)
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR_C("no such texture"));
missing = typing || (false == exists && false == typed.empty());
}
if (armed != nullptr && armed->linked)
{
field_row("Linked signal");
ImGui::InputText("##linked", tool.linked, sizeof(tool.linked));
item_tooltip("(p7) of the template: the warning signal a semaphore drives, the previous block signal, \"none\" for no link");
}
auto offset{static_cast<float>(EditorSettings.signal_offset())};
field_row("From the axis (m)");
if (ImGui::InputFloat("##signaloffset", &offset, 0.05f, 0.25f, "%.2f"))
{
EditorSettings.signal_offset(std::clamp(static_cast<double>(offset), 0.0, 10.0));
EditorSettings.save();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Distance of the mast from the axis of the track. 2.25 m is what the signals of the sceneries stand at"));
if (armed != nullptr && armed->mount == "gantry")
{
field_row("Over the rails (m)");
ImGui::InputFloat("##signalheight", &tool.height, 0.1f, 0.5f, "%.2f");
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("A head for a gantry hangs over the axis of the track this high over the rail heads"));
}
ImGui::EndTable();
}
if (missing)
{
auto const &plates{plate_textures()};
auto const typed{ToLower(tool.plate)};
int shown{0};
for (auto const &plate : plates)
{
if (typed.empty() || plate.compare(0, typed.size(), typed) != 0 || plate == typed)
continue;
if (shown % 6 != 0)
ImGui::SameLine();
if (ImGui::SmallButton(plate.c_str()))
std::snprintf(tool.plate, sizeof(tool.plate), "%s", plate.c_str());
if (++shown >= 18)
break;
}
}
ImGui::TreePop();
}
ImGui::PopID();
if (false == tool.status.empty())
ImGui::TextWrapped("%s", tool.status.c_str());
render_signal_selected();
}
void editor_mode::draw_signal_overlay() const
{
auto const &tool{m_signal};
screen_projection const projection;
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
glm::dvec3 const camera{Global.pCamera.Pos};
auto const arrow = [&](glm::dvec3 const &From, glm::dvec2 const &Direction, ImU32 const Colour) {
glm::dvec3 const tip{From.x + Direction.x * 6.0, From.y, From.z + Direction.y * 6.0};
ImVec2 a, b;
if (false == projection.project(From, a) || false == projection.project(tip, b))
return;
drawlist->AddLine(a, b, Colour, 2.5f);
auto const dx{b.x - a.x};
auto const dy{b.y - a.y};
auto const size{std::sqrt(dx * dx + dy * dy)};
if (size < 1.0f)
return;
ImVec2 const unit{dx / size, dy / size};
ImVec2 const side{-unit.y, unit.x};
drawlist->AddTriangleFilled(ImVec2(b.x + unit.x * 9.0f, b.y + unit.y * 9.0f), ImVec2(b.x + side.x * 6.0f, b.y + side.y * 6.0f), ImVec2(b.x - side.x * 6.0f, b.y - side.y * 6.0f), Colour);
};
auto const mast = [&](glm::dvec3 const &Foot, ImU32 const Colour, float const Width) { projection.line(drawlist, Foot, Foot + glm::dvec3{0.0, kSignalHead, 0.0}, Colour, Width); };
for (int i = 0; i < static_cast<int>(tool.standing.size()); ++i)
{
auto const &standing{tool.standing[i]};
auto const &location{standing.include.location};
if (glm::distance(location, camera) > kSignalRange)
continue;
auto const selected{standing.include.instance == tool.selected};
auto const hovered{m_intent.what == track_intent::kind::signal_move && m_intent.index == i};
auto const unread{false == standing.read.empty() && standing.track == nullptr};
auto const colour{selected ? overlay_color::selected : hovered ? overlay_color::highlight : unread ? overlay_color::invalid : IM_COL32(255, 210, 60, 160)};
mast(location, colour, selected || hovered ? 3.0f : 1.5f);
ImVec2 head;
if ((selected || hovered || unread) && projection.project(location + glm::dvec3{0.0, kSignalHead, 0.0}, head))
{
auto const label{standing.name + (unread ? std::string{" "} + STR(" not read") : std::string{})};
drawlist->AddText(ImVec2(head.x + 6.0f, head.y - 6.0f), colour, label.c_str());
}
if (selected || hovered)
arrow(location, travel_of(standing.include.yaw), colour);
}
auto const preview = [&](signal_spot const &Spot, ImU32 const Colour) {
projection.line(drawlist, Spot.axis + glm::dvec3{0.0, kRailTop, 0.0}, Spot.position, Colour, 2.0f);
mast(Spot.position, Colour, 3.0f);
arrow(Spot.axis + glm::dvec3{0.0, kRailTop, 0.0}, Spot.travel, Colour);
ImVec2 head;
auto const *armed{signal_armed()};
if (armed != nullptr && false == armed->lamps.empty() && projection.project(Spot.position + glm::dvec3{0.0, kSignalHead, 0.0}, head))
{
auto const lamps{static_cast<int>(armed->lamps.size())};
drawlist->AddRectFilled(ImVec2(head.x - 6.0f, head.y - 4.0f), ImVec2(head.x + 6.0f, head.y + lamps * 10.0f), IM_COL32(20, 20, 20, 220), 3.0f);
for (int i = 0; i < lamps; ++i)
drawlist->AddCircleFilled(ImVec2(head.x, head.y + 1.0f + i * 10.0f), 4.0f, lamp_colour(armed->lamps[i]));
}
};
if (tool.placing)
preview(tool.press, overlay_color::grip);
else if (tool.moving && tool.moved_valid)
preview(tool.moved, overlay_color::selected);
else if (m_intent.what == track_intent::kind::signal_place && m_hover.track != nullptr)
{
if (auto const *armed{signal_armed()}; armed != nullptr)
{
signal_spot spot;
if (signal_spot_at(*m_hover.track, m_hover.path, m_hover.point, cursor_level(m_hover.point.y), 0, 0, *armed, spot))
preview(spot, overlay_color::marked);
}
}
}