17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-29 23:17:30 +01:00

feat(track): 3D sleepers and switch timbers, blade heel and throw from templates, one rail carving pass

- sleepers3d / sleepers3dplain / sleepers3dbed in eu07.ini: timbers put together from the begin, main and end parts of a model
  and stretched to their length, plain sleepers of their own model; tracks whose plain sleepers would touch are laid as one
  cluster, lines of timbers led by priority and turned to the bisectors of frogs and crossings at a limited rate, whole timbers
  under crossings, split ones between tracks with room to part them, gaps filled per rail and overlaps shortened or dropped;
  clusters worked out in a background thread while driving, on demand in the editor
- the ballast can be left out under the beams (sleepers3dbed no)
- blades get their heel and throw from the switch template or blades <heel> <throw> in the scenery; blades of double switches
  run on into the track linking them to the next switch
- rail cutting at frogs, wing rails, movable noses and switch crossings done by one carving pass (RailCarve)
- tint of the rail treads applied to the colour only, without the matte band at the frog
- a switch trailed through with a movable nose set the other way derails the vehicle
- self-tests: sleepercheck, sharedzones, beamsnear, throw, sleepersbg
This commit is contained in:
maj00r
2026-10-17 23:28:36 +02:00
parent 21eca4d9a6
commit d25f20a56c
49 changed files with 9802 additions and 848 deletions

View File

@@ -8,6 +8,7 @@ http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "world/TrackSleepers.h"
#include <filesystem>
#include <iterator>
#include <fstream>
@@ -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<std::string> 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<float>(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)

View File

@@ -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<std::string> &Rootstatements);
bool m_nose_included{false};
bool nose_controls(std::vector<std::string> &Rootstatements);
void starter_trainset(std::vector<std::string> &Trailing);
std::optional<std::pair<glm::dvec3, glm::vec3>> 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<std::array<char, 256>, 3> m_track_materials{};
std::array<TTrack const *, 3> 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<char, 256> m_track_isolated{};
std::array<char, 256> m_track_sleeper_model{};
std::array<char, 256> m_track_sleeper_skin{};
// the event to add to the path, typed
TTrack const *m_track_events_for{nullptr};
std::array<char, 128> 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<segment_data> 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<TTrack *> create_switch_set(editor_track::switch_template const &Shape, std::vector<segment_data> const &Paths, TTrack const &Style, std::string const &Name);
void record_slip(std::vector<TTrack *> 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<TTrack *> 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<TTrack *> 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<segment_data> 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<double> 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<branch_line> 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<segment_data> const &Paths, std::string &Status);
std::optional<switch_branch> 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_branch> switch_branches(TTrack const &Track, std::vector<editor_track::joint> const &Joints, std::vector<TTrack const *> 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<editor_track::joint> const &Joints, std::vector<switch_branch> &Branches, std::vector<bool> &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<segment_data> 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<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Created, std::vector<TTrack *> &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<TTrack *> 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<TTrack *> 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<segment_data> 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<std::pair<TTrack *, std::vector<editor_track::joint>>> 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<bool(TTrack &)> 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<TTrack *> selected_tracks();
void select_tracks(std::vector<TTrack *> 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<TTrack *> const &Tracks, std::function<bool(TTrack &)> 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<TTrack *> 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<TTrack *> const &Tracks);
void render_track_set_header(std::vector<TTrack *> const &Tracks);
void render_track_select_tools(std::vector<TTrack *> const &Tracks);
void render_track_style(std::vector<TTrack *> const &Tracks);
void copy_track_style(TTrack const &Track);
std::size_t paste_track_style(std::vector<TTrack *> const &Tracks);
std::vector<TTrack *> similar_tracks(int const Property, std::vector<TTrack *> 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<std::vector<std::pair<std::string, int>>> values;
std::vector<std::size_t> 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<TTrack *> tracks;
std::array<char, 256> 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<std::vector<track_texture_use>, 2> m_track_textures; // of the rails, of the trackbed
std::array<char, 128> 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<texture_index_entry> 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<std::pair<std::string, std::string>> values;
// speed, environment, overhead line, track circuits, rails, trackbed, its shape, sleepers, ride and sound, frogs
std::array<bool, 10> 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<std::vector<TTrack *>> 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

View File

@@ -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());
}

