17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-07 18:04:47 +01:00

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

This commit is contained in:
maj00r
2026-10-08 03:12:16 +02:00
parent 4a7d839e5d
commit b02fe0ac2b
14 changed files with 1828 additions and 13 deletions

View File

@@ -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"

View File

@@ -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<std::string> 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;

View File

@@ -29,6 +29,7 @@ http://mozilla.org/MPL/2.0/.
#include "world/Sweep.h"
#include <array>
#include <map>
#include <chrono>
#include <functional>
#include <memory>
@@ -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<track_mode, 11> const &track_modes();
static std::array<track_mode, 12> 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<std::string, std::string> 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<signal_template> templates;
int revision{-1};
int kind{0};
std::array<int, 7> 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<signal_standing> standing;
std::size_t standing_history{static_cast<std::size_t>(-1)};
std::size_t standing_redo{static_cast<std::size_t>(-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<std::pair<TTrack *, editor_track::state>> &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
{

View File

@@ -288,7 +288,7 @@ std::size_t editor_mode::place_templates(std::vector<template_item> 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<float>(item.yaw), item.track, item.tilt);
directive = editor_includes::directive(item.file, info, editor_includes::parameter_count(item.file), local, static_cast<float>(item.yaw), item.track, item.tilt, item.values);
}
else
{

View File

@@ -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<std::string, std::string> 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<int>(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<int>(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<double>::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)

File diff suppressed because it is too large Load Diff

View File

@@ -561,15 +561,16 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve
profile_restore();
}
std::array<editor_mode::track_mode, 11> const &editor_mode::track_modes()
std::array<editor_mode::track_mode, 12> const &editor_mode::track_modes()
{
static std::array<track_mode, 11> const modes{{
static std::array<track_mode, 12> 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<int>(modes.size()));
row("Edit", 0, 7);
row("Analysis", 7, static_cast<int>(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::key_hint> 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)

View File

@@ -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:
{

View File

@@ -251,6 +251,14 @@ bool parse(template_text const &Text, include_info &Info, std::string &Error)
Info.switch_hand = drive["hand"].as<std::string>("");
Info.switch_drive = drive["drive"].as<std::string>("");
}
if (auto const signal{document["signal"]}; signal.IsDefined() && signal.IsMap())
{
Info.signal_kind = signal["kind"].as<std::string>("");
Info.signal_mount = signal["mount"].as<std::string>("");
Info.signal_lamps = signal["lamps"].as<std::string>("");
Info.signal_lean = signal["lean"].as<std::string>("");
Info.signal_read = signal["read"].as<std::string>("");
}
if (auto const table{document["turntable"]}; table.IsDefined() && table.IsMap())
{
Info.turntable_length = table["length"].as<double>(0.0);
@@ -644,7 +652,27 @@ std::string compose_directive(std::string const &File, std::vector<std::string>
return text + " end";
}
std::string directive(std::string const &File, include_info const &Info, int const Parameters, glm::dvec3 const &Location, std::optional<float> Yaw, std::string const &Track, glm::dvec2 const &Tilt)
std::string substitute(std::string const &Text, std::vector<std::string> 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<int>(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<float> Yaw, std::string const &Track, glm::dvec2 const &Tilt, std::map<std::string, std::string> const &Values)
{
std::vector<std::string> 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);
}

View File

@@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/.
#pragma once
#include <map>
#include <memory>
#include <optional>
#include <string>
@@ -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<include_parameter> 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<float> 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<float> Yaw, std::string const &Track = {}, glm::dvec2 const &Tilt = glm::dvec2{0.0}, std::map<std::string, std::string> const &Values = {});
std::string substitute(std::string const &Text, std::vector<std::string> 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

View File

@@ -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";

View File

@@ -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<std::string, std::pair<double, double>> m_orthophoto_origins;
double m_platform_edge{1.725};
double m_signal_offset{2.25};
};
// global editor settings instance

View File

@@ -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"

View File

@@ -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 <data root> [--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())