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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 §ion{simulation::Region->section(track->location())};
|
||||
if (false == section.m_geometrycreated)
|
||||
continue;
|
||||
track->rebuild_geometry(section.m_geometrybank);
|
||||
++count;
|
||||
}
|
||||
WriteLog(format("SELFTEST rebuildall: %d paths in %.1f ms", count, std::chrono::duration<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 §iontime : sectiontimes)
|
||||
slowest = std::max(slowest, sectiontime.second);
|
||||
WriteLog(format("SELFTEST rebuildall sleepers3d: %d tracks laid in %.1f ms, %zu parts; %zu sections, %.2f ms per section, %.2f ms the slowest", laidcount, laytime, parts, sectiontimes.size(),
|
||||
(sectiontimes.empty() ? 0.0 : laytime / sectiontimes.size()), slowest));
|
||||
return selftest_result::handled;
|
||||
}
|
||||
if (command == "beams")
|
||||
{
|
||||
// the beams the track lays, between two distances along it: their middles, lengths and the way they lie square to
|
||||
std::string name;
|
||||
double from{-1e9}, to{1e9};
|
||||
words >> name >> from >> to;
|
||||
auto *track{simulation::Paths.find(name)};
|
||||
if (track == nullptr || false == track->m_sleepers3d.active)
|
||||
{
|
||||
WriteLog("SELFTEST beams: no track with beams " + name);
|
||||
return selftest_result::handled;
|
||||
}
|
||||
std::string text;
|
||||
for (auto const &beam : track_sleepers::place(*track))
|
||||
{
|
||||
auto const middle{0.5 * (beam.from + beam.to)};
|
||||
if (middle.z < from || middle.z > to)
|
||||
continue;
|
||||
text += format(" [%.2f %.2f L%.2f a%.2f%s]", middle.x, middle.z, glm::length(beam.to - beam.from), glm::degrees(std::atan2(beam.along.x, beam.along.z)), beam.plain ? " p" : "");
|
||||
}
|
||||
WriteLog("SELFTEST beams " + name + ":" + text);
|
||||
return selftest_result::handled;
|
||||
}
|
||||
if (command == "sharedzones")
|
||||
{
|
||||
// how far the timbers shared with other tracks reach along the paths of the track and of the tracks joined to its ends
|
||||
std::string name;
|
||||
words >> name;
|
||||
auto *track{simulation::Paths.find(name)};
|
||||
if (track == nullptr)
|
||||
{
|
||||
WriteLog("SELFTEST sharedzones: no track " + name);
|
||||
return selftest_result::handled;
|
||||
}
|
||||
auto const zones_text = [](TTrack const &Track, int const Path) {
|
||||
std::string text;
|
||||
for (auto const &zone : track_sleepers::shared_zones(Track, Path))
|
||||
text += format(" %.2f..%.2f", zone.first, zone.second);
|
||||
return text.empty() ? std::string{" none"} : text;
|
||||
};
|
||||
auto const paths{track->eType == tt_Switch ? 2 : 1};
|
||||
for (int path = 0; path < paths; ++path)
|
||||
{
|
||||
auto const length{track->eType == tt_Switch ? track->SwitchExtension->Segments[path]->GetLength() : track->Segment->GetLength()};
|
||||
WriteLog(format("SELFTEST sharedzones %s path %d (%.2f m):%s", name.c_str(), path, length, zones_text(*track, path).c_str()));
|
||||
// the track going on from the end of the path, and how far past the end the shared timbers reach along it
|
||||
auto *next{track->eType == tt_Switch ? track->SwitchExtension->pNexts[path] : track->trNext};
|
||||
if (next == nullptr || next->eType != tt_Normal || next->Segment == nullptr)
|
||||
continue;
|
||||
glm::dvec3 end, start, unused;
|
||||
glm::vec3 angles;
|
||||
(track->eType == tt_Switch ? track->SwitchExtension->Segments[path] : track->Segment)->RaPositionGet(length, end, angles);
|
||||
next->Segment->RaPositionGet(0.0, start, angles);
|
||||
auto const fromstart{glm::length(start - end) < 0.1};
|
||||
auto past{0.0};
|
||||
for (auto const &zone : track_sleepers::shared_zones(*next, 0))
|
||||
{
|
||||
if (fromstart && zone.first < 0.05)
|
||||
past = zone.second;
|
||||
else if (false == fromstart && zone.second > next->Segment->GetLength() - 0.05)
|
||||
past = next->Segment->GetLength() - zone.first;
|
||||
}
|
||||
WriteLog(format("SELFTEST sharedzones %s path %d goes on into %s:%s; shared timbers reach %.2f m past the end", name.c_str(), path, next->name().c_str(), zones_text(*next, 0).c_str(), past));
|
||||
}
|
||||
return selftest_result::handled;
|
||||
}
|
||||
if (command == "sleepersbg")
|
||||
{
|
||||
// the sleepers worked out in the background as in the simulation, to see they come out the same, or back to as they're needed
|
||||
int on{1};
|
||||
words >> on;
|
||||
track_sleepers::lay_in_background(on != 0);
|
||||
for (auto *track : simulation::Paths.sequence())
|
||||
if (track != nullptr)
|
||||
track->m_sleepers3d.dirty = true;
|
||||
track_sleepers::unsettle_around(*simulation::Paths.sequence().front());
|
||||
WriteLog(format("SELFTEST sleepersbg %d", on));
|
||||
return selftest_result::handled;
|
||||
}
|
||||
if (command == "beamsnear")
|
||||
{
|
||||
double x{0.0}, z{0.0}, radius{3.0};
|
||||
words >> x >> z >> radius;
|
||||
for (auto *track : simulation::Paths.sequence())
|
||||
{
|
||||
if (track == nullptr || track->m_editorremoved || false == track->m_sleepers3d.active)
|
||||
continue;
|
||||
for (auto const &beam : track_sleepers::place(*track))
|
||||
{
|
||||
auto const middle{0.5 * (beam.from + beam.to)};
|
||||
if (glm::length(glm::dvec2{middle.x - x, middle.z - z}) > radius)
|
||||
continue;
|
||||
WriteLog(format("SELFTEST beam %s: %.2f %.2f -> %.2f %.2f, %.2f m, along %.1f deg%s", track->name().c_str(), beam.from.x, beam.from.z, beam.to.x, beam.to.z, glm::length(beam.to - beam.from), glm::degrees(std::atan2(beam.along.x, beam.along.z)), beam.plain ? " plain" : ""));
|
||||
}
|
||||
}
|
||||
return selftest_result::handled;
|
||||
}
|
||||
if (command == "sleepercheck")
|
||||
{
|
||||
auto const start{std::chrono::steady_clock::now()};
|
||||
auto const result{track_sleepers::check()};
|
||||
WriteLog(format("SELFTEST sleepercheck: %zu beams, %zu overlaps, %zu gaps, %zu bare rails, %zu too long (longest %.2f m), widest spacing %.2f m at %s; %zu shared off one plane, %zu switch panels off one plane, %zu ends clipped, %zu beams dropped, slowest cluster %.1f ms of %zu tracks; in %.1f ms",
|
||||
result.beams, result.overlaps, result.gaps, result.bare, result.toolong, result.longest, result.widest, result.widestat.c_str(), result.offplane, result.panels, result.clipped, result.dropped, result.slowest, result.biggest,
|
||||
std::chrono::duration<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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
1571
application/editormodetrackprops.cpp
Normal file
1571
application/editormodetrackprops.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user