diff --git a/CMakeLists.txt b/CMakeLists.txt index 97c50b4f..bb68ef6d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,11 +96,13 @@ set(SOURCES "model/Texture.cpp" "utilities/Timer.cpp" "world/Track.cpp" +"world/TrackSleepers.cpp" "world/GaugeLine.cpp" "world/RailCrossings.cpp" "world/RailMaterials.cpp" "world/RailProfile.cpp" "world/RailPlan.cpp" +"world/RailCarve.cpp" "world/RailLayer.cpp" "world/Road.cpp" "world/Sweep.cpp" @@ -204,6 +206,7 @@ set(SOURCES "application/editormodeswitchdesign.cpp" "application/editormodetrackbed.cpp" "application/editormodetrackux.cpp" +"application/editormodetrackprops.cpp" "application/editormoderoad.cpp" "application/editormodeprofile.cpp" "application/editormodetextures.cpp" diff --git a/McZapkie/MOVER.h b/McZapkie/MOVER.h index 29b3e75c..ccd0935b 100644 --- a/McZapkie/MOVER.h +++ b/McZapkie/MOVER.h @@ -169,6 +169,8 @@ enum DerailReason WRONG_TRACK_TYPE = 4, // Ra: powód wykolejenia: nieodpowiednia trajektoria /// Collision with another vehicle. COLLISION = 5, // powód wykolejenia: zderzenie z innym pojazdem + /// Trailing through a switch with a movable nose set the other way. + TRAILED_SWITCH = 6, // powód wykolejenia: rozprucie rozjazdu z ruchomym dziobem }; /*wagi prawdopodobienstwa dla funkcji FuzzyLogic*/ diff --git a/McZapkie/Mover.cpp b/McZapkie/Mover.cpp index 3b5b6215..69d20723 100644 --- a/McZapkie/Mover.cpp +++ b/McZapkie/Mover.cpp @@ -1346,6 +1346,8 @@ void TMoverParameters::Derail(DerailReason const Reason) ErrorLog("Bad dynamic: " + Name + " derailed due to wrong track type"); // błąd w scenerii else if (Reason == COLLISION) WriteLog("Bad driving: " + Name + " derailed"); // This reason also generates its own message in `TMoverParameters::CollisionDetect()` + else if (Reason == TRAILED_SWITCH) + ErrorLog("Bad driving: " + Name + " derailed trailing through a switch with a movable nose set the other way"); } } diff --git a/application/editormode.cpp b/application/editormode.cpp index 57f99e8d..323c3113 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -8,6 +8,7 @@ http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" +#include "world/TrackSleepers.h" #include #include #include @@ -2952,6 +2953,7 @@ bool editor_mode::save() } auto const terrainadded{false == rootstatements.empty()}; auto const controls{turntable_controls(rootstatements)}; + auto const noses{nose_controls(rootstatements)}; std::vector trailing; std::erase_if(m_vehicle.trainsets, [](auto const &Trainset) { auto const *track{simulation::Paths.find(Trainset.track)}; @@ -2965,6 +2967,8 @@ bool editor_mode::save() auto const result = scene::Layers.save(rootstatements, trailing); if (result.success && controls) m_turntable_included = true; + if (result.success && noses) + m_nose_included = true; if (result.success && placed > 0) { m_vehicle.trainsets.clear(); @@ -4756,7 +4760,7 @@ void editor_mode::render_first_steps() {{"RMB + move", "Hold the right button and move the mouse: look around"}, {"RMB + W S A D", "With the right button held: fly forwards, back and to the sides; E and Q: up and down"}, {"O", "The top view, and back to 3D"}, - {"wheel", "In the top view: zoom in and out where the cursor points"}, + {"wheel", "Zoom in and out where the cursor points"}, {"MMB + move", "In the top view: hold the wheel and drag to move the map"}, {"F", "Fly to what is selected"}}, {{first_step::look, "Look around with the right button held"}, {first_step::top, "Switch to the top view"}}, @@ -5389,8 +5393,28 @@ void editor_mode::on_scroll(double const Xoffset, double const Yoffset) { if (false == ImGui::GetIO().WantCaptureMouse && track_wheel(Yoffset != 0.0 ? Yoffset : Xoffset)) return; - if (false == Global.EditorOrtho || ImGui::GetIO().WantCaptureMouse) + if (ImGui::GetIO().WantCaptureMouse || Yoffset == 0.0) return; + if (false == Global.EditorOrtho) + { + // in 3D the camera flies along the ray under the cursor, by a share of the distance to what it points at; + // a touchpad sends many small steps, together they come to the same + screen_projection const projection; + glm::dvec3 origin; + glm::dvec3 direction; + projection.ray(ImGui::GetIO().MousePos, origin, direction); + glm::dvec3 level; + auto const distance{std::min( + GfxRenderer->Mouse_Hit() ? glm::length(GfxRenderer->Mouse_Position()) : projection.on_level(ImGui::GetIO().MousePos, 0.0, level) ? glm::distance(level, origin) : std::max(20.0, std::abs(Camera.Pos.y) * 2.0), + 20000.0)}; + auto step{distance * (1.0 - std::pow(0.85, Yoffset))}; + // no nearer than 2 m to the point, not through it + if (Yoffset > 0.0) + step = std::max(0.0, std::min(step, distance - 2.0)); + Camera.Pos += direction * step; + Global.pCamera = Camera; + return; + } auto const before{Global.EditorOrthoExtent}; auto const after{std::clamp(before * static_cast(std::pow(0.85, Yoffset)), 5.0f, 50000.0f)}; screen_projection const projection; @@ -5450,6 +5474,8 @@ void editor_mode::update_camera(double const Deltatime) void editor_mode::enter() { m_statebackup = {Global.pCamera, FreeFlyModeFlag, Global.ControlPicking}; + // the editor changes the tracks, their sleepers are worked out as they're needed + track_sleepers::lay_in_background(false); if (simulation::Region != nullptr) { @@ -5544,6 +5570,7 @@ void editor_mode::enter() void editor_mode::exit() { + track_sleepers::lay_in_background(true); Global.editor_tracks = false; EditorModeFlag = false; Global.ControlPicking = m_statebackup.picking; @@ -5739,6 +5766,31 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in return; } + // the properties of a track copied and pasted on the selected ones + if (Global.ctrlState && Global.shiftState && (Key == GLFW_KEY_C || Key == GLFW_KEY_V) && ui()->mode() == nodebank_panel::TRACK) + { + if (is_press(Action) && false == ImGui::GetIO().WantTextInput) + { + auto const tracks{selected_tracks()}; + if (Key == GLFW_KEY_C) + { + // the selected track, or the one under the cursor when there are more of them + auto *from{tracks.size() == 1 ? tracks.front() : m_hover.track}; + if (from != nullptr) + copy_track_style(*from); + else + ui()->set_status(STR_C("Select one track, or point at one, to copy its properties"), true); + } + else if (m_track_style.values.empty()) + ui()->set_status(STR_C("Copy the properties of a track first: Ctrl+Shift+C"), true); + else if (tracks.empty()) + ui()->set_status(STR_C("Select the tracks to paste the properties on"), true); + else + paste_track_style(tracks); + } + return; + } + // save; checked ahead of the camera keys, which have S bound to moving back if (Global.ctrlState && false == Global.shiftState && Key == GLFW_KEY_S) { @@ -5808,6 +5860,9 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in break; case GLFW_KEY_ESCAPE: + // an open list or menu takes this Esc for itself, the selection behind it stays + if (ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) + break; if (is_press(Action)) { road_cancel(); @@ -5817,13 +5872,15 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in { if (m_lay.active && false == m_lay.points.empty()) lay_cancel(); + else if (m_track_style.picking) + m_track_style.picking = false; else if (track_busy()) { cancel_track_tools(); m_straights.tool = 0; } - else if (m_track_tab == track_tab::path && false == m_track_set.empty()) - m_track_set.clear(); + else if (m_track_tab == track_tab::path && m_track_set.size() >= 2) + select_tracks({}); else if (m_track_tab != track_tab::path) show_track_tab(track_tab::path); else if (selected_track() != nullptr) diff --git a/application/editormode.h b/application/editormode.h index 2a81a03d..aa0b250e 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -487,6 +487,8 @@ class editor_mode : public application_mode, private editor_track::observer bool m_opened_at_track{false}; bool m_turntable_included{false}; bool turntable_controls(std::vector &Rootstatements); + bool m_nose_included{false}; + bool nose_controls(std::vector &Rootstatements); void starter_trainset(std::vector &Trailing); std::optional> m_saved_camera; char m_openscenery_search[64]{}; @@ -748,17 +750,12 @@ class editor_mode : public application_mode, private editor_track::observer bool m_track_dirty{false}; std::chrono::steady_clock::time_point m_track_last_commit; editor_track::state m_track_field_before; - std::array, 3> m_track_materials{}; - std::array m_track_material_edited{}; // path whose texture name is being typed - // typed names of the track circuits, the sleeper model and its skin, and the event to add, for the path - TTrack const *m_track_names_for{nullptr}; - std::array m_track_isolated{}; - std::array m_track_sleeper_model{}; - std::array m_track_sleeper_skin{}; + // the event to add to the path, typed + TTrack const *m_track_events_for{nullptr}; std::array m_track_event{}; int m_track_event_list{0}; std::string m_track_events_missing; - void render_path_circuits(TTrack &Track); + void render_path_events(TTrack &Track); std::string m_track_mode_notice; // structure gauge along the line of the selected path, shown in the 3d view struct gauge_state @@ -853,7 +850,7 @@ class editor_mode : public application_mode, private editor_track::observer static std::vector double_paths(double_switch const &Set); // the switch of the template, or the two switches of a double one with the plain track between them, committed and recorded std::vector create_switch_set(editor_track::switch_template const &Shape, std::vector const &Paths, TTrack const &Style, std::string const &Name); - void record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base); + void record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base, double const Middle = 0.0); void render_switch_record(TTrack const &Track) const; void route_store(); bool digitize_drawn(std::vector const &Created, bool const Alone = true); @@ -963,6 +960,22 @@ class editor_mode : public application_mode, private editor_track::observer float roll{0.f}; float radius{0.f}; }; + // the line leaving an end of a switch off its main straight, taken before the switch moves: its paths up to the end of + // the straight past the curve next to the switch. When the switch moves only the paths up to a joint on that straight + // are laid again, so the curve keeps to the end of the switch and the rest of the network stays where it is + struct switch_branch + { + std::size_t joint{0}; + editor_track::chain chain; + std::size_t straight{0}; // the first path of the straight past the curve + alignment::design design; // as the paths lie + alignment::result result; + // made for the new end of the switch + editor_track::chain part; // the paths laid again + std::vector pieces; + alignment::design laid; // of the whole chain + double factor{1.0}; // of the radius and the transitions of the curve + }; struct switch_tool { bool curved{false}; @@ -1001,6 +1014,14 @@ class editor_mode : public application_mode, private editor_track::observer double distance{0.0}; double length{0.0}; std::vector joints; + // the lines leaving the switch off its straight: the joint they leave at, the path of the switch in the shown ones + struct branch_line + { + editor_track::joint joint; + std::size_t path{0}; + switch_branch branch; + }; + std::vector branches; }; slide_state slide; double slide_typed{0.0}; @@ -1023,6 +1044,14 @@ class editor_mode : public application_mode, private editor_track::observer bool switch_room(TTrack &Track, double &Low, double &High, std::string &Reason); bool switch_vacate(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, double const Shift, editor_track::straight const &Line, editor_track::edit &Edit, std::string &Status); bool move_switch_paths(TTrack &Track, std::vector const &Paths, std::string &Status); + std::optional take_switch_branch(TTrack const &Track, editor_track::joint const &Joint, std::size_t const Index) const; + // for the path of the switch the line leaves, as it'll lie + bool plan_switch_branch(segment_data const &Path, editor_track::joint const &Joint, switch_branch &Branch) const; + void lay_switch_branch(switch_branch const &Branch, editor_track::edit &Edit); + // of the joints off the main straight of the switch, the neighbours in Skip left out + std::vector switch_branches(TTrack const &Track, std::vector const &Joints, std::vector const &Skip = {}) const; + // for the paths the switch has now; the joints of the branches which can be laid are handled, the others are left out. Gives the notes for the user + std::string plan_switch_branches(TTrack const &Track, std::vector const &Joints, std::vector &Branches, std::vector &Handled) const; // room on the straight of the whole double switch along the axis of its first switch, and the length of the set bool double_room(double_switch const &Set, double &Low, double &High, double &Length, std::string &Reason); bool move_double_paths(double_switch const &Set, std::vector const &Paths, std::string &Status); @@ -1207,7 +1236,7 @@ class editor_mode : public application_mode, private editor_track::observer bool cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack **Style); bool insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape); bool replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape); - std::vector build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, double const Radius, TTrack &Style, std::string const &Type = "Rkpd"); + std::vector build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, editor_track::slip_curve const &Curve, TTrack &Style, std::string const &Type = "Rkpd"); bool start_extend(editor_track::snap_target const &End); void finish_extend(); std::vector extend_pieces() const; @@ -1297,7 +1326,6 @@ class editor_mode : public application_mode, private editor_track::observer }; track_name_edit m_track_name; void render_track_name(TTrack &Track); - void render_path_parameters(TTrack &Track); void render_straight_ui(); track_tab m_track_tab{track_tab::path}; void delete_selected_track(); @@ -1329,7 +1357,7 @@ class editor_mode : public application_mode, private editor_track::observer void draw_track_hover() const; struct track_intent { - enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide }; + enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style }; kind what{kind::none}; TTrack *track{nullptr}; int path{0}; @@ -1418,25 +1446,21 @@ class editor_mode : public application_mode, private editor_track::observer struct track_box { bool active{false}; + bool replace{false}; // the tracks in the box become the selection, else they join it glm::vec2 from{0.f}; glm::vec2 to{0.f}; TTrack *hovered{nullptr}; }; track_box m_track_box; - struct track_set_fields - { - double velocity{100.0}; - char rail[128]{}; - char trackbed[128]{}; - char circuits[256]{}; - int environment{0}; - }; - track_set_fields m_track_set_fields; glm::mat4 m_track_set_gizmo{1.0f}; bool m_track_set_using{false}; glm::dvec3 m_track_set_pivot{0.0}; std::vector>> m_track_set_joints; bool in_track_set(TTrack const *Track) const; + // the track is selected: alone or in the set + bool track_selected(TTrack const *Track) const; + // a click in the view: the track under the cursor becomes the selection, or nothing + void track_click_select(TTrack *Hovered); bool track_set_begin(bool const Rotate); void track_set_turn(double const Angle); void track_set_move(glm::dvec3 const &Offset); @@ -1444,7 +1468,7 @@ class editor_mode : public application_mode, private editor_track::observer glm::dvec3 track_set_centre() const; void track_set_toggle(TTrack &Track); void track_set_prune(); - void start_track_box(); + void start_track_box(bool const Replace = false); void finish_track_box(); void draw_track_set() const; void render_track_set_ui(); @@ -1469,8 +1493,82 @@ class editor_mode : public application_mode, private editor_track::observer void track_set_grow(); void render_track_spread_ui(); void draw_track_spread() const; - // changes each path of the set which can be changed; false from Change: the path stays as it was - void track_set_apply(std::function const &Change, bool const Rebuild, char const *What); + // the properties of the selected tracks in the inspector, one sheet for one track and for many; see editormodetrackprops.cpp + // the set when there's one, the selected track otherwise + std::vector selected_tracks(); + void select_tracks(std::vector const &Tracks); + // changes each track which can be changed, in one step of the undo; false from Change: the track stays as it was + std::size_t tracks_apply(std::vector const &Tracks, std::function const &Change, bool const Rebuild, char const *What); + int track_property_index(std::string const &Key) const; + std::size_t track_property_apply(int const Property, std::vector const &Tracks, std::string const &Text); + void track_field_settle(); + void track_textures_scan(); + // every texture and material under the folder of the textures, read on the first use of the picker + void track_textures_index(); + // the picker of a texture of the rails (Kind 0) or of the trackbed (1), in an open combo. returns: true when one is chosen, its name in Chosen, "none" for no texture + bool track_texture_picker(int const Kind, std::string const &Current, bool const Mixed, std::string &Chosen); + // image of the texture of the name for the picker, 0 while there's none + std::uint64_t track_texture_image(int const Kind, std::string const &Name); + void render_track_properties(std::vector const &Tracks); + void render_track_set_header(std::vector const &Tracks); + void render_track_select_tools(std::vector const &Tracks); + void render_track_style(std::vector const &Tracks); + void copy_track_style(TTrack const &Track); + std::size_t paste_track_style(std::vector const &Tracks); + std::vector similar_tracks(int const Property, std::vector const &Tracks) const; + // values of the properties over the selected tracks, each with the number of the tracks having it + struct track_values + { + std::size_t signature{0}; + int generation{0}; + int generation_seen{-1}; + std::size_t history{0}; + int frame{-100}; + std::vector>> values; + std::vector applies; + }; + track_values m_track_values; + // the field being typed in, with the tracks it was started on + struct track_field + { + int property{-1}; + std::vector tracks; + std::array text{}; + std::string initial; + int frame{0}; + }; + track_field m_track_field; + struct track_texture_use + { + material_handle material{null_handle}; + std::string name; + int count{0}; + }; + std::array, 2> m_track_textures; // of the rails, of the trackbed + std::array m_track_texture_filter{}; + struct texture_index_entry + { + std::string name; // as the materials are named: relative to the folder of the textures, without the extension + std::string lowered; + bool material{false}; // there's a .mat file of the name + }; + std::vector m_track_texture_index; // sorted by the name + bool m_track_texture_indexed{false}; + std::string m_track_texture_hovered; // the tile under the mouse in the picker, shown large above them + // properties copied from a track, to be pasted on others + struct track_style + { + std::string from; + std::vector> values; + // speed, environment, overhead line, track circuits, rails, trackbed, its shape, sleepers, ride and sound, frogs + std::array groups{false, false, false, false, true, true, true, true, false, true}; + bool open{false}; + bool picking{false}; // the next click on a track in the view copies its properties + }; + track_style m_track_style; + std::vector> m_track_similar; + TTrack const *m_track_list_hover{nullptr}; // in the list of the selected tracks + int m_track_list_hover_frame{-1}; // second track alongside the selected path or the line through it, at a spacing, curves included struct parallel_tool { @@ -1937,6 +2035,7 @@ class editor_mode : public application_mode, private editor_track::observer selftest_result selftest_join(std::string const &command, std::istringstream &words); selftest_result selftest_signal(std::string const &command, std::istringstream &words); selftest_result selftest_terrain(std::string const &command, std::istringstream &words); + selftest_result selftest_props(std::string const &command, std::istringstream &words); selftest_result selftest_dispatch(std::string const &command, std::istringstream &words); void selftest_step(); // a value typed during a gesture, taken instead of the one the cursor gives diff --git a/application/editormodeconnection.cpp b/application/editormodeconnection.cpp index 73f2513b..b5f9142e 100644 --- a/application/editormodeconnection.cpp +++ b/application/editormodeconnection.cpp @@ -313,7 +313,7 @@ void editor_mode::insert_connection() auto second{glm::normalize(plan_of(plan.ends[3] - plan.ends[2]))}; if (glm::dot(first, second) < 0.0) second = -second; - auto const parts{build_double_slip(plan.ends[0], plan.ends[1], plan.ends[2], plan.ends[3], plan_of(plan.centre), plan.centre.y, first, second, plan.angle, 0.0, *style, "Skrz")}; + auto const parts{build_double_slip(plan.ends[0], plan.ends[1], plan.ends[2], plan.ends[3], plan_of(plan.centre), plan.centre.y, first, second, plan.angle, editor_track::slip_curve{}, *style, "Skrz")}; editor_track::commit(parts); created.insert(created.end(), parts.begin(), parts.end()); } diff --git a/application/editormoderecords.cpp b/application/editormoderecords.cpp index 85501650..5a3ac2ce 100644 --- a/application/editormoderecords.cpp +++ b/application/editormoderecords.cpp @@ -346,7 +346,7 @@ std::vector editor_mode::double_paths(double_switch const &Set) return {Set.first->m_paths[0], Set.first->m_paths[1], Set.second->m_paths[0], Set.second->m_paths[1], link}; } -void editor_mode::record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base) +void editor_mode::record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base, double const Middle) { if (Parts.empty() || scene::Layers.empty()) return; @@ -355,6 +355,7 @@ void editor_mode::record_slip(std::vector const &Parts, std::string co entry.name = Base; entry.type = Type; entry.radius = Radius; + entry.middle = Middle; entry.ratio = Angle > 1e-9 ? 1.0 / std::tan(Angle) : 0.0; for (auto const *part : Parts) if (part != nullptr && false == part->name().empty() && (Kind == "crossing" || part->eType == tt_Switch)) diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 7ae14bb0..5c61c2f8 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -40,15 +40,29 @@ http://mozilla.org/MPL/2.0/. #include "scene/sceneterrain.h" #include "editor/editorSpeed.hpp" +#include #include #include #include #include #include +namespace { + +// frames gone through the selftest, for the measurements of perf +std::size_t selftest_frames{0}; + +std::string one_line(std::string Text) +{ + std::replace(Text.begin(), Text.end(), '\n', ';'); + return Text; +} + +} // namespace + editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &command, std::istringstream &words) { - for (auto const group : {&editor_mode::selftest_scene, &editor_mode::selftest_sweep, &editor_mode::selftest_track, &editor_mode::selftest_switch, &editor_mode::selftest_rails, &editor_mode::selftest_join, &editor_mode::selftest_signal, &editor_mode::selftest_terrain}) + for (auto const group : {&editor_mode::selftest_scene, &editor_mode::selftest_sweep, &editor_mode::selftest_track, &editor_mode::selftest_switch, &editor_mode::selftest_rails, &editor_mode::selftest_join, &editor_mode::selftest_signal, &editor_mode::selftest_terrain, &editor_mode::selftest_props}) if (auto const result{(this->*group)(command, words)}; result != selftest_result::unhandled) return result; return selftest_result::unhandled; @@ -69,6 +83,13 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm m_focus_active = false; return selftest_result::handled; } + if (command == "zoom") + { + float factor{1.0f}; + words >> factor; + Global.ZoomFactor = std::max(factor, 0.01f); + return selftest_result::handled; + } if (command == "selectat") { glm::dvec3 point{0.0}; @@ -102,6 +123,170 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm WriteLog("SELFTEST select " + name + (m_node != nullptr ? "" : ": no such track")); return selftest_result::handled; } + if (command == "perf") + { + // the frames drawn and the time gone since the last perf, with the timings and the counts the renderer keeps + std::string label; + words >> label; + static auto last{std::chrono::steady_clock::now()}; + static std::size_t lastframe{0}; + auto const now{std::chrono::steady_clock::now()}; + auto const frames{selftest_frames - lastframe}; + auto const elapsed{std::chrono::duration(now - last).count()}; + WriteLog(format("SELFTEST perf %s: %zu frames, %.3f ms per frame | %s | %s", label.c_str(), frames, frames > 0 ? elapsed / frames : 0.0, one_line(GfxRenderer->info_times()).c_str(), one_line(GfxRenderer->info_stats()).c_str())); + last = now; + lastframe = selftest_frames; + return selftest_result::handled; + } + if (command == "rebuildall") + { + // the geometry of every path made anew, as the editor does it after a change, timed + auto const start{std::chrono::steady_clock::now()}; + auto count{0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved) + continue; + auto §ion{simulation::Region->section(track->location())}; + if (false == section.m_geometrycreated) + continue; + track->rebuild_geometry(section.m_geometrybank); + ++count; + } + WriteLog(format("SELFTEST rebuildall: %d paths in %.1f ms", count, std::chrono::duration(std::chrono::steady_clock::now() - start).count())); + // the sleepers laid again on all the tracks, which the renderer otherwise does as they come in sight, timed by section + std::map sectiontimes; + auto laidcount{0}; + std::size_t parts{0}; + auto laytime{0.0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || false == track->m_sleepers3d.active) + continue; + auto const laystart{std::chrono::steady_clock::now()}; + track_sleepers::lay(*track); + auto const took{std::chrono::duration(std::chrono::steady_clock::now() - laystart).count()}; + laytime += took; + sectiontimes[&simulation::Region->section(track->location())] += took; + ++laidcount; + for (auto const &instances : track->m_sleepers3d.instances) + parts += instances.size(); + } + auto slowest{0.0}; + for (auto const §iontime : sectiontimes) + slowest = std::max(slowest, sectiontime.second); + WriteLog(format("SELFTEST rebuildall sleepers3d: %d tracks laid in %.1f ms, %zu parts; %zu sections, %.2f ms per section, %.2f ms the slowest", laidcount, laytime, parts, sectiontimes.size(), + (sectiontimes.empty() ? 0.0 : laytime / sectiontimes.size()), slowest)); + return selftest_result::handled; + } + if (command == "beams") + { + // the beams the track lays, between two distances along it: their middles, lengths and the way they lie square to + std::string name; + double from{-1e9}, to{1e9}; + words >> name >> from >> to; + auto *track{simulation::Paths.find(name)}; + if (track == nullptr || false == track->m_sleepers3d.active) + { + WriteLog("SELFTEST beams: no track with beams " + name); + return selftest_result::handled; + } + std::string text; + for (auto const &beam : track_sleepers::place(*track)) + { + auto const middle{0.5 * (beam.from + beam.to)}; + if (middle.z < from || middle.z > to) + continue; + text += format(" [%.2f %.2f L%.2f a%.2f%s]", middle.x, middle.z, glm::length(beam.to - beam.from), glm::degrees(std::atan2(beam.along.x, beam.along.z)), beam.plain ? " p" : ""); + } + WriteLog("SELFTEST beams " + name + ":" + text); + return selftest_result::handled; + } + if (command == "sharedzones") + { + // how far the timbers shared with other tracks reach along the paths of the track and of the tracks joined to its ends + std::string name; + words >> name; + auto *track{simulation::Paths.find(name)}; + if (track == nullptr) + { + WriteLog("SELFTEST sharedzones: no track " + name); + return selftest_result::handled; + } + auto const zones_text = [](TTrack const &Track, int const Path) { + std::string text; + for (auto const &zone : track_sleepers::shared_zones(Track, Path)) + text += format(" %.2f..%.2f", zone.first, zone.second); + return text.empty() ? std::string{" none"} : text; + }; + auto const paths{track->eType == tt_Switch ? 2 : 1}; + for (int path = 0; path < paths; ++path) + { + auto const length{track->eType == tt_Switch ? track->SwitchExtension->Segments[path]->GetLength() : track->Segment->GetLength()}; + WriteLog(format("SELFTEST sharedzones %s path %d (%.2f m):%s", name.c_str(), path, length, zones_text(*track, path).c_str())); + // the track going on from the end of the path, and how far past the end the shared timbers reach along it + auto *next{track->eType == tt_Switch ? track->SwitchExtension->pNexts[path] : track->trNext}; + if (next == nullptr || next->eType != tt_Normal || next->Segment == nullptr) + continue; + glm::dvec3 end, start, unused; + glm::vec3 angles; + (track->eType == tt_Switch ? track->SwitchExtension->Segments[path] : track->Segment)->RaPositionGet(length, end, angles); + next->Segment->RaPositionGet(0.0, start, angles); + auto const fromstart{glm::length(start - end) < 0.1}; + auto past{0.0}; + for (auto const &zone : track_sleepers::shared_zones(*next, 0)) + { + if (fromstart && zone.first < 0.05) + past = zone.second; + else if (false == fromstart && zone.second > next->Segment->GetLength() - 0.05) + past = next->Segment->GetLength() - zone.first; + } + WriteLog(format("SELFTEST sharedzones %s path %d goes on into %s:%s; shared timbers reach %.2f m past the end", name.c_str(), path, next->name().c_str(), zones_text(*next, 0).c_str(), past)); + } + return selftest_result::handled; + } + if (command == "sleepersbg") + { + // the sleepers worked out in the background as in the simulation, to see they come out the same, or back to as they're needed + int on{1}; + words >> on; + track_sleepers::lay_in_background(on != 0); + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr) + track->m_sleepers3d.dirty = true; + track_sleepers::unsettle_around(*simulation::Paths.sequence().front()); + WriteLog(format("SELFTEST sleepersbg %d", on)); + return selftest_result::handled; + } + if (command == "beamsnear") + { + double x{0.0}, z{0.0}, radius{3.0}; + words >> x >> z >> radius; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || false == track->m_sleepers3d.active) + continue; + for (auto const &beam : track_sleepers::place(*track)) + { + auto const middle{0.5 * (beam.from + beam.to)}; + if (glm::length(glm::dvec2{middle.x - x, middle.z - z}) > radius) + continue; + WriteLog(format("SELFTEST beam %s: %.2f %.2f -> %.2f %.2f, %.2f m, along %.1f deg%s", track->name().c_str(), beam.from.x, beam.from.z, beam.to.x, beam.to.z, glm::length(beam.to - beam.from), glm::degrees(std::atan2(beam.along.x, beam.along.z)), beam.plain ? " plain" : "")); + } + } + return selftest_result::handled; + } + if (command == "sleepercheck") + { + auto const start{std::chrono::steady_clock::now()}; + auto const result{track_sleepers::check()}; + WriteLog(format("SELFTEST sleepercheck: %zu beams, %zu overlaps, %zu gaps, %zu bare rails, %zu too long (longest %.2f m), widest spacing %.2f m at %s; %zu shared off one plane, %zu switch panels off one plane, %zu ends clipped, %zu beams dropped, slowest cluster %.1f ms of %zu tracks; in %.1f ms", + result.beams, result.overlaps, result.gaps, result.bare, result.toolong, result.longest, result.widest, result.widestat.c_str(), result.offplane, result.panels, result.clipped, result.dropped, result.slowest, result.biggest, + std::chrono::duration(std::chrono::steady_clock::now() - start).count())); + for (auto const &example : result.examples) + WriteLog("SELFTEST sleepercheck " + example); + return selftest_result::handled; + } if (command == "frames") { std::string name; @@ -230,7 +415,7 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm if (command == "mapstate") { auto const marked{scene::Layers.empty() ? std::vector{} : scene::Layers.marked(scene::layer_handle{1}, "//$o")}; - WriteLog(format("SELFTEST mapstate orthophoto %d top %d extent %.0f mark %s", m_orthophoto.enabled() ? 1 : 0, Global.EditorOrtho ? 1 : 0, Global.EditorOrthoExtent, marked.empty() ? "-" : marked.front().c_str())); + WriteLog(format("SELFTEST mapstate orthophoto %d top %d extent %.0f mark %s camera %.1f %.1f %.1f", m_orthophoto.enabled() ? 1 : 0, Global.EditorOrtho ? 1 : 0, Global.EditorOrthoExtent, marked.empty() ? "-" : marked.front().c_str(), Camera.Pos.x, Camera.Pos.y, Camera.Pos.z)); return selftest_result::handled; } if (command == "trainset") @@ -1160,6 +1345,36 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm WriteLog(format("SELFTEST vprofile zone %.3f %.3f %s", zone.from, zone.to, zone.name.c_str())); return selftest_result::handled; } + if (command == "vprofileapply") + { + // a plain grade line from the height of the start of the open route, applied to it + double grade{0.0}; + words >> grade; + auto &state{m_profile}; + if (state.samples.empty()) + { + WriteLog("SELFTEST vprofileapply: no route open"); + return selftest_result::handled; + } + auto const start{state.samples.front().position.y}; + state.line.points = {{0.0, start, 0.0, false}, {state.route.length, start + grade / 1000.0 * state.route.length, 0.0, false}}; + state.line.join_start = false; + state.line.join_end = false; + profile_check(); + profile_apply(); + WriteLog("SELFTEST vprofileapply: " + state.status + " " + state.error); + for (auto const &span : state.route.spans) + { + if (span.track->eType != tt_Switch || span.track->SwitchExtension == nullptr) + continue; + for (std::size_t i = 0; i < span.track->m_paths.size() && i < 2; ++i) + { + auto const &path{span.track->m_paths[i]}; + WriteLog(format("SELFTEST switch %s path %zu%s: %zu pieces kept, %zu in use, ends %.3f %.3f", span.track->name().c_str(), i + 1, static_cast(i) == span.path ? " (route)" : "", path.pieces.size(), span.track->SwitchExtension->Segments[i]->PieceCount(), path.points[segment_data::point::start].y, path.points[segment_data::point::end].y)); + } + } + return selftest_result::handled; + } if (command == "backwardcurve") { std::string name; @@ -1359,6 +1574,7 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com glm::dvec3 at{0.0}; glm::dvec3 toward{0.0}; words >> name >> at.x >> at.z >> toward.x >> toward.z >> label; + std::replace(label.begin(), label.end(), '_', ' '); m_node = simulation::Paths.find(name); ui()->set_node(m_node); show_track_tab(track_tab::turnout); @@ -1581,6 +1797,22 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com WriteLog("SELFTEST conecheck " + name + ":" + text); return selftest_result::handled; } + if (command == "throw") + { + std::string name; + int state{0}; + words >> name >> state; + auto *track{simulation::Paths.find(name)}; + if (track != nullptr && track->eType == tt_Switch) + { + track->Switch(state, 0.f, 0.f); + // the blades go over at once, the editor doesn't run the animations of the switches + track->SwitchExtension->fOffset = track->SwitchExtension->fDesiredOffset; + track->RaAnimate(); + } + WriteLog(format("SELFTEST throw %s %d%s", name.c_str(), state, track != nullptr && track->eType == tt_Switch ? "" : ": no such switch")); + return selftest_result::handled; + } if (command == "switchdrive") { int index{-1}; @@ -2874,6 +3106,105 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm } return selftest_result::handled; } + if (command == "beddump") + { + // the trackbed triangles and the axes of the paths of the tracks near a point, in plan, for a look at where the trackbed has holes + glm::dvec3 point{0.0}; + double radius{30.0}; + std::string file; + words >> point.x >> point.z >> radius >> file; + std::ofstream output{file, std::ios_base::trunc}; + auto triangles{0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->m_paths.empty()) + continue; + if (geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point) > radius) + continue; + auto const paths{track->eType == tt_Switch ? 2 : 1}; + for (int path = 0; path < paths; ++path) + { + auto const &segment{track->eType == tt_Switch ? track->SwitchExtension->Segments[path] : track->Segment}; + if (segment == nullptr) + continue; + output << "axis " << track->name() << ' ' << path; + for (auto at{0.0}; at <= segment->GetLength() + 1e-6; at += 0.25) + { + glm::dvec3 spot, across; + segment->plan_frame(std::min(at, segment->GetLength()), spot, across); + output << format(" %.4f %.4f", spot.x, spot.z); + } + output << "\n"; + } + std::vector beds; + if (track->eType == tt_Switch && track->SwitchExtension != nullptr) + beds.push_back(track->SwitchExtension->Geometry3); + else + beds.insert(beds.end(), track->Geometry2.begin(), track->Geometry2.end()); + for (auto const &bed : beds) + { + if (bed == null_handle) + continue; + auto const &vertices{GfxRenderer->Vertices(bed)}; + for (std::size_t i = 2; i < vertices.size(); ++i) + { + glm::dvec3 const a{glm::dvec3{vertices[i - 2].position} + track->m_origin}; + glm::dvec3 const b{glm::dvec3{vertices[i - 1].position} + track->m_origin}; + glm::dvec3 const c{glm::dvec3{vertices[i].position} + track->m_origin}; + output << format("tri %s %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f\n", track->name().c_str(), a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z); + ++triangles; + } + } + } + WriteLog(format("SELFTEST beddump: %d triangles into %s", triangles, file.c_str())); + return selftest_result::handled; + } + if (command == "raildump") + { + // the rail geometry of the tracks near a point as drawn: s strips of the lofts, t triangles of the cut faces and the rest + glm::dvec3 point{0.0}; + double radius{30.0}; + std::string file; + words >> point.x >> point.z >> radius >> file; + std::ofstream output{file, std::ios_base::trunc}; + auto chunks{0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->m_paths.empty()) + continue; + if (geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point) > radius) + continue; + output << format("track %s %.4f\n", track->name().c_str(), track->m_paths.front().points[segment_data::point::start].y); + auto const dump = [&](TTrack::geometryhandle_sequence const &Sequence, char const *List, char const Kind) { + for (std::size_t index = 0; index < Sequence.size(); ++index) + { + auto const &vertices{GfxRenderer->Vertices(Sequence[index])}; + if (vertices.empty()) + continue; + auto const primitive{GfxRenderer->Primitive(Sequence[index])}; + auto const kind{primitive == GL_TRIANGLES ? 't' : primitive == GL_TRIANGLE_STRIP ? 's' : Kind}; + output << format("chunk %s %s %zu %c %zu\n", track->name().c_str(), List, index, kind, vertices.size()); + for (auto const &vertex : vertices) + { + glm::dvec3 const at{glm::dvec3{vertex.position} + track->m_origin}; + output << format("%.4f %.4f %.4f\n", at.x, at.y, at.z); + } + ++chunks; + } + }; + dump(track->Geometry1, "rails1", 's'); + dump(track->Geometry2, track->eType == tt_Switch ? "rails2" : "bed", 's'); + dump(track->GeometryCut1, "cut1", 't'); + dump(track->GeometryCut2, "cut2", 't'); + for (std::size_t i = 0; i < track->GeometryTreads.size(); ++i) + { + dump(track->GeometryTreads[i].geometry, "tread", 's'); + dump(track->GeometryTreads[i].cuts, "treadcut", 't'); + } + } + WriteLog(format("SELFTEST raildump: %d chunks into %s", chunks, file.c_str())); + return selftest_result::handled; + } if (command == "geomhash") { std::string name; @@ -2974,6 +3305,507 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm } return selftest_result::handled; } + if (command == "headgap") + { + // the least gap between the heads of the rails of two tracks running side by side, by the gauge lines and the head of the profile + std::string first, second; + words >> first >> second; + auto *a{simulation::Paths.find(first)}; + auto *b{simulation::Paths.find(second)}; + if (a == nullptr || b == nullptr) + { + WriteLog("SELFTEST headgap: no such tracks"); + return selftest_result::handled; + } + auto const head_a{a->rail_dimensions().head}; + auto const head_b{b->rail_dimensions().head}; + for (int pa = 0; pa < (a->eType == tt_Switch ? 2 : 1); ++pa) + for (auto const sa : {-1, 1}) + for (int pb = 0; pb < (b->eType == tt_Switch ? 2 : 1); ++pb) + for (auto const sb : {-1, 1}) + { + auto const length{a->eType == tt_Switch ? a->SwitchExtension->Segments[pa]->GetLength() : a->Segment->GetLength()}; + auto const lengthb{b->eType == tt_Switch ? b->SwitchExtension->Segments[pb]->GetLength() : b->Segment->GetLength()}; + auto least{1e9}; + auto where{0.0}; + for (auto at{0.0}; at <= length; at += 0.02) + { + double d, l; + track_rails::rail_offset(*b, pb, sb, track_rails::rail_point(*a, pa, sa, at, head_a), d, l); + if (d < 0.0 || d > lengthb || l < -0.3 || l > 0.3) + continue; + if (l - head_b < least) + { + least = l - head_b; + where = at; + } + } + if (least < 1e8) + WriteLog(format("SELFTEST headgap %s/%d%+d to %s/%d%+d: %.1f mm at %.2f m (heads %.1f %.1f mm, width %.4f)", first.c_str(), pa, sa, second.c_str(), pb, sb, least * 1000.0, where, head_a * 1000.0, + head_b * 1000.0, a->fTrackWidth)); + } + return selftest_result::handled; + } + if (command == "checks") + { + // check rails of a track as they're drawn, and the rails of the tracks they run into + std::string name; + words >> name; + int count{0}; + int faults{0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->name() != name) + continue; + auto const layout{track->plan_rails(true)}; + auto const size{track->rail_dimensions()}; + std::vector others; + for (auto *other : simulation::Paths.sequence()) + if (other != nullptr && false == other->m_editorremoved && (other->iCategoryFlag & 15) == 1 && glm::length(other->location() - track->location()) < other->radius() + track->radius() + 1.0) + others.push_back(other); + for (int path = 0; path < (track->eType == tt_Switch ? 2 : 1); ++path) + for (int right = 0; right < 2; ++right) + { + auto const side{right ? 1 : -1}; + for (auto const &check : layout.checks[path][right]) + { + if (track_rails::replaced_by_wing(check, layout.wingends[path][right])) + continue; + auto const ownlength{track->eType == tt_Switch ? track->SwitchExtension->Segments[path]->GetLength() : track->Segment->GetLength()}; + auto const from{std::max(check.from, 0.0)}; + auto const to{std::min(check.to, ownlength)}; + if (to - from <= 0.01) + continue; + ++count; + std::string hits; + for (auto at{from}; at <= to; at += 0.05) + // the head of the check rail as drawn, 40 mm at the head of the rail of 73.5 + for (auto const lateral : {-track_rails::check_flangeway, -track_rails::check_flangeway - 0.020 * size.head / 0.0735, -track_rails::check_flangeway - 0.040 * size.head / 0.0735}) + { + auto const point{track_rails::rail_point(*track, path, side, at, lateral)}; + for (auto *other : others) + for (int otherpath = 0; otherpath < (other->eType == tt_Switch ? 2 : 1); ++otherpath) + for (auto const otherside : {-1, 1}) + { + if (other == track && otherpath == path && otherside == side) + continue; + auto const length{other->eType == tt_Switch ? other->SwitchExtension->Segments[otherpath]->GetLength() : other->Segment->GetLength()}; + double d, l; + track_rails::rail_offset(*other, otherpath, otherside, point, d, l); + if (d < 0.0 || d > length || l < 0.0 || l > other->rail_dimensions().head) + continue; + auto const hit{format(" %s/%d%+d", other->name().c_str(), otherpath, otherside)}; + if (hits.find(hit) == std::string::npos) + hits += hit + format(" at %.2f m", at); + } + } + if (false == hits.empty()) + ++faults; + WriteLog(format("SELFTEST check %s path %d side %+d: %.3f-%.3f m drawn of %.3f-%.3f m%s", name.c_str(), path, side, from, to, check.from, check.to, hits.empty() ? ", clear" : (", over" + hits + " FAULT").c_str())); + } + } + } + WriteLog(format("SELFTEST checks: %d check rails, %d faults", count, faults)); + return selftest_result::handled; + } + if (command == "cuts") + { + // the cuts of the rails of a track, with the ends of the parts in the world + std::string name, detail; + words >> name >> detail; + auto *track{simulation::Paths.find(name)}; + if (track == nullptr) + return selftest_result::handled; + auto const layout{track->plan_rails(true)}; + auto const head{track->rail_dimensions().head}; + // with knots: the lines of the parts too + auto const knots_of = [](knots::line const &Line) { + std::string result; + for (auto const &knot : Line) + result += format(" %.3f:%.4f", knot.x, knot.y); + return result; + }; + for (int path = 0; path < (track->eType == tt_Switch ? 2 : 1); ++path) + for (int right = 0; right < 2; ++right) + for (auto const &cut : layout.plans[path][right].cuts) + { + auto const side{right ? 1 : -1}; + auto const where = [&](double const At, knots::line const &Offsets) { + auto const point{track_rails::rail_point(*track, path, side, At, knots::sample(Offsets, At, 0.0) + 0.5 * head)}; + return format("%.3f m (%.3f %.3f) offset %.3f", At, point.x, point.z, knots::sample(Offsets, At, 0.0)); + }; + WriteLog(format("SELFTEST cut %s path %d side %+d at %.3f: end %s%s, start %s%s, of %.3f m", name.c_str(), path, side, cut.at, where(cut.end, cut.endoffsets).c_str(), cut.endextents.empty() ? "" : " extents", where(cut.start, cut.startoffsets).c_str(), cut.startextents.empty() ? "" : " extents", track_rails::path_segment(*track, path).GetLength())); + if (detail == "knots") + for (auto const &[label, line] : std::initializer_list>{ + {"endoffsets", &cut.endoffsets}, {"endextents", &cut.endextents}, {"endinner", &cut.endinner}, {"endrolled", &cut.endrolled}, {"endworn", &cut.endworn}, + {"startoffsets", &cut.startoffsets}, {"startextents", &cut.startextents}, {"startinner", &cut.startinner}, {"startrolled", &cut.startrolled}, {"startworn", &cut.startworn}}) + if (false == line->empty()) + WriteLog(format("SELFTEST %s%s", label, knots_of(*line).c_str())); + } + return selftest_result::handled; + } + if (command == "wings") + { + // the heads of the rails bent out at the cuts, the wing rails mostly, against the heads of the other rails as they're laid out + std::string name; + words >> name; + std::map layouts; + auto const layout_of = [&](TTrack *Track) -> track_rails::rail_layout const & { + auto found{layouts.find(Track)}; + if (found == layouts.end()) + found = layouts.emplace(Track, Track->plan_rails(true)).first; + return found->second; + }; + auto const length_of = [](TTrack const &Track, int const Path) { return Track.eType == tt_Switch ? Track.SwitchExtension->Segments[Path]->GetLength() : Track.Segment->GetLength(); }; + // whether the head of a rail lies at a distance along it and out from its gauge line, the part of the nearest cut there + auto const occupied = [](track_rails::rail_plan const &Plan, double const Head, double const At, double const Lateral) { + if (Plan.cuts.empty()) + return Lateral >= 0.0 && Lateral <= Head; + auto const *cut{&Plan.cuts.front()}; + for (auto const &each : Plan.cuts) + if (std::abs(each.at - At) < std::abs(cut->at - At)) + cut = &each; + auto const part = [&](knots::line const &Offsets, knots::line const &Extents, knots::line const &Inner) { + auto const offset{knots::sample(Offsets, At, 0.0)}; + return Lateral >= offset + std::max(0.0, knots::sample(Inner, At, 0.0)) && Lateral <= offset + std::min(Head, knots::sample(Extents, At, Head)); + }; + return (At <= cut->end && part(cut->endoffsets, cut->endextents, cut->endinner)) || (At >= cut->start && part(cut->startoffsets, cut->startextents, cut->startinner)); + }; + int count{0}; + int faults{0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->m_paths.empty()) + continue; + if (name != "all" && track->name() != name) + continue; + auto const &layout{layout_of(track)}; + auto const head{track->rail_dimensions().head}; + std::vector others; + for (auto *other : simulation::Paths.sequence()) + if (other != nullptr && false == other->m_editorremoved && (other->iCategoryFlag & 15) == 1 && false == other->m_paths.empty() && glm::length(other->location() - track->location()) < other->radius() + track->radius() + 1.0) + others.push_back(other); + for (int path = 0; path < (track->eType == tt_Switch ? 2 : 1); ++path) + for (int right = 0; right < 2; ++right) + { + auto const side{right ? 1 : -1}; + auto const ownlength{length_of(*track, path)}; + for (auto const &cut : layout.plans[path][right].cuts) + for (auto const coming : {true, false}) + { + auto const &offsets{coming ? cut.endoffsets : cut.startoffsets}; + if (offsets.size() < 2 || offsets.back().x - offsets.front().x < 0.05) + continue; + auto const from{std::max(0.0, coming ? offsets.front().x : std::max(offsets.front().x, cut.start))}; + auto const to{std::min(ownlength, coming ? std::min(offsets.back().x, cut.end) : offsets.back().x)}; + if (to - from < 0.05) + continue; + ++count; + std::string hits; + for (auto at{from}; at <= to; at += 0.02) + { + auto const offset{knots::sample(offsets, at, 0.0)}; + if (offset <= 0.002) + continue; + // only what is left of the head where the wing rail is planed + auto const lo{offset + std::max(0.0, knots::sample(coming ? cut.endinner : cut.startinner, at, 0.0))}; + auto const hi{offset + std::min(head, knots::sample(coming ? cut.endextents : cut.startextents, at, head))}; + if (hi - lo < 0.012) + continue; + for (auto const lateral : {lo + 0.005, 0.5 * (lo + hi), hi - 0.005}) + { + auto const point{track_rails::rail_point(*track, path, side, at, lateral)}; + for (auto *other : others) + for (int otherpath = 0; otherpath < (other->eType == tt_Switch ? 2 : 1); ++otherpath) + for (auto const otherside : {-1, 1}) + { + if (other == track && otherpath == path && otherside == side) + continue; + double d, l; + track_rails::rail_offset(*other, otherpath, otherside, point, d, l); + if (d < 0.0 || d > length_of(*other, otherpath) || l < -0.3 || l > 0.3) + continue; + // a wing rail planed flush against another head from the outside covers its rounded shoulder + auto const theirs{other->rail_dimensions()}; + if (false == occupied(layout_of(other).plans[otherpath][otherside > 0], theirs.head - theirs.shoulder, d, l)) + continue; + auto const hit{format(" %s/%d%+d", other->name().c_str(), otherpath, otherside)}; + if (hits.find(hit) == std::string::npos) + hits += hit + format(" at %.2f m (theirs %.2f m, %.0f mm in)", at, d, l * 1000.0); + } + } + } + if (false == hits.empty()) + ++faults; + if (false == hits.empty() || name != "all") + WriteLog(format("SELFTEST wing %s path %d side %+d cut at %.3f, %s part %.3f-%.3f m, out %.0f-%.0f mm%s", track->name().c_str(), path, side, cut.at, coming ? "coming" : "going", from, to, offsets.front().y * 1000.0, + offsets.back().y * 1000.0, hits.empty() ? ", clear" : (", over" + hits + " FAULT").c_str())); + } + } + } + WriteLog(format("SELFTEST wings: %d bent parts, %d faults", count, faults)); + return selftest_result::handled; + } + if (command == "space") + { + // the rails as laid out, every 2 cm along their parts: a head in the flangeway of another rail where its wheels run (F), over the head of + // another rail past its shoulder (O), a line of a part planing on past its last knot (C), the head jumping in width (J), nothing left of the + // head inside a part (V) + std::string name; + double tolerance{0.5}; + words >> name >> tolerance; + if (name.empty()) + name = "all"; + tolerance *= 0.001; + struct piece + { + double from{-1e9}; + double to{1e9}; + track_rails::rail_cut const *before{nullptr}; + track_rails::rail_cut const *after{nullptr}; + }; + struct rail + { + TTrack *track{nullptr}; + int path{0}; + int side{0}; + double length{0.0}; + double laidfrom{0.0}; + double heel{0.0}; + track_rails::rail_size size; + track_rails::gauge_line const *line{nullptr}; + std::vector pieces; + }; + std::map layouts; + std::map> rails; + auto const rails_of = [&](TTrack *Track) -> std::vector & { + auto found{rails.find(Track)}; + if (found != rails.end()) + return found->second; + auto &result{rails[Track]}; + auto const &layout{layouts.emplace(Track, Track->plan_rails(true)).first->second}; + auto const heel{Track->eType == tt_Switch ? std::max(track_rails::blade_length(*Track, 0), track_rails::blade_length(*Track, 1)) : 0.0}; + for (int path = 0; path < track_rails::path_count(*Track); ++path) + for (int right = 0; right < 2; ++right) + { + rail each; + each.track = Track; + each.path = path; + each.side = (right ? 1 : -1); + each.length = track_rails::path_segment(*Track, path).GetLength(); + each.laidfrom = (Track->eType == tt_Switch && each.side == (Track->SwitchExtension->RightSwitch == (path == 0) ? 1 : -1) ? track_rails::blade_length(*Track, path) : 0.0); + each.heel = heel; + each.size = Track->rail_dimensions(); + each.line = &track_rails::rail_line(*Track, path, right, false); + std::vector cuts; + for (auto const &cut : layout.plans[path][right].cuts) + cuts.push_back(&cut); + std::stable_sort(cuts.begin(), cuts.end(), [](track_rails::rail_cut const *A, track_rails::rail_cut const *B) { return A->at < B->at; }); + for (std::size_t j = 0; j <= cuts.size(); ++j) + { + piece part; + part.before = (j > 0 ? cuts[j - 1] : nullptr); + part.after = (j < cuts.size() ? cuts[j] : nullptr); + if (part.before != nullptr) + part.from = part.before->start; + if (part.after != nullptr) + part.to = part.after->end; + each.pieces.push_back(part); + } + result.push_back(each); + } + return result; + }; + knots::line const none; + struct lines + { + knots::line const *offsets; + knots::line const *extents; + knots::line const *inner; + }; + // the lines of the part from the nearer of its cuts + auto const lines_at = [&](piece const &Part, double const S) -> lines { + auto const before{Part.before != nullptr && (Part.after == nullptr || std::abs(S - Part.before->at) <= std::abs(S - Part.after->at))}; + if (before) + return {&Part.before->startoffsets, &Part.before->startextents, &Part.before->startinner}; + if (Part.after != nullptr) + return {&Part.after->endoffsets, &Part.after->endextents, &Part.after->endinner}; + return {&none, &none, &none}; + }; + auto const head_of = [&](rail const &Rail, piece const &Part, double const S, double &Offset, double &Low, double &High) { + auto const l{lines_at(Part, S)}; + Offset = knots::sample(*l.offsets, S, 0.0); + Low = Offset + std::max(0.0, knots::sample(*l.inner, S, 0.0)); + High = Offset + std::min(Rail.size.head, knots::sample(*l.extents, S, Rail.size.head)); + }; + // whether the head of a rail covers a point out from its gauge line, past its rounded shoulder and Tolerance + auto const covers = [&](rail const &Rail, double const D, double const L) { + for (auto const &part : Rail.pieces) + { + if (D < part.from || D > part.to) + continue; + double offset, low, high; + head_of(Rail, part, D, offset, low, high); + if (L > low + tolerance && L < std::min(high, offset + Rail.size.head - Rail.size.shoulder) - tolerance) + return true; + } + return false; + }; + // the parts near an obtuse crossing of either rail are designed to meet + auto const obtuse_near = [](piece const &Part, double const S) { + for (auto const *cut : {Part.before, Part.after}) + if (cut != nullptr && cut->obtuse && std::abs(S - cut->at) < 0.3) + return true; + return false; + }; + auto const throwable = [](piece const &Part) { return Part.before != nullptr && Part.before->throwable; }; + int counts[5]{}; + int parts{0}; + char const kinds[]{"FOCJV"}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->m_paths.empty() || false == track_rails::cut_rails(*track) || track->rail_dimensions().grooved) + continue; + if (name != "all" && track->name() != name) + continue; + std::vector others; + for (auto *other : simulation::Paths.sequence()) + if (other != nullptr && false == other->m_editorremoved && (other->iCategoryFlag & 15) == 1 && false == other->m_paths.empty() && track_rails::cut_rails(*other) && false == other->rail_dimensions().grooved && + glm::length(other->location() - track->location()) < other->radius() + track->radius() + 1.0) + others.push_back(other); + for (auto const &ours : rails_of(track)) + for (auto const &part : ours.pieces) + { + auto const from{std::max({part.from, ours.laidfrom, 0.0})}; + auto const to{std::min(part.to, ours.length)}; + if (to - from < 0.04) + continue; + ++parts; + bool found[5]{}; + std::string where[5]; + auto const fault = [&](int const Kind, std::string const &What) { + if (false == found[Kind]) + { + found[Kind] = true; + where[Kind] = What; + } }; + // past the last knots of its lines, the lines planing on as they end + for (auto s{from}; s <= to + 1e-9; s += 0.02) + { + auto const l{lines_at(part, s)}; + for (auto const *line : {l.extents, l.inner}) + { + if (line->empty()) + continue; + auto const value{knots::sample(*line, s, 0.0)}; + auto const planing{line == l.extents ? value < ours.size.head - 1e-4 : value > 1e-4}; + // the lines have knots every 2 cm at most + if (planing && (s > line->back().x + 0.025 || s < line->front().x - 0.025)) + fault(2, format("%s at %.2f m, past its knots %.2f-%.2f m", line == l.extents ? "extents" : "inner", s, line->front().x, line->back().x)); + } + } + // along the part: nothing of the head left, the head jumping in width + auto start{from + 0.05}; + auto end{to - 0.05}; + if (part.before != nullptr && part.before->startkind == track_rails::rail_end::sharp) + for (auto s{from}; s < to; s += 0.02) + { + double offset, low, high; + head_of(ours, part, s, offset, low, high); + if (high - low >= 0.001) + { + start = std::max(start, s + 0.04); + break; + } + } + std::vector stations, widths; + for (auto s{std::ceil(start / 0.02) * 0.02}; s <= end; s += 0.02) + { + double offset, low, high; + head_of(ours, part, s, offset, low, high); + auto const width{std::max(0.0, high - low)}; + if (width < 0.001 && false == obtuse_near(part, s)) + fault(4, format("at %.2f m, %.1f mm left", s, width * 1000.0)); + stations.push_back(s); + widths.push_back(width); + } + // a step in width well past those around it: a head tapering off steeply at a wide angle aside + for (std::size_t i = 1; i < widths.size(); ++i) + { + auto const step{std::abs(widths[i] - widths[i - 1])}; + auto const around{std::max(i > 1 ? std::abs(widths[i - 1] - widths[i - 2]) : 0.0, i + 1 < widths.size() ? std::abs(widths[i + 1] - widths[i]) : 0.0)}; + if (step > 0.003 && step > 2.0 * around + 0.001 && false == obtuse_near(part, stations[i])) + fault(3, format("at %.2f m, %.1f to %.1f mm", stations[i], widths[i - 1] * 1000.0, widths[i] * 1000.0)); + } + // against the rails around + for (auto *other : others) + for (auto const &theirs : rails_of(other)) + { + if (theirs.track == ours.track && theirs.path == ours.path) + continue; + auto const &low{theirs.line->low}; + auto const &high{theirs.line->high}; + auto distance{-1e9}; + for (auto s{std::ceil(from / 0.02) * 0.02}; s <= to; s += 0.02) + { + double offset, lo, hi; + head_of(ours, part, s, offset, lo, hi); + if (hi - lo < 2.0 * tolerance) + continue; + auto const centre{track_rails::rail_point(*ours.track, ours.path, ours.side, s, offset + 0.5 * ours.size.head)}; + if (centre.x < low.x - 0.5 || centre.x > high.x + 0.5 || centre.z < low.y - 0.5 || centre.z > high.y + 0.5) + continue; + if (distance < -1e8) + distance = track_rails::nearest_distance(*theirs.line, centre); + auto const across{track_rails::lateral(*theirs.line, theirs.side, centre, distance)}; + if (distance < 0.0 || distance > theirs.length || std::abs(across) > 0.3) + continue; + if (theirs.track == ours.track && (theirs.side == ours.side && distance < theirs.heel + 0.1 || throwable(part))) + continue; + // the same rail going on past a joint + { + auto d{distance}; + if (std::abs(track_rails::lateral(*theirs.line, theirs.side, track_rails::rail_point(*ours.track, ours.path, ours.side, s, 0.0), d)) < 0.002) + continue; + } + // the head of ours keeps out of the flangeways, the top of it out from under other heads: their rounded shoulders lie under + // it, ours under theirs + auto const outer{hi - tolerance}; + auto const crown{std::min(hi, offset + ours.size.head - ours.size.shoulder) - tolerance}; + for (auto const lateral : {lo + tolerance, 0.5 * (lo + crown), crown, outer}) + { + auto const top{lateral <= crown + 1e-9}; + auto const point{track_rails::rail_point(*ours.track, ours.path, ours.side, s, lateral)}; + auto d{distance}; + auto const l{track_rails::lateral(*theirs.line, theirs.side, point, d)}; + // rails meeting end on at joints aside + if (d < 0.05 || d > theirs.length - 0.05 || s < 0.05 || s > ours.length - 0.05) + continue; + if (l > -track_rails::flangeway + tolerance && l < -tolerance) + fault(0, format("at %.2f m, %.1f mm into the flangeway of %s/%d%+d at %.2f m", s, (-l) * 1000.0, theirs.track->name().c_str(), theirs.path, theirs.side, d)); + if (top && false == obtuse_near(part, s) && covers(theirs, d, l)) + { + auto inobtuse{false}; + for (auto const &their : theirs.pieces) + if (d >= their.from && d <= their.to && obtuse_near(their, d)) + inobtuse = true; + if (false == inobtuse) + fault(1, format("at %.2f m, over the head of %s/%d%+d at %.2f m, %.1f mm in", s, theirs.track->name().c_str(), theirs.path, theirs.side, d, l * 1000.0)); + } + } + } + } + for (int kind = 0; kind < 5; ++kind) + if (found[kind]) + { + ++counts[kind]; + WriteLog(format("SELFTEST space %c %s/%d%+d part %.3f-%.3f m: %s", kinds[kind], ours.track->name().c_str(), ours.path, ours.side, from, to, where[kind].c_str())); + } + } + } + WriteLog(format("SELFTEST space: %d parts, F %d, O %d, C %d, J %d, V %d", parts, counts[0], counts[1], counts[2], counts[3], counts[4])); + return selftest_result::handled; + } if (command == "guards") { std::string name; @@ -3008,7 +3840,7 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm std::string text; auto const end = [&](glm::dvec2 const &Miter, bool const Flared, bool const Glued) { if (Miter.y == 0.0) - return std::string{Flared ? "flared" : "square"}; + return std::string{Flared ? "planed" : "square"}; if (false == drawn(Miter, 0.0)) return std::string{"past the end of the track"}; glm::dvec3 points[2]; @@ -3080,15 +3912,24 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm } if (command == "throw") { + // throw [part] [name]: the switches set over to the path, by a part of the way as in the middle of the animation int path{0}; + double part{1.0}; + std::string name; words >> path; + if (false == static_cast(words >> part)) + part = 1.0; + words >> name; int count{0}; for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->SwitchExtension == nullptr) continue; + if (false == name.empty() && track->name() != name) + continue; + auto const before{track->SwitchExtension->fOffset}; track->Switch(path); - track->SwitchExtension->fOffset = track->SwitchExtension->fDesiredOffset; + track->SwitchExtension->fOffset = before + static_cast(part) * (track->SwitchExtension->fDesiredOffset - before); track->RaAnimate(); ++count; } @@ -3137,21 +3978,22 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm if (false == name.empty() && track->name() != name) continue; ++tracks; - for (auto const &crossing : track->rail_crossings()) - { - ++found; - auto const middle{crossing.angle > track_rails::middle_angle && crossing.angle <= track_rails::wide_angle}; - middles += middle ? 1 : 0; - auto const kind{crossing.angle > track_rails::wide_angle ? "wide" : crossing.facing < 0.0 ? (middle ? "middle acute" : "acute") : (middle ? "middle obtuse" : "obtuse")}; - WriteLog(format("SELFTEST crossing %s path %d side %d at %.3f m: %s with %s, 1:%.2f, into %.3f, facing %.3f, nose %d", track->name().c_str(), crossing.path, crossing.side, crossing.at, kind, - crossing.track->name().c_str(), 1.0 / std::tan(crossing.angle), crossing.into, crossing.facing, crossing.nose ? 1 : 0)); - } auto const gauge = [](TTrack *Track, track_rails::rail_crossing const &Crossing) { auto const &segment{Track->eType == tt_Switch ? *Track->SwitchExtension->Segments[Crossing.path] : *Track->Segment}; glm::dvec3 point, across; segment.plan_frame(Crossing.at, point, across); return point + across * (Crossing.side * 0.5 * std::abs(Track->fTrackWidth)); }; + for (auto const &crossing : track->rail_crossings()) + { + ++found; + auto const middle{crossing.angle > track_rails::middle_angle && crossing.angle <= track_rails::wide_angle}; + middles += middle ? 1 : 0; + auto const kind{crossing.angle > track_rails::wide_angle ? "wide" : crossing.facing < 0.0 ? (middle ? "middle acute" : "acute") : (middle ? "middle obtuse" : "obtuse")}; + auto const where{gauge(track, crossing)}; + WriteLog(format("SELFTEST crossing %s path %d side %d at %.3f m: %s with %s, 1:%.2f, into %.3f, facing %.3f, nose %d, plan %.2f %.2f", track->name().c_str(), crossing.path, crossing.side, crossing.at, kind, + crossing.track->name().c_str(), 1.0 / std::tan(crossing.angle), crossing.into, crossing.facing, crossing.nose ? 1 : 0, where.x, where.z)); + } for (auto const &crossing : track->rail_crossings(false)) { if (crossing.angle > track_rails::wide_angle || crossing.facing >= 0.0) @@ -3257,6 +4099,10 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm // switchbreak [name] the first piece of the diverging path moved off its neighbour, the switch committed // switches logs the switches kept in the scenery and the speed of each diverging track // deletetrack , deletekind the path deleted as Del does it, or the first path over an element of the kind +// tracksel ... the tracks selected together, as Shift+clicks select them; none: nothing selected +// trackprop [value] the value given to the selected tracks, as typed into the inspector; trackprops logs their values +// trackstyle copy [name] | groups ... | paste | panel the properties of a track copied, the groups chosen, pasted on the selected tracks +// tracksimilar the tracks of the scenery with the value of the property of the selected ones selected // btfcheck the .txtf converted to .btf, the editor lines the binary file keeps logged // mouse , mouseat the cursor moved to the point of the window, or to where the point of the scene is seen, as the mouse moves it // glide [shot] the cursor led to where the point is seen in steps, frames apart, a screenshot after each step with shot @@ -3267,6 +4113,12 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm // screenshot // save // log +// perf