View File

@@ -346,7 +346,7 @@ std::vector<segment_data> 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<TTrack *> 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<TTrack *> 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<TTrack *> 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))

View File

@@ -40,15 +40,29 @@ http://mozilla.org/MPL/2.0/.
#include "scene/sceneterrain.h"
#include "editor/editorSpeed.hpp"
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <map>
#include <sstream>
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<double, std::milli>(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 &section{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<double, std::milli>(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<scene::basic_section const *, double> 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<double, std::milli>(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 &sectiontime : 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<double, std::milli>(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<std::string>{} : 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<int>(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<gfx::geometry_handle> 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<TTrack *> 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<std::pair<char const *, knots::line const *>>{
{"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<TTrack const *, track_rails::rail_layout> 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<TTrack *> 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<piece> pieces;
};
std::map<TTrack const *, track_rails::rail_layout> layouts;
std::map<TTrack const *, std::vector<rail>> rails;
auto const rails_of = [&](TTrack *Track) -> std::vector<rail> & {
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<track_rails::rail_cut const *> 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<TTrack *> 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<double> 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 <path> [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<bool>(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<float>(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 <name>, deletekind <kind> the path deleted as Del does it, or the first path over an element of the kind
// tracksel <name>... the tracks selected together, as Shift+clicks select them; none: nothing selected
// trackprop <key> [value] the value given to the selected tracks, as typed into the inspector; trackprops logs their values
// trackstyle copy [name] | groups <n>... | paste | panel the properties of a track copied, the groups chosen, pasted on the selected tracks
// tracksimilar <key> the tracks of the scenery with the value of the property of the selected ones selected
// btfcheck <terrain file> the .txtf converted to .btf, the editor lines the binary file keeps logged
// mouse <x> <y>, mouseat <x> <y> <z> 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 <x> <y> <z> <steps> <frames> [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 <text>
// perf <label> 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 <track> <z from> <z to> the beams the track lays with their middles between the two z
// sharedzones <track> 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;

View File

@@ -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<switch_design::part>{};
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<std::size_t>(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");

View File

@@ -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 <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <map>
#include <sstream>
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<std::string> &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<char>(input), std::istreambuf_iterator<char>());
}
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<scene::layer_handle>(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;
}

View File

@@ -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<editor_track::joint> 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<segment_data> &Pieces, double const Rise)
{
std::vector<double> 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::switch_branch> 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<TTrack *> 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<int>(i));
alignment_store::store(design, result, origin, layer, source, fit);
return;
}
}
bool editor_mode::move_switch_paths(TTrack &Track, std::vector<segment_data> 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<segment_data> con
for (auto const &joint : joints)
positions.push_back(editor_track::point_position(Track, joint.own));
std::vector<bool> 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<segment_data> 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_branch> editor_mode::switch_branches(TTrack const &Track, std::vector<editor_track::joint> const &Joints, std::vector<TTrack const *> const &Skip) const
{
std::vector<switch_branch> 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<editor_track::joint> const &Joints, std::vector<switch_branch> &Branches, std::vector<bool> &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::vector<segmen
auto const joint{Set.first->m_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<std::vector<switch_branch>, 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::vector<segmen
Set.first->m_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::vector<segmen
loose += (loose.empty() ? "" : ", ") + left;
}
editor_track::commit(edit.changed);
for (auto const &set : branches)
for (auto const &branch : set)
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;
}
@@ -404,6 +791,19 @@ void editor_mode::switch_slide_start(TTrack &Track)
slide.joints.push_back(axis->along(path.points[index]));
}
}
std::vector<TTrack const *> 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<std::size_t>(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]};

View File

@@ -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<long long>(tracks.size())), static_cast<int>(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<int>(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<TEnvironmentType>(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<char, 256> &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<std::string> 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<char, 256> &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<TTrack *> 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<int>(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::key_hint> 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<TTrack *> 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<std::string> driven;
for (auto *track : Created)
@@ -5533,6 +5358,19 @@ void editor_mode::switch_placed(std::vector<TTrack *> 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<TTrack *> editor_mode::link_tracks(editor_track::snap_target const &
return pieces;
}
std::vector<TTrack *> 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<TTrack *> 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<TTrack *> 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<float>(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<float>(Radius);
return path;
};
std::vector<std::pair<double, double>> 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<segment_data> 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<TTrack *> parts;
if (Type == "Skrz")
@@ -6530,19 +6426,19 @@ std::vector<TTrack *> 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<int>(radius)) + STR_C(" m, the straights end ") + std::to_string(static_cast<int>(available)) + STR_C(" m from the crossing");
state.status = STR_C("R reduced to ") + std::to_string(static_cast<int>(curve.radius)) + STR_C(" m, the straights end ") + std::to_string(static_cast<int>(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<std::pair<TTrack *, editor_track::state>> 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<int>(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<int>(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<TTrack *> 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<scene::instance_handle> 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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -212,7 +212,7 @@ void editor_mode::draw_track_hover() const
{
using kind = track_intent::kind;
auto const *track{m_hover.track};
if (track == nullptr || (m_intent.what != kind::select && m_intent.what != kind::box && m_intent.what != kind::straight_set) || m_intent.track != track)
if (track == nullptr || (m_intent.what != kind::select && m_intent.what != kind::box && m_intent.what != kind::straight_set && m_intent.what != kind::pick_style) || m_intent.track != track)
return;
screen_projection const projection;
if (track != selected_track())
@@ -319,6 +319,13 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
}
return intent;
}
if (m_track_style.picking && (m_track_tab == track_tab::path || m_track_tab == track_tab::turnout))
{
intent.track = m_hover.track;
if (m_hover.track != nullptr)
intent.position = m_hover.point;
return make(kind::pick_style, m_hover.track != nullptr ? "Click: take the properties of this track" : "Click a track to take its properties, Esc: cancel");
}
if (m_track_tab == track_tab::path)
{
if (shift && control && m_hover.track != nullptr && rail(*m_hover.track))
@@ -333,7 +340,7 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
if (shift && false == gizmo_frame::over())
{
intent.track = m_hover.track;
return make(kind::box, m_hover.track == nullptr ? "Drag: the tracks in a box join the selection" : in_track_set(m_hover.track) ? "Click: take out of the selection, drag: a box" : "Click: add to the selection, drag: a box");
return make(kind::box, m_hover.track == nullptr ? "Drag: the tracks in a box join the selection" : track_selected(m_hover.track) ? "Click: take out of the selection, drag: a box" : "Click: add to the selection, drag: a box");
}
if (auto const hit{track_handle_hit()}; hit.valid() && selected != nullptr)
{
@@ -356,6 +363,11 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
return make(kind::extend_end, diverging ? "Drag: the curve of the switch goes on; Ctrl+drag: move the end" : "Drag: a new track out of this end, as in laying; Ctrl+drag: move the end");
}
}
if (control && false == alt && m_hover.track != nullptr && false == gizmo_frame::over())
{
intent.track = m_hover.track;
return make(kind::box, track_selected(m_hover.track) ? "Click: take out of the selection" : "Click: add to the selection");
}
}
if (m_track_tab == track_tab::straights)
{
@@ -413,7 +425,7 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
intent.track = m_hover.track;
intent.path = m_hover.path;
intent.position = m_hover.point;
return make(kind::switch_slide, "Click: edit this switch, drag: slide it along the straight");
return make(kind::switch_slide, m_track_tab == track_tab::path ? "Click: select this switch, drag: slide it along the straight" : "Click: edit this switch, drag: slide it along the straight");
}
if (selected != nullptr && false == selected->m_paths.empty())
{
@@ -441,6 +453,12 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods)
default: return make(kind::select, "Click: select");
}
}
// a drag on the empty ground selects the tracks in a box, a click there clears the selection
if (m_track_tab == track_tab::path && false == alt)
{
intent.index = 1;
intent.what = kind::box;
}
return intent;
}
@@ -462,6 +480,10 @@ void editor_mode::update_track_intent()
case kind::straight_handle:
case kind::route_vertex:
case kind::route_grip: ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); break;
case kind::box:
if (m_intent.track != nullptr)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
break;
default: ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); break;
}
}
@@ -482,7 +504,15 @@ void editor_mode::track_press(track_intent const &Intent)
});
return;
case kind::sweep: sweep_press(); return;
case kind::box: start_track_box(); return;
case kind::box: start_track_box(Intent.index == 1); return;
case kind::pick_style:
if (Intent.track != nullptr)
{
copy_track_style(*Intent.track);
m_track_style.picking = false;
m_track_style.open = true;
}
return;
case kind::spread:
if (Intent.track != nullptr)
track_spread_apply(*Intent.track);
@@ -541,31 +571,31 @@ void editor_mode::track_press(track_intent const &Intent)
}
return;
case kind::select:
case kind::none:
{
auto *hovered{Intent.track};
GfxRenderer->Pick_Node_Callback([this, hovered](scene::basic_node *node) {
if (false == viewport_click())
return;
auto *track{hovered != nullptr ? hovered : dynamic_cast<TTrack *>(node)};
if (m_track_tab == track_tab::path)
case kind::none: track_click_select(Intent.track); return;
}
}
void editor_mode::track_click_select(TTrack *Hovered)
{
GfxRenderer->Pick_Node_Callback([this, Hovered](scene::basic_node *node) {
if (false == viewport_click())
return;
auto *track{Hovered != nullptr ? Hovered : dynamic_cast<TTrack *>(node)};
if (m_track_tab == track_tab::path)
{
m_track_set.clear();
if (track == nullptr)
{
m_track_set.clear();
if (track == nullptr)
{
m_node = nullptr;
m_track_point = {};
ui()->set_node(nullptr);
return;
}
m_node = nullptr;
m_track_point = {};
ui()->set_node(nullptr);
return;
}
select_track(track != nullptr ? track : node);
if (m_track_tab == track_tab::lay && track != nullptr)
show_track_tab(track_tab::parallel);
});
return;
}
}
}
select_track(track != nullptr ? track : node);
if (m_track_tab == track_tab::lay && track != nullptr)
show_track_tab(track_tab::parallel);
});
}
void editor_mode::draw_track_intent() const
@@ -688,7 +718,9 @@ void editor_mode::render_track_context()
context.pending = false;
if (track != nullptr && false == track->m_editorremoved)
{
select_track(track);
// a track of the set keeps the set, the menu acts on all of them
if (false == (m_track_set.size() >= 2 && in_track_set(track)))
select_track(track);
ImGui::OpenPopup("##trackcontext");
ImGui::SetNextWindowPos(ImVec2(context.mouse.x, context.mouse.y));
}
@@ -703,7 +735,32 @@ void editor_mode::render_track_context()
}
auto const point{context.target.point};
bool const normal{track->eType == tt_Normal && editor_track::is_supported(*track)};
auto const tracks{selected_tracks()};
auto const many{tracks.size() >= 2};
ImGui::TextDisabled("%s (%s, %.1f m)", name_of(*track).c_str(), STR_C(type_of(*track)), track->Length());
if (many)
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.2f, 1.0f), STR_P("one of %d selected tracks", "one of %d selected tracks", static_cast<long long>(tracks.size())), static_cast<int>(tracks.size()));
ImGui::Separator();
if (ImGui::MenuItem(STR_C("Copy its properties"), "Ctrl+Shift+C"))
copy_track_style(*track);
auto const paste{many ? format(STR_P("Paste the properties on %d track", "Paste the properties on %d tracks", static_cast<long long>(tracks.size())), static_cast<int>(tracks.size())) : std::string{STR_C("Paste the properties")}};
if (ImGui::MenuItem(paste.c_str(), "Ctrl+Shift+V", false, false == m_track_style.values.empty()))
paste_track_style(tracks);
if (false == m_track_style.values.empty() && ImGui::IsItemHovered())
ImGui::SetTooltip(STR_C("Properties of %s"), m_track_style.from.c_str());
if (ImGui::BeginMenu(STR_C("Select alike")))
{
char const *const keys[] = {"rails", "trackbed", "speed", "environment", "circuits"};
char const *const labels[] = {"The same rails", "The same trackbed", "The same speed", "The same environment", "The same track circuits"};
for (std::size_t i = 0; i < std::size(keys); ++i)
if (ImGui::MenuItem(STR_C(labels[i])))
{
auto const found{similar_tracks(track_property_index(keys[i]), {track})};
select_tracks(found);
ui()->set_status(format(STR_P("%s: %d track selected in the scenery", "%s: %d tracks selected in the scenery", static_cast<long long>(found.size())), STR_C(labels[i]), static_cast<int>(found.size())), false);
}
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::MenuItem(STR_C("Split here"), "K", false, normal))
split_track_at(*track, point);
@@ -743,14 +800,19 @@ void editor_mode::render_track_context()
speed_start();
}
ImGui::Separator();
if (ImGui::MenuItem(in_track_set(track) ? STR_C("Take out of the selection") : STR_C("Add to the selection"), "Shift+LMB"))
if (ImGui::MenuItem(track_selected(track) ? STR_C("Take out of the selection") : STR_C("Add to the selection"), "Shift+LMB"))
track_set_toggle(*track);
if (ImGui::MenuItem(STR_C("Look at it")))
focus_track(*track, context.target.path, point);
if (ImGui::MenuItem(STR_C("Copy the name"), nullptr, false, false == track->name().empty()))
ImGui::SetClipboardText(track->name().c_str());
ImGui::Separator();
if (ImGui::MenuItem(STR_C("Delete"), "Del"))
if (many)
{
if (ImGui::MenuItem(format(STR_P("Delete %d track", "Delete %d tracks", static_cast<long long>(tracks.size())), static_cast<int>(tracks.size())).c_str(), "Del"))
track_set_delete();
}
else if (ImGui::MenuItem(STR_C("Delete"), "Del"))
delete_selected_track();
ImGui::EndPopup();
}
@@ -907,14 +969,30 @@ bool editor_mode::in_track_set(TTrack const *Track) const
return std::find(m_track_set.begin(), m_track_set.end(), Track) != m_track_set.end();
}
bool editor_mode::track_selected(TTrack const *Track) const
{
return Track != nullptr && (m_track_set.size() >= 2 ? in_track_set(Track) : Track == m_node);
}
void editor_mode::track_set_prune()
{
m_track_set.erase(std::remove_if(m_track_set.begin(), m_track_set.end(), [](TTrack const *Track) { return Track == nullptr || Track->m_editorremoved; }), m_track_set.end());
// the set is the selection only together with the selected track, another one chosen elsewhere ends it
if (false == m_track_set.empty() && false == in_track_set(selected_track()))
m_track_set.clear();
}
void editor_mode::track_set_toggle(TTrack &Track)
{
track_set_prune();
// the set is the selection only with the selected track in it, a set left from before is forgotten
if (m_track_set.size() < 2 || false == in_track_set(selected_track()))
m_track_set.clear();
if (m_track_set.empty() && &Track == selected_track())
{
select_tracks({});
return;
}
if (m_track_set.empty())
if (auto *selected{selected_track()}; selected != nullptr && selected != &Track)
m_track_set.push_back(selected);
@@ -927,10 +1005,11 @@ void editor_mode::track_set_toggle(TTrack &Track)
ui()->set_node(m_node);
}
void editor_mode::start_track_box()
void editor_mode::start_track_box(bool const Replace)
{
auto const &io{ImGui::GetIO()};
m_track_box.active = true;
m_track_box.replace = Replace;
m_track_box.to = {io.MousePos.x, io.MousePos.y};
m_track_box.from = m_track_box.to;
m_track_box.hovered = m_hover.track;
@@ -946,7 +1025,10 @@ void editor_mode::finish_track_box()
box.to = {io.MousePos.x, io.MousePos.y};
if (glm::length(box.to - box.from) < 6.0f)
{
if (box.hovered != nullptr && false == box.hovered->m_editorremoved)
// a click on the empty ground: what the renderer finds there, a part of a track seen close up, or nothing
if (box.replace)
track_click_select(nullptr);
else if (box.hovered != nullptr && false == box.hovered->m_editorremoved)
track_set_toggle(*box.hovered);
return;
}
@@ -954,7 +1036,9 @@ void editor_mode::finish_track_box()
glm::vec2 const high{std::max(box.from.x, box.to.x), std::max(box.from.y, box.to.y)};
screen_projection const projection;
track_set_prune();
if (m_track_set.empty())
if (m_track_set.size() < 2 || false == in_track_set(selected_track()) || box.replace)
m_track_set.clear();
if (m_track_set.empty() && false == box.replace)
if (auto *selected{selected_track()}; selected != nullptr)
m_track_set.push_back(selected);
for (auto *track : simulation::Paths.sequence())
@@ -977,12 +1061,8 @@ void editor_mode::finish_track_box()
if (inside)
m_track_set.push_back(track);
}
if (false == m_track_set.empty())
{
m_node = m_track_set.back();
m_track_point = {};
ui()->set_node(m_node);
}
auto const found{m_track_set};
select_tracks(found);
}
std::vector<TTrack *> editor_mode::track_spread_from(TTrack &Start) const
@@ -1063,6 +1143,8 @@ std::vector<TTrack *> const &editor_mode::track_spread_preview(TTrack &Start) co
void editor_mode::track_spread_apply(TTrack &Start)
{
track_set_prune();
if (m_track_set.size() < 2 || false == in_track_set(selected_track()))
m_track_set.clear();
if (m_track_set.empty())
if (auto *selected{selected_track()}; selected != nullptr && selected != &Start)
m_track_set.push_back(selected);
@@ -1077,9 +1159,8 @@ void editor_mode::track_spread_apply(TTrack &Start)
if (false == in_track_set(track))
m_track_set.push_back(track);
m_spread.from = nullptr;
m_node = m_track_set.empty() ? nullptr : m_track_set.back();
m_track_point = {};
ui()->set_node(m_node);
auto const found{m_track_set};
select_tracks(found);
}
void editor_mode::track_set_grow()
@@ -1158,7 +1239,21 @@ void editor_mode::draw_track_set() const
drawlist->AddRectFilled(from, io.MousePos, IM_COL32(255, 160, 40, 40));
drawlist->AddRect(from, io.MousePos, IM_COL32(255, 160, 40, 220), 0.0f, 0, 1.5f);
}
if (m_track_set.size() < 2)
auto const *listed{m_track_list_hover_frame >= ImGui::GetFrameCount() - 1 ? m_track_list_hover : nullptr};
if (listed != nullptr && false == listed->m_editorremoved)
for (auto const &path : listed->m_paths)
{
bezier const curve{path};
auto const count{std::clamp(static_cast<int>(curve.plan_length() / 3.0), 6, 64)};
auto previous{curve.point(0.0)};
for (int k = 1; k <= count; ++k)
{
auto const next{curve.point(static_cast<double>(k) / count)};
projection.line(drawlist, previous, next, IM_COL32(255, 255, 255, 240), 6.0f);
previous = next;
}
}
if (m_track_set.size() < 2 || false == in_track_set(selected_track()))
return;
for (auto const *track : m_track_set)
{
@@ -1179,41 +1274,6 @@ void editor_mode::draw_track_set() const
}
}
void editor_mode::track_set_apply(std::function<bool(TTrack &)> const &Change, bool const Rebuild, char const *What)
{
track_set_prune();
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> changed;
std::size_t locked{0};
for (auto *track : m_track_set)
{
if (false == scene::Layers.editable(track))
{
++locked;
continue;
}
auto const before{editor_track::capture(*track)};
if (false == Change(*track))
continue;
states.emplace_back(track, before);
changed.push_back(track);
}
if (changed.empty())
return;
push_track_snapshot(std::move(states));
for (auto *track : changed)
{
if (Rebuild)
editor_track::commit_parameters(*track);
else
track->mark_dirty();
}
auto status{format(STR_C("%s set on %zu paths, Ctrl+Z takes it back"), STR_C(What), changed.size())};
if (locked > 0)
status += format(STR_C(", %zu in the files which can't be changed are left as they are"), locked);
ui()->set_status(status, false);
}
void editor_mode::track_set_delete()
{
track_set_prune();
@@ -1246,93 +1306,13 @@ void editor_mode::track_set_delete()
void editor_mode::render_track_set_ui()
{
track_set_prune();
if (m_track_set.size() < 2)
if (m_track_set.size() < 2 || false == in_track_set(selected_track()))
return;
auto &fields{m_track_set_fields};
double length{0.0};
for (auto const *track : m_track_set)
length += track->Length();
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.2f, 1.0f), STR_C("%zu paths selected, %.1f m"), m_track_set.size(), length);
ImGui::SameLine();
if (ImGui::SmallButton(STR_C("Clear")))
{
m_track_set.clear();
return;
}
ImGui::TextDisabled("%s", STR_C("The gizmo moves them together; Shift+LMB adds or takes out a path"));
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.2f, 1.0f), STR_P("%d track selected", "%d tracks selected", static_cast<long long>(m_track_set.size())), static_cast<int>(m_track_set.size()));
ImGui::PushTextWrapPos(0.0f);
ImGui::TextDisabled("%s", STR_C("Their properties are in the inspector, the gizmo moves them together"));
ImGui::PopTextWrapPos();
render_parallel_set_ui();
if (ImGui::SmallButton(STR_C("Grow by the neighbours")))
track_set_grow();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Adds the paths joined to the set, by the rules of the spread"));
auto const button_width{90.0f};
ImGui::PushItemWidth(-button_width - ImGui::GetStyle().ItemSpacing.x);
ImGui::InputDouble("##setvelocity", &fields.velocity, 0.0, 0.0, "%.0f");
ImGui::SameLine();
if (ImGui::Button(format("%s##setv", STR_C("Velocity")).c_str(), ImVec2(button_width, 0.0f)))
{
auto const velocity{fields.velocity};
track_set_apply([velocity](TTrack &Track) { editor_track::velocity(Track, velocity); return true; }, false, "Velocity");
}
ImGui::InputTextWithHint("##setrail", STR_C("texture of the rails"), fields.rail, sizeof(fields.rail));
ImGui::SameLine();
if (ImGui::Button(format("%s##setr", STR_C("Rails")).c_str(), ImVec2(button_width, 0.0f)) && fields.rail[0] != '\0')
{
auto const material{editor_track::fetch_material(fields.rail)};
track_set_apply([material](TTrack &Track) {
Track.m_material1 = material;
if (Track.eType == tt_Switch)
Track.m_material2 = material;
return true;
}, true, "Rails");
}
ImGui::InputTextWithHint("##settrackbed", STR_C("texture of the trackbed"), fields.trackbed, sizeof(fields.trackbed));
ImGui::SameLine();
if (ImGui::Button(format("%s##setb", STR_C("Trackbed")).c_str(), ImVec2(button_width, 0.0f)) && fields.trackbed[0] != '\0')
{
auto const material{editor_track::fetch_material(fields.trackbed)};
track_set_apply([material](TTrack &Track) {
if (Track.eType == tt_Switch)
{
if (Track.SwitchExtension == nullptr)
return false;
Track.SwitchExtension->m_material3 = material;
}
else
Track.m_material2 = material;
return true;
}, true, "Trackbed");
}
ImGui::InputTextWithHint("##setcircuits", STR_C("track circuits, separated by spaces"), fields.circuits, sizeof(fields.circuits));
ImGui::SameLine();
if (ImGui::Button(format("%s##setc", STR_C("Circuits")).c_str(), ImVec2(button_width, 0.0f)))
{
std::vector<std::string> names;
std::string const text{fields.circuits};
std::size_t at{0};
while (at < text.size())
{
auto const end{text.find(' ', at)};
auto const word{text.substr(at, end == std::string::npos ? std::string::npos : end - at)};
if (false == word.empty())
names.push_back(word);
if (end == std::string::npos)
break;
at = end + 1;
}
track_set_apply([names](TTrack &Track) { editor_track::isolated(Track, names); return true; }, false, "Circuits");
}
char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"};
ImGui::Combo("##setenvironment", &fields.environment, environments, IM_ARRAYSIZE(environments));
ImGui::SameLine();
if (ImGui::Button(format("%s##sete", STR_C("Environment")).c_str(), ImVec2(button_width, 0.0f)))
{
auto const environment{static_cast<TEnvironmentType>(fields.environment)};
track_set_apply([environment](TTrack &Track) { Track.eEnvironment = environment; return true; }, false, "Environment");
}
ImGui::PopItemWidth();
if (ImGui::Button(format(STR_C("Delete %zu paths"), m_track_set.size()).c_str()))
track_set_delete();
ImGui::Separator();
}

