From b02fe0ac2b3bde04f8daf0acc35c6c6652fc2c2a Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 8 Oct 2026 03:12:16 +0200 Subject: [PATCH] feat(editor): Signals mode of the Tracks window (H): semaphores, block, warning, shunting, stop, substitute signals and repeaters placed by the track from the templates of the scenery, described by //$e signal: (kind, mount, lamps, lean, the event the train reads them by) which tools/editor/signal_headers.py writes into them; a click by a track puts the signal on that side 2.25 m from the axis at the rail heads, facing the trains which have it on their right, a drag along turns it to the trains going the way of the drag; its (p1)_sem_info goes to event1 or event2 of the path, warning signals and repeaters, which the train doesn't read, get none; a head leaning away from the track swapped for its counterpart; the plate named like the signal; signals dragged along the track with their event, removed with it, renamed in the node properties with it, each a single step of the undo history; Polish translation --- CMakeLists.txt | 1 + application/editormode.cpp | 27 + application/editormode.h | 95 ++- application/editormodelineside.cpp | 2 +- application/editormodeselftest.cpp | 146 ++++ application/editormodesignal.cpp | 1018 ++++++++++++++++++++++++++++ application/editormodetrack.cpp | 32 +- application/editormodetrackux.cpp | 38 +- editor/editorIncludeInfo.cpp | 40 +- editor/editorIncludeInfo.hpp | 9 +- editor/editorSettings.cpp | 3 + editor/editorSettings.hpp | 3 + lang/editor_pl.po | 190 ++++++ tools/editor/signal_headers.py | 237 +++++++ 14 files changed, 1828 insertions(+), 13 deletions(-) create mode 100644 application/editormodesignal.cpp create mode 100644 tools/editor/signal_headers.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 116f032d..1613f0a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,6 +195,7 @@ set(SOURCES "application/editormodeparallel.cpp" "application/editormodelineside.cpp" "application/editormodevehicle.cpp" +"application/editormodesignal.cpp" "application/editormodesweep.cpp" "application/editormodeturntable.cpp" "application/editormodeselftest.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 7958465c..0ee3d1db 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1355,6 +1355,12 @@ bool editor_mode::update() point_drag_update(); if (m_track_tab == track_tab::turntable) draw_turntable_overlay(); + if (m_track_tab == track_tab::signals) + { + signal_refresh(); + signal_move_update(); + draw_signal_overlay(); + } if (m_track_tab == track_tab::lineside) { draw_parallel_preview(); @@ -2423,6 +2429,17 @@ void editor_mode::apply_include() auto const directive = editor_includes::compose_directive(m_include.target, m_include.values); if (directive == before) return; + std::string read_before, read_after; + if (false == m_include.info.signal_read.empty()) + { + std::string file; + std::vector previous; + if (editor_includes::parse_directive(before, file, previous)) + { + read_before = editor_includes::substitute(m_include.info.signal_read, previous); + read_after = editor_includes::substitute(m_include.info.signal_read, m_include.values); + } + } if (false == m_include_gesture) { // a single undo step for the whole drag @@ -2430,10 +2447,14 @@ void editor_mode::apply_include() snap.instance = m_instance; snap.node_name = *scene::Layers.instance(m_instance).file; snap.serialized = std::move(before); + if (read_before != read_after) + signal_renamed(read_before, read_after, snap); m_history.push_back(std::move(snap)); g_redo.clear(); m_include_gesture = true; } + else if (read_before != read_after && false == m_history.empty()) + signal_renamed(read_before, read_after, m_history.back()); set_include_directive(m_instance, directive); } @@ -4209,6 +4230,10 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in { break; } + if (is_press(Action) && m_instance != 0 && signal_delete(m_instance)) + { + break; + } if (is_press(Action) && m_instance != 0) { // include of a scenery template. what it shows goes out of sight, the directive is erased on save @@ -4534,6 +4559,8 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const finish_switch_placement(); if (m_turntable.placing) turntable_finish_placement(); + if (m_signal.placing || m_signal.moving) + signal_release(); } m_dragging = false; diff --git a/application/editormode.h b/application/editormode.h index b911e0e1..95e9bc8f 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -29,6 +29,7 @@ http://mozilla.org/MPL/2.0/. #include "world/Sweep.h" #include +#include #include #include #include @@ -814,7 +815,7 @@ class editor_mode : public application_mode, private editor_track::observer void update_build_tools(); void draw_build_overlay() const; // modes of the track editor, one at a time: path is the plain selection - enum class track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable }; + enum class track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals }; struct track_mode { char const *label; @@ -822,7 +823,7 @@ class editor_mode : public application_mode, private editor_track::observer track_tab tab; char const *tooltip; }; - static std::array const &track_modes(); + static std::array const &track_modes(); // the objects along the track: a tab for each tool int m_lineside_tab{0}; void render_lineside_ui(); @@ -878,7 +879,7 @@ class editor_mode : public application_mode, private editor_track::observer void draw_track_hover() const; struct track_intent { - enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end }; + enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move }; kind what{kind::none}; TTrack *track{nullptr}; int path{0}; @@ -1043,6 +1044,7 @@ class editor_mode : public application_mode, private editor_track::observer bool described{false}; std::string track; glm::dvec2 tilt{0.0}; // rot.x and rot.z of a described template, degrees + std::map values; }; struct standing_template { @@ -1192,6 +1194,93 @@ class editor_mode : public application_mode, private editor_track::observer void render_vehicle_ui(); void draw_vehicle_marker() const; + struct signal_template + { + std::string file; + std::string name; + std::string description; + std::string kind; + std::string mount; + std::string lamps; + std::string lean; + std::string read; + int parameters{0}; + bool plate{false}; + bool linked{false}; + }; + struct signal_spot + { + TTrack *track{nullptr}; + int path{0}; + glm::dvec3 axis{0.0}; + glm::dvec2 travel{0.0, 1.0}; + int side{1}; + int list{2}; + double offset{0.0}; + glm::dvec3 position{0.0}; + double yaw{0.0}; + }; + struct signal_standing + { + standing_template include; + int kind{-1}; + std::string name; + std::string read; + TTrack *track{nullptr}; + int list{0}; + signal_spot spot; + bool on_track{false}; + }; + struct signal_tool + { + std::vector templates; + int revision{-1}; + int kind{0}; + std::array chosen{-1, -1, -1, -1, -1, -1, -1}; + char filter[64]{}; + char name[64]{"A"}; + char plate[64]{}; + char linked[64]{"none"}; + float height{0.0f}; + bool placing{false}; + signal_spot press; + glm::vec2 pressed{0.f}; + int facing{0}; + std::vector standing; + std::size_t standing_history{static_cast(-1)}; + std::size_t standing_redo{static_cast(-1)}; + double standing_time{-1.0}; + int hover{-1}; + scene::instance_handle selected{0}; + bool moving{false}; + signal_spot moved; + bool moved_valid{false}; + std::string status; + }; + signal_tool m_signal; + void signal_scan(); + signal_template const *signal_armed() const; + signal_template const *signal_template_of(std::string const &File) const; + bool signal_spot_at(TTrack &Track, int const Path, glm::dvec3 const &Point, glm::dvec3 const &Cursor, int const Facing, int const Side, signal_template const &Template, signal_spot &Spot) const; + void signal_refresh(bool const Force = false); + int signal_hit() const; + void signal_press(track_intent const &Intent); + void signal_release(); + bool signal_place(signal_spot const &Spot, signal_template const &Template); + void signal_move_update(); + void signal_move_finish(); + bool signal_delete(scene::instance_handle const Instance); + void signal_detach_events(std::string const &Read, std::vector> &States); + void signal_renamed(std::string const &Before, std::string const &After, EditorSnapshot &Snapshot); + void signal_select(scene::instance_handle const Instance); + std::string signal_next_name(std::string const &Name) const; + void render_signal_ui(); + void render_signal_palette(); + void render_signal_selected(); + void draw_signal_overlay() const; + std::string signal_intent_label(signal_template const &Template, signal_spot const &Spot) const; + std::string signal_variant(signal_template const &Template, signal_spot const &Spot) const; + // a model laid along the track: bent to follow it, or repeated along it struct sweep_tool { diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp index ea686900..37d793d3 100644 --- a/application/editormodelineside.cpp +++ b/application/editormodelineside.cpp @@ -288,7 +288,7 @@ std::size_t editor_mode::place_templates(std::vector const &Items include_info info; if (false == editor_includes::load(item.file, info, Error)) continue; - directive = editor_includes::directive(item.file, info, editor_includes::parameter_count(item.file), local, static_cast(item.yaw), item.track, item.tilt); + directive = editor_includes::directive(item.file, info, editor_includes::parameter_count(item.file), local, static_cast(item.yaw), item.track, item.tilt, item.values); } else { diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index a731d2d0..8bb4f2a5 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -918,6 +918,152 @@ void editor_mode::selftest_step() std::getline(words, text); WriteLog("SELFTEST" + text); } + else if (command == "signal" || command == "signalmove" || command == "signaldelete" || command == "signalrename" || command == "signalprobe") + { + std::map values; + std::string pair; + while (words >> pair) + { + auto const equals{pair.find('=')}; + if (equals != std::string::npos) + values[pair.substr(0, equals)] = pair.substr(equals + 1); + } + show_track_tab(track_tab::signals); + signal_refresh(true); + auto &tool{m_signal}; + auto const standing_named = [&](std::string const &Name) -> signal_standing const * { + auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.name == Name; })}; + return found != tool.standing.end() ? &*found : nullptr; + }; + auto const spot_on = [&](signal_template const &Template, int const Facing, int const Side, signal_spot &Spot) { + auto *track{simulation::Paths.find(values["track"])}; + if (track == nullptr) + return false; + auto const path{values.count("path") ? std::stoi(values["path"]) : 0}; + auto const at{values.count("at") ? std::stod(values["at"]) : 0.5}; + geometry::bezier const curve{track->m_paths[std::clamp(path, 0, static_cast(track->m_paths.size()) - 1)]}; + return signal_spot_at(*track, path, curve.point(at), curve.point(at), Facing, Side, Template, Spot); + }; + if (command == "signal") + { + auto const *item{signal_template_of(values["file"])}; + if (item == nullptr) + { + WriteLog("SELFTEST signal: no signal template " + values["file"]); + continue; + } + for (int k = 0; k < static_cast(tool.templates.size()); ++k) + if (&tool.templates[k] == item) + { + static char const *const kinds[] = {"main", "block", "warning", "shunt", "stop", "substitute", "repeater"}; + for (int i = 0; i < 7; ++i) + if (item->kind == kinds[i]) + { + tool.kind = i; + tool.chosen[i] = k; + } + } + if (values.count("name")) + std::snprintf(tool.name, sizeof(tool.name), "%s", values["name"].c_str()); + if (values.count("plate")) + std::snprintf(tool.plate, sizeof(tool.plate), "%s", values["plate"].c_str()); + auto const side{values["side"] == "left" ? -1 : 1}; + auto const facing{values["facing"] == "along" ? 1 : values["facing"] == "against" ? -1 : 0}; + signal_spot spot; + if (false == spot_on(*signal_armed(), facing, side, spot)) + { + WriteLog("SELFTEST signal: no such track " + values["track"]); + continue; + } + auto const placed{signal_place(spot, *signal_armed())}; + WriteLog(format("SELFTEST signal %s: %s", placed ? "placed" : "failed", tool.status.c_str())); + } + else if (command == "signalmove") + { + auto const *standing{standing_named(values["name"])}; + auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr}; + if (item == nullptr) + { + WriteLog("SELFTEST signalmove: no signal " + values["name"]); + continue; + } + auto const yaw{glm::radians(standing->include.yaw + 180.0)}; + glm::dvec2 const travel{std::sin(yaw), std::cos(yaw)}; + signal_spot probe; + if (false == spot_on(*item, 1, 1, probe)) + { + WriteLog("SELFTEST signalmove: no such track " + values["track"]); + continue; + } + auto const facing{glm::dot(probe.travel, travel) >= 0.0 ? 1 : -1}; + auto const sideoftravel{glm::dot(geometry::plan_of(standing->include.location) - geometry::plan_of(standing->spot.axis), glm::dvec2{-travel.y, travel.x}) >= 0.0 ? 1 : -1}; + signal_select(standing->include.instance); + tool.moving = true; + tool.moved_valid = spot_on(*item, facing, sideoftravel * facing, tool.moved); + tool.pressed = {-1000.f, -1000.f}; + signal_release(); + WriteLog("SELFTEST signalmove: " + tool.status); + } + else if (command == "signaldelete") + { + auto const *standing{standing_named(values["name"])}; + auto const deleted{standing != nullptr && signal_delete(standing->include.instance)}; + WriteLog(std::string{"SELFTEST signaldelete "} + values["name"] + (deleted ? ": removed with its event" : ": not removed by the signal tool")); + } + else if (command == "signalrename") + { + auto const *standing{standing_named(values["name"])}; + if (standing == nullptr) + { + WriteLog("SELFTEST signalrename: no signal " + values["name"]); + continue; + } + select_include(standing->include.instance); + m_include.values[0] = values["to"]; + apply_include(); + m_include_gesture = false; + WriteLog("SELFTEST signalrename " + values["name"] + " -> " + values["to"]); + } + else + { + signal_refresh(true); + WriteLog(format("SELFTEST signalprobe: %zu signals", tool.standing.size())); + for (auto const &standing : tool.standing) + { + std::string where{"not read"}; + if (standing.track != nullptr) + { + auto const &curve{standing.track->m_paths.front()}; + geometry::bezier const bez{curve}; + auto best{std::numeric_limits::max()}; + double parameter{0.0}; + for (int k = 0; k <= 1024; ++k) + { + auto const d{geometry::plan_distance(bez.point(k / 1024.0), standing.include.location)}; + if (d < best) + { + best = d; + parameter = k / 1024.0; + } + } + auto const axis{bez.point(parameter)}; + auto tangent{bez.first(parameter)}; + glm::dvec2 travel{tangent.x, tangent.z}; + travel = glm::normalize(travel) * (standing.list == 1 ? -1.0 : 1.0); + glm::dvec2 const offset{standing.include.location.x - axis.x, standing.include.location.z - axis.z}; + auto const cross{travel.x * offset.y - travel.y * offset.x}; + auto heading{glm::degrees(std::atan2(travel.x, travel.y))}; + auto turn{std::fmod(standing.include.yaw - heading + 720.0, 360.0)}; + auto const &list{editor_track::events(*standing.track, standing.list)}; + auto const bound{std::any_of(list.begin(), list.end(), [&](auto const &Event) { return Event.second != nullptr && ToLower(Event.first) == ToLower(standing.read); })}; + where = std::string{bound ? "bound, " : "unbound, "} + format("%s of %s, %.3f m from the axis, %s of the trains, %.3f m over the axis, model turned %.1f deg from the travel", editor_track::event_keyword(standing.list), standing.track->name().c_str(), glm::length(offset), cross > 0.0 ? "right" : "left", + standing.include.location.y - axis.y, turn); + } + WriteLog(format("SELFTEST signalprobe %s %s at %.3f %.3f %.3f yaw %.2f read %s: %s", standing.name.c_str(), standing.include.file.c_str(), standing.include.location.x, standing.include.location.y, standing.include.location.z, standing.include.yaw, + standing.read.empty() ? "-" : standing.read.c_str(), where.c_str())); + } + } + } else if (command == "quit") { if (false == test.quitting) diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp new file mode 100644 index 00000000..d071f942 --- /dev/null +++ b/application/editormodesignal.cpp @@ -0,0 +1,1018 @@ +/* +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 +#include +#include +#include +#include + +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(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::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 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 const &plate_textures() +{ + static std::vector 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(1, static_cast(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(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(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 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(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(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(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(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(side) * Spot.offset}; + Spot.position = {axis.x + across.x, axis.y + kRailTop + (Template.mount == "gantry" ? static_cast(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> 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(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(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(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() || m_hover.track == nullptr) + { + tool.moved_valid = false; + return; + } + auto const *item{signal_template_of(found->include.file)}; + if (item == nullptr) + return; + 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(*m_hover.track, std::clamp(m_hover.path, 0, static_cast(m_hover.track->m_paths.size()) - 1), m_hover.point)}; + auto const forward{forward_at(*m_hover.track, std::clamp(m_hover.path, 0, static_cast(m_hover.track->m_paths.size()) - 1), parameter)}; + auto const facing{glm::dot(forward, travel) >= 0.0 ? 1 : -1}; + auto const sideoftravel{glm::dot(plan_of(found->include.location) - plan_of(m_hover.point), right_of(travel)) >= 0.0 ? 1 : -1}; + tool.moved_valid = signal_spot_at(*m_hover.track, m_hover.path, m_hover.point, m_hover.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 (false == tool.moved_valid || glm::length(glm::vec2{mouse.x, mouse.y} - tool.pressed) <= kDragStart) + 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() || false == scene::Layers.tracked(found->include.instance) || false == scene::Layers.removable(found->include.instance)) + 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> 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::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(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(Name.back())}; + if (std::isalpha(last) && last != 'z' && last != 'Z') + return Name.substr(0, Name.size() - 1) + static_cast(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> &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 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> 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> groups; + std::vector keys; + for (int i = 0; i < static_cast(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((ImGui::GetContentRegionAvail().x + spacing) / (tile.x + spacing)))}; + auto *draw{ImGui::GetWindowDrawList()}; + for (int g = 0; g < static_cast(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(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(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(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()}; + 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(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(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); + } + } +} diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index e3e6bafe..ee91503e 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -561,15 +561,16 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve profile_restore(); } -std::array const &editor_mode::track_modes() +std::array const &editor_mode::track_modes() { - static std::array const modes{{ + static std::array const modes{{ {"Select", "Esc", track_tab::path, "LMB on a path selects it: its points, control vectors and parameters"}, {"Lay track", "L", track_tab::lay, "Lays new track through the clicked points, on the ground or from a free end of a track"}, {"Switch", "T", track_tab::turnout, "Puts switches into the selected straight or curve, edits the geometry of the selected switch"}, {"Turntable", "U", track_tab::turntable, "Places a turntable, leads tracks out of it and fits the tracks around to it"}, {"Straight", "G", track_tab::straights, "The whole straight through the selected path: drag its ends or the middle, break it, shift it"}, {"Curve", "C", track_tab::route, "The line through the path, switch to switch: vertices with the radii, transitions and cant of the curves"}, + {"Signals", "H", track_tab::signals, "Signals by the track, from the templates of the scenery, with the event the train reads them by"}, {"Objects", "B", track_tab::lineside, "Along the track: hectometre posts, fouling point markers, a parallel track, a vehicle to drive"}, {"Profile", "P", track_tab::profile, "Vertical profile (grade line) along the line"}, {"Speed", "V", track_tab::speed, "Speed limits of the paths against the speed their geometry allows"}, @@ -636,6 +637,10 @@ void editor_mode::show_track_tab(track_tab const Tab) scan_turntables(); turntable_read(track); break; + case track_tab::signals: + signal_scan(); + signal_refresh(true); + break; default: break; } @@ -676,7 +681,7 @@ bool editor_mode::track_shortcut(int const Key) bool editor_mode::track_busy() const { auto const &straights{m_straights}; - return (m_lay.active && false == m_lay.points.empty()) || m_turntable.placing || straights.dragging || m_extend.active || m_point_drag.active || m_handle_drag.active || m_switch.placing || m_track_point.valid() || m_route.vertex >= 0 || m_route.grip >= 0 || false == straights.detour.empty() || straights.tool != 0 || straights.handle >= 0; + return (m_lay.active && false == m_lay.points.empty()) || m_turntable.placing || m_signal.placing || m_signal.moving || straights.dragging || m_extend.active || m_point_drag.active || m_handle_drag.active || m_switch.placing || m_track_point.valid() || m_route.vertex >= 0 || m_route.grip >= 0 || false == straights.detour.empty() || straights.tool != 0 || straights.handle >= 0; } std::string editor_mode::track_mode_name() const @@ -707,6 +712,12 @@ std::string editor_mode::track_mode_name() const case track_tab::lineside: return STR_C("Objects along the track"); case track_tab::infra: return STR_C("Infrastructure along the track"); + case track_tab::signals: + if (m_signal.moving) + return STR_C("Signal: drag along the track, release to put it there"); + if (m_signal.placing) + return STR_C("Signal: drag along the track sets the direction of the trains, release to place it"); + return signal_armed() != nullptr ? STR_C("Signal: LMB by the track places it on that side") : STR_C("Signal: choose its type below"); case track_tab::turntable: if (m_turntable.placing) return STR_C("Turntable: drag to turn the bridge, release to place it"); @@ -770,6 +781,7 @@ void editor_mode::render_track_inspector() case track_tab::speed: render_speed_body(); break; case track_tab::joints: render_joints_body(); break; case track_tab::lineside: render_lineside_ui(); break; + case track_tab::signals: render_signal_ui(); break; case track_tab::infra: render_infra_body(); break; case track_tab::turntable: render_turntable_ui(); break; default: @@ -815,8 +827,8 @@ void editor_mode::render_track_modes(TTrack *Track) ImGui::SetTooltip("%s [%s]", STR_C(mode.tooltip), mode.key); } }; - row("Edit", 0, 6); - row("Analysis", 6, static_cast(modes.size())); + row("Edit", 0, 7); + row("Analysis", 7, static_cast(modes.size())); ImGui::SameLine(); ImGui::Checkbox(STR_C("Structure gauge"), &m_gauge.open); if (ImGui::IsItemHovered()) @@ -3007,6 +3019,8 @@ void editor_mode::cancel_track_tools() } m_extend.active = false; m_turntable.placing = false; + m_signal.placing = false; + m_signal.moving = false; m_handle_drag = {}; if (m_point_drag.active) { @@ -3468,6 +3482,14 @@ std::vector editor_mode::track_key_hints(bool const All) hints = {{"LMB on a row", "show the path"}}; else if (m_track_window_open && m_track_tab == track_tab::lineside) hints = {{"LMB on a track", "the line the objects go along"}}; + else if (m_track_window_open && m_track_tab == track_tab::signals && (m_signal.placing || m_signal.moving)) + hints = {{"Drag along", m_signal.moving ? "moves the signal along the track" : "the trains it faces run that way"}, {"Release", "done"}, {"Esc", "cancel"}}; + else if (m_track_window_open && m_track_tab == track_tab::signals) + { + hints = {{"LMB by a track", "places the signal on that side, facing the trains which have it on their right"}, {"Press+drag along", "the trains it faces run the way of the drag"}, {"LMB on a signal", "selects it"}, {"Drag a signal", "moves it along the track"}}; + if (m_signal.selected != 0) + hints.push_back({"Del", "removes the selected signal with its event"}); + } else if (m_track_window_open && m_track_tab == track_tab::joints) hints = {{"LMB on a row", "show the place"}, {"Fix", "joins the ends, aligns them"}}; else if (m_track_window_open && m_track_tab == track_tab::infra) diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 22140d33..0de31ee4 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -193,7 +193,7 @@ void editor_mode::draw_track_hover() const bool editor_mode::track_gesture_on() const { return mouseHold || m_track_set_using || m_track_box.active || m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS || m_track_gizmo_using || m_route_gizmo_using || m_extend.active || m_switch.placing || m_straights.dragging || m_straights.tool_mouse || - m_point_drag.active || m_handle_drag.active || m_sweep.dragging || m_turntable.placing || ImGuizmo::IsUsing(); + m_point_drag.active || m_handle_drag.active || m_sweep.dragging || m_turntable.placing || m_signal.placing || m_signal.moving || ImGuizmo::IsUsing(); } editor_mode::track_intent editor_mode::track_intent_at(int const Mods) @@ -232,6 +232,40 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) } if (m_track_tab == track_tab::turntable) return turntable_intent(ground, shift); + if (m_track_tab == track_tab::signals) + { + if (auto const hit{signal_hit()}; hit >= 0) + { + auto const &standing{m_signal.standing[hit]}; + intent.index = hit; + intent.position = standing.include.location; + intent.action = format(STR_C("Click: select the signal %s, drag: move it along the track"), standing.name.c_str()); + intent.what = kind::signal_move; + return intent; + } + auto const *armed{signal_armed()}; + if (m_hover.track != nullptr && 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)) + { + intent.track = m_hover.track; + intent.path = m_hover.path; + intent.position = spot.position; + intent.action = signal_intent_label(*armed, spot); + intent.what = kind::signal_place; + return intent; + } + } + if (m_hover.track != nullptr) + { + intent.track = m_hover.track; + intent.path = m_hover.path; + intent.position = m_hover.point; + return make(kind::select, "Choose the type of the signal in the Tracks window first"); + } + return intent; + } if (m_track_tab == track_tab::path) { if (shift && control && m_hover.track != nullptr && rail(*m_hover.track)) @@ -419,6 +453,8 @@ void editor_mode::track_press(track_intent const &Intent) case kind::turntable_place: case kind::turntable_exit: case kind::turntable_fit: turntable_press(Intent); return; + case kind::signal_place: + case kind::signal_move: signal_press(Intent); return; case kind::select: case kind::none: { diff --git a/editor/editorIncludeInfo.cpp b/editor/editorIncludeInfo.cpp index d57da257..d5d9ccae 100644 --- a/editor/editorIncludeInfo.cpp +++ b/editor/editorIncludeInfo.cpp @@ -251,6 +251,14 @@ bool parse(template_text const &Text, include_info &Info, std::string &Error) Info.switch_hand = drive["hand"].as(""); Info.switch_drive = drive["drive"].as(""); } + if (auto const signal{document["signal"]}; signal.IsDefined() && signal.IsMap()) + { + Info.signal_kind = signal["kind"].as(""); + Info.signal_mount = signal["mount"].as(""); + Info.signal_lamps = signal["lamps"].as(""); + Info.signal_lean = signal["lean"].as(""); + Info.signal_read = signal["read"].as(""); + } if (auto const table{document["turntable"]}; table.IsDefined() && table.IsMap()) { Info.turntable_length = table["length"].as(0.0); @@ -644,7 +652,27 @@ std::string compose_directive(std::string const &File, std::vector return text + " end"; } -std::string directive(std::string const &File, include_info const &Info, int const Parameters, glm::dvec3 const &Location, std::optional Yaw, std::string const &Track, glm::dvec2 const &Tilt) +std::string substitute(std::string const &Text, std::vector const &Values) +{ + std::string result; + std::size_t length{0}; + for (std::size_t idx = 0; idx < Text.size();) + { + auto const id{placeholder(Text, idx, length)}; + if (id > 0 && id <= static_cast(Values.size())) + { + result += Values[id - 1]; + idx += length; + } + else + { + result += Text[idx++]; + } + } + return result; +} + +std::string directive(std::string const &File, include_info const &Info, int const Parameters, glm::dvec3 const &Location, std::optional Yaw, std::string const &Track, glm::dvec2 const &Tilt, std::map const &Values) { std::vector values; for (auto id = 1; id <= Parameters; ++id) @@ -652,7 +680,15 @@ std::string directive(std::string const &File, include_info const &Info, int con auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })}; auto const role{lookup != std::end(Info.parameters) ? lookup->role : std::string{"free"}}; auto value{lookup != std::end(Info.parameters) ? lookup->value : std::string{}}; - if (role == "pos.x") + if (auto const given{Values.find("#" + std::to_string(id))}; given != Values.end()) + { + value = given->second; + } + else if (auto const given{Values.find(role)}; given != Values.end()) + { + value = given->second; + } + else if (role == "pos.x") { value = number(Location.x); } diff --git a/editor/editorIncludeInfo.hpp b/editor/editorIncludeInfo.hpp index d22a9606..d2565a2f 100644 --- a/editor/editorIncludeInfo.hpp +++ b/editor/editorIncludeInfo.hpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include #include #include #include @@ -39,6 +40,11 @@ struct include_info double turntable_length{0.0}; int turntable_angles{0}; int turntable_positions{0}; + std::string signal_kind; + std::string signal_mount; + std::string signal_lamps; + std::string signal_lean; + std::string signal_read; std::vector parameters; // description of specified parameter, created if there's none @@ -71,7 +77,8 @@ bool complete(include_info const &Info, int Parameters, std::string *Issue = nul // builds include directive which places specified template. Location and Yaw are the placement to pass through // the parameters with matching roles, in the coordinates of the place the directive goes to; without Yaw the // rotation parameters receive their default values -std::string directive(std::string const &File, include_info const &Info, int Parameters, glm::dvec3 const &Location, std::optional Yaw, std::string const &Track = {}, glm::dvec2 const &Tilt = glm::dvec2{0.0}); +std::string directive(std::string const &File, include_info const &Info, int Parameters, glm::dvec3 const &Location, std::optional Yaw, std::string const &Track = {}, glm::dvec2 const &Tilt = glm::dvec2{0.0}, std::map const &Values = {}); +std::string substitute(std::string const &Text, std::vector const &Values); // splits include directive into the name of the included file and the values of the parameters, both as they're // written. returns: false if the text isn't a complete include directive diff --git a/editor/editorSettings.cpp b/editor/editorSettings.cpp index b4c5fb31..99d6a460 100644 --- a/editor/editorSettings.cpp +++ b/editor/editorSettings.cpp @@ -94,6 +94,8 @@ bool editorSettings::load() m_orthophoto.in_scene = (value == "1"); else if (key == "platform_edge") parse(value, m_platform_edge); + else if (key == "signal_offset") + parse(value, m_signal_offset); else if (key.rfind(origin_prefix, 0) == 0) { // northing;easting @@ -133,6 +135,7 @@ bool editorSettings::save() stream << "ortho_lift " << m_orthophoto.lift << "\n"; stream << "ortho_in_scene " << (m_orthophoto.in_scene ? 1 : 0) << "\n"; stream << "platform_edge " << m_platform_edge << "\n"; + stream << "signal_offset " << m_signal_offset << "\n"; stream << std::fixed << std::setprecision(3); for (auto const &origin : m_orthophoto_origins) stream << origin.first << " " << origin.second.first << ";" << origin.second.second << "\n"; diff --git a/editor/editorSettings.hpp b/editor/editorSettings.hpp index 6e39cfa0..87be836f 100644 --- a/editor/editorSettings.hpp +++ b/editor/editorSettings.hpp @@ -41,12 +41,15 @@ public: double platform_edge() const { return m_platform_edge; } void platform_edge(double Distance) { m_platform_edge = Distance; } + double signal_offset() const { return m_signal_offset; } + void signal_offset(double Distance) { m_signal_offset = Distance; } private: movement_scheme m_movement{movement_scheme::wsad}; orthophoto_settings m_orthophoto; std::map> m_orthophoto_origins; double m_platform_edge{1.725}; + double m_signal_offset{2.25}; }; // global editor settings instance diff --git a/lang/editor_pl.po b/lang/editor_pl.po index cd6f4948..03c7c4b6 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -4403,3 +4403,193 @@ msgstr "1,650 m: najmniej dopuszczalne w eksploatacji" msgid "edge of the platform from the axis" msgstr "krawędź peronu od osi toru" + +# scenery editor: signals +msgid "Signals" +msgstr "Sygnalizatory" + +msgid "Signals by the track, from the templates of the scenery, with the event the train reads them by" +msgstr "Sygnalizatory przy torze, z szablonów scenerii, z eventem, którym pociąg je odczytuje" + +msgid "Signal: drag along the track, release to put it there" +msgstr "Sygnalizator: przeciągnij wzdłuż toru, puść, żeby go tam postawić" + +msgid "Signal: drag along the track sets the direction of the trains, release to place it" +msgstr "Sygnalizator: przeciąganie wzdłuż toru ustawia kierunek pociągów, puść, żeby go postawić" + +msgid "Signal: LMB by the track places it on that side" +msgstr "Sygnalizator: LPM przy torze stawia go po tej stronie" + +msgid "Signal: choose its type below" +msgstr "Sygnalizator: wybierz jego typ poniżej" + +msgid "moves the signal along the track" +msgstr "przesuwa sygnalizator wzdłuż toru" + +msgid "the trains it faces run that way" +msgstr "pociągi, do których jest zwrócony, jadą w tę stronę" + +msgid "LMB by a track" +msgstr "LPM przy torze" + +msgid "places the signal on that side, facing the trains which have it on their right" +msgstr "stawia sygnalizator po tej stronie, zwrócony do pociągów, które mają go po prawej" + +msgid "Press+drag along" +msgstr "Wciśnij i przeciągnij wzdłuż" + +msgid "the trains it faces run the way of the drag" +msgstr "pociągi, do których jest zwrócony, jadą w stronę przeciągnięcia" + +msgid "LMB on a signal" +msgstr "LPM na sygnalizatorze" + +msgid "Drag a signal" +msgstr "Przeciągnij sygnalizator" + +msgid "moves it along the track" +msgstr "przesuwa go wzdłuż toru" + +msgid "removes the selected signal with its event" +msgstr "usuwa zaznaczony sygnalizator razem z jego eventem" + +msgid "Click: select the signal %s, drag: move it along the track" +msgstr "Klik: zaznacz sygnalizator %s, przeciągnij: przesuń go wzdłuż toru" + +msgid "Choose the type of the signal in the Tracks window first" +msgstr "Najpierw wybierz typ sygnalizatora w oknie Tory" + +msgid "Semaphore" +msgstr "Semafor" + +msgid "Automatic block" +msgstr "SBL" + +msgid "Warning signal" +msgstr "Tarcza ostrzegawcza" + +msgid "Shunting signal" +msgstr "Tarcza manewrowa" + +msgid "Stop signal" +msgstr "Zaporowy" + +msgid "Substitute signal" +msgstr "Sygnał zastępczy" + +msgid "Repeater" +msgstr "Powtarzacz" + +msgid " on the right of the trains" +msgstr " po prawej pociągów" + +msgid " on the left of the trains" +msgstr " po lewej pociągów" + +msgid "Click: %s %s (%s)%s, the train doesn't read it" +msgstr "Klik: %s %s (%s)%s, pociąg go nie odczytuje" + +msgid "Drag along: the trains it faces run the way of the drag" +msgstr "Przeciągnij wzdłuż: pociągi, do których jest zwrócony, jadą w stronę przeciągnięcia" + +msgid "Click: %s %s (%s)%s, read by %s %s of %s" +msgstr "Klik: %s %s (%s)%s, pociąg odczytuje go przez %s %s toru %s" + +msgid "Signal %s moved" +msgstr "Sygnalizator %s przesunięty" + +msgid "Signal %s moved, read by %s %s of %s" +msgstr "Sygnalizator %s przesunięty, pociąg odczytuje go przez %s %s toru %s" + +msgid "Give the signal a name without spaces first" +msgstr "Najpierw nadaj sygnalizatorowi nazwę bez spacji" + +msgid "The signal couldn't be placed: " +msgstr "Nie udało się postawić sygnalizatora: " + +msgid "Signal %s placed (%s), the train doesn't read it" +msgstr "Sygnalizator %s postawiony (%s), pociąg go nie odczytuje" + +msgid "Signal %s placed (%s), but vehicles stand on the path: add %s to its %s by hand" +msgstr "Sygnalizator %s postawiony (%s), ale na torze stoją pojazdy: dopisz %s do jego %s ręcznie" + +msgid "Signal %s placed (%s), but the path can't take its event: %s" +msgstr "Sygnalizator %s postawiony (%s), ale tor nie przyjmie jego eventu: %s" + +msgid "Signal %s placed (%s), read by %s %s of %s. Its logic works after the scenery is saved and opened again" +msgstr "Sygnalizator %s postawiony (%s), pociąg odczytuje go przez %s %s toru %s. Jego logika zadziała po zapisaniu i ponownym otwarciu scenerii" + +msgid "Signal %s removed with its event %s, Ctrl+Z brings both back" +msgstr "Sygnalizator %s usunięty razem z eventem %s, Ctrl+Z przywraca oba" + +msgid "filter: name, w24, sbl..." +msgstr "filtr: nazwa, w24, sbl..." + +msgid "No template of this kind matches the filter" +msgstr "Żaden szablon tego rodzaju nie pasuje do filtra" + +msgid "%zu variants, e.g. %s" +msgstr "wariantów: %zu, np. %s" + +msgid "The train doesn't read it" +msgstr "Pociąg go nie odczytuje" + +msgid "The train reads it by an event of the path" +msgstr "Pociąg odczytuje go przez event toru" + +msgid "Variant of the chosen type. A head leaning to one side is swapped for its counterpart leaning towards the track on the other side" +msgstr "Wariant wybranego typu. Głowica odchylona w jedną stronę zostaje zamieniona na odpowiednik odchylony w stronę toru, gdy sygnalizator stanie po drugiej stronie" + +msgid "The train doesn't read it: it repeats what the signal ahead of it shows" +msgstr "Pociąg go nie odczytuje: powtarza to, co pokazuje sygnalizator przed nim" + +msgid "The train reads it by %s on the path it stands by" +msgstr "Pociąg odczytuje go przez %s na torze, przy którym stoi" + +msgid "Selected: %s (%s)" +msgstr "Zaznaczony: %s (%s)" + +msgid "Read by %s %s of %s" +msgstr "Odczytywany przez %s %s toru %s" + +msgid "No path has %s among its events: the train doesn't see it" +msgstr "Żaden tor nie ma %s wśród swoich eventów: pociąg go nie widzi" + +msgid "Name, plate and the linked signal: in the node properties. Del removes it with its event" +msgstr "Nazwa, tablica i powiązany sygnalizator: we właściwościach węzła. Del usuwa go razem z eventem" + +msgid "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." +msgstr "Brak szablonów sygnalizatorów: żaden szablon katalogu scenerii nie ma opisu z category: signal. tools/editor/signal_headers.py dopisuje opisy do szablonów sygnalizatorów." + +msgid "Name of the next signal; it counts up after each one placed. Its events and memory cell are named after it" +msgstr "Nazwa następnego sygnalizatora; po każdym postawionym rośnie o jeden. Od niej biorą nazwy jego eventy i komórka pamięci" + +msgid "auto: %s" +msgstr "auto: %s" + +msgid "Plate" +msgstr "Tablica" + +msgid "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" +msgstr "Tekstura tablicy z textures/tabl, np. a-12 albo tob. Puste: ta nazwana tak jak sygnalizator, jeśli istnieje" + +msgid "no such texture" +msgstr "nie ma takiej tekstury" + +msgid "Linked signal" +msgstr "Powiązany sygnalizator" + +msgid "(p7) of the template: the warning signal a semaphore drives, the previous block signal, \"none\" for no link" +msgstr "(p7) szablonu: tarcza ostrzegawcza, którą steruje semafor, poprzedni semafor SBL, \"none\": bez powiązania" + +msgid "Distance of the mast from the axis of the track. 2.25 m is what the signals of the sceneries stand at" +msgstr "Odległość masztu od osi toru. Na 2,25 m stoją sygnalizatory w sceneriach" + +msgid "Over the rails (m)" +msgstr "Nad szynami (m)" + +msgid "A head for a gantry hangs over the axis of the track this high over the rail heads" +msgstr "Głowica na bramkę wisi nad osią toru tak wysoko nad główkami szyn" + +msgid " not read" +msgstr " nieodczytywany" diff --git a/tools/editor/signal_headers.py b/tools/editor/signal_headers.py new file mode 100644 index 00000000..c208406d --- /dev/null +++ b/tools/editor/signal_headers.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +"""Writes //$e descriptions into the signal templates of the scenery directory, so the editor can place them. + +usage: signal_headers.py [--write] +without --write it only reports what it would write +""" + +import os +import re +import sys +import tarfile +import time + +FAMILIES = [ + (re.compile(r'^ss[dow]?\d'), 'main'), + (re.compile(r'^sk(\d|-un)'), 'main'), + (re.compile(r'^sbl\d'), 'block'), + (re.compile(r'^(ts\d|tk\d|top|tsosp)'), 'warning'), + (re.compile(r'^(ps\d|sp3)'), 'repeater'), + (re.compile(r'^(ms\d|zs2nb)'), 'shunt'), + (re.compile(r'^(zs1c|zs2cb|tzm|z1)'), 'stop'), + (re.compile(r'^sz(i|k|y|yn|-ksztalt\d?)\.inc$'), 'substitute'), +] + +COLOURS = [('zielon', 'z'), ('czerwon', 'c'), ('pomara', 'p'), ('żółt', 'p'), ('biał', 'b'), ('mleczn', 'b'), ('niebiesk', 'n'), ('ślep', 'x'), ('brak', 'x')] + +LINEMARK = '//$e' + + +def tokens(text): + text = re.sub(r'//[^\n]*', ' ', text) + text = re.sub(r'/\*.*?\*/', ' ', text, flags=re.S) + return re.split(r'[\s;]+', text.lower()) + + +def statements(words, keyword): + out = [] + idx = 0 + while idx < len(words): + if words[idx] == keyword: + if keyword == 'event': + stop = idx + while stop < len(words) and words[stop] != 'endevent': + stop += 1 + else: + stop = idx + 10 + out.append(words[idx:stop]) + idx += 1 + return out + + +def examine(path, file): + raw = open(path, 'rb').read() + try: + text = raw.decode('utf-8') + encoding = 'utf-8' + except UnicodeDecodeError: + text = raw.decode('cp1250') + encoding = 'cp1250' + lines = [line for line in text.splitlines() if not line.startswith(LINEMARK)] + body = '\n'.join(lines) + words = tokens(body) + count = max([int(m) for m in re.findall(r'\(p(\d+)\)', body)] or [0]) + + if 'origin' not in words or words[words.index('origin') + 1:words.index('origin') + 4] != ['(p2)', '(p3)', '(p4)']: + return None, 'no origin (p2) (p3) (p4)' + if 'rotate' not in words or words[words.index('rotate') + 1:words.index('rotate') + 4] != ['0', '(p5)', '0']: + return None, 'no rotate 0 (p5) 0' + if count < 5: + return None, 'fewer than 5 parameters' + + models = [n for n in statements(words, 'node') if len(n) > 9 and n[4] == 'model'] + head = next((n for n in models if n[3].startswith('(p1)')), None) + if head is None: + return None, 'no model named after (p1)' + model = head[9] + + cells = {} + for n in statements(words, 'node'): + if len(n) > 8 and n[4] == 'memcell': + cells[n[3]] = n[8] + read = '' + for e in statements(words, 'event'): + velocity = (len(e) > 4 and e[2] == 'getvalues' and cells.get(e[4]) in ('setvelocity', 'shuntvelocity')) or (len(e) > 8 and e[2] == 'putvalues' and e[8] in ('setvelocity', 'shuntvelocity')) + if velocity and e[1].startswith('(p1)'): + if not read or e[1].endswith('_sem_info'): + read = e[1] + + kind = next((k for pattern, k in FAMILIES if pattern.search(file)), None) + if kind is None: + return None, 'not a signal family' + if kind == 'repeater' and cells: + return None, 'repeater with a memory cell' + if kind in ('main', 'block', 'shunt', 'stop', 'substitute') and not read and cells: + return None, 'memory cell which no event reads for the train' + if kind in ('warning', 'repeater'): + read = '' + + models_used = [n[9] for n in models] + lamps = '' + lean = '' + m = re.match(r'sem/(?:[a-z]+)_(\d)_([lpyk])_([a-z]+)[_.]', model) + if m: + lamps = m.group(3) + lean = {'l': 'left', 'y': 'right', 'p': '', 'k': ''}[m.group(2)] + else: + m = re.match(r'sem/karzelki/k(?:[a-z]*?\d|tm|to|s)([zcpbnx]+)[.+]', model) or re.match(r'sem/glowice/(?:s|to)\d(?:i|y[lp])([zcpbnx]+)_dd\.', model) + if m: + lamps = m.group(1) + if not lamps: + for line in lines: + if line.lower().lstrip('/ ').startswith('komory:'): + for word in line.split(':', 1)[1].split(','): + word = word.strip().lower() + lamps += next((c for prefix, c in COLOURS if word.startswith(prefix)), '') + break + + if 'karzelki/' in model or re.search(r'k(w\d+)?(\+.*)?\.inc$', file) or 'pod_betonowa' in body: + mount = 'dwarf' + elif '_drb_' in model and not any(('slupy/' in m or 'post' in m or 'drabinki/' in m) for m in models_used): + mount = 'gantry' + else: + mount = 'mast' + + description = '' + for line in lines: + stripped = line.strip() + if stripped.startswith('//'): + stripped = stripped.lstrip('/').strip() + if stripped.lower().startswith('typ:'): + stripped = stripped[4:].strip() + if stripped: + description = stripped + break + elif stripped: + break + + said = description.lower() + if 'w lewo' in said: + lean = 'left' + elif 'w prawo' in said: + lean = 'right' + elif 'prost' in said: + lean = '' + + params = [ + "{id: 1, role: name, label: Name, default: A}", + "{id: 2, role: pos.x}", + "{id: 3, role: pos.y}", + "{id: 4, role: pos.z}", + "{id: 5, role: rot.y}", + ] + if count >= 6: + params.append("{id: 6, role: texture, label: Plate, default: 0_nothing}" if 'tabl/(p6)' in body.lower() else "{id: 6, role: text, label: Symbol, default: none}") + for id in range(7, count + 1): + label = 'Linked signal' if id == 7 else 'Parameter %d' % id + params.append("{id: %d, role: text, label: %s, default: none}" % (id, label)) + + signal = ['kind: ' + kind, 'mount: ' + mount] + if lamps: + signal.append('lamps: ' + lamps) + if lean: + signal.append('lean: ' + lean) + if read: + signal.append('read: "' + read + '"') + header = [ + 'version: 1', + 'name: ' + file[:-4], + 'category: signal', + ] + if description: + header.append('description: "' + description.replace('\\', '\\\\').replace('"', '\\"') + '"') + header.append('signal: {' + ', '.join(signal) + '}') + header.append('params:') + header += [' - ' + p for p in params] + return {'raw': raw, 'text': text, 'encoding': encoding, 'header': header, 'kind': kind, 'model': model, 'read': read, 'lamps': lamps, 'mount': mount, 'lean': lean, 'count': count}, '' + + +def rewrite(info): + text = info['text'] + eol = '\r\n' if '\r\n' in text else '\n' + bom = text.startswith('') + if bom: + text = text[1:] + kept = [line for line in text.split(eol) if not line.startswith(LINEMARK)] + block = [LINEMARK + ' ' + line for line in info['header']] + out = eol.join(block + kept) + return (('' if bom else '') + out).encode(info['encoding']) + + +def main(): + if len(sys.argv) < 2: + print(__doc__) + return 1 + root = os.path.join(sys.argv[1], 'scenery') + write = '--write' in sys.argv + files = sorted(f for f in os.listdir(root) if f.endswith('.inc')) + files += ['pkp/semafory/' + f for f in sorted(os.listdir(os.path.join(root, 'pkp', 'semafory'))) if f.endswith('.inc')] + done = [] + skipped = [] + for file in files: + name = os.path.basename(file).lower() + if not any(pattern.search(name) for pattern, _ in FAMILIES): + continue + info, reason = examine(os.path.join(root, file), name) + if info is None: + skipped.append((file, reason)) + continue + done.append((file, info)) + + for file, info in done: + print('%-34s %-10s %-6s %-6s %-6s p%d %-22s %s' % (file, info['kind'], info['mount'], info['lean'] or '-', info['lamps'] or '-', info['count'], info['read'] or '-', info['model'])) + for file, reason in skipped: + print('SKIP %-34s %s' % (file, reason)) + kinds = {} + for _, info in done: + kinds[info['kind']] = kinds.get(info['kind'], 0) + 1 + print('described: %d %s, skipped: %d' % (len(done), kinds, len(skipped))) + + if write: + backup = os.path.join(sys.argv[1], 'editor_backup') + os.makedirs(backup, exist_ok=True) + archive = os.path.join(backup, time.strftime('signals_%Y%m%d_%H%M%S.tar')) + with tarfile.open(archive, 'w') as tar: + for file, _ in done: + tar.add(os.path.join(root, file), arcname='scenery/' + file) + for file, info in done: + content = rewrite(info) + if content != info['raw']: + with open(os.path.join(root, file), 'wb') as out: + out.write(content) + print('written, originals in', archive) + return 0 + + +if __name__ == '__main__': + sys.exit(main())