mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-20 18:34:53 +02:00
- a press on the mast of a selected signal grabbed the gizmo, whose up axis runs along the mast, and lifted the signal instead of moving it along the track; the gizmo gives way over a signal and while one is dragged - dragged beside its track, off every track, the signal follows the nearest place of the track it stands by instead of staying put - a drop away from any track, and a signal in a file the editor doesn't change, say so instead of doing nothing - a template without a //$e description of its own takes it from editor/templates/<its path in the scenery folder>; the header scripts write there with --sidecar, leaving the templates alone, so a check of the game files by the store doesn't take the descriptions away (the descriptions written into the templates on 8.10 and 11.10 were gone, and with them every signal and drive the editor knew)
1046 lines
41 KiB
C++
1046 lines
41 KiB
C++
/*
|
|
This Source Code Form is subject to the
|
|
terms of the Mozilla Public License, v.
|
|
2.0. If a copy of the MPL was not
|
|
distributed with this file, You can
|
|
obtain one at
|
|
http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
#include "stdafx.h"
|
|
#include "application/editormode.h"
|
|
#include "application/editoruilayer.h"
|
|
#include "application/editorprojection.h"
|
|
#include "editor/editorFormat.hpp"
|
|
#include "editor/editorGeometry.hpp"
|
|
#include "editor/editorIncludeInfo.hpp"
|
|
#include "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/Track.h"
|
|
|
|
#include "imgui/imgui.h"
|
|
#include "utilities/translation.h"
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <filesystem>
|
|
#include <limits>
|
|
#include <unordered_map>
|
|
|
|
namespace
|
|
{
|
|
|
|
using geometry::bezier;
|
|
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};
|
|
|
|
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;
|
|
}
|
|
|
|
std::string lower(std::string Text)
|
|
{
|
|
std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast<char>(std::tolower(Character)); });
|
|
return Text;
|
|
}
|
|
|
|
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};
|
|
}
|
|
|
|
double heading_of(glm::dvec2 const &Direction)
|
|
{
|
|
auto const degrees{glm::degrees(std::atan2(Direction.x, Direction.y))};
|
|
return degrees < 0.0 ? degrees + 360.0 : degrees;
|
|
}
|
|
|
|
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))
|
|
{
|
|
auto const stem{lower(file->path().stem().string())};
|
|
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{lower(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]));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void editor_mode::signal_scan()
|
|
{
|
|
auto &tool{m_signal};
|
|
if (false == EditorIncludes.scanned())
|
|
EditorIncludes.scan();
|
|
if (tool.revision == EditorIncludes.revision())
|
|
return;
|
|
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 ¶meter : 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;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
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{lower(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{Spot.track->name().empty() ? std::string{"(noname)"} : Spot.track->name()};
|
|
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};
|
|
signal_scan();
|
|
auto const now{ImGui::GetTime()};
|
|
if (false == Force && 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.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(lower(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(lower(standing.read))}; reader != readers.end())
|
|
{
|
|
standing.track = reader->second.first;
|
|
standing.list = reader->second.second;
|
|
}
|
|
}
|
|
tool.standing.push_back(std::move(standing));
|
|
}
|
|
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;
|
|
signal_select(tool.standing[Intent.index].include.instance);
|
|
tool.moving = true;
|
|
tool.moved_valid = false;
|
|
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{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })};
|
|
if (found == tool.standing.end())
|
|
{
|
|
tool.moved_valid = false;
|
|
return;
|
|
}
|
|
// the track under the cursor; off every track, as it is when the signal is dragged beside its track, the place of the
|
|
// track the signal stands by nearest the cursor
|
|
auto *track{m_hover.track};
|
|
auto path{m_hover.path};
|
|
auto point{m_hover.point};
|
|
if (track == nullptr && found->track != nullptr && false == found->track->m_editorremoved && false == found->track->m_paths.empty())
|
|
{
|
|
track = found->track;
|
|
path = std::clamp(found->spot.path, 0, static_cast<int>(track->m_paths.size()) - 1);
|
|
point = bezier{track->m_paths[path]}.point(nearest_on(*track, path, cursor_level(found->spot.axis.y)));
|
|
}
|
|
if (track == nullptr)
|
|
{
|
|
tool.moved_valid = false;
|
|
return;
|
|
}
|
|
auto const *item{signal_template_of(found->include.file)};
|
|
if (item == nullptr)
|
|
return;
|
|
path = std::clamp(path, 0, static_cast<int>(track->m_paths.size()) - 1);
|
|
auto const yaw{glm::radians(found->include.yaw + 180.0)};
|
|
glm::dvec2 const travel{std::sin(yaw), std::cos(yaw)};
|
|
auto const parameter{nearest_on(*track, path, point)};
|
|
auto const forward{forward_at(*track, path, parameter)};
|
|
auto const facing{glm::dot(forward, travel) >= 0.0 ? 1 : -1};
|
|
auto const sideoftravel{glm::dot(plan_of(found->include.location) - plan_of(point), right_of(travel)) >= 0.0 ? 1 : -1};
|
|
tool.moved_valid = signal_spot_at(*track, path, point, point, facing, sideoftravel * facing, *item, tool.moved);
|
|
}
|
|
|
|
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 = STR_C("Drop the signal by a track");
|
|
return;
|
|
}
|
|
tool.moved_valid = false;
|
|
auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })};
|
|
if (found == tool.standing.end())
|
|
return;
|
|
if (false == scene::Layers.tracked(found->include.instance) || false == scene::Layers.removable(found->include.instance))
|
|
{
|
|
tool.status = STR_C("This signal is in a file the editor doesn't change, it can't be moved");
|
|
return;
|
|
}
|
|
auto const &spot{tool.moved};
|
|
auto const &included{scene::Layers.instance(found->include.instance)};
|
|
auto values{found->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 before{scene::Layers.directive(found->include.instance)};
|
|
std::vector<std::pair<TTrack *, editor_track::state>> states;
|
|
if (false == found->read.empty() && spot.track->Dynamics.empty())
|
|
{
|
|
signal_detach_events(found->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(lower(found->read), nullptr);
|
|
std::string missing;
|
|
editor_track::bind_events(*spot.track, missing);
|
|
spot.track->mark_dirty();
|
|
}
|
|
set_include_directive(found->include.instance, editor_includes::compose_directive(*included.file, values));
|
|
if (states.empty())
|
|
{
|
|
EditorSnapshot snap;
|
|
snap.directives.emplace_back(found->include.instance, std::move(before));
|
|
snap.node_name = found->include.file;
|
|
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(found->include.instance, std::move(before));
|
|
}
|
|
tool.status = found->read.empty() ? format(STR_C("Signal %s moved"), found->name.c_str()) : format(STR_C("Signal %s moved, read by %s %s of %s"), found->name.c_str(), editor_track::event_keyword(spot.list), found->read.c_str(), spot.track->name().empty() ? "(noname)" : spot.track->name().c_str());
|
|
signal_refresh(true);
|
|
signal_select(found->include.instance);
|
|
}
|
|
|
|
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)
|
|
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 = 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 lower(Standing.name) == lower(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 = 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{Spot.track->name().empty() ? std::string{"(noname)"} : Spot.track->name()};
|
|
std::string reason;
|
|
if (read.empty())
|
|
tool.status = format(STR_C("Signal %s placed (%s), the train doesn't read it"), 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"), 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"), 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(lower(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. 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);
|
|
return true;
|
|
}
|
|
|
|
void editor_mode::signal_detach_events(std::string const &Read, std::vector<std::pair<TTrack *, editor_track::state>> &States)
|
|
{
|
|
auto const name{lower(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 || lower(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 lower(Event.first) == name; }), events.end());
|
|
}
|
|
std::string missing;
|
|
editor_track::bind_events(*track, missing);
|
|
track->mark_dirty();
|
|
}
|
|
}
|
|
|
|
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{lower(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 || lower(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 (lower(event.first) == name)
|
|
event = {lower(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();
|
|
}
|
|
|
|
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{lower(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 && lower(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())
|
|
ImGui::SetTooltip("%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"));
|
|
if (false == armed->description.empty())
|
|
ImGui::TextWrapped("%s", armed->description.c_str());
|
|
ImGui::TextDisabled("%s", armed->read.empty() ? (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()).c_str());
|
|
}
|
|
|
|
void editor_mode::render_signal_selected()
|
|
{
|
|
auto &tool{m_signal};
|
|
auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })};
|
|
if (found == tool.standing.end())
|
|
return;
|
|
ImGui::Separator();
|
|
ImGui::TextUnformatted(format(STR_C("Selected: %s (%s)"), found->name.c_str(), found->include.file.c_str()).c_str());
|
|
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(), found->track->name().empty() ? "(noname)" : found->track->name().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());
|
|
ImGui::TextDisabled("%s", STR_C("Name, plate and the linked signal: in the node properties. Del removes it with its event"));
|
|
}
|
|
|
|
void editor_mode::render_signal_ui()
|
|
{
|
|
auto &tool{m_signal};
|
|
signal_scan();
|
|
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;
|
|
}
|
|
if (ImGui::BeginTabBar("##signalkinds"))
|
|
{
|
|
for (int k = 0; k < kKindCount; ++k)
|
|
{
|
|
if (std::none_of(tool.templates.begin(), tool.templates.end(), [&](signal_template const &Item) { return Item.kind == kKinds[k]; }))
|
|
continue;
|
|
if (ImGui::BeginTabItem(STR(kKindLabels[k]).c_str()))
|
|
{
|
|
tool.kind = k;
|
|
ImGui::EndTabItem();
|
|
}
|
|
}
|
|
ImGui::EndTabBar();
|
|
}
|
|
render_signal_palette();
|
|
auto const *armed{signal_armed()};
|
|
ImGui::Separator();
|
|
ImGui::PushItemWidth(140.0f);
|
|
ImGui::InputText(STR_C("Name"), 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"));
|
|
if (armed != nullptr && armed->plate)
|
|
{
|
|
auto const automatic{format(STR_C("auto: %s"), plate_for({}, tool.name).c_str())};
|
|
ImGui::InputTextWithHint(STR_C("Plate"), automatic.c_str(), tool.plate, sizeof(tool.plate));
|
|
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{lower(tool.plate)};
|
|
auto const exists{typed.empty() || std::binary_search(plates.begin(), plates.end(), typed)};
|
|
if (false == exists)
|
|
{
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR_C("no such texture"));
|
|
}
|
|
if (ImGui::IsItemActive() || (false == exists && false == typed.empty()))
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
if (armed != nullptr && armed->linked && ImGui::TreeNode(STR_C("Linked signal")))
|
|
{
|
|
ImGui::InputText("##linked", tool.linked, sizeof(tool.linked));
|
|
ImGui::TextDisabled("%s", STR_C("(p7) of the template: the warning signal a semaphore drives, the previous block signal, \"none\" for no link"));
|
|
ImGui::TreePop();
|
|
}
|
|
auto offset{static_cast<float>(EditorSettings.signal_offset())};
|
|
if (ImGui::InputFloat(STR_C("From the axis (m)"), &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")
|
|
{
|
|
ImGui::InputFloat(STR_C("Over the rails (m)"), &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::PopItemWidth();
|
|
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)
|
|
{
|
|
auto const yaw{glm::radians(standing.include.yaw + 180.0)};
|
|
arrow(location, glm::dvec2{std::sin(yaw), std::cos(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);
|
|
}
|
|
}
|
|
}
|