View File

@@ -589,6 +589,9 @@ void editor_ui::register_settings()
void editor_ui::render_()
{
// the keyboard navigation of ImGui is off, so Esc would not close an open list or menu by itself
if (ImGui::IsKeyPressed(ImGuiKey_Escape, false) && ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel))
ImGui::ClosePopupsExceptModals();
auto *viewport{ImGui::GetMainViewport()};
auto const flags{ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar};
// the fields of work of the editor under the menu, in a row twice as high; the tools of the one chosen in the toolbar under it

View File

@@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/.
#include "application/editoruilayer.h"
#include "imgui/imgui_impl_glfw.h"
#include "imgui/imgui_internal.h"
GLFWwindow *ui_layer::m_window{nullptr};
GLFWwindow *ui_layer::m_keywindow{nullptr};
@@ -125,6 +126,14 @@ bool ui_layer::char_callback(unsigned int c)
bool ui_layer::scroll_callback(double xoffset, double yoffset)
{
ImGui_ImplGlfw_ScrollCallback(m_window, xoffset, yoffset);
// a line a second in the log of where a scroll went, for a wheel or a touchpad that seems to do nothing
static double logged{-10.0};
if (auto const now{glfwGetTime()}; now - logged > 1.0)
{
logged = now;
auto const *hovered{ImGui::GetCurrentContext() != nullptr ? ImGui::GetCurrentContext()->HoveredWindow : nullptr};
WriteLog("scroll " + std::to_string(xoffset) + " " + std::to_string(yoffset) + ", taken by the ui: " + (m_imguiio->WantCaptureMouse ? "yes" : "no") + ", over: " + (hovered != nullptr ? hovered->Name : "the view"));
}
return m_imguiio->WantCaptureMouse;
}