diff --git a/CMakeLists.txt b/CMakeLists.txt index dfe58b66..53cc5267 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,7 @@ set(SOURCES "model/Texture.cpp" "utilities/Timer.cpp" "world/Track.cpp" +"world/TrackUneven.cpp" "world/Road.cpp" "world/Sweep.cpp" "world/RoadPoint.cpp" diff --git a/application/editormode.h b/application/editormode.h index c7dbd391..4b395ae6 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -639,6 +639,7 @@ class editor_mode : public application_mode, private editor_track::observer glm::mat4 m_track_gizmo{1.0f}; bool m_track_gizmo_using{false}; bool m_track_dirty{false}; + bool m_track_drag_on_the_way{false}; // committed, but not as the end of the drag std::chrono::steady_clock::time_point m_track_last_commit; editor_track::state m_track_field_before; std::array, 3> m_track_materials{}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 8dfd831f..c43670ca 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -442,12 +442,14 @@ void editor_mode::render_track_gizmo() void editor_mode::commit_track_drag(bool const Force) { - if (false == m_track_dirty || m_track_drag.empty()) + // on letting go the rest of what follows the dragged tracks is yet to come, even with all of the drag committed + if ((false == m_track_dirty && false == (Force && m_track_drag_on_the_way)) || m_track_drag.empty()) return; auto const now = std::chrono::steady_clock::now(); if (false == Force && now - m_track_last_commit < std::chrono::milliseconds(100)) return; - editor_track::commit(m_track_drag); + editor_track::commit(m_track_drag, false == Force); + m_track_drag_on_the_way = false == Force; m_track_dirty = false; m_track_last_commit = now; } @@ -1159,7 +1161,39 @@ void editor_mode::render_path_parameters(TTrack &Track) 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); + finish_edit(rebuild::parameters); + if ((track->eType == tt_Normal || track->eType == tt_Switch) && (track->iCategoryFlag & 15) == 1) + { + if (ImGui::InputFloat(STR_C("Unevenness (mm)"), &track->m_uneven.value, 0.0f, 0.0f, "%.1f")) + track->m_uneven.value = std::clamp(track->m_uneven.value, 0.f, track_uneven::most); + item_tooltip("Peak deviation of a rail on a 10 m chord. Id-14 limits: 6 mm at 160 km/h, 10 at 120, 18 at 80, 35 at 40.\nWith a sound distance set, the rails also dip at the joints. Joined switches use the lowest of their values"); + finish_edit(rebuild::parameters); + auto const component = [&](char const *Label, float &Value) { + if (ImGui::InputFloat(STR_C(Label), &Value, 0.0f, 0.0f, "%.1f")) + Value = Value < 0.f ? -1.f : std::min(Value, track_uneven::most); + item_tooltip("Set apart from the unevenness, mm. Negative: the unevenness"); + finish_edit(rebuild::parameters); + }; + component("Up and down (mm)", track->m_uneven.up); + component("Sideways (mm)", track->m_uneven.side); + if (track->eType == tt_Normal) + { + component("Cross level (mm)", track->m_uneven.cross); + } + else + { + component("Diverging path (mm)", track->m_uneven.branch); + component("Diverging, up and down (mm)", track->m_uneven.branchup); + component("Diverging, sideways (mm)", track->m_uneven.branchside); + } + } + if (track->eType == tt_Normal && (track->iCategoryFlag & 15) == 1) + { + if (ImGui::InputFloat(STR_C("Dip at rail joints (mm)"), &track->m_uneven.dip, 0.0f, 0.0f, "%.1f")) + track->m_uneven.dip = track->m_uneven.dip < 0.f ? -1.f : std::min(track->m_uneven.dip, track_uneven::most); + item_tooltip("Depth of the dip at each rail joint, every sound distance. Works with unevenness at 0. Negative: 0.4 of the unevenness"); + finish_edit(rebuild::parameters); + } 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); diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 418d908f..8e633991 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -152,6 +152,7 @@ editor_track::state editor_track::capture(TTrack const &Track) for (auto const *isolated : Track.Isolated) result.isolated.push_back(isolated->asName); result.overhead = Track.fOverhead; + result.uneven = Track.m_uneven; result.sleepers = Track.m_sleeper_enabled; result.sleeper_frequency = Track.m_sleeper_frequency; result.sleeper_model = Track.m_sleeper_model_name; @@ -250,6 +251,7 @@ void editor_track::apply(TTrack &Track, state const &State) Track.fTexSlope = State.texslope; isolated(Track, State.isolated); overhead(Track, State.overhead); + Track.m_uneven = State.uneven; Track.m_sleeper_enabled = State.sleepers; Track.m_sleeper_frequency = State.sleeper_frequency; Track.m_sleeper_model_name = State.sleeper_model; @@ -424,7 +426,7 @@ bool editor_track::can_edit_geometry(TTrack const &Track, std::string &Reason) return true; } -void editor_track::commit(std::vector const &Tracks) +void editor_track::commit(std::vector const &Tracks, bool const Dragging) { if (Tracks.empty()) return; @@ -455,6 +457,8 @@ void editor_track::commit(std::vector const &Tracks) add_unique(affected, neighbour); for (auto *track : affected) update_transition(*track); + if (false == Dragging) + add_followers(affected); for (auto *track : affected) rebuild_geometry(*track); for (auto *track : Tracks) @@ -476,6 +480,7 @@ void editor_track::commit_parameters(TTrack &Track) add_unique(affected, neighbour); for (auto *track : affected) update_transition(*track); + add_followers(affected); for (auto *track : affected) rebuild_geometry(*track); Track.mark_dirty(); @@ -2346,10 +2351,11 @@ void write_points(std::ostream &Text, segment_data const &Path) Text << start.x << ' ' << start.y << ' ' << start.z << ' ' << Path.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << Path.rolls[1] << ' ' << Path.radius << ' '; } -void write_ending(std::ostream &Text, TTrack const &Style) +void write_ending(std::ostream &Text, TTrack const &Style, bool const Switch) { if ((Style.iCategoryFlag & 15) == 1 && false == Style.m_profile1.first.empty()) Text << "railprofile " << Style.m_profile1.first << ' '; + Text << track_uneven::write_attribute(Style.m_uneven, Switch); // a piece of the track made from another one stays in its track circuit and under the same overhead line for (auto const *isolated : Style.Isolated) Text << "isolated " << isolated->asName << ' '; @@ -2401,7 +2407,7 @@ TTrack *editor_track::create_switch(switch_template const &Template, std::vector write_points(text, Paths[1]); if (auto const trackbed{ballast_of(Style)}; trackbed != null_handle) text << "trackbed " << material_name(trackbed) << ' '; - write_ending(text, Style); + write_ending(text, Style, true); track = load_path(text.str(), Style, Name); } track->m_paths.assign(Paths.begin(), Paths.begin() + 2); @@ -2662,7 +2668,7 @@ TTrack *editor_track::create_path(TTrack const &Style, segment_data const &Path) write_points(text, Path); if (auto const speed{velocity(Style)}; speed > 0.0) text << "velocity " << speed << ' '; - write_ending(text, Style); + write_ending(text, Style, false); auto *track{load_path(text.str(), Style)}; track->m_paths = {Path}; return track; @@ -2816,14 +2822,25 @@ void editor_track::retire(TTrack &Track) simulation::Region->erase_and_unregister(&Track); Track.m_editorremoved = true; for (auto *neighbour : adjoining) - { update_transition(*neighbour); - rebuild_geometry(*neighbour); - } + auto affected{adjoining}; + add_followers(affected); + for (auto *track : affected) + rebuild_geometry(*track); if (s_observer != nullptr) s_observer->track_retired(Track); } +void editor_track::add_followers(std::vector &Tracks) +{ + ++TTrack::s_layoutstamp; + for (auto *track : TTrack::switch_followers(Tracks)) + add_unique(Tracks, track); + for (auto *track : simulation::Paths.sequence()) + if (false == track->m_editorremoved && track->rail_joints_moved()) + add_unique(Tracks, track); +} + void editor_track::rebuild_geometry(TTrack &Track) { auto §ion{simulation::Region->section(Track.location())}; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 43d42762..efb5fdac 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -17,6 +17,7 @@ http: #include #include "world/Segment.h" +#include "world/TrackUneven.h" #include "utilities/Classes.h" class TTrack; @@ -55,6 +56,7 @@ class editor_track float texslope{0.9f}; std::vector isolated; float overhead{-1.f}; + track_uneven::attribute uneven; bool sleepers{false}; float sleeper_frequency{0.6f}; std::string sleeper_model; @@ -91,7 +93,8 @@ class editor_track static bool can_edit_geometry(TTrack const &Track, std::string &Reason); static bool is_supported(TTrack const &Track); - static void commit(std::vector const &Tracks); + // Dragging: on the way, what follows them from further off waits for the commit at the end of it + static void commit(std::vector const &Tracks, bool const Dragging = false); static void commit_parameters(TTrack &Track); static glm::dvec3 point_position(TTrack const &Track, point_ref const &Point); @@ -348,6 +351,8 @@ class editor_track static void bind_switch_path(TTrack &Switch, int const Path); static void store_switch_path(TTrack &Switch, int const Path); static void rebuild_geometry(TTrack &Track); + // adds the tracks whose shape goes by a change of the given from further off: by the switches they meet, and by rail joints running on through them + static void add_followers(std::vector &Tracks); static TTrack *clone(TTrack const &Template); static TTrack *load_text(std::string const &Text, TTrack const *Template, std::string const &Name); static void move_straight(straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End, std::function const &Member); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 58b30109..dbf7c124 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -861,6 +861,39 @@ msgstr "Odstęp stuków" msgid "Quality" msgstr "Jakość" +msgid "Unevenness (mm)" +msgstr "Nierówności toru (mm)" + +msgid "Peak deviation of a rail on a 10 m chord. Id-14 limits: 6 mm at 160 km/h, 10 at 120, 18 at 80, 35 at 40.\nWith a sound distance set, the rails also dip at the joints. Joined switches use the lowest of their values" +msgstr "Największa odchyłka toku szynowego na cięciwie 10 m. Limity Id-14: 6 mm przy 160 km/h, 10 przy 120, 18 przy 80, 35 przy 40.\nPrzy podanym odstępie styków szyny zapadają się też na stykach. Połączone rozjazdy mają najmniejszą ze swoich wartości" + +msgid "Up and down (mm)" +msgstr "W pionie (mm)" + +msgid "Sideways (mm)" +msgstr "W poziomie (mm)" + +msgid "Cross level (mm)" +msgstr "Przechyłka (mm)" + +msgid "Diverging path (mm)" +msgstr "Tor na bok (mm)" + +msgid "Diverging, up and down (mm)" +msgstr "Na bok, w pionie (mm)" + +msgid "Diverging, sideways (mm)" +msgstr "Na bok, w poziomie (mm)" + +msgid "Set apart from the unevenness, mm. Negative: the unevenness" +msgstr "Osobno od nierówności, mm. Ujemna: wartość nierówności" + +msgid "Dip at rail joints (mm)" +msgstr "Zapadnięcie na stykach szyn (mm)" + +msgid "Depth of the dip at each rail joint, every sound distance. Works with unevenness at 0. Negative: 0.4 of the unevenness" +msgstr "Głębokość zapadnięcia na każdym styku szyn, co odstęp styków. Działa też przy nierównościach 0. Ujemna: 0,4 nierówności" + msgid "Damage flags" msgstr "Flagi uszkodzeń" diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp new file mode 100644 index 00000000..b55de164 --- /dev/null +++ b/tests/unit/test_track_uneven.cpp @@ -0,0 +1,336 @@ +#include "stdafx.h" +#include "doctest.h" +#include "world/TrackUneven.h" + +#include +#include +#include +#include + +namespace { + +double deviation(std::vector const &Values) +{ + auto mean{0.0}; + for (auto const value : Values) + mean += value; + auto const count{static_cast(Values.size())}; + mean /= count; + auto sum{0.0}; + for (auto const value : Values) + sum += (value - mean) * (value - mean); + return std::sqrt(sum / count); +} + +double peak(std::vector Values) +{ + for (auto &value : Values) + value = std::abs(value); + std::ranges::sort(Values); + return Values[Values.size() * 999 / 1000]; +} + +void add_chord(std::vector &Output, std::vector const &Line, int const Half) +{ + for (int i = Half; i + Half < static_cast(Line.size()); ++i) + Output.push_back(Line[i] - 0.5 * (Line[i - Half] + Line[i + Half])); +} + +} // namespace + +TEST_CASE("uneven track: the cab keeps what the ride on the track doesn't stand in for") +{ + auto const replaced = [](double const Up, double const Side, double const Cross) { + return track_uneven::replaced_shake({Up, Side, Cross}); + }; + // all of it, as "uneven 18" + CHECK(replaced(18.0, 18.0, 18.0).sway); + CHECK(replaced(18.0, 18.0, 18.0).jolts); + // nothing at all, as on a track with dips at its joints alone + CHECK_FALSE(replaced(0.0, 0.0, 0.0).sway); + CHECK_FALSE(replaced(0.0, 0.0, 0.0).jolts); + // up and down only: there is nothing to sway with + CHECK_FALSE(replaced(18.0, 0.0, 0.0).sway); + CHECK(replaced(18.0, 0.0, 0.0).jolts); + // sideways only + CHECK(replaced(0.0, 18.0, 0.0).sway); + CHECK(replaced(0.0, 18.0, 0.0).jolts); + // cross level only turns the body + CHECK_FALSE(replaced(0.0, 0.0, 18.0).sway); + CHECK_FALSE(replaced(0.0, 0.0, 18.0).jolts); +} + +TEST_CASE("uneven track: measures as the Id-14 limits for its value") +{ + // appendix 1, class C at 80 km/h: 18 mm vertically, 17 mm sideways, 16 mm of twist on 5 m, gauge +10/-8 mm, 20 mm of cross level, J = 5.3 + auto const amplitude{track_uneven::stray_per_mm * 18.0 * 1000.0}; // mm + double const gauge{1.435}; + double const step{0.5}; + int const steps{4000}; + int const base{10}; // 5 m + int const lines{36}; + double vertical{0.0}; + double horizontal{0.0}; + double twist{0.0}; + double wander{0.0}; + double level{0.0}; + double j{0.0}; + double away{0.0}; + for (int line = 0; line < lines; ++line) + { + auto const heading{glm::pi() * (line + 0.37) / lines}; + glm::dvec3 const start{7919.0 * std::sin(line * 1.7), 0.0, 6577.0 * std::cos(line * 2.3)}; + glm::dvec3 const along{std::cos(heading), 0.0, std::sin(heading)}; + glm::dvec3 const across{-along.z, 0.0, along.x}; + std::array, 2> up; + std::array, 2> side; + std::vector middle; + for (int i = 0; i < steps; ++i) + { + auto const point{start + along * (i * step)}; + for (int rail = 0; rail < 2; ++rail) + { + auto const sign{rail == 0 ? 1.0 : -1.0}; + auto const stray{track_uneven::field(point + across * (sign * 0.5 * gauge))}; + up[rail].push_back((stray.y - sign * 0.5 * track_uneven::level(point, across)) * amplitude); + side[rail].push_back(glm::dot(stray, across) * amplitude); + } + middle.push_back(track_uneven::field(point).y * amplitude); + } + std::vector verticals; + std::vector horizontals; + std::vector twists; + std::vector gauges; + std::vector levels; + for (int rail = 0; rail < 2; ++rail) + { + add_chord(verticals, up[rail], base); + add_chord(horizontals, side[rail], base); + } + for (int i = 0; i + base < steps; ++i) + twists.push_back((up[0][i + base] - up[1][i + base]) - (up[0][i] - up[1][i])); + for (int i = 0; i < steps; ++i) + { + gauges.push_back(side[0][i] - side[1][i]); + levels.push_back(up[0][i] - up[1][i]); + } + vertical += peak(verticals) / lines; + horizontal += peak(horizontals) / lines; + twist += peak(twists) / lines; + wander += peak(gauges) / lines; + level += peak(levels) / lines; + away += peak(middle) / lines; + j += (deviation(verticals) + deviation(horizontals) + deviation(twists) + 0.5 * deviation(gauges)) / 3.5 / lines; + } + CHECK(vertical == doctest::Approx(18.0).epsilon(0.1)); + CHECK(horizontal == doctest::Approx(17.0).epsilon(0.2)); + CHECK(twist == doctest::Approx(16.0).epsilon(0.2)); + CHECK(wander == doctest::Approx(10.0).epsilon(0.2)); + CHECK(level == doctest::Approx(20.0).epsilon(0.1)); + CHECK(j == doctest::Approx(5.3).epsilon(0.2)); + CHECK(away < 40.0); // the long waves go past the 15 mm off the survey marks of appendix 1a +} + +TEST_CASE("uneven track: a place strays the same every time") +{ + // no random numbers: changing these changes how every uneven track in every scenery looks + glm::dvec3 const place{1234.5, 0.0, -678.9}; + auto const stray{track_uneven::field(place)}; + CHECK(stray.x == doctest::Approx(0.309777548158).epsilon(1e-9)); + CHECK(stray.y == doctest::Approx(0.311350299745).epsilon(1e-9)); + CHECK(stray.z == doctest::Approx(-0.328429734019).epsilon(1e-9)); + CHECK(track_uneven::level(place, {1.0, 0.0, 0.0}) == doctest::Approx(0.53 * -0.178598957983).epsilon(1e-9)); + CHECK(track_uneven::level(place, {0.0, 0.0, 1.0}) == doctest::Approx(0.53 * -0.868291889902).epsilon(1e-9)); +} + +TEST_CASE("uneven track: tracks laid towards each other meet at one cant") +{ + // the same rail stays lower seen from either of them, and a path taken the other way sees the roll turned over + glm::dvec3 const place{812.25, 0.0, 3071.5}; + glm::dvec3 const across{0.6, 0.0, -0.8}; + CHECK(track_uneven::level(place, -across) == doctest::Approx(-track_uneven::level(place, across))); + CHECK(track_uneven::roll(place, -across, 1.435) == doctest::Approx(-track_uneven::roll(place, across, 1.435))); + // and there's no step along a curve turning through the headings + auto last{track_uneven::level(place, {1.0, 0.0, 0.0})}; + for (int step = 1; step <= 3600; ++step) + { + auto const heading{glm::two_pi() * step / 3600.0}; + auto const value{track_uneven::level(place, {std::cos(heading), 0.0, std::sin(heading)})}; + REQUIRE(std::abs(value - last) < 0.002); + last = value; + } +} + +TEST_CASE("uneven track: the field stays within its bound") +{ + auto most{0.0}; + auto highest{0.0}; + for (int i = 0; i < 20000; ++i) + { + auto const stray{track_uneven::field({i * 3.7 - 30000.0, 0.0, i * -2.9 + 41000.0})}; + most = std::max({most, std::abs(stray.x), std::abs(stray.z)}); + highest = std::max(highest, std::abs(stray.y)); + } + CHECK(most <= 1.0); + CHECK(most > 0.5); + CHECK(highest <= 1.69); + CHECK(highest > 0.5); +} + +TEST_CASE("uneven track: all the values in one attribute") +{ + auto const one{track_uneven::read_fields<6>("18")}; + CHECK(one[0] == doctest::Approx(18.f)); + CHECK(one[1] < 0.f); + auto const both{track_uneven::read_fields<6>("14;35")}; + CHECK(both[0] == doctest::Approx(14.f)); + CHECK(both[1] == doctest::Approx(35.f)); + CHECK(both[2] < 0.f); + auto const gaps{track_uneven::read_fields<6>("18.5;;;25;6")}; + CHECK(gaps[0] == doctest::Approx(18.5f)); + CHECK(gaps[1] < 0.f); + CHECK(gaps[2] < 0.f); + CHECK(gaps[3] == doctest::Approx(25.f)); + CHECK(gaps[4] == doctest::Approx(6.f)); + auto const bounds{track_uneven::read_fields<6>("500;-3;x")}; + CHECK(bounds[0] == doctest::Approx(track_uneven::most)); + CHECK(bounds[1] == doctest::Approx(0.f)); + CHECK(bounds[2] < 0.f); + CHECK(track_uneven::write_fields<6>({14.f, 35.f, -1.f, -1.f, -1.f, -1.f}) == "14;35"); + CHECK(track_uneven::write_fields<6>({18.5f, -1.f, -1.f, 25.f, 6.f, -1.f}) == "18.5;;;25;6"); + CHECK(track_uneven::write_fields<6>({0.f, -1.f, 30.f, -1.f, -1.f, -1.f}) == "0;;30"); + CHECK(track_uneven::write_fields<6>({18.f, -1.f, -1.f, -1.f, -1.f, -1.f}) == "18"); +} + +TEST_CASE("uneven track: the attribute goes by the entry it's written into") +{ + auto const plain{track_uneven::read_attribute("18;22;19;25;6", false)}; + CHECK(track_uneven::write_attribute(plain, false) == "uneven 18;22;19;25;6 "); + // a switch made in the style of a plain track: no path p3-p4 of its own, the components in their places + CHECK(track_uneven::write_attribute(plain, true) == "uneven 18;;22;19 "); + auto const made{track_uneven::read_attribute("18;;22;19", true)}; + CHECK(track_uneven::declared(made, false, 0).up == doctest::Approx(22.0)); + CHECK(track_uneven::declared(made, false, 0).side == doctest::Approx(19.0)); + CHECK(track_uneven::declared(made, false, 1).up == doctest::Approx(22.0)); + CHECK(track_uneven::declared(made, false, 0).cross == doctest::Approx(0.0)); + // a switch with its own values, and a plain track in its style + auto const points{track_uneven::read_attribute("14;35;16;17", true)}; + CHECK(track_uneven::write_attribute(points, true) == "uneven 14;35;16;17 "); + CHECK(track_uneven::declared(points, false, 1).up == doctest::Approx(35.0)); + CHECK(track_uneven::declared(points, false, 1).side == doctest::Approx(35.0)); + CHECK(track_uneven::write_attribute(points, false) == "uneven 14;16;17 "); + // a plain track: the value stands in for what's left out, only there is the cross level + auto const track{track_uneven::read_attribute("18;;30", false)}; + CHECK(track_uneven::declared(track, true, 0).up == doctest::Approx(18.0)); + CHECK(track_uneven::declared(track, true, 0).side == doctest::Approx(30.0)); + CHECK(track_uneven::declared(track, true, 0).cross == doctest::Approx(18.0)); + // nothing given, nothing written + CHECK(track_uneven::write_attribute({}, false).empty()); + CHECK(track_uneven::write_attribute({}, true).empty()); + // a value of 0 stays when there's more after it + CHECK(track_uneven::write_attribute(track_uneven::read_attribute("0;;;;6", false), false) == "uneven 0;;;;6 "); +} + +TEST_CASE("uneven track: each component comes to its neighbours along a track") +{ + double const length{500.0}; + auto const atstart{track_uneven::along(track_uneven::amounts{18.0, 16.0, 14.0}, {5.0, 4.0, 3.0}, {7.0, 8.0, 9.0}, 0.0, length)}; + CHECK(atstart.up == doctest::Approx(5.0)); + CHECK(atstart.side == doctest::Approx(4.0)); + CHECK(atstart.cross == doctest::Approx(3.0)); + auto const inside{track_uneven::along(track_uneven::amounts{18.0, 16.0, 14.0}, {5.0, 4.0, 3.0}, {7.0, 8.0, 9.0}, 0.5 * length, length)}; + CHECK(inside.up == doctest::Approx(18.0)); + CHECK(inside.side == doctest::Approx(16.0)); + CHECK(inside.cross == doctest::Approx(14.0)); +} + +TEST_CASE("uneven track: axles don't stray along the track") +{ + glm::dvec3 const across{0.6, 0.0, -0.8}; + glm::dvec3 const along{0.8, 0.0, 0.6}; + auto const moved{track_uneven::across_only(across * 0.03 + along * 0.05 + glm::dvec3{0.0, 0.02, 0.0}, across)}; + CHECK(glm::dot(moved, along) == doctest::Approx(0.0)); + CHECK(glm::dot(moved, across) == doctest::Approx(0.03)); + CHECK(moved.y == doctest::Approx(0.02)); +} + +TEST_CASE("uneven track: a path of no length gives no vector across") +{ + glm::dvec3 const none{0.0}; + CHECK(track_uneven::across_of(none, none) == none); + CHECK(glm::length(track_uneven::across_of({3.0, 0.0, 4.0}, none)) == doctest::Approx(1.0)); + // it can't pass for a joint: the check of the two vectors being alike fails + CHECK(glm::dot(track_uneven::across_of(none, none), glm::dvec3{1.0, 0.0, 0.0}) < 0.9); + // falls back on the chord + auto const chord{track_uneven::across_of(none, {0.0, 0.0, 10.0})}; + CHECK(chord.x == doctest::Approx(-1.0)); + CHECK(chord.z == doctest::Approx(0.0)); + // straight up has no direction in the plan + CHECK(glm::length(track_uneven::across_of({0.0, 5.0, 0.0}, {0.0, 0.0, 10.0})) == doctest::Approx(1.0)); +} + +TEST_CASE("uneven track: the cant at a joint") +{ + CHECK(track_uneven::joint_roll(0.1f, -0.1f, true, true) == doctest::Approx(0.f)); + CHECK(track_uneven::joint_roll(0.2f, 0.4f, false, false) == doctest::Approx(0.3f)); + CHECK(track_uneven::joint_roll(0.1f, 0.0f, true, false) == doctest::Approx(0.f)); + CHECK(track_uneven::joint_roll(0.0f, 0.1f, false, true) == doctest::Approx(0.f)); +} + +TEST_CASE("uneven track: amounts take the lowest of each component") +{ + auto const least{track_uneven::least({14.0, 9.0, 0.0}, {7.0, 35.0, 3.0})}; + CHECK(least.up == doctest::Approx(7.0)); + CHECK(least.side == doctest::Approx(9.0)); + CHECK(least.cross == doctest::Approx(0.0)); + // the diverging path of a switch: from the common end at the lowest to its own value at the far end + double const length{30.0}; + CHECK(track_uneven::along(35.0, 7.0, 35.0, 0.0, length) == doctest::Approx(7.0)); + CHECK(track_uneven::along(35.0, 7.0, 35.0, length, length) == doctest::Approx(35.0)); +} + +TEST_CASE("uneven track: the value along a track meets its neighbours exactly") +{ + double const length{500.0}; + CHECK(track_uneven::along(20.0, 5.0, 7.0, 0.0, length) == doctest::Approx(5.0)); + CHECK(track_uneven::along(20.0, 5.0, 7.0, length, length) == doctest::Approx(7.0)); + CHECK(track_uneven::along(20.0, 5.0, 7.0, 0.5 * length, length) == doctest::Approx(20.0)); + // equal values stay constant + CHECK(track_uneven::along(20.0, 20.0, 20.0, 3.0, length) == doctest::Approx(20.0)); + // a short track between ends of no value hardly strays + CHECK(track_uneven::along(20.0, 0.0, 0.0, 2.5, 5.0) < 1.0); + // a switch of more than the track: the track starts at the value of the switch + CHECK(track_uneven::along(18.0, 35.0, 18.0, 0.0, length) == doctest::Approx(35.0)); + CHECK(track_uneven::along(18.0, 35.0, 18.0, track_uneven::fade, length) == doctest::Approx(18.0)); + // the short piece inside a slip goes with its switches + CHECK(track_uneven::along(18.0, 35.0, 35.0, 0.25, 0.5) == doctest::Approx(35.0).epsilon(0.01)); + // no jump anywhere + auto last{track_uneven::along(18.0, 35.0, 0.0, 0.0, length)}; + for (int step = 1; step <= 2000; ++step) + { + auto const value{track_uneven::along(18.0, 35.0, 0.0, step * 0.25, length)}; + REQUIRE(std::abs(value - last) < 0.5); + last = value; + } +} + +TEST_CASE("uneven track: the foot of the ballast stays where it was") +{ + double const rail{0.18}; + double const height{0.38}; + CHECK(track_uneven::bed_share(0.0, rail, height) == doctest::Approx(1.0)); // rail top + CHECK(track_uneven::bed_share(rail, rail, height) == doctest::Approx(1.0)); // top of the ballast + CHECK(track_uneven::bed_share(rail + 0.5 * height, rail, height) == doctest::Approx(0.5)); + CHECK(track_uneven::bed_share(rail + height, rail, height) == doctest::Approx(0.0)); // its foot + CHECK(track_uneven::bed_share(rail + 2.0 * height, rail, height) == doctest::Approx(0.0)); + CHECK(track_uneven::bed_share(rail + 1.0, rail, 0.0) == doctest::Approx(1.0)); // no ballast +} + +TEST_CASE("uneven track: the rails dip around a joint") +{ + CHECK(track_uneven::dip(0.0) == doctest::Approx(1.0)); + CHECK(track_uneven::dip(1.0) == doctest::Approx(0.75).epsilon(0.01)); // the worst case for rings 2 m apart + CHECK(track_uneven::dip(track_uneven::dip_reach) == doctest::Approx(0.0)); + CHECK(track_uneven::rail_phase(26.0, 25.0) == doctest::Approx(1.0)); + CHECK(track_uneven::rail_phase(-1.0, 25.0) == doctest::Approx(24.0)); +} diff --git a/tools/track_uneven_grade.py b/tools/track_uneven_grade.py new file mode 100644 index 00000000..81df2ce2 --- /dev/null +++ b/tools/track_uneven_grade.py @@ -0,0 +1,665 @@ +#!/usr/bin/env python3 +"""Grades the tracks of any scenery for the uneven attribute, from what the scenery shows: textures, grass on the track, +curves, speed and road crossings. The originals stay as they are, graded copies of the files with tracks go to --out. + + python track_uneven_grade.py scenery/td.scn + python track_uneven_grade.py scenery/baltyk_skm1.scn --out graded + python track_uneven_grade.py scenery/baltyk_skm1.scn --out graded --route -107,2300 -4120,2491 --straight tor0290,tor0906 + python track_uneven_grade.py scenery/mytracks.scm --root scenery --out graded --gain 1.2 + +With no --out it only reports. See track_uneven_doc.md for the attribute. Python 3.8 or later, no other packages. +""" +import argparse +import collections +import heapq +import math +import pathlib +import re +import sys +import zlib + +# PLK Id-14 appendix 1, class C: vertical limit in mm by speed in km/h +LIMITS = [(20, 50), (30, 40), (40, 35), (50, 30), (60, 25), (70, 21), (80, 18), (90, 16), (100, 14), (110, 12), (120, 10), (130, 9), (140, 8), (150, 7), (160, 6)] +# how worn a texture looks, 0-1: the first pattern found in its name counts, anything else is 0.5 +BALLAST_WEAR = [("grass|traw", 0.95), ("old|star", 0.9), ("oil|smar", 0.6), ("new|nowa", 0.0), ("bps|beton|conc|tpb", 0.2), ("drew|wood|tpd", 0.45)] +RAIL_WEAR = [("unused2", 1.0), ("unused", 0.85), ("rused2", 0.8), ("rused", 0.65), ("used2", 0.45), ("used", 0.2), ("new", 0.0), ("old|rust", 0.8)] +WOOD = re.compile("drew|wood|tpd") +GRASS = re.compile("grass|traw|chwast|weed") +NO_SPEED = 40.0 # km/h, a stretch with no velocity anywhere: station and side tracks +SWITCH_MOST = 35.0 # the limit of 40 km/h +DIVERGING = 40.0 # km/h through the diverging path +MOST, MOST_UP = 50.0, 60.0 # of uneven and of its vertical part +REACH = 0.3 # how near the ends of tracks count as joined, m +SHORT = 2.0 # the pieces inside a slip don't tell the quality of the line +PIECE = 100.0 # a track without a speed shorter than this can take it through a switch, m +GRASS_NEAR, GRASS_FULL = 2.5, 14.0 # grass this near the middle grows on the track; plants per 100 m of an overgrown one +NEAR_LEAST, NEAR_MOST = 0.5, 0.95 # how far a chosen track goes towards the limit +CURVE_STEP = 1.0 / 2500.0 # a difference of curvature that counts as a change, 1/m +RISE = 0.0273 * 4.5 # how fast pressure lifts a pantograph, m/s, by TDynamicObject::Update +GAP_MOST = 0.006 # the gap to the wire allowed; at 0.01 m the voltage goes and the main breaker trips +OVERSPEED = 1.1 # a driver can go this much over the speed of the track +# as up_waves and the dip of the joints in world/TrackUneven.h +STRAY_PER_MM, DIP_SHARE, DIP_REACH = 0.00145, 0.4, 3.0 +GOLDEN = math.pi * (3.0 - math.sqrt(5.0)) +UP_WAVES = [(2 * math.pi / length * math.cos(GOLDEN * i), 2 * math.pi / length * math.sin(GOLDEN * i), GOLDEN * (i + 1), share) for i, (length, share) in enumerate(zip((8.3, 11.9, 16.7, 23.9, 37.1, 47.3, 61.9, 83.3), (.14, .19, .22, .22, .23, .23, .23, .23)))] +TOKEN = re.compile(r"//[^\n]*|\S+") +NUMBER = re.compile(r"^[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?$") + + +def limit(speed): + if speed <= LIMITS[0][0]: + return float(LIMITS[0][1]) + for (v0, n0), (v1, n1) in zip(LIMITS, LIMITS[1:]): + if speed <= v1: + return n0 + (n1 - n0) * (speed - v0) / (v1 - v0) + return float(LIMITS[-1][1]) + + +def wear(name, table): + name = (name or "").lower() + for pattern, value in table: + if re.search(pattern, name): + return value + return 0.5 + + +def hashed(salt, name): + return zlib.crc32(salt + name.encode("latin-1", "replace")) / 0xffffffff + + +class Scenery: + """All that's found by following the includes from the given files.""" + + def __init__(self, root): + self.root = root + self.tracks, self.roads, self.plants = [], [], collections.defaultdict(list) + self.seen = set() + self.notes = collections.Counter() + self.texts = {} + + def read(self, path, offset=(0.0, 0.0, 0.0)): + path = path.resolve() + if path not in self.seen: + self.seen.add(path) + FileReader(self, path, offset).run() + + def include(self, name, params, origin): + path = self.root / name.replace("\\", "/") + # a plant takes its place in the parameters; a file with grass in its name and none of them just lists plants + numbers = [float(w) for w in params if NUMBER.match(w)] + if GRASS.search(name.lower()) and len(numbers) >= 3: + self.plant(numbers[0] + origin[0], numbers[2] + origin[2]) + elif not path.is_file(): + self.notes["brak pliku " + name] += 1 + elif not params: + self.read(path, origin) + elif path.stat().st_size < 4000000 and re.search(rb"\btrack\s+(normal|switch)\b", path.read_bytes(), re.I): + # the same file can come many times with other parameters: its tracks can't be told apart in one copy + self.notes["pliki z parametrami, tory pominiete"] += 1 + + def plant(self, x, z): + self.plants[(round(x / 10), round(z / 10))].append((x, z)) + + +class FileReader: + """Goes through the words of one file. Each handler takes the place of its word and gives the place of the last word it used.""" + + def __init__(self, scenery, path, offset): + self.scenery, self.path = scenery, path + text = path.read_bytes().decode("latin-1") + scenery.texts[path] = text + self.matches = [m for m in TOKEN.finditer(text) if not m.group(0).startswith("//")] + self.words = [m.group(0) for m in self.matches] + self.lower = [w.lower() for w in self.words] + self.origins = [offset] + self.handlers = {"include": self.include, "origin": self.origin, "endorigin": self.endorigin, "rotate": self.rotate, "node": self.node} + + def run(self): + i = 0 + while i < len(self.words): + handler = self.handlers.get(self.lower[i]) + i = (handler(i) if handler else i) + 1 + + def include(self, i): + if i + 1 >= len(self.words): + return i + end = i + 1 + while end < len(self.words) and self.lower[end] != "end": + end += 1 + self.scenery.include(self.words[i + 1], self.words[i + 2:end], self.origins[-1]) + return end + + def origin(self, i): + values = self.words[i + 1:i + 4] + if len(values) < 3 or not all(NUMBER.match(w) for w in values): + return i + self.origins.append(tuple(o + float(w) for o, w in zip(self.origins[-1], values))) + return i + 3 + + def endorigin(self, i): + if len(self.origins) > 1: + self.origins.pop() + return i + + def rotate(self, i): + if i + 3 < len(self.words) and any(w.strip("0.-") for w in self.words[i + 1:i + 4]): + self.scenery.notes["rotate"] += 1 + return i + + def node(self, i): + if i + 4 >= len(self.words): + return i + kind = self.lower[i + 4] + if kind == "track": + return self.track(i) + if kind == "model" and i + 9 < len(self.words) and GRASS.search(self.lower[i + 9]) and all(NUMBER.match(w) for w in self.words[i + 5:i + 8]): + self.scenery.plant(float(self.words[i + 5]) + self.origins[-1][0], float(self.words[i + 7]) + self.origins[-1][2]) + return i + + def paths_from(self, cursor, count): + paths = [] + origin = self.origins[-1] + for _ in range(count): + values = self.words[cursor:cursor + 15] + if len(values) < 15 or not all(NUMBER.match(v) for v in values): + return None + n = [float(v) for v in values] + part = {"p1": [n[j] + origin[j] for j in range(3)], "c1": n[4:7], "c2": n[7:10], "p2": [n[10 + j] + origin[j] for j in range(3)], "radius": abs(n[14])} + part["line"] = line_of(part) + part["length"] = sum(math.dist(a, b) for a, b in zip(part["line"], part["line"][1:])) + paths.append(part) + cursor += 15 + return paths + + def track(self, i): + words, lower = self.words, self.lower + at = i + 4 + end = at + while end < len(words) and lower[end] != "endtrack": + end += 1 + if end >= len(words) or at + 10 >= end: + return i + kind = lower[at + 1] + if kind not in ("normal", "switch", "road"): + return end + visible = lower[at + 9] == "vis" + paths = self.paths_from(at + (16 if visible else 10), 2 if kind == "switch" else 1) + if paths is None: + self.scenery.notes["tory z parametrami, pominiete"] += 1 + elif kind == "road": + self.scenery.roads.append(paths[0]) + else: + self.add_track(i, at, end, kind, visible, paths) + return end + + def add_track(self, i, at, end, kind, visible, paths): + words = self.words + block = self.lower[at:end] + velocity = 0.0 + if "velocity" in block and NUMBER.match(words[at + block.index("velocity") + 1]): + velocity = float(words[at + block.index("velocity") + 1]) + self.scenery.tracks.append({ + "name": words[i + 3], "kind": kind, + "rail": self.lower[at + 10] if visible else None, "ballast": self.lower[at + 12] if visible else None, + "sound": float(words[at + 5]) if NUMBER.match(words[at + 5]) else -1.0, + "paths": paths, "speed": velocity if velocity > 0 else None, "source": "wpis" if velocity > 0 else None, + "file": self.path, "offset": self.matches[end].start(), + "taken": any(w in block for w in ("uneven", "unevenup", "unevenside", "unevencross", "jointdip"))}) + + +def bezier(part, t): + a = [part["p1"][i] + part["c1"][i] for i in range(3)] + b = [part["p2"][i] + part["c2"][i] for i in range(3)] + u = 1 - t + return [u * u * u * part["p1"][i] + 3 * u * u * t * a[i] + 3 * u * t * t * b[i] + t * t * t * part["p2"][i] for i in range(3)] + + +def line_of(part, step=5.0): + if not any(part["c1"]) and not any(part["c2"]): + return [list(part["p1"]), list(part["p2"])] + count = max(4, int(math.dist(part["p1"], part["p2"]) / step)) + return [bezier(part, i / count) for i in range(count + 1)] + + +def plan_radius(line): + least = math.inf + for a, b, c in zip(line, line[1:], line[2:]): + area = abs((b[0] - a[0]) * (c[2] - a[2]) - (c[0] - a[0]) * (b[2] - a[2])) / 2 + if area > 1e-6: + least = min(least, math.dist((a[0], a[2]), (b[0], b[2])) * math.dist((b[0], b[2]), (c[0], c[2])) * math.dist((c[0], c[2]), (a[0], a[2])) / (4 * area)) + return least + + +def end_curve(part, key): + line = part["line"] + if len(line) < 3: + return 1.0 / part["radius"] if part["radius"] > 0 else 0.0 + a, b, c = line[:3] if key == "p1" else line[-3:] + sides = math.dist(a, b) * math.dist(b, c) * math.dist(c, a) + return 2.0 * ((b[0] - a[0]) * (c[2] - b[2]) - (b[2] - a[2]) * (c[0] - b[0])) / sides if sides > 0 else 0.0 + + +class Joints: + def __init__(self, tracks): + self.ends = collections.defaultdict(list) + for t in tracks: + for part in t["paths"]: + for key in ("p1", "p2"): + self.ends[(round(part[key][0]), round(part[key][2]))].append((t, part, key)) + + def at(self, point): + found = [] + for cx in (round(point[0]) - 1, round(point[0]), round(point[0]) + 1): + for cz in (round(point[2]) - 1, round(point[2]), round(point[2]) + 1): + for item in self.ends.get((cx, cz), ()): + if abs(item[1][item[2]][0] - point[0]) <= REACH and abs(item[1][item[2]][2] - point[2]) <= REACH: + found.append(item) + return found + + +def plain_neighbours(t, joints): + return [other for key in ("p1", "p2") for other, _, _ in joints.at(t["paths"][0][key]) if other is not t and other["kind"] == "normal"] + + +def joined_at(t, point, joints, seen): + # the plain tracks joined at a point, and the far ends of the straight paths of the switches joined there + plain, further = [], [] + for other, part, side in joints.at(point): + if other is t or id(part) in seen: + continue + seen.add(id(part)) + if other["kind"] != "switch": + plain.append(other) + elif other["paths"].index(part) != other["bent"]: + further.append(part["p2" if side == "p1" else "p1"]) + return plain, further + + +def plain_past_switches(t, point, joints): + # the plain tracks reached from a point of a track along the straight paths of switches + points, seen, found = [point], set(), [] + for _ in range(6): + onward = [] + for reached in points: + plain, further = joined_at(t, reached, joints, seen) + found += plain + onward += further + points = onward + return found + + +def short_pieces_past_switches(t, joints): + return [o for key in ("p1", "p2") for o in plain_past_switches(t, t["paths"][0][key], joints) if o["paths"][0]["length"] < PIECE] + + +def spread_speed(tracks, joints, reached): + heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"]) + while heads: + t = heads.popleft() + for other in reached(t, joints): + if other["speed"] is None: + other["speed"], other["source"] = t["speed"], "sasiad" + heads.append(other) + + +def settle_speeds(tracks, joints): + # from the nearest track on the stretch between switches, a short piece also along the straight path of a switch + spread_speed(tracks, joints, plain_neighbours) + spread_speed(tracks, joints, short_pieces_past_switches) + for t in tracks: + if t["speed"] is None: + t["speed"], t["source"] = NO_SPEED, "brak" + + +def plain_ends(tracks): + for t in tracks: + if t["kind"] == "normal": + for part in t["paths"]: + yield part["p1"] + yield part["p2"] + + +def distances(tracks, joints, station): + best, heap = {}, [] + for point in plain_ends(tracks): + gap = math.dist((point[0], point[2]), station) + node = (round(point[0], 1), round(point[2], 1)) + if gap < 300 and gap < best.get(node, math.inf): + best[node] = gap + heapq.heappush(heap, (gap, node, point)) + while heap: + distance, node, point = heapq.heappop(heap) + if distance > best.get(node, math.inf): + continue + for _, part, key in joints.at(point): + other = part["p2" if key == "p1" else "p1"] + onward = (round(other[0], 1), round(other[2], 1)) + if distance + part["length"] < best.get(onward, math.inf): + best[onward] = distance + part["length"] + heapq.heappush(heap, (best[onward], onward, other)) + return best + + +def grass_on(part, plants): + near, count = set(), max(1, int(part["length"] / 3)) + line = part["line"] + for i in range(count + 1): + position = i / count * (len(line) - 1) + low = min(int(position), len(line) - 2) + share = position - low + x, z = line[low][0] + (line[low + 1][0] - line[low][0]) * share, line[low][2] + (line[low + 1][2] - line[low][2]) * share + for cx in (round(x / 10) - 1, round(x / 10), round(x / 10) + 1): + for cz in (round(z / 10) - 1, round(z / 10), round(z / 10) + 1): + for plant in plants.get((cx, cz), ()): + if (plant[0] - x) ** 2 + (plant[1] - z) ** 2 < GRASS_NEAR * GRASS_NEAR: + near.add(plant) + return len(near) * 100.0 / max(part["length"], 5.0) + + +def cells_under(a, b): + for x in range(math.floor(min(a[0], b[0]) / 50), math.floor(max(a[0], b[0]) / 50) + 1): + for z in range(math.floor(min(a[2], b[2]) / 50), math.floor(max(a[2], b[2]) / 50) + 1): + yield x, z + + +def road_cells(roads): + cells = collections.defaultdict(list) + for road in roads: + for a, b in zip(road["line"], road["line"][1:]): + for cell in cells_under(a, b): + cells[cell].append((a, b)) + return cells + + +def cross_in_plan(a, b, c, d): + rx, rz, sx, sz = b[0] - a[0], b[2] - a[2], d[0] - c[0], d[2] - c[2] + k = rx * sz - rz * sx + if abs(k) < 1e-9: + return False + t = ((c[0] - a[0]) * sz - (c[2] - a[2]) * sx) / k + u = ((c[0] - a[0]) * rz - (c[2] - a[2]) * rx) / k + return 0 <= t <= 1 and 0 <= u <= 1 + + +def hits_road(part, cells): + line = part["line"] + return any(cross_in_plan(a, b, c, d) for a, b in zip(line, line[1:]) for cell in cells_under(a, b) for c, d in cells.get(cell, ())) + + +def heights_per_mm(t): + # the height of the rail under a pantograph frame by frame, per mm of the vertical part + step = t["speed"] * OVERSPEED / 3.6 / 60.0 + heights, done = [], 0.0 + line = t["paths"][0]["line"] + for a, b in zip(line, line[1:]): + length = math.dist(a, b) + count = max(1, int(length / step)) + for i in range(count): + share = i / count + x, z = a[0] + (b[0] - a[0]) * share, a[2] + (b[2] - a[2]) * share + height = STRAY_PER_MM * sum(s * math.sin(wx * x + wz * z + phase) for wx, wz, phase, s in UP_WAVES) + if t["sound"] > 2.0 * DIP_REACH: + phase = math.fmod(done + length * share, t["sound"]) + joint = min(phase, t["sound"] - phase) + if joint < DIP_REACH: + height -= 0.001 * DIP_SHARE * math.cos(math.pi / 2 * joint / DIP_REACH) ** 2 + heights.append(height) + done += length + return heights + + +def largest_gap(heights, up): + gap = worst = 0.0 + climb = RISE / 60.0 + for before, after in zip(heights, heights[1:]): + gap = max(0.0, gap + (before - after) * up - climb) + worst = max(worst, gap) + return worst + + +def ceiling(t, product): + # the worst track of a speed comes up to it and the better ones stay under it, so the state of a track still shows + return product / t["speed"] * (0.7 + 0.3 * min(1.0, max(0.0, (t["state"] - 0.5) / 0.7))) + + +def path_radius(part): + return min(plan_radius(part["line"]), part["radius"] if part["radius"] > 0 else math.inf) + + +def mark_diverging_paths(switches): + # the path that bends more + for t in switches: + radii = [path_radius(p) for p in t["paths"]] + t["bent"] = 1 if len(radii) < 2 or radii[1] <= radii[0] else 0 + + +class Route: + """How far along from the first point to the second a place lies, measured along the tracks.""" + + def __init__(self, tracks, joints, first, last): + self.first, self.last = first, last + self.from_first, self.from_last = distances(tracks, joints, first), distances(tracks, joints, last) + ends = [self.from_last[n] for n in self.from_last if math.dist(n, first) < 300] + self.length = min(ends) if ends else math.dist(first, last) + if not ends: + print("uwaga: po torach nie ma polaczenia miedzy punktami --route, licze w linii prostej") + + def share(self, part): + shares = [] + for key in ("p1", "p2"): + place = (part[key][0], part[key][2]) + node = (round(place[0], 1), round(place[1], 1)) + a, b = self.from_first.get(node), self.from_last.get(node) + if a is None or b is None: + a, b = math.dist(place, self.first), math.dist(place, self.last) + shares.append(min(1.0, max(0.0, (a - b + self.length) / (2 * self.length)))) + return 0.5 * sum(shares) + + +def grade_states(scenery, route, gain): + cells = road_cells(scenery.roads) + for t in scenery.tracks: + part = t["paths"][0] + radius = path_radius(part) + curve = 0.0 if radius >= 6000 else min(1.0, max(0.0, (1200 - radius) / 950)) + plain = t["kind"] == "normal" + density = grass_on(part, scenery.plants) if plain else 0.0 + crossing = plain and hits_road(part, cells) + progress = route.share(part) if route else 0.5 + state = 0.50 + 0.20 * wear(t["ballast"], BALLAST_WEAR) + 0.10 * wear(t["rail"], RAIL_WEAR) + 0.12 * min(1.0, density / GRASS_FULL) + 0.06 * curve + 0.22 * progress + 0.18 * crossing + 0.06 * (hashed(b"", t["name"]) - 0.5) + t.update(state=min(state, 1.2) * gain, curve=curve, crossing=crossing, grass=density) + if plain: + value = min(limit(t["speed"]) * t["state"], MOST) + t.update(value=value, up=min(value * 1.25, MOST_UP), side=min(value * (0.9 + 0.4 * curve), MOST)) + + +def fit_under_pantograph(normal): + # the largest product of the vertical part and the speed with the gap small on every track + low, high = 100, 4000 + while high - low > 25: + middle = (low + high) // 2 + if all(largest_gap(t["heights"], min(t["up"], ceiling(t, middle))) <= GAP_MOST for t in normal): + low = middle + else: + high = middle + for t in normal: + up = ceiling(t, low) + if up < t["up"]: + ratio = up / t["up"] + t["up"], t["value"], t["side"] = up, t["value"] * ratio, t["side"] * ratio + return low + + +def joint_curve_changes(t, part, key, joints): + own = end_curve(part, key) + for other, theirs, side in joints.at(part[key]): + if other is not t and other["kind"] == "normal": + their = end_curve(theirs, side) + if abs(own - (-their if side == key else their)) > CURVE_STEP: + return True + return False + + +def curve_changes(t, joints): + part = t["paths"][0] + return abs(end_curve(part, "p1") - end_curve(part, "p2")) > CURVE_STEP or any(joint_curve_changes(t, part, key, joints) for key in ("p1", "p2")) + + +def move_near_limit(normal, joints, share): + # on wooden sleepers and where the curve changes, a share of the tracks goes nearer the limit, chosen for good by the place + chosen = 0 + for t in normal: + if not (WOOD.search(t["ballast"] or "") or curve_changes(t, joints)): + continue + part = t["paths"][0] + name = f"{t['name']}:{part['p1'][0]:.0f}:{part['p1'][2]:.0f}" + if hashed(b"near", name) >= share: + continue + value = t["value"] + (limit(t["speed"]) - t["value"]) * (NEAR_LEAST + (NEAR_MOST - NEAR_LEAST) * hashed(b"how", name)) + if value <= t["value"]: + continue + # sideways and the cross level don't move a pantograph, up and down only as far as it keeps to the wire on this track + up = value + while up > t["up"] and largest_gap(t["heights"], up) > GAP_MOST: + up -= 0.5 + t["value"], t["up"], t["side"] = value, max(up, t["up"]), min(value * (0.9 + 0.4 * t["curve"]), MOST) + chosen += 1 + return chosen + + +def value_from_plain(t, index, part, joints, product): + found = [o for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "normal" and theirs["length"] >= SHORT] + if not found: + return None + speed = max(o["speed"] for o in found) + if index == t["bent"]: + speed = min(speed, DIVERGING) + return min(limit(speed) * max(o["state"] for o in found), SWITCH_MOST, product / speed) + + +def value_from_switches(t, part, joints): + found = [] + for key in ("p1", "p2"): + for other, theirs, _ in joints.at(part[key]): + if other["kind"] == "switch" and other is not t: + found += [other["values"][i] for i, q in enumerate(other["paths"]) if q is theirs and other["values"][i] is not None] + return max(found) if found else None + + +def grade_switches(switches, joints, product): + # each path by the plain tracks it leads to, the worst of them; those only among switches from their neighbours + for t in switches: + t["values"] = [value_from_plain(t, index, part, joints, product) for index, part in enumerate(t["paths"])] + for _ in range(5): + for t in switches: + t["values"] = [value_from_switches(t, part, joints) if value is None else value for value, part in zip(t["values"], t["paths"])] + for t in switches: + t["values"] = [min(SWITCH_MOST, product / NO_SPEED) if v is None else v for v in t["values"]] + + +def grade(scenery, options): + tracks = scenery.tracks + normal = [t for t in tracks if t["kind"] == "normal"] + switches = [t for t in tracks if t["kind"] == "switch"] + joints = Joints(tracks) + mark_diverging_paths(switches) + settle_speeds(tracks, joints) + grade_states(scenery, Route(tracks, joints, *options.route) if options.route else None, options.gain) + for t in normal: + t["heights"] = heights_per_mm(t) + product = fit_under_pantograph(normal) + chosen = move_near_limit(normal, joints, options.near) + worst = max((largest_gap(t["heights"], t["up"]) for t in normal), default=0.0) + for t in normal: + del t["heights"] + if t["name"].lower() in options.straight: + t["value"] = t["up"] = t["side"] = 0.0 + grade_switches(switches, joints, product) + return product, chosen, worst + + +def report(scenery, product, chosen, worst): + normal = [t for t in scenery.tracks if t["kind"] == "normal"] + switches = [t for t in scenery.tracks if t["kind"] == "switch"] + print(f"pliki: {len(scenery.seen)}, tory: {len(normal)}, rozjazdy: {len(switches)}, drogi: {len(scenery.roads)}, trawa: {sum(len(v) for v in scenery.plants.values())}") + print(f"pion razy km/h najwyzej {product}, blizej granicy {chosen} torow, najwieksza przerwa do drutu {worst * 100:.2f} cm (bez wznosu samego drutu)") + groups = collections.defaultdict(list) + for t in normal: + groups[(t["source"], t["speed"])].append(t) + print("predkosc skad torow km limit uneven min/mediana/max pion max") + for key in sorted(groups, key=lambda k: (-k[1], k[0])): + rows = groups[key] + values = sorted(t["value"] for t in rows) + print(f"{key[1]:8.0f} {key[0]:8} {len(rows):6} {sum(t['paths'][0]['length'] for t in rows) / 1000:6.1f} {limit(key[1]):6.1f} {values[0]:6.1f} {values[len(values) // 2]:6.1f} {values[-1]:6.1f} {max(t['up'] for t in rows):8.1f}") + if switches: + values = sorted(v for t in switches for v in t["values"]) + print(f"rozjazdy: {values[0]:.1f}-{values[-1]:.1f} mm, mediana {values[len(values) // 2]:.1f}") + for note, count in sorted(scenery.notes.items()): + print(f"uwaga: {note}: {count}" if not note.startswith("brak") else f"uwaga: {note}") + if scenery.notes["rotate"]: + print("uwaga: bloki rotate nie sa uwzgledniane, polaczenia torow z nich moga byc nierozpoznane") + taken = sum(1 for t in scenery.tracks if t["taken"]) + if taken: + print(f"uwaga: {taken} torow ma juz nierownosci, zostaja bez zmian") + + +def attribute(t): + if t["kind"] == "normal": + return f"uneven {t['value']:.1f};{t['up']:.1f};{t['side']:.1f} " if t["value"] > 0 else "" + return "uneven " + ";".join(f"{v:.1f}" for v in t["values"]) + " " + + +def write(scenery, out): + by_file = collections.defaultdict(list) + for t in scenery.tracks: + if not t["taken"]: + by_file[t["file"]].append(t) + written = 0 + for path, tracks in by_file.items(): + text = scenery.texts[path] + pieces, last = [], 0 + for t in sorted(tracks, key=lambda t: t["offset"]): + pieces += [text[last:t["offset"]], attribute(t)] + last = t["offset"] + pieces.append(text[last:]) + target = out / path.relative_to(scenery.root) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes("".join(pieces).encode("latin-1")) + written += 1 + print(f"zapisane pliki: {written}, w {out}") + + +def point(text): + x, z = text.split(",") + return float(x), float(z) + + +def main(): + parser = argparse.ArgumentParser(description="Ocena torow dowolnej scenerii dla atrybutu uneven.") + parser.add_argument("files", nargs="+", type=pathlib.Path, help=".scn, .scm albo .inc; includy sa czytane dalej") + parser.add_argument("--root", type=pathlib.Path, help="folder scenery, wzgledem ktorego ida includy; domyslnie folder pierwszego pliku") + parser.add_argument("--out", type=pathlib.Path, help="folder na kopie plikow z torami; bez tego tylko raport") + parser.add_argument("--route", nargs=2, type=point, metavar="X,Z", help="tory pogarszaja sie od pierwszego punktu do drugiego, liczac po torach") + parser.add_argument("--gain", type=float, default=1.0, help="mnoznik stanu torow, domyslnie 1") + parser.add_argument("--near", type=float, default=0.6, help="jaka czesc torow na drewnie i przy zmianach krzywizny idzie blizej granicy Id-14, domyslnie 0.6") + parser.add_argument("--straight", default="", help="nazwy torow bez nierownosci, po przecinku") + options = parser.parse_args() + options.straight = {name.strip().lower() for name in options.straight.split(",") if name.strip()} + root =(options.root or options.files[0].parent).resolve() + if options.out: + options.out = options.out.resolve() + if options.out == root or root in options.out.parents: + sys.exit("--out nie moze lezec w folderze scenerii: oryginaly maja zostac nietkniete") + scenery = Scenery(root) + for path in options.files: + scenery.read(path) + if not scenery.tracks: + sys.exit("nie znaleziono torow") + product, chosen, worst = grade(scenery, options) + report(scenery, product, chosen, worst) + if options.out: + write(scenery, options.out) + + +if __name__ == "__main__": + main() diff --git a/track_uneven_doc.md b/track_uneven_doc.md new file mode 100644 index 00000000..e4822b97 --- /dev/null +++ b/track_uneven_doc.md @@ -0,0 +1,119 @@ +# Track unevenness + +An optional attribute of a track entry, before `endtrack`: + +``` +node -1 0 tor1 track normal 100.0 1.435 0.15 25.0 20 0 flat vis +rail_screw_used1 6 1435mm/tpbps-new2 0.2 0.5 1.1 +0.0 0.2 0.0 0.0 +0.0 0.0 0.0 +0.0 0.0 0.0 +0.0 0.2 100.0 0.0 +0.0 +uneven 18;;;25;6 +endtrack +``` + +The numbers are in mm, 0-100, separated by semicolons with no spaces: + +| position | `normal` track | `switch` | +|----------|-----------------------------------------------|--------------------------| +| 1 | peak deviation of a rail on a 10 m chord | the same, path p1-p2 | +| 2 | vertical | path p3-p4 | +| 3 | sideways | path p1-p2, vertical | +| 4 | cross level | path p1-p2, sideways | +| 5 | dip at rail joints, by default 0.4 of vertical | path p3-p4, vertical | +| 6 | | path p3-p4, sideways | + +A position left out takes the first number, and the dip follows the vertical value. Positions at the end can be dropped. Sideways alone is `uneven 0;;30`, and dips at the joints alone are `uneven 0;;;;6`. A switch with no components of its own on path p3-p4 takes the values of path p1-p2. A switch for 100 km/h on the straight path and 40 km/h on the diverging one is, for example, `uneven 14;35`. + +All of them are also in the editor, in the track panel, section "Ride and sound". On roads, rivers and turntables they are ignored. + +## Choosing a value + +`uneven` corresponds to the "vertical irregularities" column of the PLK Id-14 instruction, appendix 1 (class C): + +| speed [km/h] | 20 | 40 | 60 | 80 | 100 | 120 | 160 | +|---------------|----|----|----|----|-----|-----|-----| +| `uneven` [mm] | 50 | 35 | 25 | 18 | 14 | 10 | 6 | + +According to the instruction, a track in good condition is half of this value (class A), and one in fair condition 3/4 (class B). + +At these limits a pantograph may not keep up at speed, which trips the main breaker: see the end of "How it works". `tools/track_uneven_grade.py` keeps the values below that. + +A track with `uneven 18` measures as follows (`tests/unit/test_track_uneven.cpp`), against the Id-14 limit for 80 km/h: + +| parameter | result | Id-14 | +|--------------------------|--------|-----------| +| vertical irregularities | 18 mm | 18 mm | +| horizontal irregularities | 19 mm | 17 mm | +| twist on a 5 m base | 15 mm | 16 mm | +| gauge | ±10 mm | +10/-8 mm | +| cross level | 20 mm | 20 mm | +| J index | 6.1 | 5.3 | + +## How it works + +Unevenness is a displacement field that depends on the position on the map, made of waves 5-42 m long. Vertically there are fewer short waves, so the rails come near the peak value more often, and there are also waves of 47-83 m: they hardly count on the 10 m chord, the body rises and falls on them, and a pantograph keeps up with them. Nothing in it is random: one place bends the same way on every run. A track that is moved gets the waves of its new place. + +Each component joins separately: + +- **Track to track:** the lower of the two values applies at the joint, and each track comes to it smoothly over 20 m. A free end of a track stays as designed. +- **Switch:** each path has its own value, and the lower of the two applies at their common end. Directly joined switches take the lowest value of the same path, and where two paths meet, the lower of the two. A joined track comes to the value of the switch at the end it meets, even if that is higher than its own. A track with none of a component keeps the whole switch as designed in it. +- **Joints:** the dips come every `fSoundDistance`, over 6 m, and run on through following tracks with the same spacing. The wheel clatter falls where the dip is. With a spacing of 6 m or less, and on switches, there are no dips. +- **Cross level:** adds to the designed cant. It is a tilt of the map plan, so tracks laid towards each other meet at the same cant. On switches there is only the part coming from the waves, with no separate settling of the rails. + +The ends of joined paths meet in the middle between them, with one vector across, if they are less than 20 cm apart. This closes the cracks from coordinates that don't match in the data, on tracks with no unevenness as well. At such a joint a switch takes the cant of the plain track, and two tracks or two switches meet halfway. This applies to the physics too. + +The rails, the top of the ballast, `sleepermodel`, the axles, the roll of the body and the cab shake follow the unevenness. The slope of the ballast takes up the difference, and its foot stays as designed, on the ground. + +Models from includes (e.g. switch drives and timbers), the terrain and the catenary don't follow it, so at large values a rail shifts away from such a model by a few centimetres. + +On a track with unevenness the track itself shakes the cab, in place of two of the sources of shake that were there before, as far as it has something to put in their place. It goes by the track under the vehicle, and on a switch by the path set and what its joined tracks allow. Dips at the joints and cross level replace nothing, as they don't shake the cab all the way along or sideways. On tracks with no unevenness everything works as before. + +| source of the cab shake | on a track with unevenness | +|--------------------------------------------------------------------|----------------------------| +| side sway from the lateral acceleration: curve radius and cant, jolts sideways on a damaged switch | off with a sideways value | +| random jolts over 25 km/h | off with a vertical or a sideways value | +| the track itself, from the bend under the axles | added | +| vertical jolts at the joints of a damaged track | stays | +| fore and aft, when accelerating and braking | stays | +| diesel engine vibration | stays | +| hunting (`huntingspring`), set by the vehicle | stays | + +This is about the cab only. The physics, derailment included, and the noise of the wheels still use the lateral acceleration. + +A pantograph rises at the rate set by the pressure, about 0.14 m/s, and a gap to the wire over 1 cm cuts the voltage and trips the main breaker. Unevenness adds to this at high speed. On the tracks of Bałtyk a simulation gives a gap of 0.6 cm when the vertical value times the speed in km/h is 925, for example 8 mm at 120 km/h, 13 mm at 70 km/h and 23 mm at 40 km/h. The values in the Id-14 table are higher than that. The wire alone can do it too: at `tor0290` on Bałtyk it rises by about 1%, so a pantograph keeps up to about 46 km/h there, and above that the breaker trips on a straight track as well. + +The physics takes the curve radius and the grade from the design, so unevenness doesn't change them. It adds only the cross level towards derailment, about 0.06 m/s² at `uneven 18` against a threshold of about 3.2 m/s². + +## Grading the tracks of any scenery + +`tools/track_uneven_grade.py` works out the values from what the scenery shows: ballast and rail textures, grass on the track, curves, speed and road crossings. It reads a `.scn`, `.scm` or `.inc` file with its includes and leaves the originals as they are, writing copies of the files with tracks to `--out`. With no `--out` it only prints a report. + +``` +python tools/track_uneven_grade.py scenery/lk357.scn --out graded +python tools/track_uneven_grade.py scenery/baltyk_skm1.scn --out graded --route -107,2300 -4120,2491 +``` + +- `--route X,Z X,Z`: tracks get worse from the first point to the second, measured along the tracks. +- `--gain`: multiplier of the state of the tracks, 1 by default. +- `--near`: the share of tracks on wooden sleepers and at changes of curve that go nearer the Id-14 limit, 0.6 by default. The choice is fixed. +- `--straight`: names of tracks with no unevenness, separated by commas. +- `--root`: the `scenery` folder, when the given file lies deeper. + +The values stay under what a pantograph can follow, by a simulation over those tracks, but the rise of the wire itself isn't counted. Texture wear is read from the names, by the tables at the top of the script, and an unknown name gets 0.5. Tracks in `.inc` files taking parameters are skipped and `rotate` blocks aren't applied; the report says so in both cases. A track that already has unevenness stays as it is. + +## Performance + +A track with unevenness has geometry every 2 m instead of every 10 m on straights. Switches keep their division. + +Measured (camera moving or in the cab, 1920×1080): + +| scene | no flags | flags 18/35 | +|-------------------------------------|-----------------------|------------------------| +| Bałtyk, 56 km of tracks: vertices | 0.56 M (about 27 MB) | 2.17 M (about 104 MB) | +| Bałtyk: longest frame in a fly-through | 31.5 ms | 64 ms | +| `td.scn`, driving at 120 km/h: frame | 2.73 ms | 2.93 ms | + +The cost grows with the number of flagged tracks, and the longest frames occur when the geometry of further sections is built. Driving on such a track costs practically nothing: about 0.3 µs per axle in a physics step. diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index c6793e45..47c0c594 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3381,15 +3381,12 @@ bool TDynamicObject::Update(double dt, double dt1) && MoverParameters->eimc[eimc_p_abed] < 0.001) FzadED = 0; //pętla bezpieczeństwa - bez ED auto const FzadPN = Fzad - FrED * MoverParameters->MED_FrED_factor; - //np = 0; - // BUG: likely memory leak, allocation per inner loop, deleted only once outside - // TODO: sort this shit out - bool* PrzekrF = new bool[np]; + std::vector PrzekrF( np ); float nPrzekrF = 0; bool test = true; - float* FzED = new float[np]; - float* FzEP = new float[np]; - float* FmaxEP = new float[np]; + std::vector FzED( np ); + std::vector FzEP( np ); + std::vector FmaxEP( np ); // 3. ustaw pojazdom sile hamowania ED // - proporcjonalnie do mozliwosci @@ -3548,11 +3545,6 @@ bool TDynamicObject::Update(double dt, double dt1) } - delete[] PrzekrF; - delete[] FzED; - delete[] FzEP; - delete[] FmaxEP; - MED_oldFED = FzadED; } @@ -3627,6 +3619,7 @@ bool TDynamicObject::Update(double dt, double dt1) if( dRailLength > 0.0 ) { for( auto &axle : m_axlesounds ) { axle.distance = axle.offset; + axle.atjoints = false; } } dRailLength = MyTrack->fSoundDistance; @@ -3660,36 +3653,32 @@ bool TDynamicObject::Update(double dt, double dt1) 1 : ctOwner->Vehicle()->DirectionGet() == DirectionGet() ? 1 : -1 ) }; for( auto &axle : m_axlesounds ) { - axle.distance += dDOMoveLen * directioninconsist; - if( axle.distance < 0 - || axle.distance > dRailLength ) { - axle.distance = clamp_circular( axle.distance, dRailLength ); - if( MoverParameters->Vel > 0.1 ) { - // NOTE: for combined clatter sound we supply 1/100th of actual value, as the sound module converts does the opposite, converting received (typically) 0-1 values to 0-100 range - auto const frequency = true == axle.clatter.is_combined() ? MoverParameters->Vel * 0.01 : 1.0; - axle.clatter - .pitch( frequency ) - .gain( volume ) - .play(); - // crude bump simulation, drop down on even axles, move back up on - // the odd ones - // MoverParameters->AccVert += (MoverParameters->Vel*0.1f) * - double const headroom = std::clamp( - MoverParameters->Vmax - ( MoverParameters->Vel + MoverParameters->Vmax * 0.32f ), - 0.0, MoverParameters->Vmax ); - double const jolt = headroom * .05f * ( MyTrack->iDamageFlag * 0.25f ); - if (MyTrack->eType == tt_Normal) - { - MoverParameters->AccVert += std::clamp( jolt, 0.0, 4.0 ); - } - else if (MyTrack->eType == tt_Switch) - { - double const accHorizontal = std::clamp( jolt, 0.0, 1.0 ) * ((axleindex % 2) != 0 ? 1 : -1); - MoverParameters->AccS += accHorizontal; - MoverParameters->AccN += accHorizontal; - MoverParameters->AccVert += std::clamp( jolt, 0.0, 2.0 ); - } - } + if( axle_passed_joint( axle, dDOMoveLen, directioninconsist ) + && MoverParameters->Vel > 0.1 ) { + // NOTE: for combined clatter sound we supply 1/100th of actual value, as the sound module converts does the opposite, converting received (typically) 0-1 values to 0-100 range + auto const frequency = true == axle.clatter.is_combined() ? MoverParameters->Vel * 0.01 : 1.0; + axle.clatter + .pitch( frequency ) + .gain( volume ) + .play(); + // crude bump simulation, drop down on even axles, move back up on + // the odd ones + // MoverParameters->AccVert += (MoverParameters->Vel*0.1f) * + double const headroom = std::clamp( + MoverParameters->Vmax - ( MoverParameters->Vel + MoverParameters->Vmax * 0.32f ), + 0.0, MoverParameters->Vmax ); + double const jolt = headroom * .05f * ( MyTrack->iDamageFlag * 0.25f ); + if (MyTrack->eType == tt_Normal) + { + MoverParameters->AccVert += std::clamp( jolt, 0.0, 4.0 ); + } + else if (MyTrack->eType == tt_Switch) + { + double const accHorizontal = std::clamp( jolt, 0.0, 1.0 ) * ((axleindex % 2) != 0 ? 1 : -1); + MoverParameters->AccS += accHorizontal; + MoverParameters->AccN += accHorizontal; + MoverParameters->AccVert += std::clamp( jolt, 0.0, 2.0 ); + } } ++axleindex; } @@ -8092,17 +8081,25 @@ TDynamicObject::update_shake( double const Timedelta ) { shakevector *= 0; auto const iVel { std::min( GetVelocity(), 150.0 ) }; + // an uneven track shakes the cab by itself, in place of the sway drawn from the curve and the jolts drawn at random, as far as it does + auto const replaced { track_uneven::replaced_shake( MyTrack != nullptr ? MyTrack->uneven_underway() : track_uneven::amounts {} ) }; if( iVel > 0.5 ) { // acceleration-driven base shake shakevector += glm::dvec3( - -MoverParameters->AccN * Timedelta * 5.0 * Global.ShakingMultiplierRL, // highlight side sway + replaced.sway ? 0.0 : -MoverParameters->AccN * Timedelta * 5.0 * Global.ShakingMultiplierRL, // highlight side sway -MoverParameters->AccVert * Timedelta * Global.ShakingMultiplierUD, -MoverParameters->AccSVBased * Timedelta * 1.5 * Global.ShakingMultiplierBF); // accent acceleration/deceleration + // unevenness of the track under the outer axles + auto const ride { 0.5 * ( Axle0.UnevenBend() + Axle1.UnevenBend() ) * ( MoverParameters->V * MoverParameters->V ) }; + shakevector += glm::dvec3( + -glm::dot( ride, vLeft ) * Timedelta * 5.0 * Global.ShakingMultiplierRL, + -ride.y * Timedelta * Global.ShakingMultiplierUD, + 0.0 ); } auto shake { 1.25 * ShakeSpring.ComputateForces( shakevector, ShakeState.offset ) }; - if( LocalRandom( iVel ) > 25.0 ) { + if( false == replaced.jolts && LocalRandom( iVel ) > 25.0 ) { // extra shake at increased velocity shake += ShakeSpring.ComputateForces( glm::dvec3( @@ -8128,6 +8125,28 @@ TDynamicObject::update_shake( double const Timedelta ) { } } +bool +TDynamicObject::axle_passed_joint( axle_sounds &Axle, double const Move, int const Direction ) const { + + // from the follower on its side of the vehicle, as the two can be on different tracks. They stand fAxleDist apart, at the bogie pivots, and + // the offset of the axle is the place of its wheel set, so the two of a bogie come out half of ADist to either side of the pivot + auto const &follower { Axle.offset >= 0.0 ? Axle0 : Axle1 }; + if( auto const railphase { follower.RailPhase( Axle.offset - std::copysign( 0.5 * fAxleDist, Axle.offset ) ) }; railphase >= 0.0 ) { + // the rails under the axle, which aren't always those under the track the vehicle is on + auto const passed { Axle.atjoints && std::abs( railphase - Axle.distance ) > 0.5 * follower.GetTrack()->fSoundDistance }; + Axle.distance = railphase; + Axle.atjoints = true; + return passed; + } + Axle.atjoints = false; + Axle.distance += Move * Direction; + if( Axle.distance >= 0 && Axle.distance <= dRailLength ) { + return false; + } + Axle.distance = clamp_circular( Axle.distance, dRailLength ); + return true; +} + std::pair TDynamicObject::shake_angles() const { diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index 2d9636b2..577f0f02 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -410,7 +410,10 @@ private: double distance; // distance to rail joint double offset; // axle offset from centre of the vehicle sound_source clatter; // clatter emitter + bool atjoints { false }; // distance comes from TTrack::rail_phase }; + // whether the axle has just passed a rail joint. On a track with dipping joints it goes by them, elsewhere the axle counts a rail off itself + bool axle_passed_joint( axle_sounds &Axle, double const Move, int const Direction ) const; struct powertrain_sounds { sound_source inverter { sound_placement::engine }; diff --git a/world/Segment.cpp b/world/Segment.cpp index 69886f42..1aff4ae4 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -69,6 +69,8 @@ bool TSegment::Init(glm::dvec3 &NewPoint1, glm::dvec3 NewCPointOut, glm::dvec3 N // poprawienie przechyłki fRoll1 = static_cast(glm::radians(fNewRoll1)); // Ra: przeliczone jest bardziej przydatne do obliczeń fRoll2 = static_cast(glm::radians(fNewRoll2)); + m_setting.designrolls = { fRoll1, fRoll2 }; + m_setting.joints = {}; bCurve = bIsCurve; if (Global.bRollFix) { // Ra: poprawianie przechyłki @@ -118,6 +120,10 @@ bool TSegment::Init(glm::dvec3 &NewPoint1, glm::dvec3 NewCPointOut, glm::dvec3 N fStoop = std::atan2(Point2.y - Point1.y, fLength); // pochylenie toru prostego, żeby nie liczyć wielokrotnie fStep = fNewStep; + if( m_setting.uneven && pOwner->eType != tt_Switch ) { + // dense enough for the waves; a switch keeps its division, the blades depend on it + fStep = std::min( fStep, 2.0 ); + } // NOTE: optionally replace this part with the commented version, after solving geometry issues with double switches if( pOwner->eType == tt_Switch && fStep * (3.0 * Global.SplineFidelity) > fLength ) { @@ -365,6 +371,48 @@ Math3D::vector3 TSegment::GetPoint(double const fDistance) const }; */ +glm::dvec3 TSegment::stray( glm::dvec3 const &Point, track_uneven::amounts const &Amount, double const Distance ) const { + auto const field { track_uneven::field( Point ) }; + glm::dvec3 result { field.x * Amount.side, field.y * Amount.up, field.z * Amount.side }; + result *= track_uneven::stray_per_mm; + result.y -= pOwner->joint_dip( Distance ); + return result; +} + +glm::dvec3 TSegment::uneven_bend( double const Distance ) const { + if( false == m_setting.uneven ) { + return glm::dvec3 { 0.0 }; + } + // over about a bogie + auto const reach { 1.5 }; + auto const strayed = [ this ]( double const At ) { + // past the ends of the path straight on: the polynomial would run off on a short curved one + auto const on { std::clamp( At, 0.0, fLength ) }; + auto const t { FastGetTFromS( on ) }; + auto point { FastGetPoint( t ) }; + if( auto const direction { FastGetDirection( t, 0.1 / fLength ) }; on != At && glm::length( direction ) > 0.0 ) { + point += direction * ( ( At - on ) / glm::length( direction ) ); + } + return stray( point, pOwner->uneven_at( At, m_setting.path ), At ); }; + return ( strayed( Distance + reach ) - 2.0 * strayed( Distance ) + strayed( Distance - reach ) ) / ( reach * reach ); +} + +void TSegment::settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origin, glm::vec3 const &Position, glm::vec3 const &Parallel, double const Distance ) const { + if( false == m_setting.uneven ) { + return; + } + // the rails settling apart turn about the middle of the track, what's outside them follows the rail; the foot of the ballast stays + auto const gauge { std::max( std::abs( pOwner->fTrackWidth ), 0.1f ) }; + auto const across { std::clamp( glm::dot( Vertex.position - Position, Parallel ), -0.5f * gauge, 0.5f * gauge ) }; + auto const share { pOwner->uneven_bed_share( Position.y - Vertex.position.y ) }; + if( share <= 0.0 ) { + return; + } + auto const amount { pOwner->uneven_at( Distance, m_setting.path ) }; + Vertex.position.y -= static_cast( share * track_uneven::stray_per_mm * amount.cross * track_uneven::level( Origin + glm::dvec3 { Position }, glm::dvec3 { Parallel } ) * across / gauge ); + Vertex.position += glm::vec3 { share * stray( Origin + glm::dvec3 { Vertex.position }, amount, Distance ) }; +} + void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const { if (bCurve) { // można by wprowadzić uproszczony wzór dla okręgów płaskich @@ -388,8 +436,31 @@ void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm:: rotation.y = static_cast(fStoop); // pochylenie toru prostego rotation.z = static_cast(fDirection); // kierunek toru w planie } + if( false == m_setting.uneven ) { + return; + } + // direction and slope stay as designed: the physics takes the curve radius and the grade from them + auto const gauge { std::max( std::abs( pOwner->fTrackWidth ), 0.1f ) }; + glm::dvec3 const across { -std::cos( rotation.z ), 0.0, -std::sin( rotation.z ) }; + auto const amount { pOwner->uneven_at( fDistance, m_setting.path ) }; + rotation.x += static_cast( track_uneven::stray_per_mm * ( + amount.up * track_uneven::roll( position, across, gauge ) + + amount.cross * track_uneven::level( position, across ) / gauge ) ); + position += track_uneven::across_only( stray( position, amount, fDistance ), across ); }; +void TSegment::plan_frame( double const Distance, glm::dvec3 &Point, glm::dvec3 &Across ) const { + if( fTsBuffer.empty() ) { + // never made into a path: tt_Cross and the like + Point = Point1; + Across = glm::dvec3 { 0.0 }; + return; + } + auto const t { FastGetTFromS( std::clamp( Distance, 0.0, fLength ) ) }; + Point = FastGetPoint( t ); + Across = track_uneven::across_of( FastGetDirection( t, 0.1 / fLength ), FastGetPoint_1() - FastGetPoint_0() ); +} + glm::dvec3 TSegment::FastGetPoint(double const t) const { // return (bCurve?std::lerp(t,Point1,CPointOut,CPointIn,Point2):((1.0-t)*Point1+(t)*Point2)); @@ -441,6 +512,10 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, parallel1 = glm::vec3{ glm::dvec3{ FastGetPoint_1() - FastGetPoint_0() } }; } parallel1 = glm::normalize( parallel1 ); + if( iSkip == 0 && m_setting.joints[ 0 ] ) { + pos1 = glm::vec3{ m_setting.joints[ 0 ]->first - Origin }; + parallel1 = glm::vec3{ m_setting.joints[ 0 ]->second }; + } if( iEnd == 0 ) iEnd = iSegCount; fEnd = static_cast(fLength * double( iEnd ) / double( iSegCount )); @@ -486,6 +561,10 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, parallel2 = glm::vec3{ glm::dvec3{ FastGetPoint_1() - FastGetPoint_0() } }; } parallel2 = glm::normalize( parallel2 ); + if( i == iSegCount && m_setting.joints[ 1 ] ) { + pos2 = glm::vec3{ m_setting.joints[ 1 ]->first - Origin }; + parallel2 = glm::vec3{ m_setting.joints[ 1 ]->second }; + } // TODO: refactor the loop, there's no need to calculate starting points for each segment when we can copy the end points of the previous one if( Transition ) { @@ -500,6 +579,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, pt, glm::normalize( norm ), glm::vec2 { ( jmm1 * ShapePoints[ j ].texture.x + m1 * ShapePoints[ j + iNumShapePoints ].texture.x ) / texturescale, tv1 } ); + settle_vertex( Output.back(), Origin, pos1, parallel1, s - step ); } // jeśli jest wskaźnik do tablicy if (p && *p && !j) // to dla pierwszego punktu @@ -518,6 +598,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, pt, glm::normalize( norm ), glm::vec2 { ( jmm2 * ShapePoints[ j ].texture.x + m2 * ShapePoints[ j + iNumShapePoints ].texture.x ) / texturescale, tv2 } ); + settle_vertex( Output.back(), Origin, pos2, parallel2, s ); } // jeśli jest wskaźnik do tablicy // to dla pierwszego punktu @@ -540,6 +621,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, pt, glm::normalize( norm ), glm::vec2 { ShapePoints[ j ].texture.x / texturescale, tv1 } ); + settle_vertex( Output.back(), Origin, pos1, parallel1, s - step ); pt = parallel2 * ( jmm2 * ( ShapePoints[ j ].position.x - fOffsetX.first ) + m2 * ( ShapePoints[ j ].position.x - fOffsetX.second ) ) + pos2; pt.y += ShapePoints[ j ].position.y; @@ -550,6 +632,7 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, pt, glm::normalize( norm ), glm::vec2 { ShapePoints[ j ].texture.x / texturescale, tv2 } ); + settle_vertex( Output.back(), Origin, pos2, parallel2, s ); } } } diff --git a/world/Segment.h b/world/Segment.h index a3c83ade..05522e60 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -9,8 +9,13 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include +#include +#include + #include "utilities/Classes.h" #include "rendering/geometrybank.h" +#include "world/TrackUneven.h" #include struct map_colored_paths { @@ -47,6 +52,15 @@ class TSegment glm::dvec3 Point2; float fRoll1 { 0.f }; float fRoll2 { 0.f }; // przechyłka na końcach + // how the path sits among the others + struct setting { + std::array designrolls { 0.f, 0.f }; // as entered; fRoll1 and fRoll2 can differ at a joint + bool uneven { false }; // see TrackUneven.h + int path { 0 }; // which path of a switch + // where the path meets the next one: the middle of their ends and their common vector across + std::array>, 2> joints; + }; + setting m_setting; double fLength { -1.0 }; // długość policzona std::vector fTsBuffer; // wartości parametru krzywej t dla równych odcinków s std::vector fTsSlope; // nachylenie dt/du (= dt/ds * fStep) w tych samych punktach @@ -67,6 +81,11 @@ class TSegment glm::dvec3 RaInterpolate(double const t) const; glm::dvec3 RaInterpolate0(double const t) const; + // moves a lofted vertex with the unevenness of the track + void settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origin, glm::vec3 const &Position, glm::vec3 const &Parallel, double const Distance ) const; + // displacement of a point by the unevenness, at a distance along the path + glm::dvec3 stray( glm::dvec3 const &Point, track_uneven::amounts const &Amount, double const Distance ) const; + public: bool bCurve = false; @@ -106,6 +125,24 @@ public: /// Calculated position /// Calculated rotation void RaPositionGet(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const; + // call before Init, it sets the density of the geometry + void + set_uneven( bool const Uneven, int const Path = 0 ) { + m_setting.uneven = Uneven; + m_setting.path = Path; } + // a point of the path and the unit vector across it in the plan + void + plan_frame( double const Distance, glm::dvec3 &Point, glm::dvec3 &Across ) const; + float + design_roll( int const End ) const { + return m_setting.designrolls[ End ]; } + void + set_joint( int const End, std::optional> const &Joint, float const Roll ) { + m_setting.joints[ End ] = Joint; + ( End == 0 ? fRoll1 : fRoll2 ) = Roll; } + // second derivative of the displacement along the path; times speed squared: acceleration of an axle + glm::dvec3 + uneven_bend( double const Distance ) const; glm::dvec3 FastGetPoint(double const t) const; inline glm::dvec3 diff --git a/world/Track.cpp b/world/Track.cpp index f1a29798..c9c7ae36 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -47,6 +47,7 @@ TIsolated *TIsolated::pRoot = nullptr; TTrack::profiles_array TTrack::m_profiles; TTrack::profiles_map TTrack::m_profilesmap; +int TTrack::s_layoutstamp { 0 }; TSwitchExtension::TSwitchExtension(TTrack *owner, int const what) { // na początku wszystko puste @@ -325,6 +326,7 @@ void TTrack::ConnectPrevPrev(TTrack *pTrack, int typ) { //łączenie torów - Point1 własny do Point1 cudzego if (pTrack) { //(pTrack) może być zwrotnicą, a (this) tylko zwykłym odcinkiem + ++s_layoutstamp; trPrev = pTrack; iPrevDirection = pTrack->eType == tt_Switch ? 0 : typ & 2; pTrack->trPrev = this; @@ -335,6 +337,7 @@ void TTrack::ConnectPrevNext(TTrack *pTrack, int typ) { //łaczenie torów - Point1 własny do Point2 cudzego if (pTrack) { + ++s_layoutstamp; trPrev = pTrack; iPrevDirection = typ | 1; // 1:zwykły lub pierwszy zwrotnicy, 3:drugi zwrotnicy pTrack->trNext = this; @@ -354,6 +357,7 @@ void TTrack::ConnectNextPrev(TTrack *pTrack, int typ) { //łaczenie torów - Point2 własny do Point1 cudzego if (pTrack) { + ++s_layoutstamp; trNext = pTrack; iNextDirection = pTrack->eType == tt_Switch ? 0 : typ & 2; pTrack->trPrev = this; @@ -373,6 +377,7 @@ void TTrack::ConnectNextNext(TTrack *pTrack, int typ) { //łaczenie torów - Point2 własny do Point2 cudzego if (pTrack) { + ++s_layoutstamp; trNext = pTrack; iNextDirection = typ | 1; // 1:zwykły lub pierwszy zwrotnicy, 3:drugi zwrotnicy pTrack->trNext = this; @@ -633,6 +638,13 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) m_profile1 = fetch_track_rail_profile( railprofile ); } } + else if( str == "uneven" ) { + // uneven [;...]: see TrackUneven.h. The semicolons stay in the token + auto const values { parser->getToken( false, "\n\r\t " ) }; + if( ( eType == tt_Normal || eType == tt_Switch ) && iCategoryFlag == 1 ) { + m_uneven = track_uneven::read_attribute( values, eType == tt_Switch ); + } + } else if( str == "friction" ) { // memory cell holding friction value modifiers m_friction.first = parser->getToken(); @@ -674,6 +686,10 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) *parser >> token; str = token; } + // the segments were made before the attributes were read, and unevenness changes them + if( is_uneven() ) { + init_segments( false ); + } // alternatywny zapis nazwy odcinka izolowanego - po znaku "@" w nazwie toru if ((i = m_name.find("@")) != std::string::npos && i < m_name.length()) { @@ -705,6 +721,14 @@ void TTrack::init_segments( bool const Initial ) { fTexHeight1 -= fTexHeightOffset; fTexHeightOffset = 0.f; iTrapezoid = 0; + ++s_layoutstamp; + if( eType == tt_Switch && SwitchExtension ) { + SwitchExtension->Segments[ 0 ]->set_uneven( is_uneven() ); + SwitchExtension->Segments[ 1 ]->set_uneven( is_uneven(), 1 ); + } + else if( Segment ) { + Segment->set_uneven( is_uneven() ); + } switch (eType) { // Ra: łuki segmentowane co 5m albo 314-kątem foremnym @@ -1351,6 +1375,8 @@ glm::vec3 TTrack::get_nearest_point(const glm::dvec3 &point) const // wypełnianie tablic VBO void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { + m_drawnrun = {}; + Geometry1.clear(); Geometry2.clear(); if( SwitchExtension ) { @@ -1365,6 +1391,10 @@ void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { gfx::userdata_array empty_userdata; + share_joints(); + if( rail_phase( 0.0 ) >= 0.0 ) { + m_drawnrun = m_railrun; + } // bake per-instance sleeper transforms now that the owning cell has assigned m_origin. // safe to call here even if the track has no sleepermodel (early-outs internally). if( m_sleeper_enabled && m_sleeper_local_transforms.empty() ) { @@ -2445,6 +2475,7 @@ TTrack::export_as_text_( std::ostream &Output ) const { && false == m_profile1.first.empty() ) { Output << "railprofile " << m_profile1.first << ' '; } + Output << track_uneven::write_attribute( m_uneven, eType == tt_Switch ); if( false == m_friction.first.empty() ) { Output << "friction " << m_friction.first << ' '; } @@ -3632,6 +3663,11 @@ path_table::InitTracks() { isolated = isolated->Next(); } + // the physics goes by the cant at the joints too, and comes before any geometry is made + for( auto *track : m_items ) { + track->share_joints(); + } + TTrack::fetch_default_profiles(); } diff --git a/world/Track.h b/world/Track.h index 72a6b7ff..dc6be741 100644 --- a/world/Track.h +++ b/world/Track.h @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Classes.h" #include "world/Segment.h" +#include "world/TrackUneven.h" #include "model/material.h" #include "scene/scenenode.h" #include "utilities/Names.h" @@ -238,6 +239,7 @@ public: TEnvironmentType eEnvironment = e_flat; // dźwięk i oświetlenie int iAction = 0; // czy modyfikowany eventami (specjalna obsługa przy skanowaniu) float fOverhead = -1.0; // można normalnie pobierać prąd (0 dla jazdy bezprądowej po danym odcinku, >0-z opuszczonym i ograniczeniem prędkości) + track_uneven::attribute m_uneven; // plain tracks and switches private: double fVelocity = -1.0; // ograniczenie prędkości // prędkość dla AI (powyżej rośnie prawdopowobieństwo wykolejenia) public: @@ -339,6 +341,29 @@ public: void VelocitySet(float v); double VelocityGet(); float Friction() const; + // a switch has two paths, any other track the one + int path_count() const; + TSegment *path_segment( int const Path ) const; + TTrack *path_neighbour( int const Path, int const End ) const; + // the unevenness the track declares, on a path of a switch + track_uneven::amounts uneven_declared( int const Path = 0 ) const; + bool is_uneven() const; + // the unevenness along what the vehicles take now: the path set on a switch, with what the switches joined and the tracks by them allow + track_uneven::amounts uneven_underway(); + // the unevenness at a distance along a path, meeting the tracks joined at its ends + track_uneven::amounts uneven_at( double const Distance, int const Path = 0 ); + // how much of the displacement a point of the profile at the depth takes + double uneven_bed_share( double const Depth ) const; + // closes the gaps and steps of cant between the ends of joined paths + void share_joints(); + double joint_depth() const; // mm + double joint_dip( double const Distance ); // m + // distance from the rail joint behind; negative: the track has no joints that dip + double rail_phase( double const Distance ); + // whether the rail joints are elsewhere than the geometry of the track has them + bool rail_joints_moved(); + // the uneven tracks whose values go by the switches among the given: all the switches joined to those, and the tracks they meet + static std::vector switch_followers( std::vector const &Tracks ); void ConnectionsLog(); bool DoubleSlip() const; static void fetch_default_profiles(); @@ -370,6 +395,33 @@ private: void create_track_rail_profile( gfx::vertex_array &Right, gfx::vertex_array &Left ); void create_track_blade_profile( gfx::vertex_array &Right, gfx::vertex_array &Left ); void create_track_bed_profile( gfx::vertex_array &Output, TTrack const *Previous, TTrack const *Next ); + // rail joints run on across joined tracks, however the line is cut into them, so the clatter keeps its rhythm + struct rail_run { + double origin { 0.0 }; // position along the run of Point1 + double way { 1.0 }; // direction of the path along the run + int stamp { -1 }; + }; + void settle_rail_run(); + rail_run m_railrun; + rail_run m_drawnrun; // as the geometry has it; stamp -1: it has no joints that dip + // the value along each path of a switch: the lowest of that path in the switches joined, 0 next to a track with none. + // And at the ends of the paths, by what meets there: the other path at the common end, the path of a switch joined + struct switch_stray { + std::array paths; + std::array, 2> ends; + int stamp { -1 }; + }; + switch_stray m_switchstray; + switch_stray const &switch_uneven(); + void settle_switch_stray(); + void settle_switch_ends(); + // the lowest value of the paths ending at the point + track_uneven::amounts switch_paths_at( glm::dvec3 const &Point ) const; + // what a plain track comes to at its end, where it meets the other one + track_uneven::amounts uneven_met( TTrack *Other, glm::dvec3 const &Point ) const; + // the value of a switch where a track joined to it meets it + track_uneven::amounts switch_uneven_at( glm::dvec3 const &Point ); + static int s_layoutstamp; // bumped when tracks are joined or changed, invalidates the two above. Once per change: a run is walked again after each void create_road_profile( gfx::vertex_array &Output, bool const Forcetransition = false ); void create_road_side_profile( gfx::vertex_array &Right, gfx::vertex_array &Left, gfx::vertex_array const &Road, bool const Forcetransition = false ); /// diff --git a/world/TrackUneven.cpp b/world/TrackUneven.cpp new file mode 100644 index 00000000..10bf48d6 --- /dev/null +++ b/world/TrackUneven.cpp @@ -0,0 +1,386 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "world/Track.h" + +#include + +// The part of TTrack that goes with the unevenness of a track and with what it meets at its ends: joined tracks and switches. +// The rules themselves are in TrackUneven.h, here they get applied to the tracks. + +namespace { + +double const switch_end_reach { 0.3 }; // how near an end of a path counts as being at a point, m +double const joint_reach { 0.2 }; // ends of joined paths further apart than this stay as they are, m +double const joint_alike { 0.9 }; // ends of joined paths less alike in direction than this stay as they are, cosine + +bool is_switch( TTrack const *Track ) { + + return Track != nullptr && Track->eType == tt_Switch && Track->SwitchExtension; +} + +glm::dvec3 end_point( TSegment const &Segment, int const End ) { + + return End == 0 ? Segment.FastGetPoint_0() : Segment.FastGetPoint_1(); +} + +std::array joined_to_switch( TTrack const &Switch ) { + + auto const &extension { *Switch.SwitchExtension }; + return { extension.pPrevs[ 0 ], extension.pPrevs[ 1 ], extension.pNexts[ 0 ], extension.pNexts[ 1 ] }; +} + +// the switch and the switches joined directly to it, and to those +std::vector switch_group( TTrack &Switch ) { + + std::vector group { &Switch }; + std::vector pending { &Switch }; + while( false == pending.empty() ) { + auto const *track { pending.back() }; + pending.pop_back(); + for( auto *other : joined_to_switch( *track ) ) { + if( is_switch( other ) && std::ranges::find( group, other ) == group.end() ) { + group.push_back( other ); + pending.push_back( other ); + } + } + } + return group; +} + +// a plain track with none of a component keeps the switch to its design in it, any other comes to the value of the switch +void keep_to_plain_track( std::array &Values, TTrack const &Plain ) { + + auto const theirs { Plain.uneven_declared() }; + for( auto &path : Values ) { + if( theirs.up <= 0.0 ) { path.up = 0.0; } + if( theirs.side <= 0.0 ) { path.side = 0.0; } + } +} + +// the value along each path: the lowest of that path in the group, 0 in a component that a plain track joined has none of +std::array group_path_values( std::span const Group ) { + + std::array values { Group.front()->uneven_declared( 0 ), Group.front()->uneven_declared( 1 ) }; + for( auto const *track : Group ) { + values[ 0 ] = track_uneven::least( values[ 0 ], track->uneven_declared( 0 ) ); + values[ 1 ] = track_uneven::least( values[ 1 ], track->uneven_declared( 1 ) ); + for( auto const *other : joined_to_switch( *track ) ) { + if( other != nullptr && false == is_switch( other ) ) { + keep_to_plain_track( values, *other ); + } + } + } + return values; +} + +// the lowest Value( path, end ) of the ends of the paths at the point; Otherwise when none of them is there +template +track_uneven::amounts least_at_ends( TTrack const &Switch, glm::dvec3 const &Point, track_uneven::amounts const &Otherwise, Value_ const &Value ) { + + auto found { false }; + track_uneven::amounts result { track_uneven::most, track_uneven::most, track_uneven::most }; + for( int path = 0; path < 2; ++path ) { + for( int end = 0; end < 2; ++end ) { + if( glm::length( end_point( *Switch.path_segment( path ), end ) - Point ) < switch_end_reach ) { + result = track_uneven::least( result, Value( path, end ) ); + found = true; + } + } + } + return found ? result : Otherwise; +} + +// how the end of a path meets the track joined to it +struct path_joint { + std::optional> place; // none: too far apart, or going different ways + float roll; +}; + +path_joint meet( TTrack const &Track, TSegment const &Segment, int const End, TTrack const &Neighbour ) { + + path_joint result { {}, Segment.design_roll( End ) }; + glm::dvec3 point; + glm::dvec3 across; + Segment.plan_frame( End ? Segment.GetLength() : 0.0, point, across ); + auto closest { joint_reach }; + for( int path = 0; path < Neighbour.path_count(); ++path ) { + auto const *theirs { Neighbour.path_segment( path ) }; + if( theirs == nullptr ) { + continue; + } + for( int end = 0; end < 2; ++end ) { + // laid towards each other, they look across and lean to opposite sides + auto const opposite { end == End }; + glm::dvec3 theirpoint; + glm::dvec3 theiracross; + theirs->plan_frame( end ? theirs->GetLength() : 0.0, theirpoint, theiracross ); + if( opposite ) { + theiracross = -theiracross; + } + auto const gap { glm::length( theirpoint - point ) }; + if( gap >= closest || glm::dot( across, theiracross ) < joint_alike ) { + continue; + } + closest = gap; + result.place.emplace( 0.5 * ( point + theirpoint ), glm::normalize( across + theiracross ) ); + if( Track.iCategoryFlag & 1 ) { + result.roll = track_uneven::joint_roll( + Segment.design_roll( End ), + opposite ? -theirs->design_roll( end ) : theirs->design_roll( end ), + Track.eType == tt_Switch, + Neighbour.eType == tt_Switch ); + } + } + } + return result; +} + +} // namespace + +int TTrack::path_count() const { + + return eType == tt_Switch ? 2 : 1; +} + +TSegment *TTrack::path_segment( int const Path ) const { + + return eType == tt_Switch ? SwitchExtension->Segments[ Path ].get() : Segment.get(); +} + +TTrack *TTrack::path_neighbour( int const Path, int const End ) const { + + if( eType == tt_Switch ) { + return End == 0 ? SwitchExtension->pPrevs[ Path ] : SwitchExtension->pNexts[ Path ]; + } + return End == 0 ? trPrev : trNext; +} + +track_uneven::amounts TTrack::uneven_declared( int const Path ) const { + + return track_uneven::declared( m_uneven, eType == tt_Normal, Path ); +} + +bool TTrack::is_uneven() const { + + auto const any = []( track_uneven::amounts const &Amount ) { + return Amount.up > 0.0 || Amount.side > 0.0 || Amount.cross > 0.0; }; + return any( uneven_declared( 0 ) ) || any( uneven_declared( 1 ) ) || joint_depth() > 0.0; +} + +track_uneven::amounts TTrack::uneven_underway() { + + return is_switch( this ) ? switch_uneven().paths[ SwitchExtension->CurrentIndex ] : uneven_declared(); +} + +double TTrack::uneven_bed_share( double const Depth ) const { + + return track_uneven::bed_share( Depth, std::abs( track_rail_profile( m_profile1.second ).front().position.y ), fTexHeight1 ); +} + +track_uneven::amounts TTrack::uneven_at( double const Distance, int const Path ) { + + if( eType == tt_Switch ) { + auto const &stray { switch_uneven() }; + return track_uneven::along( stray.paths[ Path ], stray.ends[ Path ][ 0 ], stray.ends[ Path ][ 1 ], Distance, path_segment( Path )->GetLength() ); + } + return track_uneven::along( uneven_declared(), uneven_met( trPrev, end_point( *Segment, 0 ) ), uneven_met( trNext, end_point( *Segment, 1 ) ), Distance, Length() ); +} + +track_uneven::amounts TTrack::uneven_met( TTrack *Other, glm::dvec3 const &Point ) const { + + if( Other == nullptr ) { + return {}; + } + return Other->eType == tt_Switch ? Other->switch_uneven_at( Point ) : track_uneven::least( uneven_declared(), Other->uneven_declared() ); +} + +TTrack::switch_stray const &TTrack::switch_uneven() { + + if( m_switchstray.stamp != s_layoutstamp ) { + settle_switch_stray(); + } + return m_switchstray; +} + +void TTrack::settle_switch_stray() { + + auto const group { switch_group( *this ) }; + auto const values { group_path_values( group ) }; + for( auto *track : group ) { + track->m_switchstray.paths = values; + track->m_switchstray.stamp = s_layoutstamp; + } + // with the paths known in all of the group + for( auto *track : group ) { + track->settle_switch_ends(); + } +} + +void TTrack::settle_switch_ends() { + + for( int path = 0; path < 2; ++path ) { + for( int end = 0; end < 2; ++end ) { + auto const point { end_point( *path_segment( path ), end ) }; + auto value { switch_paths_at( point ) }; + if( auto const *other { path_neighbour( path, end ) }; is_switch( other ) ) { + value = track_uneven::least( value, other->switch_paths_at( point ) ); + } + m_switchstray.ends[ path ][ end ] = value; + } + } +} + +track_uneven::amounts TTrack::switch_paths_at( glm::dvec3 const &Point ) const { + + auto const &paths { m_switchstray.paths }; + return least_at_ends( *this, Point, track_uneven::least( paths[ 0 ], paths[ 1 ] ), [ &paths ]( int const Path, int ) { return paths[ Path ]; } ); +} + +track_uneven::amounts TTrack::switch_uneven_at( glm::dvec3 const &Point ) { + + auto const &stray { switch_uneven() }; + return least_at_ends( *this, Point, track_uneven::least( stray.paths[ 0 ], stray.paths[ 1 ] ), [ &stray ]( int const Path, int const End ) { return stray.ends[ Path ][ End ]; } ); +} + +double TTrack::joint_depth() const { + + if( eType != tt_Normal ) { + return 0.0; + } + return ( m_uneven.dip >= 0.f ? m_uneven.dip : track_uneven::dip_share * uneven_declared().up ); +} + +double TTrack::joint_dip( double const Distance ) { + + auto const phase { rail_phase( Distance ) }; + if( phase < 0.0 ) { + return 0.0; + } + auto const fromjoint { std::min( phase, fSoundDistance - phase ) }; + if( fromjoint >= track_uneven::dip_reach ) { + return 0.0; + } + auto const depth { joint_depth() }; + auto const atend = [ this, depth ]( TTrack const *Other ) { + return ( Other == nullptr || Other->fSoundDistance != fSoundDistance ? 0.0 : std::min( depth, Other->joint_depth() ) ); }; + return 0.001 * track_uneven::dip( fromjoint ) * track_uneven::along( depth, atend( trPrev ), atend( trNext ), Distance, Length() ); +} + +double TTrack::rail_phase( double const Distance ) { + + if( joint_depth() <= 0.0 || fSoundDistance <= 2.0 * track_uneven::dip_reach ) { + return -1.0; + } + if( m_railrun.stamp != s_layoutstamp ) { + settle_rail_run(); + } + return track_uneven::rail_phase( m_railrun.origin + m_railrun.way * Distance, fSoundDistance ); +} + +bool TTrack::rail_joints_moved() { + + return m_drawnrun.stamp != -1 + && rail_phase( 0.0 ) >= 0.0 + && ( m_railrun.origin != m_drawnrun.origin || m_railrun.way != m_drawnrun.way ); +} + +std::vector TTrack::switch_followers( std::vector const &Tracks ) { + + std::vector followers; + auto const follow = [ &followers ]( TTrack *Track ) { + if( Track != nullptr && Track->is_uneven() && std::ranges::find( followers, Track ) == followers.end() ) { + followers.push_back( Track ); + } }; + for( auto *track : Tracks ) { + if( false == is_switch( track ) ) { + continue; + } + for( auto *member : switch_group( *track ) ) { + follow( member ); + std::ranges::for_each( joined_to_switch( *member ), follow ); + } + } + return followers; +} + +void TTrack::settle_rail_run() { + + // the next track of the run past an end of a track, and which of its ends is joined there + auto const onward = []( TTrack const &Track, int const End, int &Joined ) -> TTrack * { + auto *other { End == 0 ? Track.trPrev : Track.trNext }; + if( other == nullptr || other == &Track || other->joint_depth() <= 0.0 || other->fSoundDistance != Track.fSoundDistance ) { + return nullptr; + } + Joined = ( End == 0 ? Track.iPrevDirection : Track.iNextDirection ) & 1; + return other; }; + // walks from this track to an end of the run + auto const walk = [ this, &onward ]( int &End ) { + auto *track { this }; + int joined { 0 }; + for( int step = 0; step < 1000000; ++step ) { + auto *next { onward( *track, End, joined ) }; + if( next == nullptr || next == this ) { + break; + } + track = next; + End = 1 - joined; + } + return track; }; + auto const place = []( TTrack const &Track, int const End ) { + auto const point { end_point( *Track.Segment, End ) }; + return std::make_pair( point.x, point.z ); }; + + // start at the end with the lesser coordinates, to get the same joints from any track of the run + int end { 0 }; + int otherend { 1 }; + auto *track { walk( end ) }; + if( auto *other { walk( otherend ) }; place( *other, otherend ) < place( *track, end ) ) { + track = other; + end = otherend; + } + auto along { 0.0 }; + while( track != nullptr && track->m_railrun.stamp != s_layoutstamp ) { + auto const length { track->Length() }; + track->m_railrun = { end == 0 ? along : along + length, end == 0 ? 1.0 : -1.0, s_layoutstamp }; + along += length; + int joined { 0 }; + track = onward( *track, 1 - end, joined ); + end = joined; + } + // tracks joined one way only can leave this one out of the run: it stands on its own then, not to walk all of it again on every call + if( m_railrun.stamp != s_layoutstamp ) { + m_railrun = { 0.0, 1.0, s_layoutstamp }; + } +} + +void TTrack::share_joints() { + + for( int path = 0; path < path_count(); ++path ) { + auto *segment { path_segment( path ) }; + if( segment == nullptr ) { + continue; + } + for( int end = 0; end < 2; ++end ) { + auto const *neighbour { path_neighbour( path, end ) }; + auto const joined { neighbour != nullptr && neighbour != this && ( neighbour->iCategoryFlag & 15 ) == ( iCategoryFlag & 15 ) }; + auto const joint { joined ? meet( *this, *segment, end, *neighbour ) : path_joint { {}, segment->design_roll( end ) } }; + segment->set_joint( end, joint.place, joint.roll ); + } + } + if( eType != tt_Switch && Segment && ( iCategoryFlag & 1 ) ) { + float roll1 { 0.f }; + float roll2 { 0.f }; + Segment->GetRolls( roll1, roll2 ); + if( roll1 != 0.f || roll2 != 0.f ) { + iTrapezoid |= 1; + } + } +} diff --git a/world/TrackUneven.h b/world/TrackUneven.h new file mode 100644 index 00000000..8e0f0775 --- /dev/null +++ b/world/TrackUneven.h @@ -0,0 +1,297 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Track unevenness: a displacement field over the plan of the scenery, so everything at one place moves together: joined tracks, the +// paths of a switch, sleepers, axles. Waves 5-42 m long, shares after the German track spectrum with the long waves cut down; up and down to 83 m. +// The "uneven" value is the peak deviation of a rail on a 10 m chord, as in PLK Id-14 appendix 1; see test_track_uneven.cpp. +namespace track_uneven { + +inline constexpr float most { 100.f }; // largest value of the attributes, mm +inline constexpr double stray_per_mm { 0.00145 }; // field amplitude per mm of "uneven", m +inline constexpr double cross_share { 0.53 }; // share of the field amplitude the rails settle apart by +inline constexpr double fade { 20.0 }; // length over which a track goes from the value at its end to its own, m +inline constexpr double dip_share { 0.4 }; // joint dip depth per mm of "uneven", mm +inline constexpr double dip_reach { 3.0 }; // reach of the dip on each side of a joint, m + +// the value of each component of the unevenness, mm +struct amounts { + double up { 0.0 }; + double side { 0.0 }; + double cross { 0.0 }; +}; + +inline amounts least( amounts const &Left, amounts const &Right ) { + + return { std::min( Left.up, Right.up ), std::min( Left.side, Right.side ), std::min( Left.cross, Right.cross ) }; +} + +// which of the other sources of the cab shake the ride on a track stands in for +struct shake_sources { + bool sway { false }; // from the lateral acceleration + bool jolts { false }; // drawn at random +}; + +// sideways only with something going sideways. Cross level turns the body, and the dips at the joints come and go: neither makes up for any of them +inline shake_sources replaced_shake( amounts const &Amount ) { + + return { Amount.side > 0.0, Amount.up > 0.0 || Amount.side > 0.0 }; +} + +struct wave { + double x; + double z; + double phase; + double share; +}; + +// the numbers of an entry like "uneven 14;35", negative where left out +template +std::array read_fields( std::string_view const Text ) { + + std::array result; + result.fill( -1.f ); + auto rest { Text }; + for( auto &field : result ) { + auto const end { std::min( rest.find( ';' ), rest.size() ) }; + if( auto value { 0.f }; end > 0 && std::from_chars( rest.data(), rest.data() + end, value ).ec == std::errc {} ) { + field = std::clamp( value, 0.f, most ); + } + if( end == rest.size() ) { + break; + } + rest.remove_prefix( end + 1 ); + } + return result; +} + +// and back, with the ones left out at the end dropped +template +std::string write_fields( std::array const &Fields ) { + + auto count { Count }; + while( count > 1 && Fields[ count - 1 ] < 0.f ) { + --count; + } + std::ostringstream text; + for( std::size_t index = 0; index < count; ++index ) { + if( index > 0 ) { + text << ';'; + } + if( Fields[ index ] >= 0.f ) { + text << Fields[ index ]; + } + } + return text.str(); +} + +// the values of the attribute, mm, negative where not given. Cross level and the dip at joints for plain tracks, the path p3-p4 for switches +struct attribute { + float value { 0.f }; + float up { -1.f }; + float side { -1.f }; + float cross { -1.f }; + float dip { -1.f }; + float branch { -1.f }; + float branchup { -1.f }; + float branchside { -1.f }; +}; + +// a plain track: value;up;side;cross;dip. A switch: p1-p2;p3-p4;up of p1-p2;side of p1-p2;up of p3-p4;side of p3-p4 +inline attribute read_attribute( std::string_view const Text, bool const Switch ) { + + auto const fields { read_fields<6>( Text ) }; + attribute result; + result.value = std::max( fields[ 0 ], 0.f ); + if( Switch ) { + result.branch = fields[ 1 ]; + result.up = fields[ 2 ]; + result.side = fields[ 3 ]; + result.branchup = fields[ 4 ]; + result.branchside = fields[ 5 ]; + } + else { + result.up = fields[ 1 ]; + result.side = fields[ 2 ]; + result.cross = fields[ 3 ]; + result.dip = fields[ 4 ]; + } + return result; +} + +// in the layout of the entry it goes into, which can be of another type than the track the values come from +inline std::string write_attribute( attribute const &Values, bool const Switch ) { + + auto const fields { Switch ? + std::array { Values.value, Values.branch, Values.up, Values.side, Values.branchup, Values.branchside } : + std::array { Values.value, Values.up, Values.side, Values.cross, Values.dip, -1.f } }; + if( Values.value <= 0.f && std::all_of( fields.begin() + 1, fields.end(), []( float const Field ) { return Field < 0.f; } ) ) { + return {}; + } + return "uneven " + write_fields( fields ) + ' '; +} + +// what a path comes to from the values: what's not given takes the first value, p3-p4 those of p1-p2. Cross level on plain tracks only +inline amounts declared( attribute const &Values, bool const Plain, int const Path ) { + + auto const given = []( float const Part, double const Otherwise ) { + return Part >= 0.f ? static_cast( Part ) : Otherwise; }; + amounts result { + given( Values.up, Values.value ), + given( Values.side, Values.value ), + Plain ? given( Values.cross, Values.value ) : 0.0 }; + if( Path == 1 ) { + result.up = given( Values.branchup, given( Values.branch, result.up ) ); + result.side = given( Values.branchside, given( Values.branch, result.side ) ); + } + return result; +} + +template +using wave_set = std::array; + +// the waves run in all directions of the plan, so no direction of a track is favoured +template +wave_set make_waves( double const Turn, std::array const &Lengths, std::array const &Shares ) { + + auto const golden { glm::pi() * ( 3.0 - std::sqrt( 5.0 ) ) }; + wave_set result {}; + for( std::size_t i = 0; i < Count; ++i ) { + auto const number { glm::two_pi() / Lengths[ i ] }; + auto const place { static_cast( i ) }; + auto const heading { Turn + golden * place }; + result[ i ] = { number * std::cos( heading ), number * std::sin( heading ), golden * ( place + 1.0 ) * ( 1.0 + 3.0 * Turn ), Shares[ i ] }; + } + return result; +} + +inline constexpr std::array wave_lengths { 5.3, 7.1, 9.7, 13.1, 17.9, 23.9, 31.7, 41.9 }; +inline constexpr std::array wave_shares { 0.0721, 0.0997, 0.1344, 0.1703, 0.1549, 0.1386, 0.1226, 0.1074 }; + +// fewer short ones than sideways: the rails come near the limit in the vertical more often, with the peak still at it. +// The long ones hardly count on the 10 m chord: the body rises and falls on them, slowly enough for a pantograph to keep up +inline wave_set<8> const up_waves { make_waves<8>( 0.0, { 8.3, 11.9, 16.7, 23.9, 37.1, 47.3, 61.9, 83.3 }, { 0.14, 0.19, 0.22, 0.22, 0.23, 0.23, 0.23, 0.23 } ) }; +inline wave_set<8> const x_waves { make_waves( 1.1, wave_lengths, wave_shares ) }; +inline wave_set<8> const z_waves { make_waves( 2.3, wave_lengths, wave_shares ) }; +// the tilt along each axis of the plan. Long waves only: with the short ones the cross level reaching its limit would break the limit of twist +inline wave_set<3> const tilt_x_waves { make_waves<3>( 0.7, { 23.9, 31.7, 41.9 }, { 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0 } ) }; +inline wave_set<3> const tilt_z_waves { make_waves<3>( 2.9, { 23.9, 31.7, 41.9 }, { 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0 } ) }; + +template +double sum( wave_set const &Waves, glm::dvec3 const &Point ) { + + auto total { 0.0 }; + for( auto const &wave : Waves ) { + total += wave.share * std::sin( wave.x * Point.x + wave.z * Point.z + wave.phase ); + } + return total; +} + +// displacement at the point for an amplitude of 1, sideways within +-1, up and down within +-1.69 +inline glm::dvec3 field( glm::dvec3 const &Point ) { + + return { sum( x_waves, Point ), sum( up_waves, Point ), sum( z_waves, Point ) }; +} + +// how much lower the rail towards Across lies by settling apart, for an amplitude of 1. A tilt of the plan, so tracks laid towards each +// other meet at one cant. Follows the middle of a path, so plain tracks only +inline double level( glm::dvec3 const &Point, glm::dvec3 const &Across ) { + + return cross_share * ( Across.x * sum( tilt_x_waves, Point ) + Across.z * sum( tilt_z_waves, Point ) ); +} + +// roll made by the field for an amplitude of 1; positive lowers the rail towards Across, as the roll of a path +inline double roll( glm::dvec3 const &Point, glm::dvec3 const &Across, double const Gauge ) { + + return ( sum( up_waves, Point - Across * ( 0.5 * Gauge ) ) - sum( up_waves, Point + Across * ( 0.5 * Gauge ) ) ) / Gauge; +} + +// value at a distance along a track: Full inside, exactly Atstart and Atend at its ends where it meets the neighbours +inline double along( double const Full, double const Atstart, double const Atend, double const Distance, double const Length ) { + + auto const smooth = []( double const Share ) { + auto const share { std::clamp( Share, 0.0, 1.0 ) }; + return share * share * ( 3.0 - 2.0 * share ); }; + auto const ends { Length > 0.0 ? Atstart + ( Atend - Atstart ) * smooth( Distance / Length ) : Atstart }; + return ends + ( Full - ends ) * std::min( smooth( Distance / fade ), smooth( ( Length - Distance ) / fade ) ); +} + +// how much of the displacement a point of the profile takes, by its depth under the rail top: all of it down to the foot of the rail, +// none at the foot of the ballast, which stays on the ground as designed +inline double bed_share( double const Depth, double const Rail, double const Height ) { + + return Height > 0.0 ? std::clamp( 1.0 - ( Depth - Rail ) / Height, 0.0, 1.0 ) : 1.0; +} + +// a displacement with the part along the track taken out, Across being a unit vector in the plan. Along the track +// it would only change the distance between coupled vehicles, and with it the forces between them +inline glm::dvec3 across_only( glm::dvec3 const &Offset, glm::dvec3 const &Across ) { + + return Across * glm::dot( Offset, Across ) + glm::dvec3 { 0.0, Offset.y, 0.0 }; +} + +// the unit vector across a direction in the plan, or across the chord if it has none; zero for a path of no length, which has neither +inline glm::dvec3 across_of( glm::dvec3 const &Direction, glm::dvec3 const &Chord ) { + + for( auto const &along : { Direction, Chord } ) { + glm::dvec3 const across { -along.z, 0.0, along.x }; + if( glm::dot( across, across ) > 0.0 ) { + return glm::normalize( across ); + } + } + return glm::dvec3 { 0.0 }; +} + +// the cant where two paths meet, theirs turned to the way of this one: a switch takes that of a plain track, alike ones meet halfway +inline float joint_roll( float const Own, float const Their, bool const Switch, bool const Theirswitch ) { + + if( Switch == Theirswitch ) { + return 0.5f * ( Own + Their ); + } + return Switch ? Their : Own; +} + +// each component the same way +inline amounts along( amounts const &Full, amounts const &Atstart, amounts const &Atend, double const Distance, double const Length ) { + + return { + along( Full.up, Atstart.up, Atend.up, Distance, Length ), + along( Full.side, Atstart.side, Atend.side, Distance, Length ), + along( Full.cross, Atstart.cross, Atend.cross, Distance, Length ) }; +} + +// distance from the joint behind the point +inline double rail_phase( double const Along, double const Rail ) { + + auto const phase { std::fmod( Along, Rail ) }; + return phase < 0.0 ? phase + Rail : phase; +} + +// share of the dip depth at a distance from the joint +inline double dip( double const Fromjoint ) { + + if( Fromjoint >= dip_reach ) { + return 0.0; + } + auto const height { std::cos( glm::half_pi() * Fromjoint / dip_reach ) }; + return height * height; +} + +} // namespace track_uneven diff --git a/world/TrkFoll.cpp b/world/TrkFoll.cpp index c437553d..035e0bd9 100644 --- a/world/TrkFoll.cpp +++ b/world/TrkFoll.cpp @@ -267,6 +267,11 @@ bool TTrackFollower::Move(double fDistance, bool bPrimary) } }; +double TTrackFollower::RailPhase( double const Offset ) const { + + return pCurrentTrack != nullptr ? pCurrentTrack->rail_phase( fCurrentDistance + fDirection * Offset ) : -1.0; +} + bool TTrackFollower::ComputatePosition() { // ustalenie współrzędnych XYZ if (pCurrentSegment) // o ile jest tor diff --git a/world/TrkFoll.h b/world/TrkFoll.h index bba97681..62f386a9 100644 --- a/world/TrkFoll.h +++ b/world/TrkFoll.h @@ -39,6 +39,10 @@ public: // inline double GetLength(vector3 p1, vector3 cp1, vector3 cp2, vector3 p2) //{ return pCurrentSegment->ComputeLength(p1,cp1,cp2,p2); }; // inline double GetRadius(double L, double d); //McZapkie-150503 + inline glm::dvec3 UnevenBend() const { + return pCurrentSegment ? pCurrentSegment->uneven_bend( fCurrentDistance ) : glm::dvec3 { 0.0 }; }; + // TTrack::rail_phase of the point Offset ahead of the axle along the vehicle + double RailPhase( double const Offset ) const; bool Init(TTrack *pTrack, TDynamicObject *NewOwner, double fDir); void Reset(); void Render(float fNr);