diff --git a/CMakeLists.txt b/CMakeLists.txt
index 97c50b4f..bb68ef6d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -96,11 +96,13 @@ set(SOURCES
"model/Texture.cpp"
"utilities/Timer.cpp"
"world/Track.cpp"
+"world/TrackSleepers.cpp"
"world/GaugeLine.cpp"
"world/RailCrossings.cpp"
"world/RailMaterials.cpp"
"world/RailProfile.cpp"
"world/RailPlan.cpp"
+"world/RailCarve.cpp"
"world/RailLayer.cpp"
"world/Road.cpp"
"world/Sweep.cpp"
@@ -204,6 +206,7 @@ set(SOURCES
"application/editormodeswitchdesign.cpp"
"application/editormodetrackbed.cpp"
"application/editormodetrackux.cpp"
+"application/editormodetrackprops.cpp"
"application/editormoderoad.cpp"
"application/editormodeprofile.cpp"
"application/editormodetextures.cpp"
diff --git a/McZapkie/MOVER.h b/McZapkie/MOVER.h
index 29b3e75c..ccd0935b 100644
--- a/McZapkie/MOVER.h
+++ b/McZapkie/MOVER.h
@@ -169,6 +169,8 @@ enum DerailReason
WRONG_TRACK_TYPE = 4, // Ra: powód wykolejenia: nieodpowiednia trajektoria
/// Collision with another vehicle.
COLLISION = 5, // powód wykolejenia: zderzenie z innym pojazdem
+ /// Trailing through a switch with a movable nose set the other way.
+ TRAILED_SWITCH = 6, // powód wykolejenia: rozprucie rozjazdu z ruchomym dziobem
};
/*wagi prawdopodobienstwa dla funkcji FuzzyLogic*/
diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp
index 3b5b6215..69d20723 100644
--- a/McZapkie/Mover.cpp
+++ b/McZapkie/Mover.cpp
@@ -1346,6 +1346,8 @@ void TMoverParameters::Derail(DerailReason const Reason)
ErrorLog("Bad dynamic: " + Name + " derailed due to wrong track type"); // błąd w scenerii
else if (Reason == COLLISION)
WriteLog("Bad driving: " + Name + " derailed"); // This reason also generates its own message in `TMoverParameters::CollisionDetect()`
+ else if (Reason == TRAILED_SWITCH)
+ ErrorLog("Bad driving: " + Name + " derailed trailing through a switch with a movable nose set the other way");
}
}
diff --git a/application/editormode.cpp b/application/editormode.cpp
index 57f99e8d..323c3113 100644
--- a/application/editormode.cpp
+++ b/application/editormode.cpp
@@ -8,6 +8,7 @@ http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
+#include "world/TrackSleepers.h"
#include
#include
#include
@@ -2952,6 +2953,7 @@ bool editor_mode::save()
}
auto const terrainadded{false == rootstatements.empty()};
auto const controls{turntable_controls(rootstatements)};
+ auto const noses{nose_controls(rootstatements)};
std::vector trailing;
std::erase_if(m_vehicle.trainsets, [](auto const &Trainset) {
auto const *track{simulation::Paths.find(Trainset.track)};
@@ -2965,6 +2967,8 @@ bool editor_mode::save()
auto const result = scene::Layers.save(rootstatements, trailing);
if (result.success && controls)
m_turntable_included = true;
+ if (result.success && noses)
+ m_nose_included = true;
if (result.success && placed > 0)
{
m_vehicle.trainsets.clear();
@@ -4756,7 +4760,7 @@ void editor_mode::render_first_steps()
{{"RMB + move", "Hold the right button and move the mouse: look around"},
{"RMB + W S A D", "With the right button held: fly forwards, back and to the sides; E and Q: up and down"},
{"O", "The top view, and back to 3D"},
- {"wheel", "In the top view: zoom in and out where the cursor points"},
+ {"wheel", "Zoom in and out where the cursor points"},
{"MMB + move", "In the top view: hold the wheel and drag to move the map"},
{"F", "Fly to what is selected"}},
{{first_step::look, "Look around with the right button held"}, {first_step::top, "Switch to the top view"}},
@@ -5389,8 +5393,28 @@ void editor_mode::on_scroll(double const Xoffset, double const Yoffset)
{
if (false == ImGui::GetIO().WantCaptureMouse && track_wheel(Yoffset != 0.0 ? Yoffset : Xoffset))
return;
- if (false == Global.EditorOrtho || ImGui::GetIO().WantCaptureMouse)
+ if (ImGui::GetIO().WantCaptureMouse || Yoffset == 0.0)
return;
+ if (false == Global.EditorOrtho)
+ {
+ // in 3D the camera flies along the ray under the cursor, by a share of the distance to what it points at;
+ // a touchpad sends many small steps, together they come to the same
+ screen_projection const projection;
+ glm::dvec3 origin;
+ glm::dvec3 direction;
+ projection.ray(ImGui::GetIO().MousePos, origin, direction);
+ glm::dvec3 level;
+ auto const distance{std::min(
+ GfxRenderer->Mouse_Hit() ? glm::length(GfxRenderer->Mouse_Position()) : projection.on_level(ImGui::GetIO().MousePos, 0.0, level) ? glm::distance(level, origin) : std::max(20.0, std::abs(Camera.Pos.y) * 2.0),
+ 20000.0)};
+ auto step{distance * (1.0 - std::pow(0.85, Yoffset))};
+ // no nearer than 2 m to the point, not through it
+ if (Yoffset > 0.0)
+ step = std::max(0.0, std::min(step, distance - 2.0));
+ Camera.Pos += direction * step;
+ Global.pCamera = Camera;
+ return;
+ }
auto const before{Global.EditorOrthoExtent};
auto const after{std::clamp(before * static_cast(std::pow(0.85, Yoffset)), 5.0f, 50000.0f)};
screen_projection const projection;
@@ -5450,6 +5474,8 @@ void editor_mode::update_camera(double const Deltatime)
void editor_mode::enter()
{
m_statebackup = {Global.pCamera, FreeFlyModeFlag, Global.ControlPicking};
+ // the editor changes the tracks, their sleepers are worked out as they're needed
+ track_sleepers::lay_in_background(false);
if (simulation::Region != nullptr)
{
@@ -5544,6 +5570,7 @@ void editor_mode::enter()
void editor_mode::exit()
{
+ track_sleepers::lay_in_background(true);
Global.editor_tracks = false;
EditorModeFlag = false;
Global.ControlPicking = m_statebackup.picking;
@@ -5739,6 +5766,31 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
return;
}
+ // the properties of a track copied and pasted on the selected ones
+ if (Global.ctrlState && Global.shiftState && (Key == GLFW_KEY_C || Key == GLFW_KEY_V) && ui()->mode() == nodebank_panel::TRACK)
+ {
+ if (is_press(Action) && false == ImGui::GetIO().WantTextInput)
+ {
+ auto const tracks{selected_tracks()};
+ if (Key == GLFW_KEY_C)
+ {
+ // the selected track, or the one under the cursor when there are more of them
+ auto *from{tracks.size() == 1 ? tracks.front() : m_hover.track};
+ if (from != nullptr)
+ copy_track_style(*from);
+ else
+ ui()->set_status(STR_C("Select one track, or point at one, to copy its properties"), true);
+ }
+ else if (m_track_style.values.empty())
+ ui()->set_status(STR_C("Copy the properties of a track first: Ctrl+Shift+C"), true);
+ else if (tracks.empty())
+ ui()->set_status(STR_C("Select the tracks to paste the properties on"), true);
+ else
+ paste_track_style(tracks);
+ }
+ return;
+ }
+
// save; checked ahead of the camera keys, which have S bound to moving back
if (Global.ctrlState && false == Global.shiftState && Key == GLFW_KEY_S)
{
@@ -5808,6 +5860,9 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
break;
case GLFW_KEY_ESCAPE:
+ // an open list or menu takes this Esc for itself, the selection behind it stays
+ if (ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel))
+ break;
if (is_press(Action))
{
road_cancel();
@@ -5817,13 +5872,15 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
{
if (m_lay.active && false == m_lay.points.empty())
lay_cancel();
+ else if (m_track_style.picking)
+ m_track_style.picking = false;
else if (track_busy())
{
cancel_track_tools();
m_straights.tool = 0;
}
- else if (m_track_tab == track_tab::path && false == m_track_set.empty())
- m_track_set.clear();
+ else if (m_track_tab == track_tab::path && m_track_set.size() >= 2)
+ select_tracks({});
else if (m_track_tab != track_tab::path)
show_track_tab(track_tab::path);
else if (selected_track() != nullptr)
diff --git a/application/editormode.h b/application/editormode.h
index 2a81a03d..aa0b250e 100644
--- a/application/editormode.h
+++ b/application/editormode.h
@@ -487,6 +487,8 @@ class editor_mode : public application_mode, private editor_track::observer
bool m_opened_at_track{false};
bool m_turntable_included{false};
bool turntable_controls(std::vector &Rootstatements);
+ bool m_nose_included{false};
+ bool nose_controls(std::vector &Rootstatements);
void starter_trainset(std::vector &Trailing);
std::optional> m_saved_camera;
char m_openscenery_search[64]{};
@@ -748,17 +750,12 @@ class editor_mode : public application_mode, private editor_track::observer
bool m_track_dirty{false};
std::chrono::steady_clock::time_point m_track_last_commit;
editor_track::state m_track_field_before;
- std::array, 3> m_track_materials{};
- std::array m_track_material_edited{}; // path whose texture name is being typed
- // typed names of the track circuits, the sleeper model and its skin, and the event to add, for the path
- TTrack const *m_track_names_for{nullptr};
- std::array m_track_isolated{};
- std::array m_track_sleeper_model{};
- std::array m_track_sleeper_skin{};
+ // the event to add to the path, typed
+ TTrack const *m_track_events_for{nullptr};
std::array m_track_event{};
int m_track_event_list{0};
std::string m_track_events_missing;
- void render_path_circuits(TTrack &Track);
+ void render_path_events(TTrack &Track);
std::string m_track_mode_notice;
// structure gauge along the line of the selected path, shown in the 3d view
struct gauge_state
@@ -853,7 +850,7 @@ class editor_mode : public application_mode, private editor_track::observer
static std::vector double_paths(double_switch const &Set);
// the switch of the template, or the two switches of a double one with the plain track between them, committed and recorded
std::vector create_switch_set(editor_track::switch_template const &Shape, std::vector const &Paths, TTrack const &Style, std::string const &Name);
- void record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base);
+ void record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base, double const Middle = 0.0);
void render_switch_record(TTrack const &Track) const;
void route_store();
bool digitize_drawn(std::vector const &Created, bool const Alone = true);
@@ -963,6 +960,22 @@ class editor_mode : public application_mode, private editor_track::observer
float roll{0.f};
float radius{0.f};
};
+ // the line leaving an end of a switch off its main straight, taken before the switch moves: its paths up to the end of
+ // the straight past the curve next to the switch. When the switch moves only the paths up to a joint on that straight
+ // are laid again, so the curve keeps to the end of the switch and the rest of the network stays where it is
+ struct switch_branch
+ {
+ std::size_t joint{0};
+ editor_track::chain chain;
+ std::size_t straight{0}; // the first path of the straight past the curve
+ alignment::design design; // as the paths lie
+ alignment::result result;
+ // made for the new end of the switch
+ editor_track::chain part; // the paths laid again
+ std::vector pieces;
+ alignment::design laid; // of the whole chain
+ double factor{1.0}; // of the radius and the transitions of the curve
+ };
struct switch_tool
{
bool curved{false};
@@ -1001,6 +1014,14 @@ class editor_mode : public application_mode, private editor_track::observer
double distance{0.0};
double length{0.0};
std::vector joints;
+ // the lines leaving the switch off its straight: the joint they leave at, the path of the switch in the shown ones
+ struct branch_line
+ {
+ editor_track::joint joint;
+ std::size_t path{0};
+ switch_branch branch;
+ };
+ std::vector branches;
};
slide_state slide;
double slide_typed{0.0};
@@ -1023,6 +1044,14 @@ class editor_mode : public application_mode, private editor_track::observer
bool switch_room(TTrack &Track, double &Low, double &High, std::string &Reason);
bool switch_vacate(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, double const Shift, editor_track::straight const &Line, editor_track::edit &Edit, std::string &Status);
bool move_switch_paths(TTrack &Track, std::vector const &Paths, std::string &Status);
+ std::optional take_switch_branch(TTrack const &Track, editor_track::joint const &Joint, std::size_t const Index) const;
+ // for the path of the switch the line leaves, as it'll lie
+ bool plan_switch_branch(segment_data const &Path, editor_track::joint const &Joint, switch_branch &Branch) const;
+ void lay_switch_branch(switch_branch const &Branch, editor_track::edit &Edit);
+ // of the joints off the main straight of the switch, the neighbours in Skip left out
+ std::vector switch_branches(TTrack const &Track, std::vector const &Joints, std::vector const &Skip = {}) const;
+ // for the paths the switch has now; the joints of the branches which can be laid are handled, the others are left out. Gives the notes for the user
+ std::string plan_switch_branches(TTrack const &Track, std::vector const &Joints, std::vector &Branches, std::vector &Handled) const;
// room on the straight of the whole double switch along the axis of its first switch, and the length of the set
bool double_room(double_switch const &Set, double &Low, double &High, double &Length, std::string &Reason);
bool move_double_paths(double_switch const &Set, std::vector const &Paths, std::string &Status);
@@ -1207,7 +1236,7 @@ class editor_mode : public application_mode, private editor_track::observer
bool cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack **Style);
bool insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape);
bool replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape);
- std::vector build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, double const Radius, TTrack &Style, std::string const &Type = "Rkpd");
+ std::vector build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, editor_track::slip_curve const &Curve, TTrack &Style, std::string const &Type = "Rkpd");
bool start_extend(editor_track::snap_target const &End);
void finish_extend();
std::vector extend_pieces() const;
@@ -1297,7 +1326,6 @@ class editor_mode : public application_mode, private editor_track::observer
};
track_name_edit m_track_name;
void render_track_name(TTrack &Track);
- void render_path_parameters(TTrack &Track);
void render_straight_ui();
track_tab m_track_tab{track_tab::path};
void delete_selected_track();
@@ -1329,7 +1357,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, signal_place, signal_move, switch_slide };
+ 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, switch_slide, pick_style };
kind what{kind::none};
TTrack *track{nullptr};
int path{0};
@@ -1418,25 +1446,21 @@ class editor_mode : public application_mode, private editor_track::observer
struct track_box
{
bool active{false};
+ bool replace{false}; // the tracks in the box become the selection, else they join it
glm::vec2 from{0.f};
glm::vec2 to{0.f};
TTrack *hovered{nullptr};
};
track_box m_track_box;
- struct track_set_fields
- {
- double velocity{100.0};
- char rail[128]{};
- char trackbed[128]{};
- char circuits[256]{};
- int environment{0};
- };
- track_set_fields m_track_set_fields;
glm::mat4 m_track_set_gizmo{1.0f};
bool m_track_set_using{false};
glm::dvec3 m_track_set_pivot{0.0};
std::vector>> m_track_set_joints;
bool in_track_set(TTrack const *Track) const;
+ // the track is selected: alone or in the set
+ bool track_selected(TTrack const *Track) const;
+ // a click in the view: the track under the cursor becomes the selection, or nothing
+ void track_click_select(TTrack *Hovered);
bool track_set_begin(bool const Rotate);
void track_set_turn(double const Angle);
void track_set_move(glm::dvec3 const &Offset);
@@ -1444,7 +1468,7 @@ class editor_mode : public application_mode, private editor_track::observer
glm::dvec3 track_set_centre() const;
void track_set_toggle(TTrack &Track);
void track_set_prune();
- void start_track_box();
+ void start_track_box(bool const Replace = false);
void finish_track_box();
void draw_track_set() const;
void render_track_set_ui();
@@ -1469,8 +1493,82 @@ class editor_mode : public application_mode, private editor_track::observer
void track_set_grow();
void render_track_spread_ui();
void draw_track_spread() const;
- // changes each path of the set which can be changed; false from Change: the path stays as it was
- void track_set_apply(std::function const &Change, bool const Rebuild, char const *What);
+ // the properties of the selected tracks in the inspector, one sheet for one track and for many; see editormodetrackprops.cpp
+ // the set when there's one, the selected track otherwise
+ std::vector selected_tracks();
+ void select_tracks(std::vector const &Tracks);
+ // changes each track which can be changed, in one step of the undo; false from Change: the track stays as it was
+ std::size_t tracks_apply(std::vector const &Tracks, std::function const &Change, bool const Rebuild, char const *What);
+ int track_property_index(std::string const &Key) const;
+ std::size_t track_property_apply(int const Property, std::vector const &Tracks, std::string const &Text);
+ void track_field_settle();
+ void track_textures_scan();
+ // every texture and material under the folder of the textures, read on the first use of the picker
+ void track_textures_index();
+ // the picker of a texture of the rails (Kind 0) or of the trackbed (1), in an open combo. returns: true when one is chosen, its name in Chosen, "none" for no texture
+ bool track_texture_picker(int const Kind, std::string const &Current, bool const Mixed, std::string &Chosen);
+ // image of the texture of the name for the picker, 0 while there's none
+ std::uint64_t track_texture_image(int const Kind, std::string const &Name);
+ void render_track_properties(std::vector const &Tracks);
+ void render_track_set_header(std::vector const &Tracks);
+ void render_track_select_tools(std::vector const &Tracks);
+ void render_track_style(std::vector const &Tracks);
+ void copy_track_style(TTrack const &Track);
+ std::size_t paste_track_style(std::vector const &Tracks);
+ std::vector similar_tracks(int const Property, std::vector const &Tracks) const;
+ // values of the properties over the selected tracks, each with the number of the tracks having it
+ struct track_values
+ {
+ std::size_t signature{0};
+ int generation{0};
+ int generation_seen{-1};
+ std::size_t history{0};
+ int frame{-100};
+ std::vector>> values;
+ std::vector applies;
+ };
+ track_values m_track_values;
+ // the field being typed in, with the tracks it was started on
+ struct track_field
+ {
+ int property{-1};
+ std::vector tracks;
+ std::array text{};
+ std::string initial;
+ int frame{0};
+ };
+ track_field m_track_field;
+ struct track_texture_use
+ {
+ material_handle material{null_handle};
+ std::string name;
+ int count{0};
+ };
+ std::array, 2> m_track_textures; // of the rails, of the trackbed
+ std::array m_track_texture_filter{};
+ struct texture_index_entry
+ {
+ std::string name; // as the materials are named: relative to the folder of the textures, without the extension
+ std::string lowered;
+ bool material{false}; // there's a .mat file of the name
+ };
+ std::vector m_track_texture_index; // sorted by the name
+ bool m_track_texture_indexed{false};
+ std::string m_track_texture_hovered; // the tile under the mouse in the picker, shown large above them
+ // properties copied from a track, to be pasted on others
+ struct track_style
+ {
+ std::string from;
+ std::vector> values;
+ // speed, environment, overhead line, track circuits, rails, trackbed, its shape, sleepers, ride and sound, frogs
+ std::array groups{false, false, false, false, true, true, true, true, false, true};
+ bool open{false};
+ bool picking{false}; // the next click on a track in the view copies its properties
+ };
+ track_style m_track_style;
+ std::vector> m_track_similar;
+ TTrack const *m_track_list_hover{nullptr}; // in the list of the selected tracks
+ int m_track_list_hover_frame{-1};
// second track alongside the selected path or the line through it, at a spacing, curves included
struct parallel_tool
{
@@ -1937,6 +2035,7 @@ class editor_mode : public application_mode, private editor_track::observer
selftest_result selftest_join(std::string const &command, std::istringstream &words);
selftest_result selftest_signal(std::string const &command, std::istringstream &words);
selftest_result selftest_terrain(std::string const &command, std::istringstream &words);
+ selftest_result selftest_props(std::string const &command, std::istringstream &words);
selftest_result selftest_dispatch(std::string const &command, std::istringstream &words);
void selftest_step();
// a value typed during a gesture, taken instead of the one the cursor gives
diff --git a/application/editormodeconnection.cpp b/application/editormodeconnection.cpp
index 73f2513b..b5f9142e 100644
--- a/application/editormodeconnection.cpp
+++ b/application/editormodeconnection.cpp
@@ -313,7 +313,7 @@ void editor_mode::insert_connection()
auto second{glm::normalize(plan_of(plan.ends[3] - plan.ends[2]))};
if (glm::dot(first, second) < 0.0)
second = -second;
- auto const parts{build_double_slip(plan.ends[0], plan.ends[1], plan.ends[2], plan.ends[3], plan_of(plan.centre), plan.centre.y, first, second, plan.angle, 0.0, *style, "Skrz")};
+ auto const parts{build_double_slip(plan.ends[0], plan.ends[1], plan.ends[2], plan.ends[3], plan_of(plan.centre), plan.centre.y, first, second, plan.angle, editor_track::slip_curve{}, *style, "Skrz")};
editor_track::commit(parts);
created.insert(created.end(), parts.begin(), parts.end());
}
diff --git a/application/editormoderecords.cpp b/application/editormoderecords.cpp
index 85501650..5a3ac2ce 100644
--- a/application/editormoderecords.cpp
+++ b/application/editormoderecords.cpp
@@ -346,7 +346,7 @@ std::vector editor_mode::double_paths(double_switch const &Set)
return {Set.first->m_paths[0], Set.first->m_paths[1], Set.second->m_paths[0], Set.second->m_paths[1], link};
}
-void editor_mode::record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base)
+void editor_mode::record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base, double const Middle)
{
if (Parts.empty() || scene::Layers.empty())
return;
@@ -355,6 +355,7 @@ void editor_mode::record_slip(std::vector const &Parts, std::string co
entry.name = Base;
entry.type = Type;
entry.radius = Radius;
+ entry.middle = Middle;
entry.ratio = Angle > 1e-9 ? 1.0 / std::tan(Angle) : 0.0;
for (auto const *part : Parts)
if (part != nullptr && false == part->name().empty() && (Kind == "crossing" || part->eType == tt_Switch))
diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp
index 7ae14bb0..5c61c2f8 100644
--- a/application/editormodeselftest.cpp
+++ b/application/editormodeselftest.cpp
@@ -40,15 +40,29 @@ http://mozilla.org/MPL/2.0/.
#include "scene/sceneterrain.h"
#include "editor/editorSpeed.hpp"
+#include
#include
#include
#include
#include
#include
+namespace {
+
+// frames gone through the selftest, for the measurements of perf
+std::size_t selftest_frames{0};
+
+std::string one_line(std::string Text)
+{
+ std::replace(Text.begin(), Text.end(), '\n', ';');
+ return Text;
+}
+
+} // namespace
+
editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &command, std::istringstream &words)
{
- for (auto const group : {&editor_mode::selftest_scene, &editor_mode::selftest_sweep, &editor_mode::selftest_track, &editor_mode::selftest_switch, &editor_mode::selftest_rails, &editor_mode::selftest_join, &editor_mode::selftest_signal, &editor_mode::selftest_terrain})
+ for (auto const group : {&editor_mode::selftest_scene, &editor_mode::selftest_sweep, &editor_mode::selftest_track, &editor_mode::selftest_switch, &editor_mode::selftest_rails, &editor_mode::selftest_join, &editor_mode::selftest_signal, &editor_mode::selftest_terrain, &editor_mode::selftest_props})
if (auto const result{(this->*group)(command, words)}; result != selftest_result::unhandled)
return result;
return selftest_result::unhandled;
@@ -69,6 +83,13 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm
m_focus_active = false;
return selftest_result::handled;
}
+ if (command == "zoom")
+ {
+ float factor{1.0f};
+ words >> factor;
+ Global.ZoomFactor = std::max(factor, 0.01f);
+ return selftest_result::handled;
+ }
if (command == "selectat")
{
glm::dvec3 point{0.0};
@@ -102,6 +123,170 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm
WriteLog("SELFTEST select " + name + (m_node != nullptr ? "" : ": no such track"));
return selftest_result::handled;
}
+ if (command == "perf")
+ {
+ // the frames drawn and the time gone since the last perf, with the timings and the counts the renderer keeps
+ std::string label;
+ words >> label;
+ static auto last{std::chrono::steady_clock::now()};
+ static std::size_t lastframe{0};
+ auto const now{std::chrono::steady_clock::now()};
+ auto const frames{selftest_frames - lastframe};
+ auto const elapsed{std::chrono::duration(now - last).count()};
+ WriteLog(format("SELFTEST perf %s: %zu frames, %.3f ms per frame | %s | %s", label.c_str(), frames, frames > 0 ? elapsed / frames : 0.0, one_line(GfxRenderer->info_times()).c_str(), one_line(GfxRenderer->info_stats()).c_str()));
+ last = now;
+ lastframe = selftest_frames;
+ return selftest_result::handled;
+ }
+ if (command == "rebuildall")
+ {
+ // the geometry of every path made anew, as the editor does it after a change, timed
+ auto const start{std::chrono::steady_clock::now()};
+ auto count{0};
+ for (auto *track : simulation::Paths.sequence())
+ {
+ if (track == nullptr || track->m_editorremoved)
+ continue;
+ auto §ion{simulation::Region->section(track->location())};
+ if (false == section.m_geometrycreated)
+ continue;
+ track->rebuild_geometry(section.m_geometrybank);
+ ++count;
+ }
+ WriteLog(format("SELFTEST rebuildall: %d paths in %.1f ms", count, std::chrono::duration(std::chrono::steady_clock::now() - start).count()));
+ // the sleepers laid again on all the tracks, which the renderer otherwise does as they come in sight, timed by section
+ std::map sectiontimes;
+ auto laidcount{0};
+ std::size_t parts{0};
+ auto laytime{0.0};
+ for (auto *track : simulation::Paths.sequence())
+ {
+ if (track == nullptr || track->m_editorremoved || false == track->m_sleepers3d.active)
+ continue;
+ auto const laystart{std::chrono::steady_clock::now()};
+ track_sleepers::lay(*track);
+ auto const took{std::chrono::duration(std::chrono::steady_clock::now() - laystart).count()};
+ laytime += took;
+ sectiontimes[&simulation::Region->section(track->location())] += took;
+ ++laidcount;
+ for (auto const &instances : track->m_sleepers3d.instances)
+ parts += instances.size();
+ }
+ auto slowest{0.0};
+ for (auto const §iontime : sectiontimes)
+ slowest = std::max(slowest, sectiontime.second);
+ WriteLog(format("SELFTEST rebuildall sleepers3d: %d tracks laid in %.1f ms, %zu parts; %zu sections, %.2f ms per section, %.2f ms the slowest", laidcount, laytime, parts, sectiontimes.size(),
+ (sectiontimes.empty() ? 0.0 : laytime / sectiontimes.size()), slowest));
+ return selftest_result::handled;
+ }
+ if (command == "beams")
+ {
+ // the beams the track lays, between two distances along it: their middles, lengths and the way they lie square to
+ std::string name;
+ double from{-1e9}, to{1e9};
+ words >> name >> from >> to;
+ auto *track{simulation::Paths.find(name)};
+ if (track == nullptr || false == track->m_sleepers3d.active)
+ {
+ WriteLog("SELFTEST beams: no track with beams " + name);
+ return selftest_result::handled;
+ }
+ std::string text;
+ for (auto const &beam : track_sleepers::place(*track))
+ {
+ auto const middle{0.5 * (beam.from + beam.to)};
+ if (middle.z < from || middle.z > to)
+ continue;
+ text += format(" [%.2f %.2f L%.2f a%.2f%s]", middle.x, middle.z, glm::length(beam.to - beam.from), glm::degrees(std::atan2(beam.along.x, beam.along.z)), beam.plain ? " p" : "");
+ }
+ WriteLog("SELFTEST beams " + name + ":" + text);
+ return selftest_result::handled;
+ }
+ if (command == "sharedzones")
+ {
+ // how far the timbers shared with other tracks reach along the paths of the track and of the tracks joined to its ends
+ std::string name;
+ words >> name;
+ auto *track{simulation::Paths.find(name)};
+ if (track == nullptr)
+ {
+ WriteLog("SELFTEST sharedzones: no track " + name);
+ return selftest_result::handled;
+ }
+ auto const zones_text = [](TTrack const &Track, int const Path) {
+ std::string text;
+ for (auto const &zone : track_sleepers::shared_zones(Track, Path))
+ text += format(" %.2f..%.2f", zone.first, zone.second);
+ return text.empty() ? std::string{" none"} : text;
+ };
+ auto const paths{track->eType == tt_Switch ? 2 : 1};
+ for (int path = 0; path < paths; ++path)
+ {
+ auto const length{track->eType == tt_Switch ? track->SwitchExtension->Segments[path]->GetLength() : track->Segment->GetLength()};
+ WriteLog(format("SELFTEST sharedzones %s path %d (%.2f m):%s", name.c_str(), path, length, zones_text(*track, path).c_str()));
+ // the track going on from the end of the path, and how far past the end the shared timbers reach along it
+ auto *next{track->eType == tt_Switch ? track->SwitchExtension->pNexts[path] : track->trNext};
+ if (next == nullptr || next->eType != tt_Normal || next->Segment == nullptr)
+ continue;
+ glm::dvec3 end, start, unused;
+ glm::vec3 angles;
+ (track->eType == tt_Switch ? track->SwitchExtension->Segments[path] : track->Segment)->RaPositionGet(length, end, angles);
+ next->Segment->RaPositionGet(0.0, start, angles);
+ auto const fromstart{glm::length(start - end) < 0.1};
+ auto past{0.0};
+ for (auto const &zone : track_sleepers::shared_zones(*next, 0))
+ {
+ if (fromstart && zone.first < 0.05)
+ past = zone.second;
+ else if (false == fromstart && zone.second > next->Segment->GetLength() - 0.05)
+ past = next->Segment->GetLength() - zone.first;
+ }
+ WriteLog(format("SELFTEST sharedzones %s path %d goes on into %s:%s; shared timbers reach %.2f m past the end", name.c_str(), path, next->name().c_str(), zones_text(*next, 0).c_str(), past));
+ }
+ return selftest_result::handled;
+ }
+ if (command == "sleepersbg")
+ {
+ // the sleepers worked out in the background as in the simulation, to see they come out the same, or back to as they're needed
+ int on{1};
+ words >> on;
+ track_sleepers::lay_in_background(on != 0);
+ for (auto *track : simulation::Paths.sequence())
+ if (track != nullptr)
+ track->m_sleepers3d.dirty = true;
+ track_sleepers::unsettle_around(*simulation::Paths.sequence().front());
+ WriteLog(format("SELFTEST sleepersbg %d", on));
+ return selftest_result::handled;
+ }
+ if (command == "beamsnear")
+ {
+ double x{0.0}, z{0.0}, radius{3.0};
+ words >> x >> z >> radius;
+ for (auto *track : simulation::Paths.sequence())
+ {
+ if (track == nullptr || track->m_editorremoved || false == track->m_sleepers3d.active)
+ continue;
+ for (auto const &beam : track_sleepers::place(*track))
+ {
+ auto const middle{0.5 * (beam.from + beam.to)};
+ if (glm::length(glm::dvec2{middle.x - x, middle.z - z}) > radius)
+ continue;
+ WriteLog(format("SELFTEST beam %s: %.2f %.2f -> %.2f %.2f, %.2f m, along %.1f deg%s", track->name().c_str(), beam.from.x, beam.from.z, beam.to.x, beam.to.z, glm::length(beam.to - beam.from), glm::degrees(std::atan2(beam.along.x, beam.along.z)), beam.plain ? " plain" : ""));
+ }
+ }
+ return selftest_result::handled;
+ }
+ if (command == "sleepercheck")
+ {
+ auto const start{std::chrono::steady_clock::now()};
+ auto const result{track_sleepers::check()};
+ WriteLog(format("SELFTEST sleepercheck: %zu beams, %zu overlaps, %zu gaps, %zu bare rails, %zu too long (longest %.2f m), widest spacing %.2f m at %s; %zu shared off one plane, %zu switch panels off one plane, %zu ends clipped, %zu beams dropped, slowest cluster %.1f ms of %zu tracks; in %.1f ms",
+ result.beams, result.overlaps, result.gaps, result.bare, result.toolong, result.longest, result.widest, result.widestat.c_str(), result.offplane, result.panels, result.clipped, result.dropped, result.slowest, result.biggest,
+ std::chrono::duration(std::chrono::steady_clock::now() - start).count()));
+ for (auto const &example : result.examples)
+ WriteLog("SELFTEST sleepercheck " + example);
+ return selftest_result::handled;
+ }
if (command == "frames")
{
std::string name;
@@ -230,7 +415,7 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm
if (command == "mapstate")
{
auto const marked{scene::Layers.empty() ? std::vector{} : scene::Layers.marked(scene::layer_handle{1}, "//$o")};
- WriteLog(format("SELFTEST mapstate orthophoto %d top %d extent %.0f mark %s", m_orthophoto.enabled() ? 1 : 0, Global.EditorOrtho ? 1 : 0, Global.EditorOrthoExtent, marked.empty() ? "-" : marked.front().c_str()));
+ WriteLog(format("SELFTEST mapstate orthophoto %d top %d extent %.0f mark %s camera %.1f %.1f %.1f", m_orthophoto.enabled() ? 1 : 0, Global.EditorOrtho ? 1 : 0, Global.EditorOrthoExtent, marked.empty() ? "-" : marked.front().c_str(), Camera.Pos.x, Camera.Pos.y, Camera.Pos.z));
return selftest_result::handled;
}
if (command == "trainset")
@@ -1160,6 +1345,36 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm
WriteLog(format("SELFTEST vprofile zone %.3f %.3f %s", zone.from, zone.to, zone.name.c_str()));
return selftest_result::handled;
}
+ if (command == "vprofileapply")
+ {
+ // a plain grade line from the height of the start of the open route, applied to it
+ double grade{0.0};
+ words >> grade;
+ auto &state{m_profile};
+ if (state.samples.empty())
+ {
+ WriteLog("SELFTEST vprofileapply: no route open");
+ return selftest_result::handled;
+ }
+ auto const start{state.samples.front().position.y};
+ state.line.points = {{0.0, start, 0.0, false}, {state.route.length, start + grade / 1000.0 * state.route.length, 0.0, false}};
+ state.line.join_start = false;
+ state.line.join_end = false;
+ profile_check();
+ profile_apply();
+ WriteLog("SELFTEST vprofileapply: " + state.status + " " + state.error);
+ for (auto const &span : state.route.spans)
+ {
+ if (span.track->eType != tt_Switch || span.track->SwitchExtension == nullptr)
+ continue;
+ for (std::size_t i = 0; i < span.track->m_paths.size() && i < 2; ++i)
+ {
+ auto const &path{span.track->m_paths[i]};
+ WriteLog(format("SELFTEST switch %s path %zu%s: %zu pieces kept, %zu in use, ends %.3f %.3f", span.track->name().c_str(), i + 1, static_cast(i) == span.path ? " (route)" : "", path.pieces.size(), span.track->SwitchExtension->Segments[i]->PieceCount(), path.points[segment_data::point::start].y, path.points[segment_data::point::end].y));
+ }
+ }
+ return selftest_result::handled;
+ }
if (command == "backwardcurve")
{
std::string name;
@@ -1359,6 +1574,7 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com
glm::dvec3 at{0.0};
glm::dvec3 toward{0.0};
words >> name >> at.x >> at.z >> toward.x >> toward.z >> label;
+ std::replace(label.begin(), label.end(), '_', ' ');
m_node = simulation::Paths.find(name);
ui()->set_node(m_node);
show_track_tab(track_tab::turnout);
@@ -1581,6 +1797,22 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com
WriteLog("SELFTEST conecheck " + name + ":" + text);
return selftest_result::handled;
}
+ if (command == "throw")
+ {
+ std::string name;
+ int state{0};
+ words >> name >> state;
+ auto *track{simulation::Paths.find(name)};
+ if (track != nullptr && track->eType == tt_Switch)
+ {
+ track->Switch(state, 0.f, 0.f);
+ // the blades go over at once, the editor doesn't run the animations of the switches
+ track->SwitchExtension->fOffset = track->SwitchExtension->fDesiredOffset;
+ track->RaAnimate();
+ }
+ WriteLog(format("SELFTEST throw %s %d%s", name.c_str(), state, track != nullptr && track->eType == tt_Switch ? "" : ": no such switch"));
+ return selftest_result::handled;
+ }
if (command == "switchdrive")
{
int index{-1};
@@ -2874,6 +3106,105 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
}
return selftest_result::handled;
}
+ if (command == "beddump")
+ {
+ // the trackbed triangles and the axes of the paths of the tracks near a point, in plan, for a look at where the trackbed has holes
+ glm::dvec3 point{0.0};
+ double radius{30.0};
+ std::string file;
+ words >> point.x >> point.z >> radius >> file;
+ std::ofstream output{file, std::ios_base::trunc};
+ auto triangles{0};
+ for (auto *track : simulation::Paths.sequence())
+ {
+ if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->m_paths.empty())
+ continue;
+ if (geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point) > radius)
+ continue;
+ auto const paths{track->eType == tt_Switch ? 2 : 1};
+ for (int path = 0; path < paths; ++path)
+ {
+ auto const &segment{track->eType == tt_Switch ? track->SwitchExtension->Segments[path] : track->Segment};
+ if (segment == nullptr)
+ continue;
+ output << "axis " << track->name() << ' ' << path;
+ for (auto at{0.0}; at <= segment->GetLength() + 1e-6; at += 0.25)
+ {
+ glm::dvec3 spot, across;
+ segment->plan_frame(std::min(at, segment->GetLength()), spot, across);
+ output << format(" %.4f %.4f", spot.x, spot.z);
+ }
+ output << "\n";
+ }
+ std::vector beds;
+ if (track->eType == tt_Switch && track->SwitchExtension != nullptr)
+ beds.push_back(track->SwitchExtension->Geometry3);
+ else
+ beds.insert(beds.end(), track->Geometry2.begin(), track->Geometry2.end());
+ for (auto const &bed : beds)
+ {
+ if (bed == null_handle)
+ continue;
+ auto const &vertices{GfxRenderer->Vertices(bed)};
+ for (std::size_t i = 2; i < vertices.size(); ++i)
+ {
+ glm::dvec3 const a{glm::dvec3{vertices[i - 2].position} + track->m_origin};
+ glm::dvec3 const b{glm::dvec3{vertices[i - 1].position} + track->m_origin};
+ glm::dvec3 const c{glm::dvec3{vertices[i].position} + track->m_origin};
+ output << format("tri %s %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f\n", track->name().c_str(), a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z);
+ ++triangles;
+ }
+ }
+ }
+ WriteLog(format("SELFTEST beddump: %d triangles into %s", triangles, file.c_str()));
+ return selftest_result::handled;
+ }
+ if (command == "raildump")
+ {
+ // the rail geometry of the tracks near a point as drawn: s strips of the lofts, t triangles of the cut faces and the rest
+ glm::dvec3 point{0.0};
+ double radius{30.0};
+ std::string file;
+ words >> point.x >> point.z >> radius >> file;
+ std::ofstream output{file, std::ios_base::trunc};
+ auto chunks{0};
+ for (auto *track : simulation::Paths.sequence())
+ {
+ if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->m_paths.empty())
+ continue;
+ if (geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point) > radius)
+ continue;
+ output << format("track %s %.4f\n", track->name().c_str(), track->m_paths.front().points[segment_data::point::start].y);
+ auto const dump = [&](TTrack::geometryhandle_sequence const &Sequence, char const *List, char const Kind) {
+ for (std::size_t index = 0; index < Sequence.size(); ++index)
+ {
+ auto const &vertices{GfxRenderer->Vertices(Sequence[index])};
+ if (vertices.empty())
+ continue;
+ auto const primitive{GfxRenderer->Primitive(Sequence[index])};
+ auto const kind{primitive == GL_TRIANGLES ? 't' : primitive == GL_TRIANGLE_STRIP ? 's' : Kind};
+ output << format("chunk %s %s %zu %c %zu\n", track->name().c_str(), List, index, kind, vertices.size());
+ for (auto const &vertex : vertices)
+ {
+ glm::dvec3 const at{glm::dvec3{vertex.position} + track->m_origin};
+ output << format("%.4f %.4f %.4f\n", at.x, at.y, at.z);
+ }
+ ++chunks;
+ }
+ };
+ dump(track->Geometry1, "rails1", 's');
+ dump(track->Geometry2, track->eType == tt_Switch ? "rails2" : "bed", 's');
+ dump(track->GeometryCut1, "cut1", 't');
+ dump(track->GeometryCut2, "cut2", 't');
+ for (std::size_t i = 0; i < track->GeometryTreads.size(); ++i)
+ {
+ dump(track->GeometryTreads[i].geometry, "tread", 's');
+ dump(track->GeometryTreads[i].cuts, "treadcut", 't');
+ }
+ }
+ WriteLog(format("SELFTEST raildump: %d chunks into %s", chunks, file.c_str()));
+ return selftest_result::handled;
+ }
if (command == "geomhash")
{
std::string name;
@@ -2974,6 +3305,507 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
}
return selftest_result::handled;
}
+ if (command == "headgap")
+ {
+ // the least gap between the heads of the rails of two tracks running side by side, by the gauge lines and the head of the profile
+ std::string first, second;
+ words >> first >> second;
+ auto *a{simulation::Paths.find(first)};
+ auto *b{simulation::Paths.find(second)};
+ if (a == nullptr || b == nullptr)
+ {
+ WriteLog("SELFTEST headgap: no such tracks");
+ return selftest_result::handled;
+ }
+ auto const head_a{a->rail_dimensions().head};
+ auto const head_b{b->rail_dimensions().head};
+ for (int pa = 0; pa < (a->eType == tt_Switch ? 2 : 1); ++pa)
+ for (auto const sa : {-1, 1})
+ for (int pb = 0; pb < (b->eType == tt_Switch ? 2 : 1); ++pb)
+ for (auto const sb : {-1, 1})
+ {
+ auto const length{a->eType == tt_Switch ? a->SwitchExtension->Segments[pa]->GetLength() : a->Segment->GetLength()};
+ auto const lengthb{b->eType == tt_Switch ? b->SwitchExtension->Segments[pb]->GetLength() : b->Segment->GetLength()};
+ auto least{1e9};
+ auto where{0.0};
+ for (auto at{0.0}; at <= length; at += 0.02)
+ {
+ double d, l;
+ track_rails::rail_offset(*b, pb, sb, track_rails::rail_point(*a, pa, sa, at, head_a), d, l);
+ if (d < 0.0 || d > lengthb || l < -0.3 || l > 0.3)
+ continue;
+ if (l - head_b < least)
+ {
+ least = l - head_b;
+ where = at;
+ }
+ }
+ if (least < 1e8)
+ WriteLog(format("SELFTEST headgap %s/%d%+d to %s/%d%+d: %.1f mm at %.2f m (heads %.1f %.1f mm, width %.4f)", first.c_str(), pa, sa, second.c_str(), pb, sb, least * 1000.0, where, head_a * 1000.0,
+ head_b * 1000.0, a->fTrackWidth));
+ }
+ return selftest_result::handled;
+ }
+ if (command == "checks")
+ {
+ // check rails of a track as they're drawn, and the rails of the tracks they run into
+ std::string name;
+ words >> name;
+ int count{0};
+ int faults{0};
+ for (auto *track : simulation::Paths.sequence())
+ {
+ if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->name() != name)
+ continue;
+ auto const layout{track->plan_rails(true)};
+ auto const size{track->rail_dimensions()};
+ std::vector others;
+ for (auto *other : simulation::Paths.sequence())
+ if (other != nullptr && false == other->m_editorremoved && (other->iCategoryFlag & 15) == 1 && glm::length(other->location() - track->location()) < other->radius() + track->radius() + 1.0)
+ others.push_back(other);
+ for (int path = 0; path < (track->eType == tt_Switch ? 2 : 1); ++path)
+ for (int right = 0; right < 2; ++right)
+ {
+ auto const side{right ? 1 : -1};
+ for (auto const &check : layout.checks[path][right])
+ {
+ if (track_rails::replaced_by_wing(check, layout.wingends[path][right]))
+ continue;
+ auto const ownlength{track->eType == tt_Switch ? track->SwitchExtension->Segments[path]->GetLength() : track->Segment->GetLength()};
+ auto const from{std::max(check.from, 0.0)};
+ auto const to{std::min(check.to, ownlength)};
+ if (to - from <= 0.01)
+ continue;
+ ++count;
+ std::string hits;
+ for (auto at{from}; at <= to; at += 0.05)
+ // the head of the check rail as drawn, 40 mm at the head of the rail of 73.5
+ for (auto const lateral : {-track_rails::check_flangeway, -track_rails::check_flangeway - 0.020 * size.head / 0.0735, -track_rails::check_flangeway - 0.040 * size.head / 0.0735})
+ {
+ auto const point{track_rails::rail_point(*track, path, side, at, lateral)};
+ for (auto *other : others)
+ for (int otherpath = 0; otherpath < (other->eType == tt_Switch ? 2 : 1); ++otherpath)
+ for (auto const otherside : {-1, 1})
+ {
+ if (other == track && otherpath == path && otherside == side)
+ continue;
+ auto const length{other->eType == tt_Switch ? other->SwitchExtension->Segments[otherpath]->GetLength() : other->Segment->GetLength()};
+ double d, l;
+ track_rails::rail_offset(*other, otherpath, otherside, point, d, l);
+ if (d < 0.0 || d > length || l < 0.0 || l > other->rail_dimensions().head)
+ continue;
+ auto const hit{format(" %s/%d%+d", other->name().c_str(), otherpath, otherside)};
+ if (hits.find(hit) == std::string::npos)
+ hits += hit + format(" at %.2f m", at);
+ }
+ }
+ if (false == hits.empty())
+ ++faults;
+ WriteLog(format("SELFTEST check %s path %d side %+d: %.3f-%.3f m drawn of %.3f-%.3f m%s", name.c_str(), path, side, from, to, check.from, check.to, hits.empty() ? ", clear" : (", over" + hits + " FAULT").c_str()));
+ }
+ }
+ }
+ WriteLog(format("SELFTEST checks: %d check rails, %d faults", count, faults));
+ return selftest_result::handled;
+ }
+ if (command == "cuts")
+ {
+ // the cuts of the rails of a track, with the ends of the parts in the world
+ std::string name, detail;
+ words >> name >> detail;
+ auto *track{simulation::Paths.find(name)};
+ if (track == nullptr)
+ return selftest_result::handled;
+ auto const layout{track->plan_rails(true)};
+ auto const head{track->rail_dimensions().head};
+ // with knots: the lines of the parts too
+ auto const knots_of = [](knots::line const &Line) {
+ std::string result;
+ for (auto const &knot : Line)
+ result += format(" %.3f:%.4f", knot.x, knot.y);
+ return result;
+ };
+ for (int path = 0; path < (track->eType == tt_Switch ? 2 : 1); ++path)
+ for (int right = 0; right < 2; ++right)
+ for (auto const &cut : layout.plans[path][right].cuts)
+ {
+ auto const side{right ? 1 : -1};
+ auto const where = [&](double const At, knots::line const &Offsets) {
+ auto const point{track_rails::rail_point(*track, path, side, At, knots::sample(Offsets, At, 0.0) + 0.5 * head)};
+ return format("%.3f m (%.3f %.3f) offset %.3f", At, point.x, point.z, knots::sample(Offsets, At, 0.0));
+ };
+ WriteLog(format("SELFTEST cut %s path %d side %+d at %.3f: end %s%s, start %s%s, of %.3f m", name.c_str(), path, side, cut.at, where(cut.end, cut.endoffsets).c_str(), cut.endextents.empty() ? "" : " extents", where(cut.start, cut.startoffsets).c_str(), cut.startextents.empty() ? "" : " extents", track_rails::path_segment(*track, path).GetLength()));
+ if (detail == "knots")
+ for (auto const &[label, line] : std::initializer_list>{
+ {"endoffsets", &cut.endoffsets}, {"endextents", &cut.endextents}, {"endinner", &cut.endinner}, {"endrolled", &cut.endrolled}, {"endworn", &cut.endworn},
+ {"startoffsets", &cut.startoffsets}, {"startextents", &cut.startextents}, {"startinner", &cut.startinner}, {"startrolled", &cut.startrolled}, {"startworn", &cut.startworn}})
+ if (false == line->empty())
+ WriteLog(format("SELFTEST %s%s", label, knots_of(*line).c_str()));
+ }
+ return selftest_result::handled;
+ }
+ if (command == "wings")
+ {
+ // the heads of the rails bent out at the cuts, the wing rails mostly, against the heads of the other rails as they're laid out
+ std::string name;
+ words >> name;
+ std::map layouts;
+ auto const layout_of = [&](TTrack *Track) -> track_rails::rail_layout const & {
+ auto found{layouts.find(Track)};
+ if (found == layouts.end())
+ found = layouts.emplace(Track, Track->plan_rails(true)).first;
+ return found->second;
+ };
+ auto const length_of = [](TTrack const &Track, int const Path) { return Track.eType == tt_Switch ? Track.SwitchExtension->Segments[Path]->GetLength() : Track.Segment->GetLength(); };
+ // whether the head of a rail lies at a distance along it and out from its gauge line, the part of the nearest cut there
+ auto const occupied = [](track_rails::rail_plan const &Plan, double const Head, double const At, double const Lateral) {
+ if (Plan.cuts.empty())
+ return Lateral >= 0.0 && Lateral <= Head;
+ auto const *cut{&Plan.cuts.front()};
+ for (auto const &each : Plan.cuts)
+ if (std::abs(each.at - At) < std::abs(cut->at - At))
+ cut = &each;
+ auto const part = [&](knots::line const &Offsets, knots::line const &Extents, knots::line const &Inner) {
+ auto const offset{knots::sample(Offsets, At, 0.0)};
+ return Lateral >= offset + std::max(0.0, knots::sample(Inner, At, 0.0)) && Lateral <= offset + std::min(Head, knots::sample(Extents, At, Head));
+ };
+ return (At <= cut->end && part(cut->endoffsets, cut->endextents, cut->endinner)) || (At >= cut->start && part(cut->startoffsets, cut->startextents, cut->startinner));
+ };
+ int count{0};
+ int faults{0};
+ for (auto *track : simulation::Paths.sequence())
+ {
+ if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->m_paths.empty())
+ continue;
+ if (name != "all" && track->name() != name)
+ continue;
+ auto const &layout{layout_of(track)};
+ auto const head{track->rail_dimensions().head};
+ std::vector others;
+ for (auto *other : simulation::Paths.sequence())
+ if (other != nullptr && false == other->m_editorremoved && (other->iCategoryFlag & 15) == 1 && false == other->m_paths.empty() && glm::length(other->location() - track->location()) < other->radius() + track->radius() + 1.0)
+ others.push_back(other);
+ for (int path = 0; path < (track->eType == tt_Switch ? 2 : 1); ++path)
+ for (int right = 0; right < 2; ++right)
+ {
+ auto const side{right ? 1 : -1};
+ auto const ownlength{length_of(*track, path)};
+ for (auto const &cut : layout.plans[path][right].cuts)
+ for (auto const coming : {true, false})
+ {
+ auto const &offsets{coming ? cut.endoffsets : cut.startoffsets};
+ if (offsets.size() < 2 || offsets.back().x - offsets.front().x < 0.05)
+ continue;
+ auto const from{std::max(0.0, coming ? offsets.front().x : std::max(offsets.front().x, cut.start))};
+ auto const to{std::min(ownlength, coming ? std::min(offsets.back().x, cut.end) : offsets.back().x)};
+ if (to - from < 0.05)
+ continue;
+ ++count;
+ std::string hits;
+ for (auto at{from}; at <= to; at += 0.02)
+ {
+ auto const offset{knots::sample(offsets, at, 0.0)};
+ if (offset <= 0.002)
+ continue;
+ // only what is left of the head where the wing rail is planed
+ auto const lo{offset + std::max(0.0, knots::sample(coming ? cut.endinner : cut.startinner, at, 0.0))};
+ auto const hi{offset + std::min(head, knots::sample(coming ? cut.endextents : cut.startextents, at, head))};
+ if (hi - lo < 0.012)
+ continue;
+ for (auto const lateral : {lo + 0.005, 0.5 * (lo + hi), hi - 0.005})
+ {
+ auto const point{track_rails::rail_point(*track, path, side, at, lateral)};
+ for (auto *other : others)
+ for (int otherpath = 0; otherpath < (other->eType == tt_Switch ? 2 : 1); ++otherpath)
+ for (auto const otherside : {-1, 1})
+ {
+ if (other == track && otherpath == path && otherside == side)
+ continue;
+ double d, l;
+ track_rails::rail_offset(*other, otherpath, otherside, point, d, l);
+ if (d < 0.0 || d > length_of(*other, otherpath) || l < -0.3 || l > 0.3)
+ continue;
+ // a wing rail planed flush against another head from the outside covers its rounded shoulder
+ auto const theirs{other->rail_dimensions()};
+ if (false == occupied(layout_of(other).plans[otherpath][otherside > 0], theirs.head - theirs.shoulder, d, l))
+ continue;
+ auto const hit{format(" %s/%d%+d", other->name().c_str(), otherpath, otherside)};
+ if (hits.find(hit) == std::string::npos)
+ hits += hit + format(" at %.2f m (theirs %.2f m, %.0f mm in)", at, d, l * 1000.0);
+ }
+ }
+ }
+ if (false == hits.empty())
+ ++faults;
+ if (false == hits.empty() || name != "all")
+ WriteLog(format("SELFTEST wing %s path %d side %+d cut at %.3f, %s part %.3f-%.3f m, out %.0f-%.0f mm%s", track->name().c_str(), path, side, cut.at, coming ? "coming" : "going", from, to, offsets.front().y * 1000.0,
+ offsets.back().y * 1000.0, hits.empty() ? ", clear" : (", over" + hits + " FAULT").c_str()));
+ }
+ }
+ }
+ WriteLog(format("SELFTEST wings: %d bent parts, %d faults", count, faults));
+ return selftest_result::handled;
+ }
+ if (command == "space")
+ {
+ // the rails as laid out, every 2 cm along their parts: a head in the flangeway of another rail where its wheels run (F), over the head of
+ // another rail past its shoulder (O), a line of a part planing on past its last knot (C), the head jumping in width (J), nothing left of the
+ // head inside a part (V)
+ std::string name;
+ double tolerance{0.5};
+ words >> name >> tolerance;
+ if (name.empty())
+ name = "all";
+ tolerance *= 0.001;
+ struct piece
+ {
+ double from{-1e9};
+ double to{1e9};
+ track_rails::rail_cut const *before{nullptr};
+ track_rails::rail_cut const *after{nullptr};
+ };
+ struct rail
+ {
+ TTrack *track{nullptr};
+ int path{0};
+ int side{0};
+ double length{0.0};
+ double laidfrom{0.0};
+ double heel{0.0};
+ track_rails::rail_size size;
+ track_rails::gauge_line const *line{nullptr};
+ std::vector pieces;
+ };
+ std::map layouts;
+ std::map> rails;
+ auto const rails_of = [&](TTrack *Track) -> std::vector & {
+ auto found{rails.find(Track)};
+ if (found != rails.end())
+ return found->second;
+ auto &result{rails[Track]};
+ auto const &layout{layouts.emplace(Track, Track->plan_rails(true)).first->second};
+ auto const heel{Track->eType == tt_Switch ? std::max(track_rails::blade_length(*Track, 0), track_rails::blade_length(*Track, 1)) : 0.0};
+ for (int path = 0; path < track_rails::path_count(*Track); ++path)
+ for (int right = 0; right < 2; ++right)
+ {
+ rail each;
+ each.track = Track;
+ each.path = path;
+ each.side = (right ? 1 : -1);
+ each.length = track_rails::path_segment(*Track, path).GetLength();
+ each.laidfrom = (Track->eType == tt_Switch && each.side == (Track->SwitchExtension->RightSwitch == (path == 0) ? 1 : -1) ? track_rails::blade_length(*Track, path) : 0.0);
+ each.heel = heel;
+ each.size = Track->rail_dimensions();
+ each.line = &track_rails::rail_line(*Track, path, right, false);
+ std::vector cuts;
+ for (auto const &cut : layout.plans[path][right].cuts)
+ cuts.push_back(&cut);
+ std::stable_sort(cuts.begin(), cuts.end(), [](track_rails::rail_cut const *A, track_rails::rail_cut const *B) { return A->at < B->at; });
+ for (std::size_t j = 0; j <= cuts.size(); ++j)
+ {
+ piece part;
+ part.before = (j > 0 ? cuts[j - 1] : nullptr);
+ part.after = (j < cuts.size() ? cuts[j] : nullptr);
+ if (part.before != nullptr)
+ part.from = part.before->start;
+ if (part.after != nullptr)
+ part.to = part.after->end;
+ each.pieces.push_back(part);
+ }
+ result.push_back(each);
+ }
+ return result;
+ };
+ knots::line const none;
+ struct lines
+ {
+ knots::line const *offsets;
+ knots::line const *extents;
+ knots::line const *inner;
+ };
+ // the lines of the part from the nearer of its cuts
+ auto const lines_at = [&](piece const &Part, double const S) -> lines {
+ auto const before{Part.before != nullptr && (Part.after == nullptr || std::abs(S - Part.before->at) <= std::abs(S - Part.after->at))};
+ if (before)
+ return {&Part.before->startoffsets, &Part.before->startextents, &Part.before->startinner};
+ if (Part.after != nullptr)
+ return {&Part.after->endoffsets, &Part.after->endextents, &Part.after->endinner};
+ return {&none, &none, &none};
+ };
+ auto const head_of = [&](rail const &Rail, piece const &Part, double const S, double &Offset, double &Low, double &High) {
+ auto const l{lines_at(Part, S)};
+ Offset = knots::sample(*l.offsets, S, 0.0);
+ Low = Offset + std::max(0.0, knots::sample(*l.inner, S, 0.0));
+ High = Offset + std::min(Rail.size.head, knots::sample(*l.extents, S, Rail.size.head));
+ };
+ // whether the head of a rail covers a point out from its gauge line, past its rounded shoulder and Tolerance
+ auto const covers = [&](rail const &Rail, double const D, double const L) {
+ for (auto const &part : Rail.pieces)
+ {
+ if (D < part.from || D > part.to)
+ continue;
+ double offset, low, high;
+ head_of(Rail, part, D, offset, low, high);
+ if (L > low + tolerance && L < std::min(high, offset + Rail.size.head - Rail.size.shoulder) - tolerance)
+ return true;
+ }
+ return false;
+ };
+ // the parts near an obtuse crossing of either rail are designed to meet
+ auto const obtuse_near = [](piece const &Part, double const S) {
+ for (auto const *cut : {Part.before, Part.after})
+ if (cut != nullptr && cut->obtuse && std::abs(S - cut->at) < 0.3)
+ return true;
+ return false;
+ };
+ auto const throwable = [](piece const &Part) { return Part.before != nullptr && Part.before->throwable; };
+ int counts[5]{};
+ int parts{0};
+ char const kinds[]{"FOCJV"};
+ for (auto *track : simulation::Paths.sequence())
+ {
+ if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->m_paths.empty() || false == track_rails::cut_rails(*track) || track->rail_dimensions().grooved)
+ continue;
+ if (name != "all" && track->name() != name)
+ continue;
+ std::vector others;
+ for (auto *other : simulation::Paths.sequence())
+ if (other != nullptr && false == other->m_editorremoved && (other->iCategoryFlag & 15) == 1 && false == other->m_paths.empty() && track_rails::cut_rails(*other) && false == other->rail_dimensions().grooved &&
+ glm::length(other->location() - track->location()) < other->radius() + track->radius() + 1.0)
+ others.push_back(other);
+ for (auto const &ours : rails_of(track))
+ for (auto const &part : ours.pieces)
+ {
+ auto const from{std::max({part.from, ours.laidfrom, 0.0})};
+ auto const to{std::min(part.to, ours.length)};
+ if (to - from < 0.04)
+ continue;
+ ++parts;
+ bool found[5]{};
+ std::string where[5];
+ auto const fault = [&](int const Kind, std::string const &What) {
+ if (false == found[Kind])
+ {
+ found[Kind] = true;
+ where[Kind] = What;
+ } };
+ // past the last knots of its lines, the lines planing on as they end
+ for (auto s{from}; s <= to + 1e-9; s += 0.02)
+ {
+ auto const l{lines_at(part, s)};
+ for (auto const *line : {l.extents, l.inner})
+ {
+ if (line->empty())
+ continue;
+ auto const value{knots::sample(*line, s, 0.0)};
+ auto const planing{line == l.extents ? value < ours.size.head - 1e-4 : value > 1e-4};
+ // the lines have knots every 2 cm at most
+ if (planing && (s > line->back().x + 0.025 || s < line->front().x - 0.025))
+ fault(2, format("%s at %.2f m, past its knots %.2f-%.2f m", line == l.extents ? "extents" : "inner", s, line->front().x, line->back().x));
+ }
+ }
+ // along the part: nothing of the head left, the head jumping in width
+ auto start{from + 0.05};
+ auto end{to - 0.05};
+ if (part.before != nullptr && part.before->startkind == track_rails::rail_end::sharp)
+ for (auto s{from}; s < to; s += 0.02)
+ {
+ double offset, low, high;
+ head_of(ours, part, s, offset, low, high);
+ if (high - low >= 0.001)
+ {
+ start = std::max(start, s + 0.04);
+ break;
+ }
+ }
+ std::vector stations, widths;
+ for (auto s{std::ceil(start / 0.02) * 0.02}; s <= end; s += 0.02)
+ {
+ double offset, low, high;
+ head_of(ours, part, s, offset, low, high);
+ auto const width{std::max(0.0, high - low)};
+ if (width < 0.001 && false == obtuse_near(part, s))
+ fault(4, format("at %.2f m, %.1f mm left", s, width * 1000.0));
+ stations.push_back(s);
+ widths.push_back(width);
+ }
+ // a step in width well past those around it: a head tapering off steeply at a wide angle aside
+ for (std::size_t i = 1; i < widths.size(); ++i)
+ {
+ auto const step{std::abs(widths[i] - widths[i - 1])};
+ auto const around{std::max(i > 1 ? std::abs(widths[i - 1] - widths[i - 2]) : 0.0, i + 1 < widths.size() ? std::abs(widths[i + 1] - widths[i]) : 0.0)};
+ if (step > 0.003 && step > 2.0 * around + 0.001 && false == obtuse_near(part, stations[i]))
+ fault(3, format("at %.2f m, %.1f to %.1f mm", stations[i], widths[i - 1] * 1000.0, widths[i] * 1000.0));
+ }
+ // against the rails around
+ for (auto *other : others)
+ for (auto const &theirs : rails_of(other))
+ {
+ if (theirs.track == ours.track && theirs.path == ours.path)
+ continue;
+ auto const &low{theirs.line->low};
+ auto const &high{theirs.line->high};
+ auto distance{-1e9};
+ for (auto s{std::ceil(from / 0.02) * 0.02}; s <= to; s += 0.02)
+ {
+ double offset, lo, hi;
+ head_of(ours, part, s, offset, lo, hi);
+ if (hi - lo < 2.0 * tolerance)
+ continue;
+ auto const centre{track_rails::rail_point(*ours.track, ours.path, ours.side, s, offset + 0.5 * ours.size.head)};
+ if (centre.x < low.x - 0.5 || centre.x > high.x + 0.5 || centre.z < low.y - 0.5 || centre.z > high.y + 0.5)
+ continue;
+ if (distance < -1e8)
+ distance = track_rails::nearest_distance(*theirs.line, centre);
+ auto const across{track_rails::lateral(*theirs.line, theirs.side, centre, distance)};
+ if (distance < 0.0 || distance > theirs.length || std::abs(across) > 0.3)
+ continue;
+ if (theirs.track == ours.track && (theirs.side == ours.side && distance < theirs.heel + 0.1 || throwable(part)))
+ continue;
+ // the same rail going on past a joint
+ {
+ auto d{distance};
+ if (std::abs(track_rails::lateral(*theirs.line, theirs.side, track_rails::rail_point(*ours.track, ours.path, ours.side, s, 0.0), d)) < 0.002)
+ continue;
+ }
+ // the head of ours keeps out of the flangeways, the top of it out from under other heads: their rounded shoulders lie under
+ // it, ours under theirs
+ auto const outer{hi - tolerance};
+ auto const crown{std::min(hi, offset + ours.size.head - ours.size.shoulder) - tolerance};
+ for (auto const lateral : {lo + tolerance, 0.5 * (lo + crown), crown, outer})
+ {
+ auto const top{lateral <= crown + 1e-9};
+ auto const point{track_rails::rail_point(*ours.track, ours.path, ours.side, s, lateral)};
+ auto d{distance};
+ auto const l{track_rails::lateral(*theirs.line, theirs.side, point, d)};
+ // rails meeting end on at joints aside
+ if (d < 0.05 || d > theirs.length - 0.05 || s < 0.05 || s > ours.length - 0.05)
+ continue;
+ if (l > -track_rails::flangeway + tolerance && l < -tolerance)
+ fault(0, format("at %.2f m, %.1f mm into the flangeway of %s/%d%+d at %.2f m", s, (-l) * 1000.0, theirs.track->name().c_str(), theirs.path, theirs.side, d));
+ if (top && false == obtuse_near(part, s) && covers(theirs, d, l))
+ {
+ auto inobtuse{false};
+ for (auto const &their : theirs.pieces)
+ if (d >= their.from && d <= their.to && obtuse_near(their, d))
+ inobtuse = true;
+ if (false == inobtuse)
+ fault(1, format("at %.2f m, over the head of %s/%d%+d at %.2f m, %.1f mm in", s, theirs.track->name().c_str(), theirs.path, theirs.side, d, l * 1000.0));
+ }
+ }
+ }
+ }
+ for (int kind = 0; kind < 5; ++kind)
+ if (found[kind])
+ {
+ ++counts[kind];
+ WriteLog(format("SELFTEST space %c %s/%d%+d part %.3f-%.3f m: %s", kinds[kind], ours.track->name().c_str(), ours.path, ours.side, from, to, where[kind].c_str()));
+ }
+ }
+ }
+ WriteLog(format("SELFTEST space: %d parts, F %d, O %d, C %d, J %d, V %d", parts, counts[0], counts[1], counts[2], counts[3], counts[4]));
+ return selftest_result::handled;
+ }
if (command == "guards")
{
std::string name;
@@ -3008,7 +3840,7 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
std::string text;
auto const end = [&](glm::dvec2 const &Miter, bool const Flared, bool const Glued) {
if (Miter.y == 0.0)
- return std::string{Flared ? "flared" : "square"};
+ return std::string{Flared ? "planed" : "square"};
if (false == drawn(Miter, 0.0))
return std::string{"past the end of the track"};
glm::dvec3 points[2];
@@ -3080,15 +3912,24 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
}
if (command == "throw")
{
+ // throw [part] [name]: the switches set over to the path, by a part of the way as in the middle of the animation
int path{0};
+ double part{1.0};
+ std::string name;
words >> path;
+ if (false == static_cast(words >> part))
+ part = 1.0;
+ words >> name;
int count{0};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->SwitchExtension == nullptr)
continue;
+ if (false == name.empty() && track->name() != name)
+ continue;
+ auto const before{track->SwitchExtension->fOffset};
track->Switch(path);
- track->SwitchExtension->fOffset = track->SwitchExtension->fDesiredOffset;
+ track->SwitchExtension->fOffset = before + static_cast(part) * (track->SwitchExtension->fDesiredOffset - before);
track->RaAnimate();
++count;
}
@@ -3137,21 +3978,22 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
if (false == name.empty() && track->name() != name)
continue;
++tracks;
- for (auto const &crossing : track->rail_crossings())
- {
- ++found;
- auto const middle{crossing.angle > track_rails::middle_angle && crossing.angle <= track_rails::wide_angle};
- middles += middle ? 1 : 0;
- auto const kind{crossing.angle > track_rails::wide_angle ? "wide" : crossing.facing < 0.0 ? (middle ? "middle acute" : "acute") : (middle ? "middle obtuse" : "obtuse")};
- WriteLog(format("SELFTEST crossing %s path %d side %d at %.3f m: %s with %s, 1:%.2f, into %.3f, facing %.3f, nose %d", track->name().c_str(), crossing.path, crossing.side, crossing.at, kind,
- crossing.track->name().c_str(), 1.0 / std::tan(crossing.angle), crossing.into, crossing.facing, crossing.nose ? 1 : 0));
- }
auto const gauge = [](TTrack *Track, track_rails::rail_crossing const &Crossing) {
auto const &segment{Track->eType == tt_Switch ? *Track->SwitchExtension->Segments[Crossing.path] : *Track->Segment};
glm::dvec3 point, across;
segment.plan_frame(Crossing.at, point, across);
return point + across * (Crossing.side * 0.5 * std::abs(Track->fTrackWidth));
};
+ for (auto const &crossing : track->rail_crossings())
+ {
+ ++found;
+ auto const middle{crossing.angle > track_rails::middle_angle && crossing.angle <= track_rails::wide_angle};
+ middles += middle ? 1 : 0;
+ auto const kind{crossing.angle > track_rails::wide_angle ? "wide" : crossing.facing < 0.0 ? (middle ? "middle acute" : "acute") : (middle ? "middle obtuse" : "obtuse")};
+ auto const where{gauge(track, crossing)};
+ WriteLog(format("SELFTEST crossing %s path %d side %d at %.3f m: %s with %s, 1:%.2f, into %.3f, facing %.3f, nose %d, plan %.2f %.2f", track->name().c_str(), crossing.path, crossing.side, crossing.at, kind,
+ crossing.track->name().c_str(), 1.0 / std::tan(crossing.angle), crossing.into, crossing.facing, crossing.nose ? 1 : 0, where.x, where.z));
+ }
for (auto const &crossing : track->rail_crossings(false))
{
if (crossing.angle > track_rails::wide_angle || crossing.facing >= 0.0)
@@ -3257,6 +4099,10 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
// switchbreak [name] the first piece of the diverging path moved off its neighbour, the switch committed
// switches logs the switches kept in the scenery and the speed of each diverging track
// deletetrack , deletekind the path deleted as Del does it, or the first path over an element of the kind
+// tracksel ... the tracks selected together, as Shift+clicks select them; none: nothing selected
+// trackprop [value] the value given to the selected tracks, as typed into the inspector; trackprops logs their values
+// trackstyle copy [name] | groups ... | paste | panel the properties of a track copied, the groups chosen, pasted on the selected tracks
+// tracksimilar the tracks of the scenery with the value of the property of the selected ones selected
// btfcheck the .txtf converted to .btf, the editor lines the binary file keeps logged
// mouse , mouseat the cursor moved to the point of the window, or to where the point of the scene is seen, as the mouse moves it
// glide [shot] the cursor led to where the point is seen in steps, frames apart, a screenshot after each step with shot
@@ -3267,6 +4113,12 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
// screenshot
// save
// log
+// perf the frames drawn since the last perf and the time per frame logged, with the timings and counts of the renderer
+// rebuildall the geometry of every path in the sections made already laid anew, the time it took logged,
+// and the 3d sleepers of all the tracks laid anew, timed apart
+// sleepercheck the 3d sleepers of all the tracks checked for beams over each other and rails without enough under them
+// beams the beams the track lays with their middles between the two z
+// sharedzones where the timbers shared with other tracks reach along the paths of the track and past its ends
// quit
void editor_mode::selftest_step()
{
@@ -3284,6 +4136,7 @@ void editor_mode::selftest_step()
test.lines.push_back(line);
WriteLog("SELFTEST: " + std::to_string(test.lines.size()) + " steps from " + file);
}
+ ++selftest_frames;
if (test.wait > 0)
{
--test.wait;
diff --git a/application/editormodeswitchdesign.cpp b/application/editormodeswitchdesign.cpp
index 1e3f47dc..6a0a64fb 100644
--- a/application/editormodeswitchdesign.cpp
+++ b/application/editormodeswitchdesign.cpp
@@ -347,11 +347,11 @@ void editor_mode::render_switch_designer()
entry.name = name;
changed = true;
}
- char const *types[] = {STR_C("Rz ordinary"), STR_C("Rpd double, to both sides"), STR_C("Rpj double, to one side")};
- char const *codes[] = {"Rz", "Rpd", "Rpj"};
- int type{entry.type == "Rpd" ? 1 : entry.type == "Rpj" ? 2 : 0};
+ char const *types[] = {STR_C("Rz ordinary"), STR_C("Rpd double, to both sides"), STR_C("Rpj double, to one side"), STR_C("Rps double, symmetric")};
+ char const *codes[] = {"Rz", "Rpd", "Rpj", "Rps"};
+ int type{entry.type == "Rpd" ? 1 : entry.type == "Rpj" ? 2 : entry.type == "Rps" ? 3 : 0};
ImGui::SetNextItemWidth(240.0f);
- if (ImGui::Combo(STR_C("Type##designtype"), &type, types, 3))
+ if (ImGui::Combo(STR_C("Type##designtype"), &type, types, 4))
{
entry.type = codes[type];
if (type > 0 && entry.stagger <= 0.0)
@@ -374,6 +374,14 @@ void editor_mode::render_switch_designer()
ImGui::SameLine();
changed |= ImGui::InputDouble(STR_C("Blade angle, deg##designentry"), &entry.entry, 0.0, 0.0, "%.4f");
item_tooltip("The angle at which the blade meets the stock rail at its tip: the diverging track starts there at this angle instead of tangentially. Curved blades a fraction of a degree, straight blades 1 to 2 degrees; 0: tangential");
+ ImGui::SameLine();
+ changed |= ImGui::InputDouble(STR_C("Tip of a tangent blade, m##designtip"), &entry.tip, 0.0, 0.0, "%.3f");
+ item_tooltip("With the blade angle 0: the tip of the blade from the start of the switch as its scheme gives it, the diverging track is broken there into pieces; 0: where the blade gets 5 mm thick, less the 125 mm its nose is planed over");
+ changed |= ImGui::InputDouble(STR_C("Blade heel, m##designheel"), &entry.heel, 0.0, 0.0, "%.3f");
+ item_tooltip("Where the moving blades end, from the start of the switch, as its scheme gives it; the switch keeps it in the scenery. 0: as long as the flangeway of 58 mm behind them needs");
+ ImGui::SameLine();
+ changed |= ImGui::InputDouble(STR_C("Blade throw, m##designthrow"), &entry.bladethrow, 0.0, 0.0, "%.3f");
+ item_tooltip("How far the tips of the blades move over between the two paths; 0: the usual 0.160 m");
ImGui::PopItemWidth();
if (editable)
{
@@ -427,13 +435,16 @@ void editor_mode::render_switch_designer()
ImGui::SetNextItemWidth(90.0f);
changed |= ImGui::InputDouble(STR_C("Second switch starts at, m##stagger"), &entry.stagger, 0.0, 0.0, "%.3f");
item_tooltip("Distance from the start of the first switch to the start of the second one, along the main track");
- bool own{false == entry.parts2.empty()};
- if (ImGui::Checkbox(STR_C("The second switch has a diverging track of its own"), &own))
+ bool own{false == entry.parts2.empty() && entry.type != "Rps"};
+ if (entry.type == "Rps")
+ ImGui::TextDisabled("%s", STR_C("The second switch is the mirror image of the first one"));
+ else if (ImGui::Checkbox(STR_C("The second switch has a diverging track of its own"), &own))
{
entry.parts2 = own ? entry.parts : std::vector{};
changed = true;
}
- item_tooltip("Otherwise the second switch diverges the same way as the first, to the other side for Rpd, to the same side for Rpj");
+ if (entry.type != "Rps")
+ item_tooltip("Otherwise the second switch diverges the same way as the first, to the other side for Rpd, to the same side for Rpj");
if (own)
{
ImGui::PushItemWidth(90.0f);
@@ -450,12 +461,19 @@ void editor_mode::render_switch_designer()
entry.entry2 = std::max(0.0, entry2);
changed = true;
}
+ ImGui::SameLine();
+ auto tip2{entry.tip2 >= 0.0 ? entry.tip2 : entry.tip};
+ if (ImGui::InputDouble(STR_C("Tip of a tangent blade, m##designtip2"), &tip2, 0.0, 0.0, "%.3f"))
+ {
+ entry.tip2 = std::max(0.0, tip2);
+ changed = true;
+ }
ImGui::PopItemWidth();
changed |= edit_parts(entry.parts2, "##parts2", editable);
}
- else if (entry.lead2 >= 0.0 || entry.entry2 >= 0.0)
+ else if (entry.lead2 >= 0.0 || entry.entry2 >= 0.0 || entry.tip2 >= 0.0)
{
- entry.lead2 = entry.entry2 = -1.0;
+ entry.lead2 = entry.entry2 = entry.tip2 = -1.0;
changed = true;
}
}
@@ -482,8 +500,10 @@ void editor_mode::render_switch_designer()
{
ImGui::Text(STR_C("Angle 1:%.3f (%.4f deg), sharpest R %.1f m, a %.3f m, b %.3f m"), shape.ratio, glm::degrees(std::atan(1.0 / shape.ratio)), shape.radius, shape.a, shape.b);
ImGui::Text(STR_C("Length %.3f m, the diverging track ends %.3f m beside the main one, in %zu pieces"), shape.length, shape.offset, std::max(1, shape.paths[1].pieces.size()));
- if (entry.lead > 0.0 || entry.entry > 0.0)
+ if (entry.entry > 0.0)
ImGui::Text(STR_C("Tip of the blade %.3f m from the start, the diverging track leaves there at %.4f deg"), shape.tip, entry.entry);
+ else
+ ImGui::Text(STR_C("Tip of the blade %.3f m from the start, the diverging track leaves tangentially"), shape.tip);
}
ImGui::Checkbox(STR_C("Stretch across"), &designer.stretched);
item_tooltip("Draws the offsets from the main track larger, so the shape of the diverging track can be seen");
diff --git a/application/editormodeswitchdrive.cpp b/application/editormodeswitchdrive.cpp
index fc92a14e..e04d5ac2 100644
--- a/application/editormodeswitchdrive.cpp
+++ b/application/editormodeswitchdrive.cpp
@@ -13,16 +13,25 @@ http://mozilla.org/MPL/2.0/.
#include "editor/editorFormat.hpp"
#include "editor/editorIncludeInfo.hpp"
+#include "world/Segment.h"
#include "world/Track.h"
+#include "world/TrackRails.h"
#include "scene/scenelayers.h"
+#include "simulation/simulation.h"
+#include "utilities/Globals.h"
#include "utilities/translation.h"
+#include "utilities/utilities.h"
#include "imgui/imgui.h"
#include
#include
+#include
#include
+#include
+#include
#include
#include
+#include
namespace
{
@@ -307,3 +316,64 @@ bool editor_mode::render_switch_drive_choice()
}
return changed;
}
+
+// a key by the movable nose of each switch with a drive, the frog is far from the drive at the start of the switch
+bool editor_mode::nose_controls(std::vector &Rootstatements)
+{
+ if (scene::Layers.empty())
+ return false;
+ std::string root{scene::Layers.layer(scene::layer_handle{1}).name};
+ if (auto const slash{root.find_last_of("/\\")}; slash != std::string::npos)
+ root.erase(0, slash + 1);
+ if (auto const dot{root.rfind('.')}; dot != std::string::npos)
+ root.erase(dot);
+ if (root.empty())
+ return false;
+ auto const file{root + "_dzioby.scm"};
+ std::ostringstream text;
+ auto noses{0};
+ for (auto *track : simulation::Paths.sequence())
+ {
+ if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->SwitchExtension == nullptr || false == track->SwitchExtension->m_movablenose || track->name().empty())
+ continue;
+ auto const &segments{track->SwitchExtension->Segments};
+ auto const frog{track->frog()};
+ if (false == frog.valid || segments[0] == nullptr || frog.angle <= 0.0 || false == find_switch_drive(*track).has_value())
+ continue;
+ auto const &name{track->name()};
+ // where the inner rails cross, on the gauge line of the main path
+ glm::dvec3 point, across;
+ segments[0]->plan_frame(frog.at[0], point, across);
+ auto const nose{point + across * (frog.side[0] * 0.5 * std::abs(track->fTrackWidth))};
+ auto const range{5.0 + track_rails::movable_nose_length(std::tan(frog.angle))};
+ text << "node -1 0 " << name << "_dziob eventlauncher " << format("%.3f %.3f %.3f %.1f", nose.x, nose.y, nose.z, range) << " t 0 " << name << "+ " << name << "- end\r\n";
+ ++noses;
+ }
+ auto const path{Global.asCurrentSceneryPath + file};
+ std::error_code error;
+ auto const exists{std::filesystem::exists(path, error)};
+ if (noses == 0 && false == exists)
+ return false;
+ auto const content{"// made by the scenery editor on each save: T and Shift+T by the movable nose throw its switch, as by its drive\r\n" + text.str()};
+ std::string current;
+ {
+ std::ifstream input{path, std::ios_base::binary};
+ current.assign(std::istreambuf_iterator(input), std::istreambuf_iterator());
+ }
+ if (current != content)
+ {
+ std::ofstream output{path, std::ios_base::binary | std::ios_base::trunc};
+ output << content;
+ }
+ auto included{m_nose_included};
+ for (std::size_t handle = 1; handle <= scene::Layers.size() && false == included; ++handle)
+ {
+ auto name{scene::Layers.layer(static_cast(handle)).name};
+ std::replace(name.begin(), name.end(), '\\', '/');
+ included = ToLower(name) == ToLower(file) || ToLower(name).ends_with("/" + ToLower(file));
+ }
+ if (included)
+ return false;
+ Rootstatements.push_back("include " + file + " end");
+ return true;
+}
diff --git a/application/editormodeswitchslide.cpp b/application/editormodeswitchslide.cpp
index 5a13861c..f955885b 100644
--- a/application/editormodeswitchslide.cpp
+++ b/application/editormodeswitchslide.cpp
@@ -31,6 +31,9 @@ namespace
using geometry::bezier;
using geometry::has_controls;
+using geometry::cross;
+using geometry::grade_of;
+using geometry::plan_distance;
using geometry::plan_of;
struct switch_end_move
@@ -90,6 +93,80 @@ std::string switch_rejoin(TTrack &Track, std::vector const
return loose;
}
+double constexpr kBranchStraight{500.0}; // how far the straight past the curve next to a switch is followed
+
+// direction out of the path at the end
+glm::dvec3 end_outward(segment_data const &Path, bool const Atend)
+{
+ bezier const curve{Path};
+ auto direction{Atend ? curve.first(1.0) : -curve.first(0.0)};
+ if (glm::length(plan_of(direction)) < 1e-9)
+ direction = Atend ? curve.p3 - curve.p0 : curve.p0 - curve.p3;
+ return direction;
+}
+
+// straight from the start of the line to its first curve
+double opening(alignment::result const &Result)
+{
+ return Result.elements.empty() || Result.elements.front().kind != alignment::element_kind::straight ? 0.0 : Result.elements.front().length;
+}
+
+// straight from the last curve of the line to its end
+double closing(alignment::result const &Result)
+{
+ return Result.elements.empty() || Result.elements.back().kind != alignment::element_kind::straight ? 0.0 : Result.elements.back().length;
+}
+
+alignment::vertex scaled(alignment::vertex Vertex, double const Factor)
+{
+ Vertex.radius *= Factor;
+ Vertex.transition_in *= Factor;
+ Vertex.transition_out *= Factor;
+ for (auto &arc : Vertex.arcs)
+ {
+ arc.radius *= Factor;
+ arc.transition *= Factor;
+ }
+ return Vertex;
+}
+
+// where the path of the index leaves the chain
+glm::dvec3 exit_of(editor_track::chain const &Chain, std::size_t const Index)
+{
+ auto const &path{Chain.tracks[Index]->m_paths.front()};
+ return path.points[Chain.forward[Index] ? segment_data::point::end : segment_data::point::start];
+}
+
+// raises the pieces by Rise at their start, less and less up to nothing at their end
+void lift(std::vector &Pieces, double const Rise)
+{
+ std::vector lengths;
+ double total{0.0};
+ for (auto const &piece : Pieces)
+ {
+ lengths.push_back(bezier{piece}.plan_length());
+ total += lengths.back();
+ }
+ if (total <= 1e-6 || std::abs(Rise) <= 1e-6)
+ return;
+ auto const slope{Rise / total};
+ double along{0.0};
+ for (std::size_t i = 0; i < Pieces.size(); ++i)
+ {
+ auto &points{Pieces[i].points};
+ auto const controls{has_controls(Pieces[i])};
+ points[segment_data::point::start].y += Rise - slope * along;
+ points[segment_data::point::end].y += Rise - slope * (along + lengths[i]);
+ // the controls are kept from their ends
+ if (controls)
+ {
+ points[segment_data::point::control1].y -= slope * glm::length(plan_of(points[segment_data::point::control1]));
+ points[segment_data::point::control2].y += slope * glm::length(plan_of(points[segment_data::point::control2]));
+ }
+ along += lengths[i];
+ }
+}
+
} // namespace
bool editor_mode::switch_room(TTrack &Track, double &Low, double &High, std::string &Reason)
@@ -178,6 +255,258 @@ bool editor_mode::switch_vacate(TTrack &Track, glm::dvec3 const &From, glm::dvec
return true;
}
+std::optional editor_mode::take_switch_branch(TTrack const &Track, editor_track::joint const &Joint, std::size_t const Index) const
+{
+ auto *first{Joint.other};
+ if (first == nullptr || first->eType != tt_Normal || first->m_paths.empty())
+ return std::nullopt;
+ auto const category{first->iCategoryFlag & 15};
+ auto const regular = [&](TTrack const *Other) { return Other != nullptr && Other->eType == tt_Normal && false == Other->m_editorremoved && editor_track::is_supported(*Other) && (Other->iCategoryFlag & 15) == category; };
+ // from the switch on: a straight up to the curve maybe, the curve, the straight past it
+ std::vector run;
+ auto *current{first};
+ auto atstart{Joint.theirs.kind == editor_track::point_kind::start};
+ int phase{0};
+ std::size_t straight{0};
+ double past{0.0};
+ while (regular(current) && std::find(run.begin(), run.end(), current) == run.end() && past < kBranchStraight)
+ {
+ auto const plain{editor_track::is_straight(*current, m_straights.tolerance)};
+ if (false == plain && phase == 2)
+ break;
+ if (false == plain)
+ phase = 1;
+ else if (phase == 1)
+ {
+ phase = 2;
+ straight = run.size();
+ }
+ if (phase == 2)
+ past += current->Length();
+ run.push_back(current);
+ auto *next{atstart ? current->trNext : current->trPrev};
+ if (next == nullptr || (next->trPrev != current && next->trNext != current))
+ break;
+ atstart = next->trPrev == current;
+ current = next;
+ }
+ if (phase != 2)
+ return std::nullopt;
+ switch_branch branch;
+ branch.joint = Index;
+ branch.straight = straight;
+ std::string error;
+ auto &chain{branch.chain};
+ if (false == editor_track::find_chain(run.front(), run.back(), chain, error) || chain.tracks != run || plan_distance(chain.start, editor_track::point_position(Track, Joint.own)) > geometry::same_point)
+ return std::nullopt;
+ auto &design{branch.design};
+ design.norms = m_route.design.norms;
+ design.shape = m_route.design.shape;
+ design.transition_pieces = m_route.design.transition_pieces;
+ design.speed = chain.velocity > 0.0 ? chain.velocity : 100.0;
+ if (auto const hit{alignment_store::on_track(*run[straight - 1])})
+ {
+ auto const &stored{alignment_store::records()[hit->record].design};
+ design.norms = stored.norms;
+ design.shape = stored.shape;
+ design.transition_pieces = stored.transition_pieces;
+ design.arc_piece_angle = stored.arc_piece_angle;
+ design.speed = stored.speed;
+ }
+ design.norms.checked = false;
+ auto const found{editor_track::recognize_line(chain, m_straights.tolerance, design.norms.gauge, design.shape)};
+ if (found.size() != 1 || found.front().kink)
+ return std::nullopt;
+ design.start = chain.start;
+ design.end = chain.end;
+ design.start_direction = glm::normalize(plan_of(chain.start_direction));
+ design.end_direction = glm::normalize(plan_of(chain.end_direction));
+ design.start_grade = grade_of(chain.start_direction);
+ design.end_grade = grade_of(chain.end_direction);
+ auto vertex{route_vertex_of(found.front().shape)};
+ vertex.position = found.front().position;
+ design.vertices = {vertex};
+ branch.result = alignment::compute(design);
+ if (false == branch.result.valid)
+ return std::nullopt;
+ return branch;
+}
+
+bool editor_mode::plan_switch_branch(segment_data const &Path, editor_track::joint const &Joint, switch_branch &Branch) const
+{
+ auto const atend{Joint.own.kind == editor_track::point_kind::end};
+ auto const start{Path.points[atend ? segment_data::point::end : segment_data::point::start]};
+ auto const outward{end_outward(Path, atend)};
+ auto laid{Branch.design};
+ laid.start = start;
+ laid.start_direction = glm::normalize(plan_of(outward));
+ laid.start_grade = grade_of(outward);
+ auto const original{Branch.design.vertices.front()};
+ auto const make = [&](double const Factor) {
+ laid.vertices = {scaled(original, Factor)};
+ return alignment::compute(laid);
+ };
+ // compute() makes the curve tighter when it doesn't fit between the ends
+ auto const fits = [&](alignment::result const &Result) { return Result.valid && false == Result.curves.empty() && Result.curves.front().radius >= laid.vertices.front().radius * (1.0 - 1e-6); };
+ double factor{1.0};
+ auto result{make(factor)};
+ if (false == result.valid || result.curves.empty())
+ return false;
+ if (false == fits(result))
+ {
+ // the design keeps the curve the way it was made to fit
+ auto &vertex{laid.vertices.front()};
+ auto const &report{result.curves.front()};
+ auto const reduced{report.radius / vertex.radius};
+ if (vertex.compound)
+ vertex = scaled(vertex, reduced);
+ else
+ {
+ vertex.radius = report.radius;
+ vertex.transition_in = report.transition_in;
+ vertex.transition_out = report.transition_out;
+ }
+ factor = reduced;
+ result = alignment::compute(laid);
+ if (false == result.valid)
+ return false;
+ }
+ else if (opening(Branch.result) < 0.1 && opening(result) >= 0.1)
+ {
+ // the curve which adjoined the switch keeps to it: the largest one which fits
+ double low{1.0};
+ double high{1.5};
+ while (fits(make(high)) && high < 5.0)
+ high *= 1.5;
+ if (fits(make(high)))
+ return false;
+ for (int i = 0; i < 40; ++i)
+ {
+ auto const middle{0.5 * (low + high)};
+ (fits(make(middle)) ? low : high) = middle;
+ }
+ factor = low;
+ result = make(factor);
+ if (false == result.valid)
+ return false;
+ }
+ // the paths are laid up to the first joint on the straight past the old and the new curve
+ auto const &chain{Branch.chain};
+ auto const end{plan_of(chain.end)};
+ auto const behind = [&](glm::dvec3 const &Point) { return glm::dot(end - plan_of(Point), laid.end_direction); };
+ auto const curveend{closing(result)};
+ auto last{chain.tracks.size()};
+ for (auto k{Branch.straight - 1}; k < chain.tracks.size(); ++k)
+ if (behind(exit_of(chain, k)) <= curveend - 0.5)
+ {
+ last = k;
+ break;
+ }
+ if (last == chain.tracks.size())
+ return false;
+ auto part{laid};
+ auto const joint{exit_of(chain, last)};
+ part.end = joint;
+ part.end_grade = grade_of(end_outward(chain.tracks[last]->m_paths.front(), chain.forward[last]));
+ auto const partresult{alignment::compute(part)};
+ if (false == partresult.valid)
+ return false;
+ Branch.part = {};
+ Branch.part.tracks.assign(chain.tracks.begin(), chain.tracks.begin() + last + 1);
+ Branch.part.forward.assign(chain.forward.begin(), chain.forward.begin() + last + 1);
+ Branch.part.start = chain.start;
+ Branch.part.end = joint;
+ Branch.pieces = alignment::pieces(partresult, part, last + 1);
+ if (Branch.pieces.size() < last + 1)
+ return false;
+ editor_track::keep_heights(Branch.part, Branch.pieces);
+ lift(Branch.pieces, start.y - chain.start.y);
+ Branch.pieces.front().points[segment_data::point::start] = start;
+ Branch.pieces.back().points[segment_data::point::end] = joint;
+ Branch.laid = laid;
+ Branch.factor = factor;
+ return true;
+}
+
+void editor_mode::lay_switch_branch(switch_branch const &Branch, editor_track::edit &Edit)
+{
+ for (auto *track : Branch.part.tracks)
+ Edit.capture(*track);
+ auto const created{editor_track::relay(Branch.part, Branch.pieces)};
+ Edit.created.insert(Edit.created.end(), created.begin(), created.end());
+ // the design kept for the line starting or ending at the switch follows it
+ auto const &records{alignment_store::records()};
+ auto const from{Branch.chain.start};
+ auto const joint{Branch.part.end};
+ for (std::size_t i = 0; i < records.size(); ++i)
+ {
+ auto const &record{records[i]};
+ auto const atstart{plan_distance(record.design.start, from) < geometry::same_point};
+ if (false == atstart && plan_distance(record.design.end, from) >= geometry::same_point)
+ continue;
+ auto design{record.design};
+ auto &vertices{design.vertices};
+ if (vertices.empty())
+ return;
+ auto const outward{Branch.laid.start_direction};
+ auto const k{atstart ? std::size_t{0} : vertices.size() - 1};
+ if (atstart)
+ {
+ design.start = Branch.laid.start;
+ design.start_direction = outward;
+ design.start_grade = Branch.laid.start_grade;
+ }
+ else
+ {
+ design.end = Branch.laid.start;
+ design.end_direction = -outward;
+ design.end_grade = -Branch.laid.start_grade;
+ }
+ auto const &corners{record.result.vertices};
+ if (vertices.size() >= 2 && corners.size() == vertices.size())
+ {
+ // the corner moves along its other leg to the line from the switch
+ auto const corner{corners[k]};
+ auto const leg{corners[atstart ? 1 : k - 1] - corner};
+ auto const determinant{cross(outward, leg)};
+ if (std::abs(determinant) < 1e-9)
+ return;
+ auto const offset{cross(corner - plan_of(Branch.laid.start), leg) / determinant};
+ if (offset <= 0.0)
+ return;
+ vertices[k].offset = offset;
+ }
+ // the curve next to the switch takes the shape it was laid with
+ auto const &shape{Branch.laid.vertices.front()};
+ if (shape.compound || vertices[k].compound)
+ vertices[k] = scaled(vertices[k], Branch.factor);
+ else
+ {
+ vertices[k].radius = shape.radius;
+ vertices[k].transition_in = atstart ? shape.transition_in : shape.transition_out;
+ vertices[k].transition_out = atstart ? shape.transition_out : shape.transition_in;
+ }
+ auto const result{alignment::compute(design)};
+ if (false == result.valid || false == alignment_store::follows(result, Branch.part.tracks))
+ return;
+ // the km of the joint the paths are laid up to stays as it was
+ auto origin{record.origin};
+ if (atstart)
+ {
+ auto const before{alignment_store::chainage_on(record.result, joint)};
+ auto const after{alignment_store::chainage_on(result, joint)};
+ if (before.has_value() && after.has_value())
+ origin += *before - *after;
+ }
+ auto const layer{record.layer};
+ auto const source{record.source};
+ auto const fit{record.fit};
+ alignment_store::forget(static_cast(i));
+ alignment_store::store(design, result, origin, layer, source, fit);
+ return;
+ }
+}
+
bool editor_mode::move_switch_paths(TTrack &Track, std::vector const &Paths, std::string &Status)
{
if (Paths.size() < 2 || Track.m_paths.size() < 2 || false == editor_track::main_axis(Track).has_value())
@@ -199,6 +528,7 @@ bool editor_mode::move_switch_paths(TTrack &Track, std::vector con
for (auto const &joint : joints)
positions.push_back(editor_track::point_position(Track, joint.own));
std::vector handled(joints.size(), false);
+ auto branches{switch_branches(Track, joints)};
editor_track::edit edit;
edit.capture(Track);
auto const line{online ? editor_track::find_straight(Track, m_straights.tolerance) : editor_track::straight{}};
@@ -211,14 +541,56 @@ bool editor_mode::move_switch_paths(TTrack &Track, std::vector con
return false;
}
Track.m_paths.assign(Paths.begin(), Paths.begin() + 2);
+ auto const notes{plan_switch_branches(Track, joints, branches, handled)};
auto const loose{switch_rejoin(Track, joints, handled, edit)};
editor_track::commit(edit.changed);
+ for (auto const &branch : branches)
+ lay_switch_branch(branch, edit);
push_track_snapshot(std::move(edit.states), std::move(edit.created), std::move(edit.removed));
if (false == loose.empty())
Status = STR_C("Too far from the new ends of the switch, left unjoined: ") + loose;
+ if (false == notes.empty())
+ Status += (Status.empty() ? "" : "\n") + notes;
return true;
}
+std::vector editor_mode::switch_branches(TTrack const &Track, std::vector const &Joints, std::vector const &Skip) const
+{
+ std::vector branches;
+ auto const axis{editor_track::main_axis(Track)};
+ if (false == axis.has_value())
+ return branches;
+ for (std::size_t i = 0; i < Joints.size(); ++i)
+ {
+ if (std::find(Skip.begin(), Skip.end(), Joints[i].other) != Skip.end() || axis->lateral(editor_track::point_position(Track, Joints[i].own)) <= m_straights.tolerance.offset)
+ continue;
+ if (auto branch{take_switch_branch(Track, Joints[i], i)})
+ branches.push_back(std::move(*branch));
+ }
+ return branches;
+}
+
+std::string editor_mode::plan_switch_branches(TTrack const &Track, std::vector const &Joints, std::vector &Branches, std::vector &Handled) const
+{
+ std::string notes;
+ for (auto branch{Branches.begin()}; branch != Branches.end();)
+ {
+ auto const &joint{Joints[branch->joint]};
+ std::string reason;
+ if (false == plan_switch_branch(Track.m_paths[joint.own.path], joint, *branch) || false == tracks_editable(branch->part.tracks, reason))
+ {
+ notes += (notes.empty() ? "" : "\n") + format(STR_C("The curve past the end of the switch can't keep to it, %s follows the switch on its own"), track_label(*joint.other).c_str());
+ branch = Branches.erase(branch);
+ continue;
+ }
+ Handled[branch->joint] = true;
+ if (std::abs(branch->factor - 1.0) > 1e-4)
+ notes += (notes.empty() ? "" : "\n") + format(STR_C("The curve past the end of the switch keeps to it with the radius of %.1f m instead of %.1f m"), branch->laid.vertices.front().radius, branch->design.vertices.front().radius);
+ ++branch;
+ }
+ return notes;
+}
+
bool editor_mode::double_room(double_switch const &Set, double &Low, double &High, double &Length, std::string &Reason)
{
double low{0.0};
@@ -288,6 +660,9 @@ bool editor_mode::move_double_paths(double_switch const &Set, std::vectorm_paths.front().points[segment_data::point::end]};
auto const &link{Set.link->m_paths.front()};
bool const reversed{glm::distance(link.points[segment_data::point::start], joint) > glm::distance(link.points[segment_data::point::end], joint)};
+ std::array, 2> branches;
+ for (std::size_t k = 0; k < 2; ++k)
+ branches[k] = switch_branches(*switches[k], joints[k], {Set.first, Set.link, Set.second});
editor_track::edit edit;
for (auto *track : {Set.first, Set.link, Set.second})
edit.capture(*track);
@@ -304,6 +679,13 @@ bool editor_mode::move_double_paths(double_switch const &Set, std::vectorm_paths.assign(Paths.begin(), Paths.begin() + 2);
Set.second->m_paths.assign(Paths.begin() + 2, Paths.begin() + 4);
Set.link->m_paths = {reversed ? geometry::reversed(Paths[4]) : Paths[4]};
+ std::string notes;
+ for (std::size_t k = 0; k < 2; ++k)
+ {
+ auto const note{plan_switch_branches(*switches[k], joints[k], branches[k], handled[k])};
+ if (false == note.empty())
+ notes += (notes.empty() ? "" : "\n") + note;
+ }
std::string loose;
for (std::size_t k = 0; k < 2; ++k)
{
@@ -312,9 +694,14 @@ bool editor_mode::move_double_paths(double_switch const &Set, std::vectoralong(path.points[index]));
}
}
+ std::vector const skip(members.begin(), members.end());
+ for (auto *member : members)
+ {
+ if (member->eType != tt_Switch)
+ continue;
+ std::size_t const offset{set.has_value() && member == set->second ? 2u : 0u};
+ auto const joints{editor_track::joints(*member)};
+ for (auto &branch : switch_branches(*member, joints, skip))
+ {
+ auto const &joint{joints[branch.joint]};
+ slide.branches.push_back({joint, offset + static_cast(joint.own.path), std::move(branch)});
+ }
+ }
}
void editor_mode::switch_slide_update()
@@ -465,16 +865,27 @@ void editor_mode::draw_switch_slide() const
}
screen_projection const projection;
auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())};
- for (auto const &path : paths)
- {
- bezier const curve{path};
+ auto const draw = [&](segment_data const &Path, ImU32 const Color, float const Width) {
+ bezier const curve{Path};
auto previous{curve.point(0.0)};
for (int i = 1; i <= 24; ++i)
{
auto const next{curve.point(i / 24.0)};
- projection.line(drawlist, previous, next, overlay_color::marked, 3.0f);
+ projection.line(drawlist, previous, next, Color, Width);
previous = next;
}
+ };
+ for (auto const &path : paths)
+ draw(path, overlay_color::marked, 3.0f);
+ // the curves past the switch, laid again up to the straight beyond them
+ for (auto const &line : slide.branches)
+ {
+ if (line.path >= paths.size())
+ continue;
+ auto branch{line.branch};
+ if (plan_switch_branch(paths[line.path], line.joint, branch))
+ for (auto const &piece : branch.pieces)
+ draw(piece, overlay_color::grip, 2.0f);
}
ImVec2 at;
auto const start{paths.front().points[segment_data::point::start]};
diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp
index 2d4b5b13..03b4301a 100644
--- a/application/editormodetrack.cpp
+++ b/application/editormodetrack.cpp
@@ -241,6 +241,19 @@ void compound_from(editor_track::curve const &Curve, alignment::vertex &Vertex)
Vertex.arcs.push_back({std::round(Curve.arcs[i].radius), std::round(Curve.arcs[i].transition), Curve.arcs[i].turn});
}
+char const *type_name(TTrack const &Track)
+{
+ switch (Track.eType)
+ {
+ case tt_Normal: return "normal";
+ case tt_Switch: return "switch";
+ case tt_Cross: return "cross";
+ case tt_Table: return "turntable";
+ case tt_Tributary: return "tributary";
+ default: return "unknown";
+ }
+}
+
}
TTrack *editor_mode::selected_track() const
@@ -261,6 +274,9 @@ void editor_mode::select_track(scene::basic_node *Node)
m_track_point = {};
m_track_mode_notice.clear();
}
+ // a track out of the set ends it, the set is the selection only together with its tracks
+ if (false == in_track_set(track))
+ m_track_set.clear();
m_node = track;
ui()->set_node(m_node);
if (ui()->mode() != nodebank_panel::TRACK)
@@ -295,7 +311,8 @@ void editor_mode::select_track(scene::basic_node *Node)
editor_track::point_ref editor_mode::track_handle_hit() const
{
auto *track = selected_track();
- if (track == nullptr || false == editor_track::is_supported(*track) || m_track_tab != track_tab::path)
+ // the tracks of a set are moved by its gizmo, not by the points of one of them
+ if (track == nullptr || false == editor_track::is_supported(*track) || m_track_tab != track_tab::path || (m_track_set.size() >= 2 && in_track_set(track)))
return {};
screen_projection const projection;
@@ -326,6 +343,9 @@ void editor_mode::draw_track_overlay() const
screen_projection const projection;
ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport());
bool const trackmode = ui()->mode() == nodebank_panel::TRACK;
+ // a track of the set is drawn with the set, see draw_track_set()
+ if (trackmode && m_track_set.size() >= 2 && in_track_set(track))
+ return;
ImU32 const coursecolor = IM_COL32(40, 220, 255, 220);
int constexpr samples{32};
@@ -779,18 +799,45 @@ void editor_mode::render_track_tool_options()
void editor_mode::render_track_selection()
{
- auto *track{selected_track()};
- if (track == nullptr)
+ track_field_settle();
+ auto const tracks{selected_tracks()};
+ if (tracks.empty())
{
ImGui::TextDisabled("%s", STR_C("Nothing selected"));
+ if (m_track_tab == track_tab::path)
+ {
+ ImGui::PushTextWrapPos(0.0f);
+ ImGui::TextDisabled("%s", STR_C("Click a track to see its properties. Shift+click adds more tracks to the selection, a drag on the ground selects those in a box"));
+ ImGui::PopTextWrapPos();
+ }
return;
}
- char const *type = track->eType == tt_Normal ? "normal" : track->eType == tt_Switch ? "switch" : track->eType == tt_Cross ? "cross" : track->eType == tt_Table ? "turntable" : track->eType == tt_Tributary ? "tributary" : "unknown";
- ImGui::TextUnformatted((track->name().empty() ? std::string{"(noname)"} : track->name()).c_str());
- ImGui::TextDisabled("%s, %.2f m", STR_C(type), track->Length());
- // the parameters of the path belong to the select tool, the other tools show the path they work with
- if (m_track_tab == track_tab::path)
- render_path_ui();
+ // the properties belong to the select tool and to the switch tool, the other tools show the track they work with
+ auto const sheet{m_track_tab == track_tab::path || m_track_tab == track_tab::turnout};
+ if (tracks.size() >= 2)
+ {
+ render_track_set_header(tracks);
+ if (false == sheet)
+ return;
+ render_track_select_tools(tracks);
+ render_track_style(tracks);
+ render_track_properties(tracks);
+ ImGui::Spacing();
+ ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.55f, 0.18f, 0.15f, 1.0f));
+ if (ImGui::Button(format(STR_P("Delete %d track", "Delete %d tracks", static_cast(tracks.size())), static_cast(tracks.size())).c_str()))
+ track_set_delete();
+ ImGui::PopStyleColor();
+ item_tooltip("Deletes the selected tracks (Del); Ctrl+Z brings them back");
+ return;
+ }
+ auto *track{tracks.front()};
+ if (false == sheet)
+ {
+ ImGui::TextUnformatted((track->name().empty() ? std::string{"(noname)"} : track->name()).c_str());
+ ImGui::TextDisabled("%s, %.2f m", STR_C(type_name(*track)), track->Length());
+ return;
+ }
+ render_path_ui();
}
void editor_mode::render_track_guide()
@@ -1001,6 +1048,18 @@ void editor_mode::render_turnout_ui()
}
item_tooltip("Cuts the rails of the switch at its frog and lays wing rails and check rails, for rail profiles S60, UIC60 and S39.\n"
"Off for a switch whose include has its own model of them");
+ if (frogs)
+ {
+ bool movable{track->SwitchExtension->m_movablenose};
+ if (ImGui::Checkbox(STR_C("Movable nose"), &movable))
+ {
+ auto const before{editor_track::capture(*track)};
+ track->SwitchExtension->m_movablenose = movable;
+ push_track_snapshot({{track, before}});
+ editor_track::commit_parameters(*track);
+ }
+ item_tooltip("The nose of the frog moves with the blades over to the wing rail of the track set, leaving no gap; the frog has no check rails");
+ }
if (false == m_switch.status.empty())
ImGui::TextWrapped("%s", m_switch.status.c_str());
if (angle > 1e-6)
@@ -1143,279 +1202,32 @@ void editor_mode::render_turnout_ui()
ImGui::TextDisabled(STR_C("Room on the straight: %.2f m back, %.2f m ahead"), -low, high - reach);
}
ImGui::Separator();
- render_path_parameters(*track);
+ ImGui::TextDisabled("%s", STR_C("The textures, the speed and the other properties of the switch are in the inspector"));
}
-void editor_mode::render_path_parameters(TTrack &Track)
+void editor_mode::render_path_events(TTrack &Track)
{
auto *track{&Track};
- auto const before = editor_track::capture(*track);
- enum class rebuild { none, parameters, geometry };
- auto const finish_edit = [&](rebuild const Rebuild) {
- if (ImGui::IsItemActivated())
- m_track_field_before = before;
- if (ImGui::IsItemDeactivatedAfterEdit())
- {
- push_track_snapshot({{track, m_track_field_before}});
- switch (Rebuild)
- {
- case rebuild::geometry: editor_track::commit({track}); break;
- case rebuild::parameters: editor_track::commit_parameters(*track); break;
- default: track->mark_dirty(); break;
- }
- }
- };
- auto const apply_now = [&](rebuild const Rebuild) {
- push_track_snapshot({{track, before}});
- if (Rebuild == rebuild::parameters)
- editor_track::commit_parameters(*track);
- else
- track->mark_dirty();
- };
-
- ImGui::PushItemWidth(120.0f);
- double velocity = editor_track::velocity(*track);
- if (ImGui::InputDouble(STR_C("Velocity (km/h, -1: none)"), &velocity, 0.0, 0.0, "%.1f"))
- editor_track::velocity(*track, velocity);
- item_tooltip("Speed limit of the path for the AI drivers and the timetable, -1: no limit of its own");
- finish_edit(rebuild::none);
- char const *environments[] = {STR_C("flat"), STR_C("mountains"), STR_C("canyon"), STR_C("tunnel"), STR_C("bridge"), STR_C("bank")};
- int environment = std::clamp(static_cast(track->eEnvironment), 0, 5);
- if (ImGui::Combo(STR_C("Environment"), &environment, environments, IM_ARRAYSIZE(environments)))
+ if (m_track_events_for != track && false == ImGui::IsAnyItemActive())
{
- track->eEnvironment = static_cast(environment);
- apply_now(rebuild::none);
- }
- item_tooltip("Surroundings of the path, they affect the sound of the passing vehicles");
-
- ImGui::PopItemWidth();
-
- if (ImGui::CollapsingHeader(STR_C("Ride and sound")))
- {
- ImGui::PushItemWidth(120.0f);
- if (ImGui::InputFloat(STR_C("Friction"), &track->fFriction, 0.0f, 0.0f, "%.3f"))
- track->fFriction = std::max(0.0f, track->fFriction);
- item_tooltip("Friction coefficient of the rails, 0.15 for the dry rails; lower values make the wheels slip");
- finish_edit(rebuild::none);
- ImGui::InputFloat(STR_C("Sound distance"), &track->fSoundDistance, 0.0f, 0.0f, "%.1f");
- item_tooltip("Distance between the rail joints in metres, sets the rhythm of the wheel clatter; -1: the default");
- finish_edit(rebuild::none);
- ImGui::InputInt(STR_C("Quality"), &track->iQualityFlag, 0);
- item_tooltip("Quality of the track, 20 is the nominal one; lower values give a rougher ride");
- finish_edit(rebuild::none);
- int damage = track->iDamageFlag;
- if (ImGui::InputInt(STR_C("Damage flags"), &damage, 0))
- editor_track::damage(*track, damage);
- item_tooltip("0: track in order; any value adds jolts, 128: the vehicles derail on the path");
- finish_edit(rebuild::none);
-
- if (ImGui::InputFloat(STR_C("Width"), &track->fTrackWidth, 0.0f, 0.0f, "%.3f"))
- track->fTrackWidth = std::max(0.01f, track->fTrackWidth);
- item_tooltip("Gauge of the track (1.435 m for the standard gauge), width of the surface for the roads");
- finish_edit(rebuild::parameters);
- ImGui::PopItemWidth();
- }
- if (track->m_visible && ImGui::CollapsingHeader(STR_C("Textures and trackbed")))
- {
- material_handle *materials[] = {&track->m_material1, &track->m_material2, track->SwitchExtension ? &track->SwitchExtension->m_material3 : nullptr};
- char const *pathlabels[] = {"Texture 1 (rails/surface)", "Texture 2 (trackbed/side)", "Trackbed (switch)"};
- char const *switchlabels[] = {"Rails P1-P2", "Rails P3-P4", "Trackbed"};
- auto const &labels{track->eType == tt_Switch ? switchlabels : pathlabels};
- if (false == ImGui::IsAnyItemActive())
- {
- for (int i = 0; i < 3; ++i)
- {
- if (m_track_material_edited[i] != nullptr)
- continue;
- auto const name = materials[i] ? editor_track::material_name(*materials[i]) : std::string{};
- std::strncpy(m_track_materials[i].data(), name.c_str(), m_track_materials[i].size() - 1);
- m_track_materials[i].back() = '\0';
- }
- }
- for (int i = 0; i < 3; ++i)
- {
- if (materials[i] == nullptr || (i == 2 && track->eType != tt_Switch))
- continue;
- ImGui::PushItemWidth(-160.0f);
- if (ImGui::InputText(labels[i], m_track_materials[i].data(), m_track_materials[i].size()))
- m_track_material_edited[i] = track;
- if (ImGui::IsItemDeactivated() && m_track_material_edited[i] != nullptr)
- {
- auto const *edited{m_track_material_edited[i]};
- m_track_material_edited[i] = nullptr;
- if (edited == track && m_track_materials[i].data() != editor_track::material_name(*materials[i]))
- {
- *materials[i] = editor_track::fetch_material(m_track_materials[i].data());
- apply_now(rebuild::parameters);
- }
- }
- ImGui::PopItemWidth();
- }
- ImGui::TextDisabled(STR_C("The texture changes on Enter or leaving the field, \"none\" removes it"));
-
- ImGui::PushItemWidth(120.0f);
- if (ImGui::InputFloat(STR_C("Texture length"), &track->fTexLength, 0.0f, 0.0f, "%.2f"))
- track->fTexLength = std::max(0.01f, track->fTexLength);
- item_tooltip("Length in metres after which the texture of the rails and the trackbed repeats");
- finish_edit(rebuild::parameters);
- float height = editor_track::texture_height(*track);
- if (ImGui::InputFloat(STR_C("Trackbed height"), &height, 0.0f, 0.0f, "%.3f"))
- editor_track::texture_height(*track, height);
- item_tooltip("Height of the trackbed edge below the rail top, in metres");
- finish_edit(rebuild::parameters);
- ImGui::InputFloat(STR_C("Trackbed width"), &track->fTexWidth, 0.0f, 0.0f, "%.3f");
- item_tooltip("Width of the trackbed outside of the rail, in metres");
- finish_edit(rebuild::parameters);
- ImGui::InputFloat(STR_C("Trackbed slope"), &track->fTexSlope, 0.0f, 0.0f, "%.3f");
- item_tooltip("Horizontal reach of the slope at the outer edge of the trackbed, in metres");
- finish_edit(rebuild::parameters);
- ImGui::PopItemWidth();
- }
- render_path_circuits(*track);
-}
-
-void editor_mode::render_path_circuits(TTrack &Track)
-{
- auto *track{&Track};
- auto const copy = [](std::array &Buffer, std::string const &Text) {
- std::strncpy(Buffer.data(), Text.c_str(), Buffer.size() - 1);
- Buffer.back() = '\0';
- };
- auto const isolated_text = [&]() {
- std::string text;
- for (auto const *isolated : track->Isolated)
- text += (text.empty() ? "" : " ") + isolated->asName;
- return text;
- };
- if (m_track_names_for != track && false == ImGui::IsAnyItemActive())
- {
- m_track_names_for = track;
- copy(m_track_isolated, isolated_text());
- copy(m_track_sleeper_model, track->m_sleeper_model_name);
- copy(m_track_sleeper_skin, track->m_sleeper_skin_name.empty() ? std::string{"none"} : track->m_sleeper_skin_name);
+ m_track_events_for = track;
m_track_event[0] = '\0';
m_track_events_missing.clear();
}
- // one undo step for the change made by the widget just used
- auto const changed = [&](editor_track::state const &Before, bool const Geometry) {
- push_track_snapshot({{track, Before}});
- if (Geometry)
- editor_track::commit_parameters(*track);
- else
- track->mark_dirty();
- };
-
- if (false == ImGui::CollapsingHeader(STR_C("Track circuit, overhead line, sleepers, events")))
+ auto const count{[&]() {
+ std::size_t count{0};
+ for (int list = 0; list < 6; ++list)
+ count += editor_track::events(*track, list).size();
+ return count;
+ }()};
+ if (false == ImGui::CollapsingHeader(format("%s (%zu)###trackevents", STR_C("Events"), count).c_str()))
return;
auto const before{editor_track::capture(*track)};
-
- ImGui::PushItemWidth(-160.0f);
- ImGui::InputText(STR_C("Track circuits"), m_track_isolated.data(), m_track_isolated.size());
- item_tooltip("Names of the track circuits (isolated) the path belongs to, separated by spaces.\n"
- "A name which doesn't exist yet makes a new circuit. Changes on leaving the field");
- if (ImGui::IsItemDeactivated() && m_track_isolated.data() != isolated_text())
- {
- if (false == track->Dynamics.empty())
- {
- ui()->set_status(STR_C("The track circuits can't change while vehicles stand on the path"), true);
- copy(m_track_isolated, isolated_text());
- }
- else
- {
- std::istringstream names{m_track_isolated.data()};
- std::vector list;
- for (std::string name; names >> name;)
- list.push_back(name);
- editor_track::isolated(*track, list);
- changed(before, false);
- copy(m_track_isolated, isolated_text());
- }
- }
- ImGui::PopItemWidth();
-
- int overheadmode{track->fOverhead < 0.f ? 0 : track->fOverhead == 0.f ? 1 : 2};
- ImGui::PushItemWidth(220.0f);
- char const *overheadmodes[] = {STR_C("Normal"), STR_C("No current, coasting"), STR_C("Pantographs lowered, speed limit")};
- if (ImGui::Combo(STR_C("Overhead line"), &overheadmode, overheadmodes, IM_ARRAYSIZE(overheadmodes)))
- {
- editor_track::overhead(*track, overheadmode == 0 ? -1.f : overheadmode == 1 ? 0.f : std::max(1.f, track->fOverhead > 0.f ? track->fOverhead : 40.f));
- changed(before, false);
- }
- ImGui::PopItemWidth();
- item_tooltip("State of the overhead line over the path: the trains coast through a section with no current,\nor lower the pantographs and keep to the speed limit given");
- if (track->fOverhead > 0.f)
- {
- ImGui::PushItemWidth(120.0f);
- float limit{track->fOverhead};
- if (ImGui::InputFloat(STR_C("Speed with the pantographs lowered (km/h)"), &limit, 0.0f, 0.0f, "%.0f"))
- editor_track::overhead(*track, std::max(1.f, limit));
- if (ImGui::IsItemActivated())
- m_track_field_before = before;
- if (ImGui::IsItemDeactivatedAfterEdit())
- changed(m_track_field_before, false);
- ImGui::PopItemWidth();
- }
-
- ImGui::Separator();
- bool sleepers{track->m_sleeper_enabled};
- if (ImGui::Checkbox(STR_C("Sleeper models"), &sleepers))
- {
- if (sleepers && m_track_sleeper_model[0] == '\0')
- {
- ui()->set_status(STR_C("Give the sleeper model first"), true);
- }
- else
- {
- track->m_sleeper_enabled = sleepers;
- track->m_sleeper_model_name = m_track_sleeper_model.data();
- changed(before, true);
- }
- }
- item_tooltip("Repeats a model along the path, the sleepers in place of the drawn trackbed");
- ImGui::PushItemWidth(-160.0f);
- auto const name_field = [&](char const *Label, std::array &Buffer, std::string &Value, char const *Tooltip) {
- ImGui::InputText(Label, Buffer.data(), Buffer.size());
- item_tooltip(Tooltip);
- std::string typed{Buffer.data()};
- if (&Value == &track->m_sleeper_skin_name && typed == "none")
- typed.clear();
- if (ImGui::IsItemDeactivated() && typed != Value)
- {
- Value = typed;
- changed(before, track->m_sleeper_enabled);
- }
+ auto const changed = [&](editor_track::state const &Before) {
+ push_track_snapshot({{track, Before}});
+ track->mark_dirty();
};
- name_field(STR_C("Sleeper model"), m_track_sleeper_model, track->m_sleeper_model_name, STR_C("Path of the .e3d model, as in the scenery file"));
- name_field(STR_C("Sleeper skin"), m_track_sleeper_skin, track->m_sleeper_skin_name, STR_C("Replaceable skin of the model, \"none\": the one of the model"));
- ImGui::PopItemWidth();
- ImGui::PushItemWidth(120.0f);
- auto const number_field = [&](char const *Label, float &Value, char const *Format, char const *Tooltip) {
- ImGui::InputFloat(Label, &Value, 0.0f, 0.0f, Format);
- item_tooltip(Tooltip);
- if (ImGui::IsItemActivated())
- m_track_field_before = before;
- if (ImGui::IsItemDeactivatedAfterEdit())
- {
- track->m_sleeper_frequency = std::max(0.1f, track->m_sleeper_frequency);
- changed(m_track_field_before, track->m_sleeper_enabled);
- }
- };
- number_field(STR_C("Sleeper spacing (m)"), track->m_sleeper_frequency, "%.3f", STR_C("Distance between the sleepers along the path"));
- ImGui::PopItemWidth();
- ImGui::PushItemWidth(200.0f);
- ImGui::InputFloat3(STR_C("Sleeper offset (m)"), &track->m_sleeper_offset.x, "%.3f");
- item_tooltip("Shift of each model: sideways, along the path, up");
- if (ImGui::IsItemActivated())
- m_track_field_before = before;
- if (ImGui::IsItemDeactivatedAfterEdit())
- changed(m_track_field_before, track->m_sleeper_enabled);
- ImGui::PopItemWidth();
- ImGui::PushItemWidth(120.0f);
- number_field(STR_C("Trackbed shift (m)"), track->m_sleeper_ballast_z, "%.3f", STR_C("Vertical shift of the drawn trackbed under the sleepers, negative lowers it"));
- ImGui::PopItemWidth();
-
- ImGui::Separator();
- ImGui::TextUnformatted(STR_C("Events"));
+ ImGui::TextDisabled("%s", STR_C("What the track sets off as the vehicles run on it"));
item_tooltip("event0: a vehicle with a crew stands on the path, event1: runs along it towards its start, event2: towards its end.\n"
"eventall0/1/2: the same for any vehicle");
auto const editable_events{track->Dynamics.empty()};
@@ -1436,7 +1248,7 @@ void editor_mode::render_path_circuits(TTrack &Track)
{
events.erase(events.begin() + i);
editor_track::bind_events(*track, m_track_events_missing);
- changed(before, false);
+ changed(before);
ImGui::PopID();
break;
}
@@ -1466,7 +1278,7 @@ void editor_mode::render_path_circuits(TTrack &Track)
{
editor_track::events(*track, m_track_event_list).emplace_back(m_track_event.data(), nullptr);
editor_track::bind_events(*track, m_track_events_missing);
- changed(before, false);
+ changed(before);
m_track_event[0] = '\0';
}
}
@@ -1490,17 +1302,26 @@ void editor_mode::render_path_ui()
return;
}
render_track_name(*track);
+ ImGui::TextDisabled("%s, %.2f m", STR_C(type_name(*track)), track->Length());
std::string reason;
bool const geometry = editor_track::can_edit_geometry(*track, reason);
if (false == geometry)
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), STR_C("Geometry locked: %s"), reason.c_str());
- if (track->eType == tt_Switch)
+ if (track->eType == tt_Switch && m_track_tab == track_tab::path)
{
- ImGui::TextWrapped("%s", STR_C("A switch: the turnout tool sets its geometry, side and drive"));
- if (ImGui::Button(STR_C("Edit the switch (T)")))
+ ImGui::SameLine();
+ if (ImGui::SmallButton(STR_C("Edit the switch (T)")))
show_track_tab(track_tab::turnout);
- item_tooltip("Opens the turnout tool with this switch");
+ item_tooltip("A switch: the turnout tool sets its geometry, side and drive");
}
+ std::vector const tracks{track};
+ render_track_select_tools(tracks);
+ render_track_style(tracks);
+ render_track_properties(tracks);
+ render_path_events(*track);
+ // the shape is given by dragging in the view, the numbers are for the exact values
+ if (m_track_tab != track_tab::path)
+ return;
auto const before = editor_track::capture(*track);
enum class rebuild { none, parameters, geometry };
@@ -1519,7 +1340,7 @@ void editor_mode::render_path_ui()
}
};
- if (ImGui::CollapsingHeader(track->eType == tt_Switch ? STR_C("Geometry of the paths") : STR_C("Geometry"), track->eType == tt_Switch ? 0 : ImGuiTreeNodeFlags_DefaultOpen))
+ if (ImGui::CollapsingHeader(track->eType == tt_Switch ? STR_C("Geometry of the paths") : STR_C("Geometry")))
{
for (int i = 0; i < static_cast(track->m_paths.size()); ++i)
{
@@ -1603,7 +1424,6 @@ void editor_mode::render_path_ui()
ImGui::TreePop();
}
}
- render_path_parameters(*track);
}
@@ -3954,7 +3774,12 @@ std::vector editor_mode::track_key_hints(bool const All)
else if (m_track_tab == track_tab::route)
hints = {{"Drag a vertex", "the curve follows"}, {"Drag the radius grip", "changes the radius"}, {"Apply", "writes the design into the tracks"}, {"Esc", "release the vertex"}};
else if (selected == nullptr)
- hints = {{"LMB on a track", "selects it"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"RMB on a track", "what can be done with it"}, {"L", "lay a new track"}};
+ hints = {{"LMB on a track", "selects it"}, {"Drag on the ground", "selects the tracks in a box"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"RMB on a track", "what can be done with it"}, {"L", "lay a new track"}};
+ else if (m_track_tab == track_tab::path && m_track_set.size() >= 2 && in_track_set(selected))
+ {
+ hints = {{"Inspector", "the properties of all of them at once"}, {"Gizmo", "moves them together"}, {"Shift/Ctrl+LMB", "adds or takes out a track"}, {"Ctrl+Shift+V", "pastes the copied properties"}, {"Esc", "deselects"}, {"Del", "deletes them"}};
+ gizmo = true;
+ }
else if (m_point_drag.moved)
hints = {{"Drag", "moves the point, level"}, {"Over a free end", "the end joins it"}, {"Release", "done"}, {"Esc", "done"}};
else if (m_track_point.valid())
@@ -5524,7 +5349,7 @@ void editor_mode::switch_placed(std::vector const &Created)
if (slip)
{
auto const known{switch_store::named(**found)};
- if (false == known.has_value() || known->kind != "double")
+ if (false == known.has_value() || (known->kind != "double" && known->kind != "slip"))
return;
std::vector driven;
for (auto *track : Created)
@@ -5533,6 +5358,19 @@ void editor_mode::switch_placed(std::vector const &Created)
join_last_steps();
driven.push_back(track->name());
}
+ // a slip has a drive at each of its switches
+ if (known->kind == "slip")
+ {
+ if (false == driven.empty())
+ {
+ std::string names;
+ for (auto const &name : driven)
+ names += (names.empty() ? "" : ", ") + name;
+ m_switch.status = format(STR_C("Slip with drives at its switches %s: the events of the name of a switch with + and - switch it, also in the map"), names.c_str());
+ ui()->set_status(m_switch.status, false);
+ }
+ return;
+ }
if (driven.size() == 2)
{
m_switch.status = format(STR_C("Double switch %s + %s with drives: events %s+, %s- and %s+, %s- switch them, also in the map"), driven[0].c_str(), driven[1].c_str(), driven[0].c_str(), driven[0].c_str(), driven[1].c_str(), driven[1].c_str());
@@ -6472,7 +6310,7 @@ std::vector editor_mode::link_tracks(editor_track::snap_target const &
return pieces;
}
-std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, double const Radius, TTrack &Style, std::string const &Type)
+std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, editor_track::slip_curve const &Curve, TTrack &Style, std::string const &Type)
{
auto const at = [](glm::dvec2 const &Planar, double const Y) { return glm::dvec3{Planar.x, Y, Planar.y}; };
auto const straight = [](glm::dvec3 const &Start, glm::dvec3 const &End) {
@@ -6485,33 +6323,91 @@ std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, g
auto const halfsine{std::sqrt(std::max(1e-6, (1.0 - glm::dot(First, Second)) * 0.5))};
auto const gap{std::max(0.25, 0.06 / halfsine)};
auto const side{cross(First, Second) > 0.0 ? 1.0 : -1.0};
+ auto const whole{Curve.length()};
+ // the part of the slip curve from Start along Heading, turning by Turn to Finish, between the distances From and To along it;
+ // where it changes the radius made of pieces, with one curve from end to end for the versions which know no pieces
auto const arc = [&](glm::dvec3 const &Start, glm::dvec2 const &Heading, double const Turn, glm::dvec3 const &Finish, double const From, double const To) {
- glm::dvec2 const centre{plan_of(Start) + glm::dvec2{-Heading.y, Heading.x} * (Turn * Radius)};
- auto const radial{plan_of(Start) - centre};
- auto const p0{centre + turned(radial, Turn * From)};
- auto const p3{centre + turned(radial, Turn * To)};
- auto const d0{turned(Heading, Turn * From)};
- auto const d3{turned(Heading, Turn * To)};
- auto const handle{4.0 / 3.0 * std::tan((To - From) / 4.0) * Radius};
- auto const y0{Start.y + (Finish.y - Start.y) * From / Angle};
- auto const y3{Start.y + (Finish.y - Start.y) * To / Angle};
- segment_data path;
- path.points[segment_data::point::start] = {p0.x, y0, p0.y};
- path.points[segment_data::point::end] = {p3.x, y3, p3.y};
- path.points[segment_data::point::control1] = {d0.x * handle, 0.0, d0.y * handle};
- path.points[segment_data::point::control2] = {-d3.x * handle, 0.0, -d3.y * handle};
- path.radius = static_cast(Radius);
+ auto const place = [&](double const Along) {
+ auto const local{Curve.point(Along)};
+ auto const planar{plan_of(Start) + Heading * local.x + glm::dvec2{-Heading.y, Heading.x} * (Turn * local.y)};
+ return glm::dvec3{planar.x, Start.y + (Finish.y - Start.y) * Along / whole, planar.y};
+ };
+ auto const bend = [&](double const Begin, double const End, double const Radius, double const Handle0, double const Handle1) {
+ auto const d0{turned(Heading, Turn * Curve.heading(Begin))};
+ auto const d3{turned(Heading, Turn * Curve.heading(End))};
+ segment_data path;
+ path.points[segment_data::point::start] = place(Begin);
+ path.points[segment_data::point::end] = place(End);
+ if (Radius > 0.0)
+ {
+ path.points[segment_data::point::control1] = {d0.x * Handle0, 0.0, d0.y * Handle0};
+ path.points[segment_data::point::control2] = {-d3.x * Handle1, 0.0, -d3.y * Handle1};
+ }
+ path.radius = static_cast(Radius);
+ return path;
+ };
+ std::vector> zones;
+ for (auto const &zone : {std::pair{Curve.lead, 0.0}, {Curve.lead + Curve.ends, Curve.radius}, {Curve.lead + Curve.ends + Curve.between, Curve.middle}, {whole - Curve.lead, Curve.radius}, {whole, 0.0}})
+ {
+ if (zone.first <= (zones.empty() ? 0.0 : zones.back().first) + 1e-9)
+ continue;
+ if (false == zones.empty() && zone.second == zones.back().second)
+ zones.back().first = zone.first;
+ else
+ zones.push_back(zone);
+ }
+ // broken at the tips of the blades of the tables, so the model of the switch finds them there
+ if (Curve.tip > 0.0)
+ for (auto const cut : {Curve.tip, whole - Curve.tip})
+ for (std::size_t i = 0; i < zones.size(); ++i)
+ if (cut > (i > 0 ? zones[i - 1].first : 0.0) + 0.05 && cut < zones[i].first - 0.05)
+ {
+ zones.insert(zones.begin() + i, {cut, zones[i].second});
+ break;
+ }
+ std::vector pieces;
+ auto from{From};
+ for (auto const &[to, radius] : zones)
+ {
+ auto const upto{std::min(To, to)};
+ if (upto <= from + 1e-6)
+ continue;
+ auto const handle{radius > 0.0 ? 4.0 / 3.0 * std::tan((upto - from) / radius / 4.0) * radius : 0.0};
+ pieces.push_back(bend(from, upto, radius, handle, handle));
+ from = upto;
+ }
+ if (pieces.size() == 1)
+ return pieces.front();
+ // the handles of the curve from end to end reach 2/3 of the way to the crossing of the tangents
+ auto const chord{plan_of(place(To) - place(From))};
+ auto const d0{turned(Heading, Turn * Curve.heading(From))};
+ auto const d3{turned(Heading, Turn * Curve.heading(To))};
+ auto const sine{cross(d0, d3)};
+ auto path{bend(From, To, Curve.radius, 2.0 / 3.0 * cross(chord, d3) / sine, 2.0 / 3.0 * cross(d0, chord) / sine)};
+ path.pieces = std::move(pieces);
return path;
};
- auto const middle{Angle * 0.5};
- auto const split{gap / Radius};
+ auto const back = [](segment_data const &Path) {
+ auto result{reversed(Path)};
+ std::reverse(result.pieces.begin(), result.pieces.end());
+ for (auto &piece : result.pieces)
+ piece = reversed(piece);
+ return result;
+ };
+ auto const middle{whole * 0.5};
+ auto const split{gap};
auto const centreA{at(Crossing - First * gap, Height)};
auto const centreB{at(Crossing + First * gap, Height)};
auto const centreC{at(Crossing - Second * gap, Height)};
auto const centreD{at(Crossing + Second * gap, Height)};
std::string base;
auto const prefix{Type == "Skrz" ? "editor_skrz" : Type == "Rkp" ? "editor_dkp" : "editor_dks"};
- for (int i = 1; base.empty() || simulation::Paths.find(base + "_a") != nullptr || simulation::Paths.find(base + "_d") != nullptr; ++i)
+ // a crossing keeps the names of its tracks, only its record holds the name
+ auto const recorded = [](std::string const &Name) {
+ auto const &records{switch_store::records()};
+ return std::any_of(records.begin(), records.end(), [&](switch_store::record const &Record) { return Record.name == Name; });
+ };
+ for (int i = 1; base.empty() || simulation::Paths.find(base + "_a") != nullptr || simulation::Paths.find(base + "_d") != nullptr || recorded(base); ++i)
base = prefix + std::to_string(i);
std::vector parts;
if (Type == "Skrz")
@@ -6530,19 +6426,19 @@ std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, g
if (single)
parts.push_back(editor_track::create_path(Style, straight(PointB, centreB)));
else
- parts.push_back(editor_track::create_switch(shape, {straight(PointB, centreB), reversed(arc(PointC, Second, -side, PointB, middle + split, Angle))}, Style, base + "_b"));
+ parts.push_back(editor_track::create_switch(shape, {straight(PointB, centreB), back(arc(PointC, Second, -side, PointB, middle + split, whole))}, Style, base + "_b"));
if (single)
parts.push_back(editor_track::create_path(Style, straight(PointC, centreC)));
else
parts.push_back(editor_track::create_switch(shape, {straight(PointC, centreC), arc(PointC, Second, -side, PointB, 0.0, middle - split)}, Style, base + "_c"));
- parts.push_back(editor_track::create_switch(shape, {straight(PointD, centreD), reversed(arc(PointA, First, side, PointD, middle + split, Angle))}, Style, base + "_d"));
+ parts.push_back(editor_track::create_switch(shape, {straight(PointD, centreD), back(arc(PointA, First, side, PointD, middle + split, whole))}, Style, base + "_d"));
parts.push_back(editor_track::create_path(Style, straight(centreA, centreB)));
parts.push_back(editor_track::create_path(Style, straight(centreC, centreD)));
parts.push_back(editor_track::create_path(Style, arc(PointA, First, side, PointD, middle - split, middle + split)));
if (false == single)
parts.push_back(editor_track::create_path(Style, arc(PointC, Second, -side, PointB, middle - split, middle + split)));
parts.erase(std::remove(parts.begin(), parts.end(), nullptr), parts.end());
- record_slip(parts, "slip", Type, Radius, Angle, base);
+ record_slip(parts, "slip", Type, Curve.radius, Angle, base, Curve.middle);
return parts;
}
@@ -6615,8 +6511,11 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv
auto const alongfirst{glm::dot(crossing - plan_of(Line.start), Line.direction)};
auto const alongsecond{glm::dot(crossing - plan_of(second.start), second.direction)};
auto const available{std::min({alongfirst, Line.length - alongfirst, alongsecond, second.length - alongsecond}) - 0.5};
- auto radius{Shape.radius};
- auto half{plain ? Shape.a : radius * std::tan(angle * 0.5)};
+ auto const *gauged{Line.tracks.front()};
+ auto const blade{switch_design::secant_blade(Shape.rail, Shape.radius, Shape.ratio)};
+ auto curve{editor_track::slip_of(Shape.radius, Shape.middle, angle, 0.5 * std::abs(gauged->fTrackWidth) + gauged->rail_dimensions().head + 0.002, blade.first, blade.second)};
+ curve.tip = switch_design::cut_tip(Shape.rail, curve.radius);
+ auto half{plain ? Shape.a : curve.tangent()};
if (plain && half > available)
{
half = available;
@@ -6628,14 +6527,14 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv
}
else if (half > available)
{
+ curve = curve.scaled(available / half);
half = available;
- radius = half / std::tan(angle * 0.5);
- if (radius < 50.0)
+ if (curve.radius < 50.0)
{
state.status = STR_C("Not enough straight track around the crossing");
return false;
}
- state.status = STR_C("R reduced to ") + std::to_string(static_cast(radius)) + STR_C(" m, the straights end ") + std::to_string(static_cast(available)) + STR_C(" m from the crossing");
+ state.status = STR_C("R reduced to ") + std::to_string(static_cast(curve.radius)) + STR_C(" m, the straights end ") + std::to_string(static_cast(available)) + STR_C(" m from the crossing");
}
auto const heightfirst = [&](double const Along) { return Line.start.y + Line.grade * Along; };
@@ -6674,8 +6573,8 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv
auto &styled{style != nullptr ? *style : *Line.tracks.front()};
// a single slip joins the pair of the tracks on the side of the click
auto const nearer{glm::distance((plan_of(pointA) + plan_of(pointD)) * 0.5, plan_of(Point)) <= glm::distance((plan_of(pointC) + plan_of(pointB)) * 0.5, plan_of(Point))};
- auto const parts{Shape.type == "Rkp" && false == nearer ? build_double_slip(pointC, pointD, pointA, pointB, crossing, heightfirst(alongfirst), direction, first, angle, radius, styled, Shape.type)
- : build_double_slip(pointA, pointB, pointC, pointD, crossing, heightfirst(alongfirst), first, direction, angle, radius, styled, Shape.type)};
+ auto const parts{Shape.type == "Rkp" && false == nearer ? build_double_slip(pointC, pointD, pointA, pointB, crossing, heightfirst(alongfirst), direction, first, angle, curve, styled, Shape.type)
+ : build_double_slip(pointA, pointB, pointC, pointD, crossing, heightfirst(alongfirst), first, direction, angle, curve, styled, Shape.type)};
editor_track::commit(parts);
created.insert(created.end(), parts.begin(), parts.end());
push_track_snapshot(std::move(states), std::move(created), std::move(removed));
@@ -6752,8 +6651,12 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat
glm::dvec2 const crossing{plan_of(a.point) + first * t};
auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))};
auto const oldhalf{glm::distance(plan_of(a.point), crossing)};
- auto radius{Shape.radius};
- auto half{radius * std::tan(angle * 0.5)};
+ auto const blade{switch_design::secant_blade(Shape.rail, Shape.radius, Shape.ratio)};
+ auto curve{editor_track::slip_of(Shape.radius, Shape.middle, angle, 0.5 * std::abs(switches[0]->fTrackWidth) + switches[0]->rail_dimensions().head + 0.002, blade.first, blade.second)};
+ curve.tip = switch_design::cut_tip(Shape.rail, curve.radius);
+ auto const full{curve.tangent()};
+ auto radius{curve.radius};
+ auto half{full};
auto const height{(a.point.y + b.point.y) * 0.5};
std::vector> states;
@@ -6766,7 +6669,7 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat
if (e->outer == nullptr || false == editor_track::is_straight(*e->outer, m_straights.tolerance))
{
half = oldhalf;
- radius = half / std::tan(angle * 0.5);
+ radius = curve.radius * half / full;
state.status = STR_C("R limited to ") + std::to_string(static_cast(radius)) + STR_C(" m, a neighbouring path isn't straight");
break;
}
@@ -6776,7 +6679,7 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat
if (std::max(reach, reachstart) - 0.5 < half)
{
half = std::min(half, std::max(reach, reachstart) - 0.5);
- radius = half / std::tan(angle * 0.5);
+ radius = curve.radius * half / full;
state.status = STR_C("R reduced to ") + std::to_string(static_cast(radius)) + STR_C(" m to fit the straights");
}
}
@@ -6811,7 +6714,7 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat
cut_straight(line, std::min(alongold, alongnew), std::max(alongold, alongnew), states, created, removed, nullptr);
}
}
- auto const parts2{build_double_slip(points[0], points[1], points[2], points[3], crossing, height, first, direction, angle, radius, *style)};
+ auto const parts2{build_double_slip(points[0], points[1], points[2], points[3], crossing, height, first, direction, angle, curve.scaled(half / full), *style)};
std::vector commitlist(parts2.begin(), parts2.end());
for (auto *track : created)
if (std::find(commitlist.begin(), commitlist.end(), track) == commitlist.end())
@@ -6820,6 +6723,16 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat
created.insert(created.end(), parts2.begin(), parts2.end());
push_track_snapshot(std::move(states), std::move(created), std::move(removed));
straight_refresh();
+ // the drives of the switches replaced go with them, the new ones get theirs
+ std::vector drives;
+ for (auto const *track : switches)
+ if (auto const drive{find_switch_drive(*track)})
+ drives.push_back(drive->standing.instance);
+ if (false == drives.empty() && remove_templates(drives) > 0)
+ join_last_steps();
+ for (auto *track : parts2)
+ if (track != nullptr && track->eType == tt_Switch && place_switch_drive(*track))
+ join_last_steps();
return true;
}
diff --git a/application/editormodetrackprops.cpp b/application/editormodetrackprops.cpp
new file mode 100644
index 00000000..4317dbda
--- /dev/null
+++ b/application/editormodetrackprops.cpp
@@ -0,0 +1,1571 @@
+/*
+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/.
+*/
+
+// properties of the selected tracks in the inspector: one sheet for one track and for many, a value shared by all of
+// them shown as it is and values which differ as "different", a value typed or chosen going to all of them in one
+// step of the undo; the properties copied from one track and pasted on others; the tracks like the selected ones
+// selected in the whole scenery
+
+#include "stdafx.h"
+#include "application/editormode.h"
+#include "application/editoruilayer.h"
+#include "application/editormodeshared.h"
+#include "editor/editorFormat.hpp"
+#include "editor/editorGeometry.hpp"
+
+#include "rendering/renderer.h"
+#include "world/Track.h"
+#include "world/TrackSleepers.h"
+#include "scene/scenelayers.h"
+#include "simulation/simulation.h"
+#include "utilities/Logs.h"
+#include "utilities/translation.h"
+#include "utilities/utilities.h"
+
+#include "imgui/imgui.h"
+#include "imgui/imgui_internal.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace
+{
+
+using geometry::bezier;
+
+int const kRailCategory{1};
+std::size_t const kListedValues{8}; // in the tooltip of a property whose values differ
+std::size_t const kLargeSelection{300}; // above it the values are gathered again only every few frames
+
+bool rail(TTrack const &Track)
+{
+ return (Track.iCategoryFlag & 15) == kRailCategory && false == Track.m_editorremoved && Track.visible();
+}
+
+std::string name_of(TTrack const &Track)
+{
+ return Track.name().empty() ? std::string{"(noname)"} : Track.name();
+}
+
+char const *type_of(TTrack const &Track)
+{
+ switch (Track.eType)
+ {
+ case tt_Normal: return "normal";
+ case tt_Switch: return "switch";
+ case tt_Cross: return "cross";
+ case tt_Table: return "turntable";
+ case tt_Tributary: return "tributary";
+ default: return "unknown";
+ }
+}
+
+// a number as short as it can be written: no trailing zeros
+std::string decimal(double const Value, int const Digits)
+{
+ auto text{format("%.*f", Digits, Value)};
+ if (text.find('.') != std::string::npos)
+ {
+ while (text.back() == '0')
+ text.pop_back();
+ if (text.back() == '.')
+ text.pop_back();
+ }
+ return text == "-0" ? std::string{"0"} : text;
+}
+
+bool parse_number(std::string Text, double &Value)
+{
+ std::replace(Text.begin(), Text.end(), ',', '.');
+ auto const *start{Text.c_str()};
+ char *end{nullptr};
+ Value = std::strtod(start, &end);
+ if (end == start)
+ return false;
+ while (*end == ' ')
+ ++end;
+ return *end == '\0' && std::isfinite(Value);
+}
+
+std::vector words_of(std::string const &Text)
+{
+ std::istringstream stream{Text};
+ std::vector words;
+ for (std::string word; stream >> word;)
+ words.push_back(word);
+ return words;
+}
+
+material_handle bed_of(TTrack const &Track)
+{
+ if (Track.eType == tt_Switch)
+ return Track.SwitchExtension ? Track.SwitchExtension->m_material3 : null_handle;
+ return Track.m_material2;
+}
+
+// image of the first texture of a material, 0 while there's none; reading a texture takes a moment, a few of them go in a frame
+std::uint64_t material_image(material_handle const Material)
+{
+ static int frame{-1};
+ static int loads{0};
+ if (Material == null_handle)
+ return 0;
+ auto const *data{GfxRenderer->Material(Material)};
+ if (data == nullptr)
+ return 0;
+ auto const texture{data->GetTexture(0)};
+ if (texture == null_handle)
+ return 0;
+ auto &image{GfxRenderer->Texture(texture)};
+ if (false == image.get_is_ready())
+ {
+ if (frame != ImGui::GetFrameCount())
+ {
+ frame = ImGui::GetFrameCount();
+ loads = 0;
+ }
+ if (loads >= 3)
+ return 0;
+ ++loads;
+ image.create();
+ }
+ return image.get_is_ready() ? static_cast(image.get_id()) : 0;
+}
+
+std::string lowered(std::string Text)
+{
+ std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); });
+ return Text;
+}
+
+bool texture_extension(std::string const &Extension)
+{
+ return Extension == ".dds" || Extension == ".tga" || Extension == ".png" || Extension == ".bmp" || Extension == ".jpg" || Extension == ".ktx" || Extension == ".tex";
+}
+
+// a name in two lines under a tile: as much as fits, then the rest; the end, where the names differ, is kept when the rest is too long
+std::pair two_lines(std::string const &Text, float const Width)
+{
+ if (ImGui::CalcTextSize(Text.c_str()).x <= Width)
+ return {Text, {}};
+ std::size_t split{0};
+ while (split < Text.size() && ImGui::CalcTextSize(Text.c_str(), Text.c_str() + split + 1).x <= Width)
+ ++split;
+ while (split > 0 && (static_cast(Text[split]) & 0xC0) == 0x80)
+ --split;
+ auto rest{Text.substr(split)};
+ if (ImGui::CalcTextSize(rest.c_str()).x > Width)
+ {
+ while (rest.size() > 1 && ImGui::CalcTextSize(("..." + rest).c_str()).x > Width)
+ {
+ rest.erase(0, 1);
+ while (false == rest.empty() && (static_cast(rest.front()) & 0xC0) == 0x80)
+ rest.erase(0, 1);
+ }
+ rest = "..." + rest;
+ }
+ return {Text.substr(0, split), rest};
+}
+
+// files of the folder of the textures shown in the picker at most, the rest wait for more of the name typed
+std::size_t const kPickerFiles{120};
+
+// the width of each of Count buttons in a row across the panel
+float button_width(int const Count)
+{
+ auto const spacing{ImGui::GetStyle().ItemSpacing.x};
+ return std::max(1.0f, (ImGui::GetContentRegionAvail().x - spacing * static_cast(Count - 1)) / static_cast(Count));
+}
+
+enum class field
+{
+ number,
+ text,
+ choice,
+ check,
+ texture
+};
+
+// parts of the sheet, in their order
+enum section : int
+{
+ traffic,
+ look,
+ look_bed,
+ look_sleepers,
+ ride
+};
+
+// what can be pasted, each by itself
+enum group : int
+{
+ speed,
+ surroundings,
+ overhead_line,
+ circuits,
+ rails,
+ trackbed,
+ trackbed_shape,
+ sleepers,
+ ride_sound,
+ frogs,
+ group_count
+};
+
+char const *const kGroupLabels[group_count] = {"Speed", "Environment", "Overhead line", "Track circuits", "Rails", "Trackbed", "Shape of the trackbed", "Sleepers", "Ride and sound", "Frog and check rails"};
+
+struct track_property
+{
+ char const *key{""}; // names it in the self-tests and in the copied properties
+ field kind{field::number};
+ int part{section::traffic};
+ int group{group::speed};
+ bool rebuild{false}; // the geometry of the track is made again after a change
+ bool late{false}; // pasted after the others, it depends on them
+ int texture{-1}; // which list of the textures on the tracks: 0 rails, 1 trackbed
+ char const *label{""};
+ char const *tooltip{""};
+ char const *empty{nullptr}; // hint of an empty field
+ std::vector choices;
+ std::function applies;
+ std::function get;
+ // false: the text doesn't fit the track or changes nothing, the track stays as it was
+ std::function set;
+};
+
+track_property &add(std::vector &List, char const *Key, field const Kind, int const Part, int const Group, char const *Label, char const *Tooltip)
+{
+ auto &property{List.emplace_back()};
+ property.key = Key;
+ property.kind = Kind;
+ property.part = Part;
+ property.group = Group;
+ property.label = Label;
+ property.tooltip = Tooltip;
+ property.applies = [](TTrack const &Track) { return Track.m_visible; };
+ return property;
+}
+
+// a number kept in a member of the track, Least the lowest value it takes
+void add_float(std::vector &List, char const *Key, int const Part, int const Group, char const *Label, char const *Tooltip, float TTrack::*Member, int const Digits, float const Least, bool const Rebuild)
+{
+ auto &property{add(List, Key, field::number, Part, Group, Label, Tooltip)};
+ property.rebuild = Rebuild;
+ property.get = [Member, Digits](TTrack const &Track) { return decimal(Track.*Member, Digits); };
+ property.set = [Member, Digits, Least](TTrack &Track, std::string const &Text) {
+ double value;
+ if (false == parse_number(Text, value))
+ return false;
+ auto const clamped{std::max(Least, static_cast(value))};
+ if (decimal(clamped, Digits) == decimal(Track.*Member, Digits))
+ return false;
+ Track.*Member = clamped;
+ return true;
+ };
+}
+
+std::vector make_properties()
+{
+ std::vector list;
+ auto const any = [](TTrack const &) { return true; };
+ float const lowest{std::numeric_limits::lowest()};
+
+ // traffic
+ {
+ auto &property{add(list, "speed", field::number, section::traffic, group::speed, "Speed (km/h)", "Speed limit of the track for the AI drivers and the timetable.\nEmpty or -1: no limit of its own")};
+ property.applies = any;
+ property.empty = "no limit";
+ property.get = [](TTrack const &Track) {
+ auto const velocity{editor_track::velocity(Track)};
+ return velocity < 0.0 ? std::string{} : decimal(velocity, 1);
+ };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ double velocity{-1.0};
+ if (false == words_of(Text).empty() && false == parse_number(Text, velocity))
+ return false;
+ if (velocity < 0.0)
+ velocity = -1.0;
+ if (std::abs(editor_track::velocity(Track) - velocity) < 1e-6)
+ return false;
+ editor_track::velocity(Track, velocity);
+ return true;
+ };
+ }
+ {
+ auto &property{add(list, "environment", field::choice, section::traffic, group::surroundings, "Environment", "Surroundings of the track, they affect the sound of the passing vehicles")};
+ property.applies = any;
+ property.choices = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"};
+ property.get = [](TTrack const &Track) { return std::to_string(std::clamp(static_cast(Track.eEnvironment), 0, 5)); };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ double index;
+ if (false == parse_number(Text, index) || index < 0.0 || index > 5.0 || static_cast(index) == static_cast(Track.eEnvironment))
+ return false;
+ Track.eEnvironment = static_cast(static_cast(index));
+ return true;
+ };
+ }
+ {
+ auto &property{add(list, "overhead", field::choice, section::traffic, group::overhead_line, "Overhead line", "State of the overhead line over the track: the trains coast through a section with no current,\nor lower the pantographs and keep to the speed limit given")};
+ property.applies = any;
+ property.choices = {"Normal", "No current, coasting", "Pantographs lowered, speed limit"};
+ property.get = [](TTrack const &Track) { return std::string{Track.fOverhead < 0.f ? "0" : Track.fOverhead == 0.f ? "1" : "2"}; };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ double index;
+ if (false == parse_number(Text, index) || index < 0.0 || index > 2.0)
+ return false;
+ auto const mode{static_cast(index)};
+ auto const overhead{mode == 0 ? -1.f : mode == 1 ? 0.f : Track.fOverhead > 0.f ? Track.fOverhead : 40.f};
+ if (overhead == Track.fOverhead)
+ return false;
+ editor_track::overhead(Track, overhead);
+ return true;
+ };
+ }
+ {
+ auto &property{add(list, "overheadspeed", field::number, section::traffic, group::overhead_line, "Speed with the pantographs lowered (km/h)", "Speed limit of the trains with the pantographs lowered")};
+ property.applies = [](TTrack const &Track) { return Track.fOverhead > 0.f; };
+ property.get = [](TTrack const &Track) { return decimal(Track.fOverhead, 0); };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ double limit;
+ if (false == parse_number(Text, limit))
+ return false;
+ auto const overhead{std::max(1.f, static_cast(limit))};
+ if (overhead == Track.fOverhead)
+ return false;
+ editor_track::overhead(Track, overhead);
+ return true;
+ };
+ }
+ {
+ auto &property{add(list, "circuits", field::text, section::traffic, group::circuits, "Track circuits", "Names of the track circuits (isolated) the track belongs to, separated by spaces.\nA name which doesn't exist yet makes a new circuit. Tracks with vehicles on them keep theirs")};
+ property.applies = any;
+ property.empty = "no circuits";
+ property.get = [](TTrack const &Track) {
+ std::string text;
+ for (auto const *isolated : Track.Isolated)
+ text += (text.empty() ? "" : " ") + isolated->asName;
+ return text;
+ };
+ property.set = [get = property.get](TTrack &Track, std::string const &Text) {
+ auto const names{words_of(Text)};
+ std::string joined;
+ for (auto const &name : names)
+ joined += (joined.empty() ? "" : " ") + name;
+ if (false == Track.Dynamics.empty() || joined == get(Track))
+ return false;
+ editor_track::isolated(Track, names);
+ return true;
+ };
+ }
+
+ // look
+ {
+ auto &property{add(list, "rails", field::texture, section::look, group::rails, "Rails", "Texture of the rails; of both paths of a switch.\nA material dragged from the texture browser can be dropped here")};
+ property.rebuild = true;
+ property.texture = 0;
+ property.empty = "No texture";
+ property.get = [](TTrack const &Track) { return editor_track::material_name(Track.m_material1); };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ auto const material{editor_track::fetch_material(Text)};
+ if (material == Track.m_material1 && (Track.eType != tt_Switch || material == Track.m_material2))
+ return false;
+ Track.m_material1 = material;
+ if (Track.eType == tt_Switch)
+ Track.m_material2 = material;
+ return true;
+ };
+ }
+ {
+ auto &property{add(list, "trackbed", field::texture, section::look, group::trackbed, "Trackbed", "Texture of the trackbed (ballast) under the rails.\nA material dragged from the texture browser can be dropped here")};
+ property.rebuild = true;
+ property.texture = 1;
+ property.empty = "No texture";
+ property.applies = [](TTrack const &Track) { return Track.m_visible && (Track.eType != tt_Switch || Track.SwitchExtension != nullptr); };
+ property.get = [](TTrack const &Track) { return editor_track::material_name(bed_of(Track)); };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ auto const material{editor_track::fetch_material(Text)};
+ if (material == bed_of(Track))
+ return false;
+ if (Track.eType == tt_Switch)
+ Track.SwitchExtension->m_material3 = material;
+ else
+ Track.m_material2 = material;
+ return true;
+ };
+ }
+ {
+ auto &property{add(list, "frogs", field::check, section::look, group::frogs, "Frog and check rails made from the rails", "Cuts the rails of the switch at its frog and lays wing rails and check rails, for rail profiles S60, UIC60 and S39.\nOff for a switch whose include has its own model of them")};
+ property.rebuild = true;
+ property.applies = [](TTrack const &Track) { return Track.eType == tt_Switch && Track.SwitchExtension != nullptr; };
+ property.get = [](TTrack const &Track) { return std::string{Track.SwitchExtension->m_frogs ? "1" : "0"}; };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ auto const frogs{Text == "1"};
+ if (frogs == Track.SwitchExtension->m_frogs)
+ return false;
+ Track.SwitchExtension->m_frogs = frogs;
+ return true;
+ };
+ }
+ {
+ auto &property{add(list, "movablenose", field::check, section::look, group::frogs, "Movable nose", "The nose of the frog moves with the blades over to the wing rail of the track set, leaving no gap; the frog has no check rails")};
+ property.rebuild = true;
+ property.applies = [](TTrack const &Track) { return Track.eType == tt_Switch && Track.SwitchExtension != nullptr; };
+ property.get = [](TTrack const &Track) { return std::string{Track.SwitchExtension->m_movablenose ? "1" : "0"}; };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ auto const movable{Text == "1"};
+ if (movable == Track.SwitchExtension->m_movablenose)
+ return false;
+ Track.SwitchExtension->m_movablenose = movable;
+ return true;
+ };
+ }
+ {
+ auto const blades = [&](char const *Key, char const *Label, char const *Tooltip, double TSwitchExtension::*Member) {
+ auto &property{add(list, Key, field::number, section::look, group::frogs, Label, Tooltip)};
+ property.rebuild = true;
+ property.applies = [](TTrack const &Track) { return Track.eType == tt_Switch && Track.SwitchExtension != nullptr; };
+ property.get = [Member](TTrack const &Track) { return decimal(( *Track.SwitchExtension ).*Member, 3); };
+ property.set = [Member](TTrack &Track, std::string const &Text) {
+ double value;
+ if (false == parse_number(Text, value))
+ return false;
+ value = std::max(0.0, value);
+ if (decimal(value, 3) == decimal(( *Track.SwitchExtension ).*Member, 3))
+ return false;
+ ( *Track.SwitchExtension ).*Member = value;
+ return true;
+ };
+ };
+ blades("bladeheel", "Blade heel (m)", "Where the moving blades end, from the start of the switch, as the template of the switch gives it.\n0: as long as the flangeway of 58 mm behind them needs", &TSwitchExtension::m_bladeheel);
+ blades("bladethrow", "Blade throw (m)", "How far the tips of the blades move over between the two paths.\n0: the usual 0.160 m", &TSwitchExtension::m_bladethrow);
+ }
+ add_float(list, "texlength", section::look_bed, group::trackbed_shape, "Texture length (m)", "Length after which the texture of the rails and the trackbed repeats", &TTrack::fTexLength, 2, 0.01f, true);
+ {
+ auto &property{add(list, "bedheight", field::number, section::look_bed, group::trackbed_shape, "Trackbed height (m)", "Height of the trackbed edge below the rail top")};
+ property.rebuild = true;
+ property.get = [](TTrack const &Track) { return decimal(editor_track::texture_height(Track), 3); };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ double height;
+ if (false == parse_number(Text, height) || decimal(height, 3) == decimal(editor_track::texture_height(Track), 3))
+ return false;
+ editor_track::texture_height(Track, static_cast(height));
+ return true;
+ };
+ }
+ add_float(list, "bedwidth", section::look_bed, group::trackbed_shape, "Trackbed width (m)", "Width of the trackbed outside of the rail", &TTrack::fTexWidth, 3, lowest, true);
+ add_float(list, "bedslope", section::look_bed, group::trackbed_shape, "Trackbed slope (m)", "Horizontal reach of the slope at the outer edge of the trackbed", &TTrack::fTexSlope, 3, lowest, true);
+ {
+ auto &property{add(list, "sleepermodel", field::text, section::look_sleepers, group::sleepers, "Sleeper model", "Path of the .e3d model, as in the scenery file")};
+ property.rebuild = true;
+ property.empty = "no model";
+ property.get = [](TTrack const &Track) { return Track.m_sleeper_model_name; };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ auto const words{words_of(Text)};
+ auto const model{words.empty() ? std::string{} : words.front()};
+ if (model == Track.m_sleeper_model_name)
+ return false;
+ Track.m_sleeper_model_name = model;
+ if (model.empty())
+ Track.m_sleeper_enabled = false;
+ return true;
+ };
+ }
+ {
+ auto &property{add(list, "sleepers", field::check, section::look_sleepers, group::sleepers, "Sleeper models", "Repeats the model along the track, the sleepers in place of the drawn trackbed.\nOnly on the tracks with a sleeper model")};
+ property.rebuild = true;
+ property.late = true;
+ property.get = [](TTrack const &Track) { return std::string{Track.m_sleeper_enabled ? "1" : "0"}; };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ auto const enabled{Text == "1"};
+ if (enabled == Track.m_sleeper_enabled || (enabled && Track.m_sleeper_model_name.empty()))
+ return false;
+ Track.m_sleeper_enabled = enabled;
+ return true;
+ };
+ }
+ {
+ auto &property{add(list, "sleeperskin", field::text, section::look_sleepers, group::sleepers, "Sleeper skin", "Replaceable skin of the model, empty: the one of the model")};
+ property.rebuild = true;
+ property.empty = "of the model";
+ property.get = [](TTrack const &Track) { return Track.m_sleeper_skin_name; };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ auto const words{words_of(Text)};
+ auto skin{words.empty() ? std::string{} : words.front()};
+ if (skin == "none")
+ skin.clear();
+ if (skin == Track.m_sleeper_skin_name)
+ return false;
+ Track.m_sleeper_skin_name = skin;
+ return true;
+ };
+ }
+ {
+ // the models of sleepers and timbers of the scenery data, chosen by the name; the first choice leaves the one of eu07.ini
+ static std::vector sleepermodels;
+ static std::vector timbermodels;
+ if (sleepermodels.empty())
+ {
+ for (auto const &model : track_sleepers::pack_models())
+ (model.timber ? timbermodels : sleepermodels).push_back(model.name);
+ }
+ auto const choices = [](std::vector const &Models) {
+ std::vector result{"(as in eu07.ini)"};
+ for (auto const &model : Models)
+ result.push_back(model.c_str());
+ return result;
+ };
+ auto const index_of = [](std::vector const &Models, std::string const &Name) {
+ auto const found{std::find(Models.begin(), Models.end(), Name)};
+ return found == Models.end() ? 0 : static_cast(found - Models.begin()) + 1;
+ };
+ {
+ auto &property{add(list, "sleeperpack", field::choice, section::look_sleepers, group::sleepers, "Sleeper model from the pack", "Model of the sleepers of the track running alone, of the models of sleepers in the scenery data, laid as 3D sleepers\nin place of the one set with sleepers3dplain in eu07.ini. (as in eu07.ini): the one set there")};
+ property.rebuild = true;
+ property.choices = choices(sleepermodels);
+ property.get = [index_of](TTrack const &Track) { return std::to_string(Track.m_sleeper_enabled ? index_of(sleepermodels, Track.m_sleeper_model_name) : 0); };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ double index;
+ if (false == parse_number(Text, index) || index < 0.0 || index > static_cast(sleepermodels.size()))
+ return false;
+ auto const model{index < 1.0 ? std::string{} : sleepermodels[static_cast(index) - 1]};
+ if (model == (Track.m_sleeper_enabled ? Track.m_sleeper_model_name : std::string{}))
+ return false;
+ Track.m_sleeper_model_name = model;
+ Track.m_sleeper_enabled = (false == model.empty());
+ return true;
+ };
+ }
+ {
+ auto &property{add(list, "timbermodel", field::choice, section::look_sleepers, group::sleepers, "Timber model", "Model of the timbers under the switch or the crossing the track is part of, of the models of timbers in the scenery data,\nin place of the one set with sleepers3d in eu07.ini. It's put together from its submodels begin, main and end to the length of each timber")};
+ property.rebuild = true;
+ property.choices = choices(timbermodels);
+ property.get = [index_of](TTrack const &Track) { return std::to_string(index_of(timbermodels, Track.m_timber_model_name)); };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ double index;
+ if (false == parse_number(Text, index) || index < 0.0 || index > static_cast(timbermodels.size()))
+ return false;
+ auto const model{index < 1.0 ? std::string{} : timbermodels[static_cast(index) - 1]};
+ if (model == Track.m_timber_model_name)
+ return false;
+ Track.m_timber_model_name = model;
+ return true;
+ };
+ }
+ {
+ auto &property{add(list, "timberskin", field::text, section::look_sleepers, group::sleepers, "Timber skin", "Replaceable skin of the timber model, empty: the one of the model")};
+ property.rebuild = true;
+ property.empty = "of the model";
+ property.get = [](TTrack const &Track) { return Track.m_timber_skin_name; };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ auto const words{words_of(Text)};
+ auto skin{words.empty() ? std::string{} : words.front()};
+ if (skin == "none")
+ skin.clear();
+ if (skin == Track.m_timber_skin_name)
+ return false;
+ Track.m_timber_skin_name = skin;
+ return true;
+ };
+ }
+ }
+ add_float(list, "sleeperspacing", section::look_sleepers, group::sleepers, "Sleeper spacing (m)", "Distance between the sleepers along the track", &TTrack::m_sleeper_frequency, 3, 0.1f, true);
+ char const *const offsets[][3] = {{"sleeperside", "Sleeper shift sideways (m)", "Shift of each model sideways, to the right of the track"},
+ {"sleeperalong", "Sleeper shift along (m)", "Shift of each model along the track"},
+ {"sleeperup", "Sleeper shift up (m)", "Shift of each model up"}};
+ for (int axis = 0; axis < 3; ++axis)
+ {
+ auto &property{add(list, offsets[axis][0], field::number, section::look_sleepers, group::sleepers, offsets[axis][1], offsets[axis][2])};
+ property.rebuild = true;
+ property.get = [axis](TTrack const &Track) { return decimal(Track.m_sleeper_offset[axis], 3); };
+ property.set = [axis](TTrack &Track, std::string const &Text) {
+ double offset;
+ if (false == parse_number(Text, offset) || decimal(offset, 3) == decimal(Track.m_sleeper_offset[axis], 3))
+ return false;
+ Track.m_sleeper_offset[axis] = static_cast(offset);
+ return true;
+ };
+ }
+ add_float(list, "bedshift", section::look_sleepers, group::sleepers, "Trackbed shift (m)", "Vertical shift of the drawn trackbed under the sleepers, negative lowers it", &TTrack::m_sleeper_ballast_z, 3, lowest, true);
+
+ // ride and sound
+ add_float(list, "friction", section::ride, group::ride_sound, "Friction", "Friction coefficient of the rails, 0.15 for the dry rails; lower values make the wheels slip", &TTrack::fFriction, 3, 0.f, false);
+ list.back().applies = any;
+ add_float(list, "sounddistance", section::ride, group::ride_sound, "Sound distance (m)", "Distance between the rail joints, sets the rhythm of the wheel clatter; -1: the default", &TTrack::fSoundDistance, 1, -1.f, false);
+ list.back().applies = any;
+ {
+ auto &property{add(list, "quality", field::number, section::ride, group::ride_sound, "Quality", "Quality of the track, 20 is the nominal one; lower values give a rougher ride")};
+ property.applies = any;
+ property.get = [](TTrack const &Track) { return std::to_string(Track.iQualityFlag); };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ double quality;
+ if (false == parse_number(Text, quality) || static_cast(quality) == Track.iQualityFlag)
+ return false;
+ Track.iQualityFlag = static_cast(quality);
+ return true;
+ };
+ }
+ {
+ auto &property{add(list, "damage", field::number, section::ride, group::ride_sound, "Damage flags", "0: track in order; any value adds jolts, 128: the vehicles derail on the track")};
+ property.applies = any;
+ property.get = [](TTrack const &Track) { return std::to_string(Track.iDamageFlag); };
+ property.set = [](TTrack &Track, std::string const &Text) {
+ double damage;
+ if (false == parse_number(Text, damage) || static_cast(damage) == Track.iDamageFlag)
+ return false;
+ editor_track::damage(Track, static_cast(damage));
+ return true;
+ };
+ }
+ add_float(list, "gauge", section::ride, group::ride_sound, "Gauge (m)", "Gauge of the track, 1.435 m for the standard gauge", &TTrack::fTrackWidth, 3, 0.01f, true);
+ list.back().applies = any;
+ return list;
+}
+
+std::vector const &properties()
+{
+ static std::vector const list{make_properties()};
+ return list;
+}
+
+using value_counts = std::vector>;
+
+// values of the property over the tracks it applies to, each with the number of the tracks having it, the most common first
+value_counts values_of(track_property const &Property, std::vector const &Tracks)
+{
+ std::map counted;
+ for (auto const *track : Tracks)
+ if (track != nullptr && false == track->m_editorremoved && Property.applies(*track))
+ ++counted[Property.get(*track)];
+ value_counts result(counted.begin(), counted.end());
+ std::stable_sort(result.begin(), result.end(), [](auto const &Left, auto const &Right) { return Left.second > Right.second; });
+ return result;
+}
+
+// a value as the user reads it
+std::string shown_value(track_property const &Property, std::string const &Value)
+{
+ switch (Property.kind)
+ {
+ case field::choice:
+ {
+ auto const index{std::atoi(Value.c_str())};
+ return index >= 0 && index < static_cast(Property.choices.size()) ? std::string{STR_C(Property.choices[index])} : Value;
+ }
+ case field::check: return Value == "1" ? STR_C("yes") : STR_C("no");
+ default: return Value.empty() || Value == "none" ? std::string{STR_C(Property.empty != nullptr ? Property.empty : "(none)")} : Value;
+ }
+}
+
+std::string tracks_count(std::size_t const Count)
+{
+ return format(STR_P("%d track", "%d tracks", static_cast(Count)), static_cast(Count));
+}
+
+// the tooltip of a row: what the property is, and the values of the tracks when they differ
+void row_tooltip(track_property const &Property, value_counts const &Values, std::size_t const Applies, std::size_t const Selected)
+{
+ // not over a field being typed into, it would cover the rows under it
+ if (false == ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort) || ImGui::IsItemActive())
+ return;
+ ImGui::BeginTooltip();
+ ImGui::PushTextWrapPos(ImGui::GetFontSize() * 30.0f);
+ ImGui::TextUnformatted(STR_C(Property.tooltip));
+ if (Applies < Selected)
+ ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("Applies to %d of the %d selected tracks, the others don't have it"), static_cast(Applies), static_cast(Selected));
+ if (Values.size() > 1)
+ {
+ ImGui::Separator();
+ ImGui::TextUnformatted(STR_C("The tracks differ:"));
+ for (std::size_t i = 0; i < Values.size() && i < kListedValues; ++i)
+ ImGui::BulletText("%s — %s", shown_value(Property, Values[i].first).c_str(), tracks_count(static_cast(Values[i].second)).c_str());
+ if (Values.size() > kListedValues)
+ ImGui::BulletText(STR_C("and %d more values"), static_cast(Values.size() - kListedValues));
+ ImGui::TextDisabled("%s", STR_C("A value given here goes to all of them"));
+ }
+ ImGui::PopTextWrapPos();
+ ImGui::EndTooltip();
+}
+
+} // namespace
+
+std::vector editor_mode::selected_tracks()
+{
+ track_set_prune();
+ if (m_track_set.size() >= 2)
+ return m_track_set;
+ if (auto *track{selected_track()}; track != nullptr && false == track->m_editorremoved)
+ return {track};
+ return {};
+}
+
+void editor_mode::select_tracks(std::vector