From e5266e1e6b025e5b7c53c8e02083b0e747acaebf Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 15:47:10 +0100 Subject: [PATCH 01/18] feat(track): track unevenness declared in the track entry Usage, optional attributes of a track entry, before endtrack: uneven peak deviation of a rail on a 10 m chord, 0-100; normal and switch unevenup up and down alone, instead of uneven; normal and switch unevenside sideways alone, instead of uneven; normal and switch unevencross cross level alone, instead of uneven; normal only jointdip depth of the dip at rail joints, every sound distance; normal only, default 0.4 of the up and down component Values of uneven follow PLK Id-14 appendix 1: 6 mm is the limit at 160 km/h, 10 at 120, 18 at 80, 35 at 40; half of it for a track in a good state. Example: uneven 18 unevencross 25 jointdip 6 endtrack The same fields are in the editor, track panel, section Ride and sound. See track_uneven_doc.md. The unevenness is a displacement field over the plan of the scenery, with no random numbers: one place strays the same every time, and joined tracks, the paths of a switch, sleepermodel and axles at one place move together. Tracks meet at a joint at the lower of their values. Joined switches take the lowest of theirs, the tracks joined to them come to it, and a track with none of a component keeps the switch to its design in it. The clatter comes from the dipping joints. Curve radius and grade for the physics stay as designed. Tracks with it get geometry every 2 m on straights: 4x the vertices on Baltyk with all tracks flagged, 0.2 ms more per frame on td.scn. --- application/editormodetrack.cpp | 30 ++++- editor/editorTrack.cpp | 20 +++ editor/editorTrack.hpp | 5 + lang/editor_pl.po | 24 ++++ tests/unit/test_track_uneven.cpp | 152 ++++++++++++++++++++++ track_uneven_doc.md | 81 ++++++++++++ vehicle/DynObj.cpp | 23 +++- vehicle/DynObj.h | 1 + world/Segment.cpp | 54 ++++++++ world/Segment.h | 14 +++ world/Track.cpp | 208 +++++++++++++++++++++++++++++++ world/Track.h | 32 +++++ world/TrackUneven.h | 125 +++++++++++++++++++ world/TrkFoll.cpp | 5 + world/TrkFoll.h | 4 + 15 files changed, 774 insertions(+), 4 deletions(-) create mode 100644 tests/unit/test_track_uneven.cpp create mode 100644 track_uneven_doc.md create mode 100644 world/TrackUneven.h diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 0b264b78..5b048396 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1159,7 +1159,35 @@ 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, 0.0f, 0.0f, "%.1f")) + track->m_uneven = std::clamp(track->m_uneven, 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); + struct component + { + char const *label; + float *value; + }; + for (auto const &part : {component{"Up and down (mm)", &track->m_unevenup}, component{"Sideways (mm)", &track->m_unevenside}, component{"Cross level (mm)", &track->m_unevencross}}) + { + if (part.value == &track->m_unevencross && track->eType != tt_Normal) + continue; + if (ImGui::InputFloat(STR_C(part.label), part.value, 0.0f, 0.0f, "%.1f")) + *part.value = *part.value < 0.f ? -1.f : std::min(*part.value, track_uneven::most); + item_tooltip("Set apart from the unevenness, mm. Negative: the unevenness"); + finish_edit(rebuild::parameters); + } + } + if (track->eType == tt_Normal && (track->iCategoryFlag & 15) == 1) + { + if (ImGui::InputFloat(STR_C("Dip at rail joints (mm)"), &track->m_jointdip, 0.0f, 0.0f, "%.1f")) + track->m_jointdip = track->m_jointdip < 0.f ? -1.f : std::min(track->m_jointdip, 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..cbe7c26b 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -152,6 +152,11 @@ 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.unevenup = Track.m_unevenup; + result.unevenside = Track.m_unevenside; + result.unevencross = Track.m_unevencross; + result.jointdip = Track.m_jointdip; result.sleepers = Track.m_sleeper_enabled; result.sleeper_frequency = Track.m_sleeper_frequency; result.sleeper_model = Track.m_sleeper_model_name; @@ -250,6 +255,11 @@ 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_unevenup = State.unevenup; + Track.m_unevenside = State.unevenside; + Track.m_unevencross = State.unevencross; + Track.m_jointdip = State.jointdip; Track.m_sleeper_enabled = State.sleepers; Track.m_sleeper_frequency = State.sleeper_frequency; Track.m_sleeper_model_name = State.sleeper_model; @@ -2350,6 +2360,16 @@ void write_ending(std::ostream &Text, TTrack const &Style) { if ((Style.iCategoryFlag & 15) == 1 && false == Style.m_profile1.first.empty()) Text << "railprofile " << Style.m_profile1.first << ' '; + if (Style.m_uneven > 0.f) + Text << "uneven " << Style.m_uneven << ' '; + if (Style.m_unevenup >= 0.f) + Text << "unevenup " << Style.m_unevenup << ' '; + if (Style.m_unevenside >= 0.f) + Text << "unevenside " << Style.m_unevenside << ' '; + if (Style.m_unevencross >= 0.f) + Text << "unevencross " << Style.m_unevencross << ' '; + if (Style.m_jointdip >= 0.f) + Text << "jointdip " << Style.m_jointdip << ' '; // 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 << ' '; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 43d42762..b6137bd8 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -55,6 +55,11 @@ class editor_track float texslope{0.9f}; std::vector isolated; float overhead{-1.f}; + float uneven{0.f}; + float unevenup{-1.f}; + float unevenside{-1.f}; + float unevencross{-1.f}; + float jointdip{-1.f}; bool sleepers{false}; float sleeper_frequency{0.6f}; std::string sleeper_model; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 58b30109..a9c61b83 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -861,6 +861,30 @@ 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 "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..6333b6ab --- /dev/null +++ b/tests/unit/test_track_uneven.cpp @@ -0,0 +1,152 @@ +#include "stdafx.h" +#include "doctest.h" +#include "world/TrackUneven.h" + +#include +#include + +namespace { + +double deviation(std::vector const &Values) +{ + auto mean{0.0}; + for (auto const value : Values) + mean += value; + mean /= Values.size(); + auto sum{0.0}; + for (auto const value : Values) + sum += (value - mean) * (value - mean); + return std::sqrt(sum / Values.size()); +} + +double peak(std::vector Values) +{ + for (auto &value : Values) + value = std::abs(value); + std::sort(Values.begin(), Values.end()); + return Values[static_cast(0.999 * Values.size())]; +} + +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: 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}, horizontal{0.0}, twist{0.0}, wander{0.0}, level{0.0}, j{0.0}, 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::vector up[2], side[2], 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)) * amplitude); + side[rail].push_back(glm::dot(stray, across) * amplitude); + } + middle.push_back(track_uneven::field(point).y * amplitude); + } + std::vector verticals, horizontals, twists, gauges, 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 < 25.0); // appendix 1a: 15 mm off the survey marks +} + +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.295557487607).epsilon(1e-9)); + CHECK(stray.z == doctest::Approx(-0.328429734019).epsilon(1e-9)); + CHECK(track_uneven::level(place) == doctest::Approx(0.6 * -0.178598957983).epsilon(1e-9)); +} + +TEST_CASE("uneven track: the field stays within its bound") +{ + auto most{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.y), std::abs(stray.z)}); + } + CHECK(most <= 1.0); + CHECK(most > 0.5); +} + +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 (auto distance{0.25}; distance <= length; distance += 0.25) + { + auto const value{track_uneven::along(18.0, 35.0, 0.0, distance, length)}; + REQUIRE(std::abs(value - last) < 0.5); + last = value; + } +} + +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/track_uneven_doc.md b/track_uneven_doc.md new file mode 100644 index 00000000..8cb443ac --- /dev/null +++ b/track_uneven_doc.md @@ -0,0 +1,81 @@ +# Nierówności toru + +Opcjonalne atrybuty wpisu toru, przed `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 +unevencross 25 +jointdip 6 +endtrack +``` + +| atrybut | dotyczy | opis | +|-------------------|-----------------------|---------------------------------------------------------------------------------------| +| `uneven ` | `normal`, `switch` | największa odchyłka toku szynowego na cięciwie 10 m; 0-100, domyślnie 0 | +| `unevenup ` | `normal`, `switch` | składowa w pionie, osobno od `uneven` | +| `unevenside ` | `normal`, `switch` | składowa w poziomie, osobno od `uneven` | +| `unevencross `| `normal` | składowa przechyłki, osobno od `uneven`; rozjazd jej nie ma | +| `jointdip ` | `normal` | głębokość zapadnięcia na styku szyn; 0-100, domyślnie 0,4 składowej w pionie | + +Składowa niepodana przyjmuje wartość `uneven`. Na przykład sam bok to `unevenside 30`, a same zapadnięcia na stykach to `jointdip 6`. + +Wszystkie są też w edytorze, w panelu toru, w sekcji „Ride and sound”. Na drogach, rzekach i obrotnicach są ignorowane. + +## Jaką wartość wybrać + +`uneven` odpowiada kolumnie „nierówności pionowe” z instrukcji PLK Id-14, załącznik 1 (klasa C): + +| prędkość [km/h] | 20 | 40 | 60 | 80 | 100 | 120 | 160 | +|-----------------|----|----|----|----|-----|-----|-----| +| `uneven` [mm] | 50 | 35 | 25 | 18 | 14 | 10 | 6 | + +Tor w dobrym stanie to według instrukcji połowa tej wartości (klasa A), w dostatecznym 3/4 (klasa B). + +Tor z `uneven 18` mierzy się tak (`tests/unit/test_track_uneven.cpp`), w nawiasie limit Id-14 dla 80 km/h: + +| parametr | wynik | Id-14 | +|---------------------------|---------|---------| +| nierówności pionowe | 18 mm | 18 mm | +| nierówności poziome | 19 mm | 17 mm | +| wichrowatość na bazie 5 m | 17 mm | 16 mm | +| szerokość toru | ±10 mm | +10/-8 mm | +| przechyłka | 20 mm | 20 mm | +| wskaźnik J | 6,1 | 5,3 | + +## Jak to działa + +Nierówność to pole przesunięć zależne od położenia na mapie, złożone z fal o długości 5-42 m. Nie ma w nim losowania: to samo miejsce wygina się tak samo przy każdym uruchomieniu. Przesunięty tor dostaje fale z nowego miejsca. + +Każda składowa łączy się osobno: + +- **Tor z torem:** na styku obowiązuje mniejsza z dwóch wartości, a każdy tor dochodzi do niej płynnie na 20 m. Wolny koniec toru leży w projekcie. +- **Rozjazd:** wszystkie jego drogi mają jedną wartość. Rozjazdy złączone bezpośrednio przyjmują najmniejszą ze swoich. Dołączony tor dochodzi do wartości rozjazdu, także większej od własnej. Tor bez danej składowej trzyma w niej cały rozjazd w projekcie. +- **Styki:** zapadnięcia są co `fSoundDistance`, na długości 6 m, i biegną dalej przez kolejne tory o tym samym odstępie. Stukot osi wypada w miejscu zapadnięcia. Przy odstępie 6 m lub mniejszym oraz na rozjazdach zapadnięć nie ma. +- **Przechyłka:** dolicza się do projektowej. Na rozjazdach tylko ta wynikająca z fal, bez osobnego siadania toków. + +Za nierównością idą szyny, podsypka, `sleepermodel`, osie, przechył pudła i bujanie kabiny. + +Nie idą za nią modele z includów (np. napędy i podkłady rozjazdów), teren ani sieć trakcyjna, więc przy dużych wartościach szyna rozjeżdża się z takim modelem o kilka centymetrów. + +Fizyka bierze promień łuku i pochylenie z projektu, więc nierówności nie zmieniają tych wartości, a do wykolejeń dokładają tylko przechyłkę, ok. 0,06 m/s² przy `uneven 18` wobec progu ok. 3,2 m/s². + +## Wydajność + +Tor z nierównością ma geometrię co 2 m zamiast co 10 m na prostych. Rozjazdy zachowują swój podział. + +Zmierzone (kamera w ruchu lub kabina, 1920×1080): + +| scena | bez flag | z flagami 18/35 | +|---------------------------------------|--------------------|--------------------| +| Bałtyk, 56 km torów: wierzchołki | 0,56 mln (ok. 27 MB) | 2,17 mln (ok. 104 MB) | +| Bałtyk: najdłuższa klatka w przelocie | 31,5 ms | 64 ms | +| `td.scn`, jazda 120 km/h: klatka | 2,73 ms | 2,93 ms | + +Koszt rośnie z liczbą oflagowanych torów, a najdłuższe klatki pojawiają się przy budowaniu geometrii kolejnych sekcji. Sama jazda po takim torze praktycznie nie kosztuje: ok. 0,3 µs na oś w kroku fizyki. diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 32ba307f..83354288 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3627,6 +3627,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,9 +3661,19 @@ 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 ) { + // on a track with dipping joints the clatter comes from them, elsewhere the axle counts a rail off itself + auto const railphase { Axle0.RailPhase( axle.offset - 0.5 * fAxleDist ) }; + auto crossed { false }; + if( railphase >= 0.0 ) { + crossed = axle.atjoints && std::abs( railphase - axle.distance ) > 0.5 * dRailLength; + axle.distance = railphase; + } + else { + axle.distance += dDOMoveLen * directioninconsist; + crossed = axle.distance < 0 || axle.distance > dRailLength; + } + axle.atjoints = ( railphase >= 0.0 ); + if( crossed ) { 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 @@ -8035,6 +8046,12 @@ TDynamicObject::update_shake( double const Timedelta ) { -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 ) }; diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index 36335a32..1bc50b1d 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -402,6 +402,7 @@ 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 }; struct powertrain_sounds { diff --git a/world/Segment.cpp b/world/Segment.cpp index 69886f42..ad817b5e 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -118,6 +118,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_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 ) { @@ -366,6 +370,52 @@ Math3D::vector3 TSegment::GetPoint(double const fDistance) const */ void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const { + design_position( fDistance, position, rotation ); + if( false == m_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 ) }; + rotation.x += static_cast( track_uneven::stray_per_mm * ( + amount.up * track_uneven::roll( position, across, gauge ) + + amount.cross * track_uneven::level( position ) / gauge ) ); + position += stray( position, fDistance ); +} + +glm::dvec3 TSegment::stray( glm::dvec3 const &Point, double const Distance ) const { + auto const amount { pOwner->uneven_at( Distance ) }; + 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_uneven ) { + return glm::dvec3 { 0.0 }; + } + // over about a bogie; FastGetTFromS extrapolates past the ends of the path + auto const reach { 1.5 }; + auto const strayed = [&]( double const At ) { + return stray( FastGetPoint( FastGetTFromS( At ) ), 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_uneven ) { + return; + } + // the rails settling apart turn about the middle of the track, what's outside them follows the rail + 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 ) }; + Vertex.position.y -= static_cast( track_uneven::stray_per_mm * pOwner->uneven_at( Distance ).cross * track_uneven::level( Origin + glm::dvec3 { Position } ) * across / gauge ); + Vertex.position += glm::vec3 { stray( Origin + glm::dvec3 { Vertex.position }, Distance ) }; +} + +void TSegment::design_position(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 auto const t = FastGetTFromS(fDistance); // aproksymacja dystansu na krzywej Beziera na parametr (t) @@ -500,6 +550,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 +569,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 +592,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 +603,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..492cab94 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -47,6 +47,7 @@ class TSegment glm::dvec3 Point2; float fRoll1 { 0.f }; float fRoll2 { 0.f }; // przechyłka na końcach + bool m_uneven { false }; // see TrackUneven.h 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 +68,12 @@ 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, double const Distance ) const; + void design_position( double const fDistance, glm::dvec3 &position, glm::vec3 &rotation ) const; + public: bool bCurve = false; @@ -106,6 +113,13 @@ 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 ) { + m_uneven = Uneven; } + // 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..ff5a378a 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,27 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) m_profile1 = fetch_track_rail_profile( railprofile ); } } + else if( str == "uneven" ) { + // uneven : see TrackUneven.h + auto const uneven { parser->getToken() }; + if( ( eType == tt_Normal || eType == tt_Switch ) && iCategoryFlag == 1 ) { + m_uneven = std::clamp( uneven, 0.f, track_uneven::most ); + } + } + else if( str == "unevenup" || str == "unevenside" || str == "unevencross" ) { + // the components of uneven set apart, mm + auto const value { parser->getToken() }; + if( ( eType == tt_Normal || eType == tt_Switch ) && iCategoryFlag == 1 ) { + ( str == "unevenup" ? m_unevenup : str == "unevenside" ? m_unevenside : m_unevencross ) = std::clamp( value, 0.f, track_uneven::most ); + } + } + else if( str == "jointdip" ) { + // jointdip : depth of the dip at rail joints, with or without uneven + auto const jointdip { parser->getToken() }; + if( eType == tt_Normal && iCategoryFlag == 1 ) { + m_jointdip = std::clamp( jointdip, 0.f, track_uneven::most ); + } + } else if( str == "friction" ) { // memory cell holding friction value modifiers m_friction.first = parser->getToken(); @@ -674,6 +700,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()) { @@ -687,6 +717,159 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) // to a stale origin and the renderer would draw sleepers in the wrong place). } +track_uneven::amounts TTrack::uneven_declared() const { + + return { + m_unevenup >= 0.f ? m_unevenup : m_uneven, + m_unevenside >= 0.f ? m_unevenside : m_uneven, + eType != tt_Normal ? 0.f : m_unevencross >= 0.f ? m_unevencross : m_uneven }; +} + +bool TTrack::is_uneven() const { + + auto const declared { uneven_declared() }; + return declared.up > 0.0 || declared.side > 0.0 || declared.cross > 0.0 || joint_depth() > 0.0; +} + +track_uneven::amounts TTrack::uneven_at( double const Distance ) const { + + if( eType == tt_Switch ) { + return switch_uneven(); + } + auto const own { uneven_declared() }; + auto const atend = [&]( TTrack const *Other ) -> track_uneven::amounts { + if( Other == nullptr ) { + return {}; + } + if( Other->eType == tt_Switch ) { + return Other->switch_uneven(); + } + auto const theirs { Other->uneven_declared() }; + return { std::min( own.up, theirs.up ), std::min( own.side, theirs.side ), std::min( own.cross, theirs.cross ) }; }; + auto const start { atend( trPrev ) }; + auto const end { atend( trNext ) }; + return { + track_uneven::along( own.up, start.up, end.up, Distance, Length() ), + track_uneven::along( own.side, start.side, end.side, Distance, Length() ), + track_uneven::along( own.cross, start.cross, end.cross, Distance, Length() ) }; +} + +track_uneven::amounts TTrack::switch_uneven() const { + + if( m_switchstray.stamp == s_layoutstamp ) { + return m_switchstray.value; + } + std::vector group { this }; + auto least { uneven_declared() }; + for( std::size_t index = 0; index < group.size(); ++index ) { + auto const &extension { *group[ index ]->SwitchExtension }; + for( auto const *other : { extension.pPrevs[ 0 ], extension.pPrevs[ 1 ], extension.pNexts[ 0 ], extension.pNexts[ 1 ] } ) { + if( other == nullptr ) { + continue; + } + auto const theirs { other->uneven_declared() }; + if( other->eType == tt_Switch && other->SwitchExtension ) { + least.up = std::min( least.up, theirs.up ); + least.side = std::min( least.side, theirs.side ); + if( std::find( group.begin(), group.end(), other ) == group.end() ) { + group.push_back( other ); + } + } + else { + // a track with none of a component keeps to its design in it, any other comes to the value of the switch + if( theirs.up <= 0.0 ) { least.up = 0.0; } + if( theirs.side <= 0.0 ) { least.side = 0.0; } + } + } + } + for( auto const *track : group ) { + track->m_switchstray = { least, s_layoutstamp }; + } + return least; +} + +double TTrack::joint_depth() const { + + if( eType != tt_Normal ) { + return 0.0; + } + return ( m_jointdip >= 0.f ? m_jointdip : track_uneven::dip_share * uneven_declared().up ); +} + +double TTrack::joint_dip( double const Distance ) const { + + 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 = [&]( 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 ) const { + + 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 ); +} + +void TTrack::settle_rail_run() const { + + // 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 const * { + auto const *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 = [&]( int &End ) { + auto const *track { this }; + int joined { 0 }; + for( int step = 0; step < 1000000; ++step ) { + auto const *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 == 0 ? Track.Segment->FastGetPoint_0() : Track.Segment->FastGetPoint_1() }; + 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 const *track { walk( end ) }; + auto const *other { walk( otherend ) }; + if( 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; + } +} + void TTrack::update_location() { location( ( @@ -705,6 +888,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() ); + } + else if( Segment ) { + Segment->set_uneven( is_uneven() ); + } switch (eType) { // Ra: łuki segmentowane co 5m albo 314-kątem foremnym @@ -1351,6 +1542,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 ) { + ++s_layoutstamp; + Geometry1.clear(); Geometry2.clear(); if( SwitchExtension ) { @@ -2445,6 +2638,21 @@ TTrack::export_as_text_( std::ostream &Output ) const { && false == m_profile1.first.empty() ) { Output << "railprofile " << m_profile1.first << ' '; } + if( m_uneven > 0.f ) { + Output << "uneven " << m_uneven << ' '; + } + if( m_unevenup >= 0.f ) { + Output << "unevenup " << m_unevenup << ' '; + } + if( m_unevenside >= 0.f ) { + Output << "unevenside " << m_unevenside << ' '; + } + if( m_unevencross >= 0.f ) { + Output << "unevencross " << m_unevencross << ' '; + } + if( m_jointdip >= 0.f ) { + Output << "jointdip " << m_jointdip << ' '; + } if( false == m_friction.first.empty() ) { Output << "friction " << m_friction.first << ' '; } diff --git a/world/Track.h b/world/Track.h index 72a6b7ff..ac6d1425 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,12 @@ 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) + float m_uneven { 0.f }; // mm, see TrackUneven.h; plain tracks and switches + // the components set apart from m_uneven, mm; negative: m_uneven. Cross level on plain tracks only + float m_unevenup { -1.f }; + float m_unevenside { -1.f }; + float m_unevencross { -1.f }; + float m_jointdip { -1.f }; // depth of the dip at rail joints, mm; negative: from m_uneven; plain tracks only private: double fVelocity = -1.0; // ograniczenie prędkości // prędkość dla AI (powyżej rośnie prawdopowobieństwo wykolejenia) public: @@ -339,6 +346,15 @@ public: void VelocitySet(float v); double VelocityGet(); float Friction() const; + // the unevenness the track declares + track_uneven::amounts uneven_declared() const; + bool is_uneven() const; + // the unevenness at a distance along the track, meeting the tracks joined at its ends + track_uneven::amounts uneven_at( double const Distance ) const; + double joint_depth() const; // mm + double joint_dip( double const Distance ) const; // m + // distance from the rail joint behind; negative: the track has no joints that dip + double rail_phase( double const Distance ) const; void ConnectionsLog(); bool DoubleSlip() const; static void fetch_default_profiles(); @@ -370,6 +386,22 @@ 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() const; + mutable rail_run m_railrun; + // one value along all paths of a switch: the lowest of the switches joined, 0 next to a track with none + track_uneven::amounts switch_uneven() const; + struct switch_stray { + track_uneven::amounts value; + int stamp { -1 }; + }; + mutable switch_stray m_switchstray; + static int s_layoutstamp; // bumped when tracks are joined or changed, invalidates the two above 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.h b/world/TrackUneven.h new file mode 100644 index 00000000..7486cea2 --- /dev/null +++ b/world/TrackUneven.h @@ -0,0 +1,125 @@ +/* +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 + +// 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. +// 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.6 }; // 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 }; +}; + +struct wave { + double x; + double z; + double phase; + double share; +}; + +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 heading { Turn + golden * i }; + result[ i ] = { number * std::cos( heading ), number * std::sin( heading ), golden * ( i + 1 ) * ( 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 }; + +inline wave_set<8> const up_waves { make_waves( 0.0, wave_lengths, wave_shares ) }; +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 ) }; +// long waves only: with the short ones the cross level reaching its limit would break the limit of twist +inline wave_set<3> const cross_waves { make_waves<3>( 0.7, { 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, each axis within +-1 +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 right rail lies by settling apart, for an amplitude of 1. Follows the middle of a path, so plain tracks only +inline double level( glm::dvec3 const &Point ) { + + return cross_share * sum( cross_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 ) ); +} + +// 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); From b2dcb8a42e419e60ce689462c4a10a9004bd48b0 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 18:21:28 +0100 Subject: [PATCH 02/18] fix(vehicle): wheel clatter on an uneven track doesn't loop into a buzz The phase of the rails was wrapped to the rail length of the track the vehicle is on, while an axle can be on a track with longer rails. From then on every frame looked like a joint crossed. --- vehicle/DynObj.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 83354288..2c56cb7c 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3665,16 +3665,19 @@ bool TDynamicObject::Update(double dt, double dt1) auto const railphase { Axle0.RailPhase( axle.offset - 0.5 * fAxleDist ) }; auto crossed { false }; if( railphase >= 0.0 ) { - crossed = axle.atjoints && std::abs( railphase - axle.distance ) > 0.5 * dRailLength; + // the rails under the axle, which aren't always those under the track the vehicle is on + crossed = axle.atjoints && std::abs( railphase - axle.distance ) > 0.5 * Axle0.GetTrack()->fSoundDistance; axle.distance = railphase; } else { axle.distance += dDOMoveLen * directioninconsist; crossed = axle.distance < 0 || axle.distance > dRailLength; + if( crossed ) { + axle.distance = clamp_circular( axle.distance, dRailLength ); + } } axle.atjoints = ( railphase >= 0.0 ); if( crossed ) { - 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; From e74bbf951f87e399b95a824dc70bdefa587c97e0 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 18:23:19 +0100 Subject: [PATCH 03/18] fix(track): the cross level of an uneven track goes by the place, not by how it was laid Tracks laid towards each other settled to opposite sides: a step of cant of twice the value at their joint, in the geometry and under the axles. It's now a tilt of the plan, which reads the same from either track. --- tests/unit/test_track_uneven.cpp | 23 +++++++++++++++++++++-- track_uneven_doc.md | 2 +- world/Segment.cpp | 4 ++-- world/TrackUneven.h | 14 ++++++++------ 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index 6333b6ab..195a2d72 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -59,7 +59,7 @@ TEST_CASE("uneven track: measures as the Id-14 limits for its value") { 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)) * amplitude); + 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); @@ -102,7 +102,26 @@ TEST_CASE("uneven track: a place strays the same every time") CHECK(stray.x == doctest::Approx(0.309777548158).epsilon(1e-9)); CHECK(stray.y == doctest::Approx(-0.295557487607).epsilon(1e-9)); CHECK(stray.z == doctest::Approx(-0.328429734019).epsilon(1e-9)); - CHECK(track_uneven::level(place) == doctest::Approx(0.6 * -0.178598957983).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") diff --git a/track_uneven_doc.md b/track_uneven_doc.md index 8cb443ac..ebced90e 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -58,7 +58,7 @@ Każda składowa łączy się osobno: - **Tor z torem:** na styku obowiązuje mniejsza z dwóch wartości, a każdy tor dochodzi do niej płynnie na 20 m. Wolny koniec toru leży w projekcie. - **Rozjazd:** wszystkie jego drogi mają jedną wartość. Rozjazdy złączone bezpośrednio przyjmują najmniejszą ze swoich. Dołączony tor dochodzi do wartości rozjazdu, także większej od własnej. Tor bez danej składowej trzyma w niej cały rozjazd w projekcie. - **Styki:** zapadnięcia są co `fSoundDistance`, na długości 6 m, i biegną dalej przez kolejne tory o tym samym odstępie. Stukot osi wypada w miejscu zapadnięcia. Przy odstępie 6 m lub mniejszym oraz na rozjazdach zapadnięć nie ma. -- **Przechyłka:** dolicza się do projektowej. Na rozjazdach tylko ta wynikająca z fal, bez osobnego siadania toków. +- **Przechyłka:** dolicza się do projektowej. Jest pochyleniem w planie mapy, więc tory wpisane ku sobie spotykają się na tej samej przechyłce. Na rozjazdach tylko ta wynikająca z fal, bez osobnego siadania toków. Za nierównością idą szyny, podsypka, `sleepermodel`, osie, przechył pudła i bujanie kabiny. diff --git a/world/Segment.cpp b/world/Segment.cpp index ad817b5e..84a0c22b 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -380,7 +380,7 @@ void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm:: auto const amount { pOwner->uneven_at( fDistance ) }; rotation.x += static_cast( track_uneven::stray_per_mm * ( amount.up * track_uneven::roll( position, across, gauge ) - + amount.cross * track_uneven::level( position ) / gauge ) ); + + amount.cross * track_uneven::level( position, across ) / gauge ) ); position += stray( position, fDistance ); } @@ -411,7 +411,7 @@ void TSegment::settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origi // the rails settling apart turn about the middle of the track, what's outside them follows the rail 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 ) }; - Vertex.position.y -= static_cast( track_uneven::stray_per_mm * pOwner->uneven_at( Distance ).cross * track_uneven::level( Origin + glm::dvec3 { Position } ) * across / gauge ); + Vertex.position.y -= static_cast( track_uneven::stray_per_mm * pOwner->uneven_at( Distance ).cross * track_uneven::level( Origin + glm::dvec3 { Position }, glm::dvec3 { Parallel } ) * across / gauge ); Vertex.position += glm::vec3 { stray( Origin + glm::dvec3 { Vertex.position }, Distance ) }; } diff --git a/world/TrackUneven.h b/world/TrackUneven.h index 7486cea2..5974f42d 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -22,7 +22,7 @@ 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.6 }; // share of the field amplitude the rails settle apart by +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 @@ -64,8 +64,9 @@ inline constexpr std::array wave_shares { 0.0721, 0.0997, 0.1344, 0.1 inline wave_set<8> const up_waves { make_waves( 0.0, wave_lengths, wave_shares ) }; 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 ) }; -// long waves only: with the short ones the cross level reaching its limit would break the limit of twist -inline wave_set<3> const cross_waves { make_waves<3>( 0.7, { 23.9, 31.7, 41.9 }, { 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0 } ) }; +// 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 ) { @@ -83,10 +84,11 @@ 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 right rail lies by settling apart, for an amplitude of 1. Follows the middle of a path, so plain tracks only -inline double level( glm::dvec3 const &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 * sum( cross_waves, Point ); + 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 From 6aed9ec5bd38d942a400a7085f044a28602b3c2a Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 18:25:16 +0100 Subject: [PATCH 04/18] feat(track): vertical unevenness comes near its limit more often, with longer buckles too Fewer short waves: at uneven 18 the humps over 12.6 mm on the 10 m chord go from 28 to 38 per km, with the peak still at 18 mm. Waves of 47-83 m join them. They hardly count on that chord, the body rises and falls on them. The documentation tells how far unevenness goes before a pantograph can't keep up. A wire rising fast enough trips the main breaker on a straight track too: it does at tor0290 on Baltyk, over about 46 km/h. --- tests/unit/test_track_uneven.cpp | 10 +++++++--- track_uneven_doc.md | 6 ++++-- world/TrackUneven.h | 8 +++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index 195a2d72..afea0b67 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -91,7 +91,7 @@ TEST_CASE("uneven track: measures as the Id-14 limits for its value") 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 < 25.0); // appendix 1a: 15 mm off the survey marks + 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") @@ -100,7 +100,7 @@ TEST_CASE("uneven track: a place strays the same every time") 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.295557487607).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)); @@ -127,13 +127,17 @@ TEST_CASE("uneven track: tracks laid towards each other meet at one cant") 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.y), std::abs(stray.z)}); + 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: the value along a track meets its neighbours exactly") diff --git a/track_uneven_doc.md b/track_uneven_doc.md index ebced90e..aca9ca32 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -44,14 +44,14 @@ Tor z `uneven 18` mierzy się tak (`tests/unit/test_track_uneven.cpp`), w nawias |---------------------------|---------|---------| | nierówności pionowe | 18 mm | 18 mm | | nierówności poziome | 19 mm | 17 mm | -| wichrowatość na bazie 5 m | 17 mm | 16 mm | +| wichrowatość na bazie 5 m | 15 mm | 16 mm | | szerokość toru | ±10 mm | +10/-8 mm | | przechyłka | 20 mm | 20 mm | | wskaźnik J | 6,1 | 5,3 | ## Jak to działa -Nierówność to pole przesunięć zależne od położenia na mapie, złożone z fal o długości 5-42 m. Nie ma w nim losowania: to samo miejsce wygina się tak samo przy każdym uruchomieniu. Przesunięty tor dostaje fale z nowego miejsca. +Nierówność to pole przesunięć zależne od położenia na mapie, złożone z fal o długości 5-42 m. W pionie krótkich fal jest mniej, żeby częściej dochodziły blisko wartości szczytowej, a do tego są fale 47-83 m: na cięciwie 10 m prawie się nie liczą, pudło unosi się na nich i opada, a pantograf za nimi nadąża. Nie ma w nim losowania: to samo miejsce wygina się tak samo przy każdym uruchomieniu. Przesunięty tor dostaje fale z nowego miejsca. Każda składowa łączy się osobno: @@ -64,6 +64,8 @@ Za nierównością idą szyny, podsypka, `sleepermodel`, osie, przechył pudła Nie idą za nią modele z includów (np. napędy i podkłady rozjazdów), teren ani sieć trakcyjna, więc przy dużych wartościach szyna rozjeżdża się z takim modelem o kilka centymetrów. +Pantograf podnosi się z szybkością ciśnienia, ok. 0,14 m/s, a szczelina do drutu większa niż 1 cm odcina napięcie i wybija wyłącznik szybki. Nierówność dokłada się do tego przy dużej prędkości. Na torach Bałtyku symulacja daje szczelinę 0,6 cm, gdy składowa w pionie razy prędkość w km/h wynosi 925, czyli np. 8 mm przy 120 km/h, 13 mm przy 70 km/h i 23 mm przy 40 km/h. Wartości z tabeli Id-14 są od tego większe. Sam drut też to potrafi: na Bałtyku przy `tor0290` wznosi się o ok. 1%, więc pantograf nadąża tam do ok. 46 km/h, a powyżej wyłącznik wybija także na prostym torze. + Fizyka bierze promień łuku i pochylenie z projektu, więc nierówności nie zmieniają tych wartości, a do wykolejeń dokładają tylko przechyłkę, ok. 0,06 m/s² przy `uneven 18` wobec progu ok. 3,2 m/s². ## Wydajność diff --git a/world/TrackUneven.h b/world/TrackUneven.h index 5974f42d..9031c6ca 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -16,7 +16,7 @@ http://mozilla.org/MPL/2.0/. #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. +// 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 { @@ -61,7 +61,9 @@ wave_set make_waves( double const Turn, std::array const & 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 }; -inline wave_set<8> const up_waves { make_waves( 0.0, wave_lengths, wave_shares ) }; +// 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 @@ -78,7 +80,7 @@ double sum( wave_set const &Waves, glm::dvec3 const &Point ) { return total; } -// displacement at the point for an amplitude of 1, each axis within +-1 +// 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 ) }; From 3dbe8a75f80fd109ccd0dab5bccf96b23faf33d8 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 18:27:19 +0100 Subject: [PATCH 05/18] feat(track): the foot of the ballast stays on the ground as designed Rails, sleepers and the top of the ballast go with the unevenness, the slope of the ballast takes up the difference. --- tests/unit/test_track_uneven.cpp | 12 ++++++++++++ track_uneven_doc.md | 2 +- world/Segment.cpp | 7 ++++--- world/Track.cpp | 5 +++++ world/Track.h | 2 ++ world/TrackUneven.h | 7 +++++++ 6 files changed, 31 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index afea0b67..46bd1e5f 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -165,6 +165,18 @@ TEST_CASE("uneven track: the value along a track meets its neighbours exactly") } } +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)); diff --git a/track_uneven_doc.md b/track_uneven_doc.md index aca9ca32..8570fd94 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -60,7 +60,7 @@ Każda składowa łączy się osobno: - **Styki:** zapadnięcia są co `fSoundDistance`, na długości 6 m, i biegną dalej przez kolejne tory o tym samym odstępie. Stukot osi wypada w miejscu zapadnięcia. Przy odstępie 6 m lub mniejszym oraz na rozjazdach zapadnięć nie ma. - **Przechyłka:** dolicza się do projektowej. Jest pochyleniem w planie mapy, więc tory wpisane ku sobie spotykają się na tej samej przechyłce. Na rozjazdach tylko ta wynikająca z fal, bez osobnego siadania toków. -Za nierównością idą szyny, podsypka, `sleepermodel`, osie, przechył pudła i bujanie kabiny. +Za nierównością idą szyny, wierzch podsypki, `sleepermodel`, osie, przechył pudła i bujanie kabiny. Skarpa podsypki przejmuje różnicę, a jej spód zostaje w projekcie, na terenie. Nie idą za nią modele z includów (np. napędy i podkłady rozjazdów), teren ani sieć trakcyjna, więc przy dużych wartościach szyna rozjeżdża się z takim modelem o kilka centymetrów. diff --git a/world/Segment.cpp b/world/Segment.cpp index 84a0c22b..3668ed13 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -408,11 +408,12 @@ void TSegment::settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origi if( false == m_uneven ) { return; } - // the rails settling apart turn about the middle of the track, what's outside them follows the rail + // 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 ) }; - Vertex.position.y -= static_cast( track_uneven::stray_per_mm * pOwner->uneven_at( Distance ).cross * track_uneven::level( Origin + glm::dvec3 { Position }, glm::dvec3 { Parallel } ) * across / gauge ); - Vertex.position += glm::vec3 { stray( Origin + glm::dvec3 { Vertex.position }, Distance ) }; + auto const share { pOwner->uneven_bed_share( Position.y - Vertex.position.y ) }; + Vertex.position.y -= static_cast( share * track_uneven::stray_per_mm * pOwner->uneven_at( Distance ).cross * track_uneven::level( Origin + glm::dvec3 { Position }, glm::dvec3 { Parallel } ) * across / gauge ); + Vertex.position += glm::vec3 { share * stray( Origin + glm::dvec3 { Vertex.position }, Distance ) }; } void TSegment::design_position(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const { diff --git a/world/Track.cpp b/world/Track.cpp index ff5a378a..b5589eed 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -731,6 +731,11 @@ bool TTrack::is_uneven() const { return declared.up > 0.0 || declared.side > 0.0 || declared.cross > 0.0 || joint_depth() > 0.0; } +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 ) const { if( eType == tt_Switch ) { diff --git a/world/Track.h b/world/Track.h index ac6d1425..92516ee6 100644 --- a/world/Track.h +++ b/world/Track.h @@ -351,6 +351,8 @@ public: bool is_uneven() const; // the unevenness at a distance along the track, meeting the tracks joined at its ends track_uneven::amounts uneven_at( double const Distance ) const; + // how much of the displacement a point of the profile at the depth takes + double uneven_bed_share( double const Depth ) const; double joint_depth() const; // mm double joint_dip( double const Distance ) const; // m // distance from the rail joint behind; negative: the track has no joints that dip diff --git a/world/TrackUneven.h b/world/TrackUneven.h index 9031c6ca..182f3799 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -109,6 +109,13 @@ inline double along( double const Full, double const Atstart, double const Atend 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; +} + // distance from the joint behind the point inline double rail_phase( double const Along, double const Rail ) { From 7aff1ee9ff646eff64e18fce96e52daae76b1831 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 18:29:22 +0100 Subject: [PATCH 06/18] feat(vehicle): an uneven track shakes the cab by itself On such a track the sway drawn from the lateral acceleration, that is from the curve radius, and the jolts drawn at random are off. Tracks with no unevenness shake the cab as before. Hunting stays: it goes by the vehicle and its speed, not by the track. --- track_uneven_doc.md | 2 ++ vehicle/DynObj.cpp | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/track_uneven_doc.md b/track_uneven_doc.md index 8570fd94..4b71140b 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -64,6 +64,8 @@ Za nierównością idą szyny, wierzch podsypki, `sleepermodel`, osie, przechył Nie idą za nią modele z includów (np. napędy i podkłady rozjazdów), teren ani sieć trakcyjna, więc przy dużych wartościach szyna rozjeżdża się z takim modelem o kilka centymetrów. +Na torze z nierównością kabinę buja sam tor. Odpada kołysanie wyliczane z przyspieszenia poprzecznego, czyli z promienia łuku, oraz losowe szarpnięcia przy większej prędkości. Na torach bez nierówności działają jak dotąd. + Pantograf podnosi się z szybkością ciśnienia, ok. 0,14 m/s, a szczelina do drutu większa niż 1 cm odcina napięcie i wybija wyłącznik szybki. Nierówność dokłada się do tego przy dużej prędkości. Na torach Bałtyku symulacja daje szczelinę 0,6 cm, gdy składowa w pionie razy prędkość w km/h wynosi 925, czyli np. 8 mm przy 120 km/h, 13 mm przy 70 km/h i 23 mm przy 40 km/h. Wartości z tabeli Id-14 są od tego większe. Sam drut też to potrafi: na Bałtyku przy `tor0290` wznosi się o ok. 1%, więc pantograf nadąża tam do ok. 46 km/h, a powyżej wyłącznik wybija także na prostym torze. Fizyka bierze promień łuku i pochylenie z projektu, więc nierówności nie zmieniają tych wartości, a do wykolejeń dokładają tylko przechyłkę, ok. 0,06 m/s² przy `uneven 18` wobec progu ok. 3,2 m/s². diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 2c56cb7c..af882828 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -8043,10 +8043,12 @@ 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 + auto const uneven { MyTrack != nullptr && MyTrack->is_uneven() }; if( iVel > 0.5 ) { // acceleration-driven base shake shakevector += glm::dvec3( - -MoverParameters->AccN * Timedelta * 5.0 * Global.ShakingMultiplierRL, // highlight side sway + uneven ? 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 @@ -8059,7 +8061,7 @@ TDynamicObject::update_shake( double const Timedelta ) { auto shake { 1.25 * ShakeSpring.ComputateForces( shakevector, ShakeState.offset ) }; - if( LocalRandom( iVel ) > 25.0 ) { + if( false == uneven && LocalRandom( iVel ) > 25.0 ) { // extra shake at increased velocity shake += ShakeSpring.ComputateForces( glm::dvec3( From 88609a1657e7c41286e24b37733789a28d9a21d6 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 18:31:28 +0100 Subject: [PATCH 07/18] fix(track): the ends of joined paths meet, in place and in cant Ends less than 20 cm apart meet in the middle with a common vector across, which 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 alike ones meet halfway: the slips on Baltyk had steps of 0.2 degrees. The physics goes by the same cant. --- track_uneven_doc.md | 2 ++ world/Segment.cpp | 24 ++++++++++++++++ world/Segment.h | 15 ++++++++++ world/Track.cpp | 69 +++++++++++++++++++++++++++++++++++++++++++++ world/Track.h | 2 ++ 5 files changed, 112 insertions(+) diff --git a/track_uneven_doc.md b/track_uneven_doc.md index 4b71140b..a9b888ae 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -60,6 +60,8 @@ Każda składowa łączy się osobno: - **Styki:** zapadnięcia są co `fSoundDistance`, na długości 6 m, i biegną dalej przez kolejne tory o tym samym odstępie. Stukot osi wypada w miejscu zapadnięcia. Przy odstępie 6 m lub mniejszym oraz na rozjazdach zapadnięć nie ma. - **Przechyłka:** dolicza się do projektowej. Jest pochyleniem w planie mapy, więc tory wpisane ku sobie spotykają się na tej samej przechyłce. Na rozjazdach tylko ta wynikająca z fal, bez osobnego siadania toków. +Końce złączonych dróg spotykają się w środku między sobą, z jednym kierunkiem poprzecznym, jeśli są bliżej niż 20 cm, co zamyka pęknięcia z niezgodnych współrzędnych w danych, także na torach bez nierówności. Przechyłkę na takim styku rozjazd bierze od zwykłego toru, a dwa tory albo dwa rozjazdy spotykają się w połowie. To dotyczy też fizyki. + Za nierównością idą szyny, wierzch podsypki, `sleepermodel`, osie, przechył pudła i bujanie kabiny. Skarpa podsypki przejmuje różnicę, a jej spód zostaje w projekcie, na terenie. Nie idą za nią modele z includów (np. napędy i podkłady rozjazdów), teren ani sieć trakcyjna, więc przy dużych wartościach szyna rozjeżdża się z takim modelem o kilka centymetrów. diff --git a/world/Segment.cpp b/world/Segment.cpp index 3668ed13..16f878c9 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -69,6 +69,10 @@ 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_designrolls[ 0 ] = fRoll1; + m_designrolls[ 1 ] = fRoll2; + m_joints[ 0 ].reset(); + m_joints[ 1 ].reset(); bCurve = bIsCurve; if (Global.bRollFix) { // Ra: poprawianie przechyłki @@ -441,6 +445,18 @@ void TSegment::design_position(double const fDistance, glm::dvec3 &position, glm } }; +void TSegment::plan_frame( double const Distance, glm::dvec3 &Point, glm::dvec3 &Across ) const { + auto const t { FastGetTFromS( std::clamp( Distance, 0.0, fLength ) ) }; + Point = FastGetPoint( t ); + auto const dir { FastGetDirection( t, 0.1 / fLength ) }; + Across = glm::dvec3{ -dir.z, 0.0, dir.x }; + if( glm::length2( Across ) == 0.0 ) { + auto const chord { FastGetPoint_1() - FastGetPoint_0() }; + Across = glm::dvec3{ -chord.z, 0.0, chord.x }; + } + Across = glm::normalize( Across ); +} + glm::dvec3 TSegment::FastGetPoint(double const t) const { // return (bCurve?std::lerp(t,Point1,CPointOut,CPointIn,Point2):((1.0-t)*Point1+(t)*Point2)); @@ -492,6 +508,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_joints[ 0 ] ) { + pos1 = glm::vec3{ m_joints[ 0 ]->first - Origin }; + parallel1 = glm::vec3{ m_joints[ 0 ]->second }; + } if( iEnd == 0 ) iEnd = iSegCount; fEnd = static_cast(fLength * double( iEnd ) / double( iSegCount )); @@ -537,6 +557,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_joints[ 1 ] ) { + pos2 = glm::vec3{ m_joints[ 1 ]->first - Origin }; + parallel2 = glm::vec3{ m_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 ) { diff --git a/world/Segment.h b/world/Segment.h index 492cab94..7af8a894 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -9,6 +9,8 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include + #include "utilities/Classes.h" #include "rendering/geometrybank.h" #include @@ -47,6 +49,9 @@ class TSegment glm::dvec3 Point2; float fRoll1 { 0.f }; float fRoll2 { 0.f }; // przechyłka na końcach + float m_designrolls[ 2 ] { 0.f, 0.f }; // as entered; the two above can differ at a joint + // where the path meets the next one: the middle of their ends and their common vector across + std::optional> m_joints[ 2 ]; bool m_uneven { false }; // see TrackUneven.h double fLength { -1.0 }; // długość policzona std::vector fTsBuffer; // wartości parametru krzywej t dla równych odcinków s @@ -117,6 +122,16 @@ public: void set_uneven( bool const Uneven ) { m_uneven = Uneven; } + // 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_designrolls[ End ]; } + void + set_joint( int const End, std::optional> const &Joint, float const Roll ) { + m_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; diff --git a/world/Track.cpp b/world/Track.cpp index b5589eed..6ac1ce7c 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -1561,8 +1561,72 @@ void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { create_geometry( Bank ); } +void TTrack::share_joints() { + + int const paths { eType == tt_Switch ? 2 : 1 }; + auto const segment_of = []( TTrack const &Track, int const Path ) -> TSegment * { + return ( Track.eType == tt_Switch ? Track.SwitchExtension->Segments[ Path ].get() : Track.Segment.get() ); }; + for( int path = 0; path < paths; ++path ) { + auto *segment { segment_of( *this, path ) }; + if( segment == nullptr ) { + continue; + } + for( int end = 0; end < 2; ++end ) { + std::optional> joint; + auto roll { segment->design_roll( end ) }; + auto const *neighbour { eType == tt_Switch ? + ( end ? SwitchExtension->pNexts[ path ] : SwitchExtension->pPrevs[ path ] ) : + ( end ? trNext : trPrev ) }; + if( neighbour != nullptr && neighbour != this && ( neighbour->iCategoryFlag & 15 ) == ( iCategoryFlag & 15 ) ) { + glm::dvec3 point; + glm::dvec3 across; + segment->plan_frame( end ? segment->GetLength() : 0.0, point, across ); + auto closest { 0.2 }; + for( int theirpath = 0; theirpath < ( neighbour->eType == tt_Switch ? 2 : 1 ); ++theirpath ) { + auto const *theirs { segment_of( *neighbour, theirpath ) }; + if( theirs == nullptr ) { + continue; + } + for( int theirend = 0; theirend < 2; ++theirend ) { + glm::dvec3 theirpoint; + glm::dvec3 theiracross; + theirs->plan_frame( theirend ? theirs->GetLength() : 0.0, theirpoint, theiracross ); + if( theirend == end ) { + theiracross = -theiracross; + } + auto const gap { glm::length( theirpoint - point ) }; + if( gap >= closest || glm::dot( across, theiracross ) < 0.9 ) { + continue; + } + closest = gap; + joint.emplace( 0.5 * ( point + theirpoint ), glm::normalize( across + theiracross ) ); + if( iCategoryFlag & 1 ) { + // the cant of a plain track is what was designed, a switch takes it; alike ones meet halfway + auto const own { segment->design_roll( end ) }; + auto const their { theirend == end ? -theirs->design_roll( theirend ) : theirs->design_roll( theirend ) }; + roll = ( + ( eType == tt_Switch ) == ( neighbour->eType == tt_Switch ) ? 0.5f * ( own + their ) : + eType == tt_Switch ? their : + own ); + } + } + } + } + segment->set_joint( end, joint, roll ); + } + } + if( eType != tt_Switch && Segment && ( iCategoryFlag & 1 ) ) { + float roll1, roll2; + Segment->GetRolls( roll1, roll2 ); + if( roll1 != 0.f || roll2 != 0.f ) { + iTrapezoid |= 1; + } + } +} + void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { gfx::userdata_array empty_userdata; + share_joints(); // 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() ) { @@ -3845,6 +3909,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 92516ee6..f7b581b8 100644 --- a/world/Track.h +++ b/world/Track.h @@ -353,6 +353,8 @@ public: track_uneven::amounts uneven_at( double const Distance ) const; // 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 ) const; // m // distance from the rail joint behind; negative: the track has no joints that dip From 02abc7df0ab4baa288d977e3d2650ec1127fe999 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 18:33:35 +0100 Subject: [PATCH 08/18] feat(track): all of the unevenness in one attribute, with values for each path of a switch Usage, before endtrack, in mm, 0-100, separated by semicolons with no spaces: a plain track: uneven value;up;side;cross;dip a switch: uneven p1-p2;p3-p4;up p1-p2;side p1-p2;up p3-p4;side p3-p4 uneven 18 the value alone uneven 18;;;25;6 with its own cross level and dip at the joints uneven 0;;30 sideways alone uneven 14;35 a switch, 14 on the straight path, 35 on the diverging What's left out takes the first number, the dip goes by the up and down one, and a path p3-p4 with no numbers of its own takes those of p1-p2. This replaces unevenup, unevenside, unevencross and jointdip. The fields in the editor stay, track panel, section Ride and sound. At the end they share, the paths of a switch keep to the lower value, and each comes to its own value along the way. Joined switches take the lowest of the same path, a path joined to another path the lower of the two, and a plain track comes to the value of the path it meets. See track_uneven_doc.md. --- application/editormodetrack.cpp | 4 +- editor/editorTrack.cpp | 17 ++- editor/editorTrack.hpp | 3 + lang/editor_pl.po | 9 ++ tests/unit/test_track_uneven.cpp | 37 +++++++ track_uneven_doc.md | 27 ++--- world/Segment.cpp | 6 +- world/Segment.h | 8 +- world/Track.cpp | 179 +++++++++++++++++++++---------- world/Track.h | 26 +++-- world/TrackUneven.h | 46 ++++++++ 11 files changed, 266 insertions(+), 96 deletions(-) diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 5b048396..717a3657 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1171,10 +1171,12 @@ void editor_mode::render_path_parameters(TTrack &Track) char const *label; float *value; }; - for (auto const &part : {component{"Up and down (mm)", &track->m_unevenup}, component{"Sideways (mm)", &track->m_unevenside}, component{"Cross level (mm)", &track->m_unevencross}}) + for (auto const &part : {component{"Up and down (mm)", &track->m_unevenup}, component{"Sideways (mm)", &track->m_unevenside}, component{"Cross level (mm)", &track->m_unevencross}, component{"Diverging path (mm)", &track->m_unevenbranch}, component{"Diverging, up and down (mm)", &track->m_unevenbranchup}, component{"Diverging, sideways (mm)", &track->m_unevenbranchside}}) { if (part.value == &track->m_unevencross && track->eType != tt_Normal) continue; + if ((part.value == &track->m_unevenbranch || part.value == &track->m_unevenbranchup || part.value == &track->m_unevenbranchside) && track->eType != tt_Switch) + continue; if (ImGui::InputFloat(STR_C(part.label), part.value, 0.0f, 0.0f, "%.1f")) *part.value = *part.value < 0.f ? -1.f : std::min(*part.value, track_uneven::most); item_tooltip("Set apart from the unevenness, mm. Negative: the unevenness"); diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index cbe7c26b..da426007 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -156,6 +156,9 @@ editor_track::state editor_track::capture(TTrack const &Track) result.unevenup = Track.m_unevenup; result.unevenside = Track.m_unevenside; result.unevencross = Track.m_unevencross; + result.unevenbranch = Track.m_unevenbranch; + result.unevenbranchup = Track.m_unevenbranchup; + result.unevenbranchside = Track.m_unevenbranchside; result.jointdip = Track.m_jointdip; result.sleepers = Track.m_sleeper_enabled; result.sleeper_frequency = Track.m_sleeper_frequency; @@ -259,6 +262,9 @@ void editor_track::apply(TTrack &Track, state const &State) Track.m_unevenup = State.unevenup; Track.m_unevenside = State.unevenside; Track.m_unevencross = State.unevencross; + Track.m_unevenbranch = State.unevenbranch; + Track.m_unevenbranchup = State.unevenbranchup; + Track.m_unevenbranchside = State.unevenbranchside; Track.m_jointdip = State.jointdip; Track.m_sleeper_enabled = State.sleepers; Track.m_sleeper_frequency = State.sleeper_frequency; @@ -2360,16 +2366,7 @@ void write_ending(std::ostream &Text, TTrack const &Style) { if ((Style.iCategoryFlag & 15) == 1 && false == Style.m_profile1.first.empty()) Text << "railprofile " << Style.m_profile1.first << ' '; - if (Style.m_uneven > 0.f) - Text << "uneven " << Style.m_uneven << ' '; - if (Style.m_unevenup >= 0.f) - Text << "unevenup " << Style.m_unevenup << ' '; - if (Style.m_unevenside >= 0.f) - Text << "unevenside " << Style.m_unevenside << ' '; - if (Style.m_unevencross >= 0.f) - Text << "unevencross " << Style.m_unevencross << ' '; - if (Style.m_jointdip >= 0.f) - Text << "jointdip " << Style.m_jointdip << ' '; + Text << Style.uneven_text(); // 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 << ' '; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index b6137bd8..61858336 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -59,6 +59,9 @@ class editor_track float unevenup{-1.f}; float unevenside{-1.f}; float unevencross{-1.f}; + float unevenbranch{-1.f}; + float unevenbranchup{-1.f}; + float unevenbranchside{-1.f}; float jointdip{-1.f}; bool sleepers{false}; float sleeper_frequency{0.6f}; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index a9c61b83..dbf7c124 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -876,6 +876,15 @@ 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" diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index 46bd1e5f..22970fa6 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -140,6 +140,43 @@ TEST_CASE("uneven track: the field stays within its bound") 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: 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}; diff --git a/track_uneven_doc.md b/track_uneven_doc.md index a9b888ae..cbbb70c0 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -1,6 +1,6 @@ # Nierówności toru -Opcjonalne atrybuty wpisu toru, przed `endtrack`: +Opcjonalny atrybut wpisu toru, przed `endtrack`: ``` node -1 0 tor1 track normal 100.0 1.435 0.15 25.0 20 0 flat vis @@ -10,21 +10,22 @@ rail_screw_used1 6 1435mm/tpbps-new2 0.2 0.5 1.1 0.0 0.0 0.0 0.0 0.2 100.0 0.0 0.0 -uneven 18 -unevencross 25 -jointdip 6 +uneven 18;;;25;6 endtrack ``` -| atrybut | dotyczy | opis | -|-------------------|-----------------------|---------------------------------------------------------------------------------------| -| `uneven ` | `normal`, `switch` | największa odchyłka toku szynowego na cięciwie 10 m; 0-100, domyślnie 0 | -| `unevenup ` | `normal`, `switch` | składowa w pionie, osobno od `uneven` | -| `unevenside ` | `normal`, `switch` | składowa w poziomie, osobno od `uneven` | -| `unevencross `| `normal` | składowa przechyłki, osobno od `uneven`; rozjazd jej nie ma | -| `jointdip ` | `normal` | głębokość zapadnięcia na styku szyn; 0-100, domyślnie 0,4 składowej w pionie | +Liczby w mm, 0-100, rozdzielone średnikiem, bez spacji: -Składowa niepodana przyjmuje wartość `uneven`. Na przykład sam bok to `unevenside 30`, a same zapadnięcia na stykach to `jointdip 6`. +| pozycja | tor `normal` | rozjazd `switch` | +|---------|--------------------------------------------------|-----------------------------| +| 1 | największa odchyłka toku na cięciwie 10 m | to samo, droga p1-p2 | +| 2 | w pionie | droga p3-p4 | +| 3 | w poziomie | droga p1-p2, w pionie | +| 4 | przechyłka | droga p1-p2, w poziomie | +| 5 | zapadnięcie na styku, domyślnie 0,4 pionu | droga p3-p4, w pionie | +| 6 | | droga p3-p4, w poziomie | + +Pozycję pominiętą zastępuje pierwsza liczba, a zapadnięcie liczy się z pionu. Końcowe można opuścić. Sam bok to `uneven 0;;30`, a same zapadnięcia na stykach to `uneven 0;;;;6`. Rozjazd bez składowych na drodze p3-p4 przyjmuje wartości z drogi p1-p2. Rozjazd na 100 km/h na wprost i 40 km/h na bok to na przykład `uneven 14;35`. Wszystkie są też w edytorze, w panelu toru, w sekcji „Ride and sound”. Na drogach, rzekach i obrotnicach są ignorowane. @@ -56,7 +57,7 @@ Nierówność to pole przesunięć zależne od położenia na mapie, złożone z Każda składowa łączy się osobno: - **Tor z torem:** na styku obowiązuje mniejsza z dwóch wartości, a każdy tor dochodzi do niej płynnie na 20 m. Wolny koniec toru leży w projekcie. -- **Rozjazd:** wszystkie jego drogi mają jedną wartość. Rozjazdy złączone bezpośrednio przyjmują najmniejszą ze swoich. Dołączony tor dochodzi do wartości rozjazdu, także większej od własnej. Tor bez danej składowej trzyma w niej cały rozjazd w projekcie. +- **Rozjazd:** każda droga ma swoją wartość, a na wspólnym końcu obu obowiązuje mniejsza z nich. Rozjazdy złączone bezpośrednio przyjmują najmniejszą z tej samej drogi, a na styku dwóch dróg mniejszą z obu. Dołączony tor dochodzi do wartości rozjazdu na końcu, do którego dochodzi, także większej od własnej. Tor bez danej składowej trzyma w niej cały rozjazd w projekcie. - **Styki:** zapadnięcia są co `fSoundDistance`, na długości 6 m, i biegną dalej przez kolejne tory o tym samym odstępie. Stukot osi wypada w miejscu zapadnięcia. Przy odstępie 6 m lub mniejszym oraz na rozjazdach zapadnięć nie ma. - **Przechyłka:** dolicza się do projektowej. Jest pochyleniem w planie mapy, więc tory wpisane ku sobie spotykają się na tej samej przechyłce. Na rozjazdach tylko ta wynikająca z fal, bez osobnego siadania toków. diff --git a/world/Segment.cpp b/world/Segment.cpp index 16f878c9..dd236c61 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -381,7 +381,7 @@ void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm:: // 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 ) }; + auto const amount { pOwner->uneven_at( fDistance, m_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 ) ); @@ -389,7 +389,7 @@ void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm:: } glm::dvec3 TSegment::stray( glm::dvec3 const &Point, double const Distance ) const { - auto const amount { pOwner->uneven_at( Distance ) }; + auto const amount { pOwner->uneven_at( Distance, m_path ) }; 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; @@ -416,7 +416,7 @@ void TSegment::settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origi 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 ) }; - Vertex.position.y -= static_cast( share * track_uneven::stray_per_mm * pOwner->uneven_at( Distance ).cross * track_uneven::level( Origin + glm::dvec3 { Position }, glm::dvec3 { Parallel } ) * across / gauge ); + Vertex.position.y -= static_cast( share * track_uneven::stray_per_mm * pOwner->uneven_at( Distance, m_path ).cross * track_uneven::level( Origin + glm::dvec3 { Position }, glm::dvec3 { Parallel } ) * across / gauge ); Vertex.position += glm::vec3 { share * stray( Origin + glm::dvec3 { Vertex.position }, Distance ) }; } diff --git a/world/Segment.h b/world/Segment.h index 7af8a894..77e61f90 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -50,9 +50,10 @@ class TSegment float fRoll1 { 0.f }; float fRoll2 { 0.f }; // przechyłka na końcach float m_designrolls[ 2 ] { 0.f, 0.f }; // as entered; the two above can differ at a joint + bool m_uneven { false }; // see TrackUneven.h + int m_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::optional> m_joints[ 2 ]; - bool m_uneven { false }; // see TrackUneven.h 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 @@ -120,8 +121,9 @@ public: 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 ) { - m_uneven = Uneven; } + set_uneven( bool const Uneven, int const Path = 0 ) { + m_uneven = Uneven; + m_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; diff --git a/world/Track.cpp b/world/Track.cpp index 6ac1ce7c..8359f78a 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -49,6 +49,9 @@ TTrack::profiles_array TTrack::m_profiles; TTrack::profiles_map TTrack::m_profilesmap; int TTrack::s_layoutstamp { 0 }; +// how near an end of a path counts as being at a point, m +static double const switch_end_reach { 0.3 }; + TSwitchExtension::TSwitchExtension(TTrack *owner, int const what) { // na początku wszystko puste pNexts[0] = nullptr; // wskaźniki do kolejnych odcinków ruchu @@ -639,24 +642,10 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) } } else if( str == "uneven" ) { - // uneven : see TrackUneven.h - auto const uneven { parser->getToken() }; + // uneven [;...]: see read_uneven and 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 = std::clamp( uneven, 0.f, track_uneven::most ); - } - } - else if( str == "unevenup" || str == "unevenside" || str == "unevencross" ) { - // the components of uneven set apart, mm - auto const value { parser->getToken() }; - if( ( eType == tt_Normal || eType == tt_Switch ) && iCategoryFlag == 1 ) { - ( str == "unevenup" ? m_unevenup : str == "unevenside" ? m_unevenside : m_unevencross ) = std::clamp( value, 0.f, track_uneven::most ); - } - } - else if( str == "jointdip" ) { - // jointdip : depth of the dip at rail joints, with or without uneven - auto const jointdip { parser->getToken() }; - if( eType == tt_Normal && iCategoryFlag == 1 ) { - m_jointdip = std::clamp( jointdip, 0.f, track_uneven::most ); + read_uneven( values ); } } else if( str == "friction" ) { @@ -717,18 +706,56 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) // to a stale origin and the renderer would draw sleepers in the wrong place). } -track_uneven::amounts TTrack::uneven_declared() const { +void TTrack::read_uneven( std::string const &Text ) { - return { + auto const fields { track_uneven::read_fields<6>( Text ) }; + m_uneven = std::max( fields[ 0 ], 0.f ); + if( eType == tt_Switch ) { + m_unevenbranch = fields[ 1 ]; + m_unevenup = fields[ 2 ]; + m_unevenside = fields[ 3 ]; + m_unevenbranchup = fields[ 4 ]; + m_unevenbranchside = fields[ 5 ]; + } + else { + m_unevenup = fields[ 1 ]; + m_unevenside = fields[ 2 ]; + m_unevencross = fields[ 3 ]; + m_jointdip = fields[ 4 ]; + } +} + +std::string TTrack::uneven_text() const { + + auto const fields { eType == tt_Switch ? + std::array { m_uneven, m_unevenbranch, m_unevenup, m_unevenside, m_unevenbranchup, m_unevenbranchside } : + std::array { m_uneven, m_unevenup, m_unevenside, m_unevencross, m_jointdip, -1.f } }; + if( m_uneven <= 0.f && std::all_of( fields.begin() + 1, fields.end(), []( float const Field ) { return Field < 0.f; } ) ) { + return {}; + } + return "uneven " + track_uneven::write_fields( fields ) + ' '; +} + +track_uneven::amounts TTrack::uneven_declared( int const Path ) const { + + track_uneven::amounts result { m_unevenup >= 0.f ? m_unevenup : m_uneven, m_unevenside >= 0.f ? m_unevenside : m_uneven, eType != tt_Normal ? 0.f : m_unevencross >= 0.f ? m_unevencross : m_uneven }; + if( Path == 1 ) { + auto const branch = [ this ]( float const Part, double const Straight ) -> double { + return Part >= 0.f ? Part : m_unevenbranch >= 0.f ? m_unevenbranch : Straight; }; + result.up = branch( m_unevenbranchup, result.up ); + result.side = branch( m_unevenbranchside, result.side ); + } + return result; } bool TTrack::is_uneven() const { - auto const declared { uneven_declared() }; - return declared.up > 0.0 || declared.side > 0.0 || declared.cross > 0.0 || joint_depth() > 0.0; + 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; } double TTrack::uneven_bed_share( double const Depth ) const { @@ -736,61 +763,109 @@ 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 ) const { +track_uneven::amounts TTrack::uneven_at( double const Distance, int const Path ) const { + auto const along = [ Distance ]( track_uneven::amounts const &Own, track_uneven::amounts const &Start, track_uneven::amounts const &End, double const Length ) -> track_uneven::amounts { + return { + track_uneven::along( Own.up, Start.up, End.up, Distance, Length ), + track_uneven::along( Own.side, Start.side, End.side, Distance, Length ), + track_uneven::along( Own.cross, Start.cross, End.cross, Distance, Length ) }; }; if( eType == tt_Switch ) { - return switch_uneven(); + auto const &stray { switch_uneven() }; + return along( stray.paths[ Path ], stray.ends[ Path ][ 0 ], stray.ends[ Path ][ 1 ], SwitchExtension->Segments[ Path ]->GetLength() ); } auto const own { uneven_declared() }; - auto const atend = [&]( TTrack const *Other ) -> track_uneven::amounts { + auto const atend = [&]( TTrack const *Other, glm::dvec3 const &Point ) -> track_uneven::amounts { if( Other == nullptr ) { return {}; } if( Other->eType == tt_Switch ) { - return Other->switch_uneven(); + return Other->switch_uneven_at( Point ); } - auto const theirs { Other->uneven_declared() }; - return { std::min( own.up, theirs.up ), std::min( own.side, theirs.side ), std::min( own.cross, theirs.cross ) }; }; - auto const start { atend( trPrev ) }; - auto const end { atend( trNext ) }; - return { - track_uneven::along( own.up, start.up, end.up, Distance, Length() ), - track_uneven::along( own.side, start.side, end.side, Distance, Length() ), - track_uneven::along( own.cross, start.cross, end.cross, Distance, Length() ) }; + return track_uneven::least( own, Other->uneven_declared() ); }; + return along( own, atend( trPrev, Segment->FastGetPoint_0() ), atend( trNext, Segment->FastGetPoint_1() ), Length() ); } -track_uneven::amounts TTrack::switch_uneven() const { +TTrack::switch_stray const &TTrack::switch_uneven() const { if( m_switchstray.stamp == s_layoutstamp ) { - return m_switchstray.value; + return m_switchstray; } std::vector group { this }; - auto least { uneven_declared() }; + std::array least { uneven_declared( 0 ), uneven_declared( 1 ) }; for( std::size_t index = 0; index < group.size(); ++index ) { auto const &extension { *group[ index ]->SwitchExtension }; for( auto const *other : { extension.pPrevs[ 0 ], extension.pPrevs[ 1 ], extension.pNexts[ 0 ], extension.pNexts[ 1 ] } ) { if( other == nullptr ) { continue; } - auto const theirs { other->uneven_declared() }; if( other->eType == tt_Switch && other->SwitchExtension ) { - least.up = std::min( least.up, theirs.up ); - least.side = std::min( least.side, theirs.side ); + least[ 0 ] = track_uneven::least( least[ 0 ], other->uneven_declared( 0 ) ); + least[ 1 ] = track_uneven::least( least[ 1 ], other->uneven_declared( 1 ) ); if( std::find( group.begin(), group.end(), other ) == group.end() ) { group.push_back( other ); } } else { // a track with none of a component keeps to its design in it, any other comes to the value of the switch - if( theirs.up <= 0.0 ) { least.up = 0.0; } - if( theirs.side <= 0.0 ) { least.side = 0.0; } + auto const theirs { other->uneven_declared() }; + for( auto &path : least ) { + if( theirs.up <= 0.0 ) { path.up = 0.0; } + if( theirs.side <= 0.0 ) { path.side = 0.0; } + } } } } for( auto const *track : group ) { - track->m_switchstray = { least, s_layoutstamp }; + track->m_switchstray.paths = least; + track->m_switchstray.stamp = s_layoutstamp; } - return least; + // the lowest of the paths of a switch ending at a point + auto const paths_at = []( TTrack const &Track, glm::dvec3 const &Point ) { + auto result { track_uneven::least( Track.m_switchstray.paths[ 0 ], Track.m_switchstray.paths[ 1 ] ) }; + auto found { false }; + track_uneven::amounts ending { track_uneven::most, track_uneven::most, track_uneven::most }; + for( int path = 0; path < 2; ++path ) { + auto const &segment { *Track.SwitchExtension->Segments[ path ] }; + if( glm::length( segment.FastGetPoint_0() - Point ) < switch_end_reach || glm::length( segment.FastGetPoint_1() - Point ) < switch_end_reach ) { + ending = track_uneven::least( ending, Track.m_switchstray.paths[ path ] ); + found = true; + } + } + return found ? ending : result; }; + for( auto const *track : group ) { + auto const &extension { *track->SwitchExtension }; + for( int path = 0; path < 2; ++path ) { + auto const &segment { *extension.Segments[ path ] }; + for( int end = 0; end < 2; ++end ) { + auto const point { end == 0 ? segment.FastGetPoint_0() : segment.FastGetPoint_1() }; + auto value { paths_at( *track, point ) }; + auto const *other { end == 0 ? extension.pPrevs[ path ] : extension.pNexts[ path ] }; + if( other != nullptr && other->eType == tt_Switch && other->SwitchExtension ) { + value = track_uneven::least( value, paths_at( *other, point ) ); + } + track->m_switchstray.ends[ path ][ end ] = value; + } + } + } + return m_switchstray; +} + +track_uneven::amounts TTrack::switch_uneven_at( glm::dvec3 const &Point ) const { + + auto const &stray { switch_uneven() }; + auto found { false }; + track_uneven::amounts result { track_uneven::most, track_uneven::most, track_uneven::most }; + for( int path = 0; path < 2; ++path ) { + auto const &segment { *SwitchExtension->Segments[ path ] }; + for( int end = 0; end < 2; ++end ) { + if( glm::length( ( end == 0 ? segment.FastGetPoint_0() : segment.FastGetPoint_1() ) - Point ) < switch_end_reach ) { + result = track_uneven::least( result, stray.ends[ path ][ end ] ); + found = true; + } + } + } + return found ? result : track_uneven::least( stray.paths[ 0 ], stray.paths[ 1 ] ); } double TTrack::joint_depth() const { @@ -896,7 +971,7 @@ void TTrack::init_segments( bool const Initial ) { ++s_layoutstamp; if( eType == tt_Switch && SwitchExtension ) { SwitchExtension->Segments[ 0 ]->set_uneven( is_uneven() ); - SwitchExtension->Segments[ 1 ]->set_uneven( is_uneven() ); + SwitchExtension->Segments[ 1 ]->set_uneven( is_uneven(), 1 ); } else if( Segment ) { Segment->set_uneven( is_uneven() ); @@ -2707,21 +2782,7 @@ TTrack::export_as_text_( std::ostream &Output ) const { && false == m_profile1.first.empty() ) { Output << "railprofile " << m_profile1.first << ' '; } - if( m_uneven > 0.f ) { - Output << "uneven " << m_uneven << ' '; - } - if( m_unevenup >= 0.f ) { - Output << "unevenup " << m_unevenup << ' '; - } - if( m_unevenside >= 0.f ) { - Output << "unevenside " << m_unevenside << ' '; - } - if( m_unevencross >= 0.f ) { - Output << "unevencross " << m_unevencross << ' '; - } - if( m_jointdip >= 0.f ) { - Output << "jointdip " << m_jointdip << ' '; - } + Output << uneven_text(); if( false == m_friction.first.empty() ) { Output << "friction " << m_friction.first << ' '; } diff --git a/world/Track.h b/world/Track.h index f7b581b8..1d366ebf 100644 --- a/world/Track.h +++ b/world/Track.h @@ -244,6 +244,10 @@ public: float m_unevenup { -1.f }; float m_unevenside { -1.f }; float m_unevencross { -1.f }; + // the diverging path of a switch, mm; negative: as the straight one + float m_unevenbranch { -1.f }; + float m_unevenbranchup { -1.f }; + float m_unevenbranchside { -1.f }; float m_jointdip { -1.f }; // depth of the dip at rail joints, mm; negative: from m_uneven; plain tracks only private: double fVelocity = -1.0; // ograniczenie prędkości // prędkość dla AI (powyżej rośnie prawdopowobieństwo wykolejenia) @@ -346,11 +350,15 @@ public: void VelocitySet(float v); double VelocityGet(); float Friction() const; - // the unevenness the track declares - track_uneven::amounts uneven_declared() const; + // the attribute of the entry. A plain track: the value;up and down;sideways;cross level;dip at the joints. + // A switch: path p1-p2;path p3-p4;up and down of the first;sideways of the first;up and down of the second;sideways of the second + void read_uneven( std::string const &Text ); + std::string uneven_text() 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 at a distance along the track, meeting the tracks joined at its ends - track_uneven::amounts uneven_at( double const Distance ) const; + // 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 ) const; // 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 @@ -398,13 +406,17 @@ private: }; void settle_rail_run() const; mutable rail_run m_railrun; - // one value along all paths of a switch: the lowest of the switches joined, 0 next to a track with none - track_uneven::amounts switch_uneven() const; + // 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 { - track_uneven::amounts value; + std::array paths; + std::array, 2> ends; int stamp { -1 }; }; mutable switch_stray m_switchstray; + switch_stray const &switch_uneven() const; + // the value of a switch where a track joined to it meets it + track_uneven::amounts switch_uneven_at( glm::dvec3 const &Point ) const; static int s_layoutstamp; // bumped when tracks are joined or changed, invalidates the two above 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.h b/world/TrackUneven.h index 182f3799..f4a50eb7 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -11,7 +11,10 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include +#include +#include #include #include @@ -34,6 +37,11 @@ struct amounts { 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 ) }; +} + struct wave { double x; double z; @@ -41,6 +49,44 @@ struct wave { double share; }; +// the numbers of an entry like "uneven 14;35", negative where left out +template +std::array read_fields( std::string const &Text ) { + + std::array result; + result.fill( -1.f ); + std::size_t start { 0 }; + for( std::size_t index = 0; index < Count && start < Text.size(); ++index ) { + auto const end { std::min( Text.find( ';', start ), Text.size() ) }; + auto value { 0.f }; + if( end > start && std::from_chars( Text.data() + start, Text.data() + end, value ).ec == std::errc {} ) { + result[ index ] = std::clamp( value, 0.f, most ); + } + start = 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(); +} + template using wave_set = std::array; From 798e8d6a843b5376f1a99f4b82839f84d20a5b4a Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 18:35:00 +0100 Subject: [PATCH 09/18] feat(tools): grade the tracks of any scenery for the uneven attribute Usage: 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 It follows the includes from a .scn, .scm or .inc, and takes the state of each track from what the scenery shows: ballast and rail textures, grass on the track, curves, speed and road crossings. The originals stay as they are, copies of the files with tracks go to --out, and with no --out it only reports. Values stay under what a pantograph can follow on those tracks, by simulation, the rise of the wire itself not counted. Tracks in .inc files taking parameters are skipped and rotate blocks not applied, the report says so. See track_uneven_doc.md. --- tools/track_uneven_grade.py | 549 ++++++++++++++++++++++++++++++++++++ track_uneven_doc.md | 17 ++ 2 files changed, 566 insertions(+) create mode 100644 tools/track_uneven_grade.py diff --git a/tools/track_uneven_grade.py b/tools/track_uneven_grade.py new file mode 100644 index 00000000..eb39d7c6 --- /dev/null +++ b/tools/track_uneven_grade.py @@ -0,0 +1,549 @@ +#!/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+([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 in self.seen: + return + self.seen.add(path) + text = path.read_bytes().decode("latin-1") + self.texts[path] = text + matches = [m for m in TOKEN.finditer(text) if not m.group(0).startswith("//")] + words = [m.group(0) for m in matches] + lower = [w.lower() for w in words] + origins = [offset] + i = 0 + while i < len(words): + word = lower[i] + if word == "include" and i + 1 < len(words): + end = i + 1 + while end < len(words) and lower[end] != "end": + end += 1 + self.include(words[i + 1], words[i + 2:end], origins[-1]) + i = end + elif word == "origin" and i + 3 < len(words) and all(NUMBER.match(w) for w in words[i + 1:i + 4]): + origins.append(tuple(o + float(w) for o, w in zip(origins[-1], words[i + 1:i + 4]))) + i += 3 + elif word == "endorigin" and len(origins) > 1: + origins.pop() + elif word == "rotate" and i + 3 < len(words) and any(w.strip("0.-") for w in words[i + 1:i + 4]): + self.notes["rotate"] += 1 + elif word == "node" and i + 4 < len(words): + kind = lower[i + 4] + if kind == "track": + i = self.track(path, matches, words, lower, i, origins[-1]) + elif kind == "model" and i + 9 < len(words) and GRASS.search(lower[i + 9]) and all(NUMBER.match(w) for w in words[i + 5:i + 8]): + self.plant(float(words[i + 5]) + origins[-1][0], float(words[i + 7]) + origins[-1][2]) + i += 1 + + def include(self, name, params, origin): + path = self.root / name.replace("\\", "/") + if GRASS.search(name.lower()): + numbers = [float(w) for w in params if NUMBER.match(w)] + if len(numbers) >= 3: + self.plant(numbers[0] + origin[0], numbers[2] + origin[2]) + return + if not path.is_file(): + self.notes["brak pliku " + name] += 1 + return + if params: + # the same file can come many times with other parameters: its tracks can't be told apart in one copy + if path.stat().st_size < 4000000 and re.search(rb"\btrack\s+(normal|switch)\b", path.read_bytes(), re.I): + self.notes["pliki z parametrami, tory pominiete"] += 1 + return + self.read(path, origin) + + def plant(self, x, z): + self.plants[(round(x / 10), round(z / 10))].append((x, z)) + + def track(self, path, matches, words, lower, i, origin): + 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 + cursor = at + 10 + rail = ballast = None + if lower[at + 9] == "vis": + rail, ballast = lower[cursor], lower[cursor + 2] + cursor += 6 + paths = [] + for _ in range(2 if kind == "switch" else 1): + values = words[cursor:cursor + 15] + if len(values) < 15 or not all(NUMBER.match(v) for v in values): + self.notes["tory z parametrami, pominiete"] += 1 + return end + n = [float(v) for v in values] + part = dict(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 + if kind == "road": + self.roads.append(paths[0]) + return end + block = lower[at:end] + taken = any(w in block for w in ("uneven", "unevenup", "unevenside", "unevencross", "jointdip")) + velocity = float(words[at + block.index("velocity") + 1]) if "velocity" in block and NUMBER.match(words[at + block.index("velocity") + 1]) else 0.0 + sound = float(words[at + 5]) if NUMBER.match(words[at + 5]) else -1.0 + self.tracks.append(dict(name=words[i + 3], kind=kind, rail=rail, ballast=ballast, sound=sound, paths=paths, speed=velocity if velocity > 0 else None, source="wpis" if velocity > 0 else None, file=path, offset=matches[end].start(), taken=taken)) + return end + + +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 settle_speeds(tracks, joints): + # from the nearest track on the stretch between switches, a short piece also along the straight path of a switch + heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"]) + while heads: + t = heads.popleft() + for key in ("p1", "p2"): + for other, _, _ in joints.at(t["paths"][0][key]): + if other is not t and other["kind"] == "normal" and other["speed"] is None: + other["speed"], other["source"] = t["speed"], "sasiad" + heads.append(other) + heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"]) + while heads: + t = heads.popleft() + for key in ("p1", "p2"): + points, seen = [t["paths"][0][key]], set() + for _ in range(6): + onward = [] + for point in points: + 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": + if other["paths"].index(part) != other["bent"]: + onward.append(part["p2" if side == "p1" else "p1"]) + elif other["speed"] is None and other["paths"][0]["length"] < PIECE: + other["speed"], other["source"] = t["speed"], "sasiad" + heads.append(other) + points = onward + for t in tracks: + if t["speed"] is None: + t["speed"], t["source"] = NO_SPEED, "brak" + + +def distances(tracks, joints, station): + best, heap = {}, [] + for t in tracks: + for part in t["paths"]: + for key in ("p1", "p2"): + gap = math.dist((part[key][0], part[key][2]), station) + node = (round(part[key][0], 1), round(part[key][2], 1)) + if gap < 300 and t["kind"] == "normal" and gap < best.get(node, math.inf): + best[node] = gap + heapq.heappush(heap, (gap, node, part[key])) + 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 road_cells(roads): + cells = collections.defaultdict(list) + for road in roads: + for a, b in zip(road["line"], road["line"][1:]): + 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): + cells[(x, z)].append((a, b)) + return cells + + +def hits_road(part, cells): + for a, b in zip(part["line"], part["line"][1:]): + 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): + for c, d in cells.get((x, z), ()): + 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: + continue + t = ((c[0] - a[0]) * sz - (c[2] - a[2]) * sx) / k + u = ((c[0] - a[0]) * rz - (c[2] - a[2]) * rx) / k + if 0 <= t <= 1 and 0 <= u <= 1: + return True + return False + + +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 grade(scenery, options): + tracks = scenery.tracks + joints = Joints(tracks) + for t in tracks: + if t["kind"] == "switch": + radii = [min(plan_radius(p["line"]), p["radius"] if p["radius"] > 0 else math.inf) for p in t["paths"]] + t["bent"] = 1 if len(radii) < 2 or radii[1] <= radii[0] else 0 + settle_speeds(tracks, joints) + route = None + if options.route: + first, last = options.route + from_first, from_last = distances(tracks, joints, first), distances(tracks, joints, last) + ends = [from_last[n] for n in from_last if math.dist(n, first) < 300] + route = (from_first, from_last, 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") + cells = road_cells(scenery.roads) + normal = [t for t in tracks if t["kind"] == "normal"] + for t in tracks: + part = t["paths"][0] + radius = min(plan_radius(part["line"]), part["radius"] if part["radius"] > 0 else math.inf) + curve = 0.0 if radius >= 6000 else min(1.0, max(0.0, (1200 - radius) / 950)) + density = grass_on(part, scenery.plants) if t["kind"] == "normal" else 0.0 + crossing = t["kind"] == "normal" and hits_road(part, cells) + progress = 0.5 + if route: + shares = [] + for key in ("p1", "p2"): + node = (round(part[key][0], 1), round(part[key][2], 1)) + a, b = route[0].get(node), route[1].get(node) + if a is None or b is None: + a, b = math.dist((part[key][0], part[key][2]), options.route[0]), math.dist((part[key][0], part[key][2]), options.route[1]) + shares.append(min(1.0, max(0.0, (a - b + route[2]) / (2 * route[2])))) + progress = 0.5 * sum(shares) + 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) * options.gain, curve=curve, crossing=crossing, grass=density) + if t["kind"] == "normal": + 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)) + for t in normal: + t["heights"] = heights_per_mm(t) + # 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 + product = low + for t in normal: + up = ceiling(t, product) + if up < t["up"]: + ratio = up / t["up"] + t["up"], t["value"], t["side"] = up, t["value"] * ratio, t["side"] * ratio + # 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: + part = t["paths"][0] + change = abs(end_curve(part, "p1") - end_curve(part, "p2")) > CURVE_STEP + for key in ("p1", "p2"): + 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(end_curve(part, key) - (-their if side == key else their)) > CURVE_STEP: + change = True + if not (change or WOOD.search(t["ballast"] or "")): + continue + name = f"{t['name']}:{part['p1'][0]:.0f}:{part['p1'][2]:.0f}" + if hashed(b"near", name) >= options.near: + continue + value = t["value"] + (limit(t["speed"]) - t["value"]) * (NEAR_LEAST + (NEAR_MOST - NEAR_LEAST) * hashed(b"how", name)) + if value <= t["value"]: + continue + 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 + 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 + # each path of a switch by the plain tracks it leads to, the worst of them; those only among switches from their neighbours + switches = [t for t in tracks if t["kind"] == "switch"] + for t in switches: + t["values"] = [None] * len(t["paths"]) + for index, part in enumerate(t["paths"]): + found = [o for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "normal" and theirs["length"] >= SHORT] + if found: + speed = max(o["speed"] for o in found) + if index == t["bent"]: + speed = min(speed, DIVERGING) + t["values"][index] = min(limit(speed) * max(o["state"] for o in found), SWITCH_MOST, product / speed) + for _ in range(5): + for t in switches: + for index, part in enumerate(t["paths"]): + if t["values"][index] is None: + found = [o["values"][i] for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "switch" and o is not t for i, q in enumerate(o["paths"]) if q is theirs and o["values"][i] is not None] + if found: + t["values"][index] = max(found) + for t in switches: + t["values"] = [min(SWITCH_MOST, product / NO_SPEED) if v is None else v for v in t["values"]] + 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 index cbbb70c0..325e8944 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -73,6 +73,23 @@ Pantograf podnosi się z szybkością ciśnienia, ok. 0,14 m/s, a szczelina do d Fizyka bierze promień łuku i pochylenie z projektu, więc nierówności nie zmieniają tych wartości, a do wykolejeń dokładają tylko przechyłkę, ok. 0,06 m/s² przy `uneven 18` wobec progu ok. 3,2 m/s². +## Ocena torów dowolnej scenerii + +`tools/track_uneven_grade.py` wylicza wartości z tego, co widać w scenerii: z tekstur podsypki i szyn, trawy na torze, łuków, prędkości i przejazdów drogowych. Czyta plik `.scn`, `.scm` albo `.inc` razem z jego includami, oryginałów nie zmienia, a kopie plików z torami zapisuje do `--out`. Bez `--out` tylko wypisuje raport. + +``` +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`: tory pogarszają się od pierwszego punktu do drugiego, licząc po torach. +- `--gain`: mnożnik stanu torów, domyślnie 1. +- `--near`: jaka część torów na drewnie i przy zmianach krzywizny idzie bliżej granicy Id-14, domyślnie 0,6. Wybór jest stały. +- `--straight`: nazwy torów bez nierówności, po przecinku. +- `--root`: folder `scenery`, gdy podany plik leży głębiej. + +Wartości trzymają się pod sufitem pantografu, liczonym symulacją na tych torach, ale bez wznosu samego drutu. Zużycie tekstur odczytywane jest z ich nazw, według tabel na początku skryptu, a nieznana nazwa dostaje 0,5. Tory z plików `.inc` z parametrami są pomijane, a bloki `rotate` nieuwzględniane. W obu przypadkach raport wypisuje uwagę. Tor, który ma już nierówność, zostaje bez zmian. + ## Wydajność Tor z nierównością ma geometrię co 2 m zamiast co 10 m na prostych. Rozjazdy zachowują swój podział. From ec2d22668876c358723bc0925436864eac10aa5e Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 18:58:20 +0100 Subject: [PATCH 10/18] fix(editor): the uneven attribute is written in the layout of the entry it goes into Its numbers go by position, and the positions differ between a plain track and a switch. A switch made in the style of a plain track wrote them in the layout of the plain one, so they were read back as other values: the up and down part as the value of the path p3-p4, and so on. The values are now one struct with pure functions to read, write and resolve them, which replaces eight fields and which the unit tests can reach. --- application/editormodetrack.cpp | 14 ++++---- editor/editorTrack.cpp | 22 +++--------- editor/editorTrack.hpp | 10 ++---- tests/unit/test_track_uneven.cpp | 29 +++++++++++++++ world/Track.cpp | 50 +++----------------------- world/Track.h | 15 +------- world/TrackUneven.h | 62 ++++++++++++++++++++++++++++++++ 7 files changed, 110 insertions(+), 92 deletions(-) diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 717a3657..4e313bfd 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1162,8 +1162,8 @@ void editor_mode::render_path_parameters(TTrack &Track) 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, 0.0f, 0.0f, "%.1f")) - track->m_uneven = std::clamp(track->m_uneven, 0.f, track_uneven::most); + 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); struct component @@ -1171,11 +1171,11 @@ void editor_mode::render_path_parameters(TTrack &Track) char const *label; float *value; }; - for (auto const &part : {component{"Up and down (mm)", &track->m_unevenup}, component{"Sideways (mm)", &track->m_unevenside}, component{"Cross level (mm)", &track->m_unevencross}, component{"Diverging path (mm)", &track->m_unevenbranch}, component{"Diverging, up and down (mm)", &track->m_unevenbranchup}, component{"Diverging, sideways (mm)", &track->m_unevenbranchside}}) + for (auto const &part : {component{"Up and down (mm)", &track->m_uneven.up}, component{"Sideways (mm)", &track->m_uneven.side}, component{"Cross level (mm)", &track->m_uneven.cross}, 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 (part.value == &track->m_unevencross && track->eType != tt_Normal) + if (part.value == &track->m_uneven.cross && track->eType != tt_Normal) continue; - if ((part.value == &track->m_unevenbranch || part.value == &track->m_unevenbranchup || part.value == &track->m_unevenbranchside) && track->eType != tt_Switch) + if ((part.value == &track->m_uneven.branch || part.value == &track->m_uneven.branchup || part.value == &track->m_uneven.branchside) && track->eType != tt_Switch) continue; if (ImGui::InputFloat(STR_C(part.label), part.value, 0.0f, 0.0f, "%.1f")) *part.value = *part.value < 0.f ? -1.f : std::min(*part.value, track_uneven::most); @@ -1185,8 +1185,8 @@ void editor_mode::render_path_parameters(TTrack &Track) } if (track->eType == tt_Normal && (track->iCategoryFlag & 15) == 1) { - if (ImGui::InputFloat(STR_C("Dip at rail joints (mm)"), &track->m_jointdip, 0.0f, 0.0f, "%.1f")) - track->m_jointdip = track->m_jointdip < 0.f ? -1.f : std::min(track->m_jointdip, track_uneven::most); + 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); } diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index da426007..b7050174 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -153,13 +153,6 @@ editor_track::state editor_track::capture(TTrack const &Track) result.isolated.push_back(isolated->asName); result.overhead = Track.fOverhead; result.uneven = Track.m_uneven; - result.unevenup = Track.m_unevenup; - result.unevenside = Track.m_unevenside; - result.unevencross = Track.m_unevencross; - result.unevenbranch = Track.m_unevenbranch; - result.unevenbranchup = Track.m_unevenbranchup; - result.unevenbranchside = Track.m_unevenbranchside; - result.jointdip = Track.m_jointdip; result.sleepers = Track.m_sleeper_enabled; result.sleeper_frequency = Track.m_sleeper_frequency; result.sleeper_model = Track.m_sleeper_model_name; @@ -259,13 +252,6 @@ void editor_track::apply(TTrack &Track, state const &State) isolated(Track, State.isolated); overhead(Track, State.overhead); Track.m_uneven = State.uneven; - Track.m_unevenup = State.unevenup; - Track.m_unevenside = State.unevenside; - Track.m_unevencross = State.unevencross; - Track.m_unevenbranch = State.unevenbranch; - Track.m_unevenbranchup = State.unevenbranchup; - Track.m_unevenbranchside = State.unevenbranchside; - Track.m_jointdip = State.jointdip; Track.m_sleeper_enabled = State.sleepers; Track.m_sleeper_frequency = State.sleeper_frequency; Track.m_sleeper_model_name = State.sleeper_model; @@ -2362,11 +2348,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 << Style.uneven_text(); + 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 << ' '; @@ -2418,7 +2404,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); @@ -2679,7 +2665,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; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 61858336..14f9e9a4 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,14 +56,7 @@ class editor_track float texslope{0.9f}; std::vector isolated; float overhead{-1.f}; - float uneven{0.f}; - float unevenup{-1.f}; - float unevenside{-1.f}; - float unevencross{-1.f}; - float unevenbranch{-1.f}; - float unevenbranchup{-1.f}; - float unevenbranchside{-1.f}; - float jointdip{-1.f}; + track_uneven::attribute uneven; bool sleepers{false}; float sleeper_frequency{0.6f}; std::string sleeper_model; diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index 22970fa6..247f9cbf 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -165,6 +165,35 @@ TEST_CASE("uneven track: all the values in one attribute") 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: 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})}; diff --git a/world/Track.cpp b/world/Track.cpp index 8359f78a..31b5f5c5 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -642,10 +642,10 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) } } else if( str == "uneven" ) { - // uneven [;...]: see read_uneven and TrackUneven.h. The semicolons stay in the token + // 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 ) { - read_uneven( values ); + m_uneven = track_uneven::read_attribute( values, eType == tt_Switch ); } } else if( str == "friction" ) { @@ -706,49 +706,9 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) // to a stale origin and the renderer would draw sleepers in the wrong place). } -void TTrack::read_uneven( std::string const &Text ) { - - auto const fields { track_uneven::read_fields<6>( Text ) }; - m_uneven = std::max( fields[ 0 ], 0.f ); - if( eType == tt_Switch ) { - m_unevenbranch = fields[ 1 ]; - m_unevenup = fields[ 2 ]; - m_unevenside = fields[ 3 ]; - m_unevenbranchup = fields[ 4 ]; - m_unevenbranchside = fields[ 5 ]; - } - else { - m_unevenup = fields[ 1 ]; - m_unevenside = fields[ 2 ]; - m_unevencross = fields[ 3 ]; - m_jointdip = fields[ 4 ]; - } -} - -std::string TTrack::uneven_text() const { - - auto const fields { eType == tt_Switch ? - std::array { m_uneven, m_unevenbranch, m_unevenup, m_unevenside, m_unevenbranchup, m_unevenbranchside } : - std::array { m_uneven, m_unevenup, m_unevenside, m_unevencross, m_jointdip, -1.f } }; - if( m_uneven <= 0.f && std::all_of( fields.begin() + 1, fields.end(), []( float const Field ) { return Field < 0.f; } ) ) { - return {}; - } - return "uneven " + track_uneven::write_fields( fields ) + ' '; -} - track_uneven::amounts TTrack::uneven_declared( int const Path ) const { - track_uneven::amounts result { - m_unevenup >= 0.f ? m_unevenup : m_uneven, - m_unevenside >= 0.f ? m_unevenside : m_uneven, - eType != tt_Normal ? 0.f : m_unevencross >= 0.f ? m_unevencross : m_uneven }; - if( Path == 1 ) { - auto const branch = [ this ]( float const Part, double const Straight ) -> double { - return Part >= 0.f ? Part : m_unevenbranch >= 0.f ? m_unevenbranch : Straight; }; - result.up = branch( m_unevenbranchup, result.up ); - result.side = branch( m_unevenbranchside, result.side ); - } - return result; + return track_uneven::declared( m_uneven, eType == tt_Normal, Path ); } bool TTrack::is_uneven() const { @@ -873,7 +833,7 @@ double TTrack::joint_depth() const { if( eType != tt_Normal ) { return 0.0; } - return ( m_jointdip >= 0.f ? m_jointdip : track_uneven::dip_share * uneven_declared().up ); + return ( m_uneven.dip >= 0.f ? m_uneven.dip : track_uneven::dip_share * uneven_declared().up ); } double TTrack::joint_dip( double const Distance ) const { @@ -2782,7 +2742,7 @@ TTrack::export_as_text_( std::ostream &Output ) const { && false == m_profile1.first.empty() ) { Output << "railprofile " << m_profile1.first << ' '; } - Output << uneven_text(); + Output << track_uneven::write_attribute( m_uneven, eType == tt_Switch ); if( false == m_friction.first.empty() ) { Output << "friction " << m_friction.first << ' '; } diff --git a/world/Track.h b/world/Track.h index 1d366ebf..7d07f434 100644 --- a/world/Track.h +++ b/world/Track.h @@ -239,16 +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) - float m_uneven { 0.f }; // mm, see TrackUneven.h; plain tracks and switches - // the components set apart from m_uneven, mm; negative: m_uneven. Cross level on plain tracks only - float m_unevenup { -1.f }; - float m_unevenside { -1.f }; - float m_unevencross { -1.f }; - // the diverging path of a switch, mm; negative: as the straight one - float m_unevenbranch { -1.f }; - float m_unevenbranchup { -1.f }; - float m_unevenbranchside { -1.f }; - float m_jointdip { -1.f }; // depth of the dip at rail joints, mm; negative: from m_uneven; plain tracks only + 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: @@ -350,10 +341,6 @@ public: void VelocitySet(float v); double VelocityGet(); float Friction() const; - // the attribute of the entry. A plain track: the value;up and down;sideways;cross level;dip at the joints. - // A switch: path p1-p2;path p3-p4;up and down of the first;sideways of the first;up and down of the second;sideways of the second - void read_uneven( std::string const &Text ); - std::string uneven_text() 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; diff --git a/world/TrackUneven.h b/world/TrackUneven.h index f4a50eb7..b46ebf7a 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -87,6 +87,68 @@ std::string write_fields( std::array const &Fields ) { 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 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 ) { + + amounts result { + Values.up >= 0.f ? Values.up : Values.value, + Values.side >= 0.f ? Values.side : Values.value, + false == Plain ? 0.f : Values.cross >= 0.f ? Values.cross : Values.value }; + if( Path == 1 ) { + auto const branch = [ &Values ]( float const Part, double const Straight ) -> double { + return Part >= 0.f ? Part : Values.branch >= 0.f ? Values.branch : Straight; }; + result.up = branch( Values.branchup, result.up ); + result.side = branch( Values.branchside, result.side ); + } + return result; +} + template using wave_set = std::array; From 371de52342a501ceca7df43ddf2abfde06301cd3 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 19:02:55 +0100 Subject: [PATCH 11/18] fix(track): what the review of unevenness found in the engine - A path of no length gave a joint with a vector across of NaN to the path joined to it, on any scenery, with unevenness or not. It now has no vector across, and so makes no joint. - Axles no longer stray along the track. It moved vehicles against each other, changing the distance their couplers go by. - The bend under an axle goes straight on past the ends of a path: the polynomial ran off on a short curved one, with a kick in the cab shake. - A track left out of its rail run, where tracks are joined one way only, no longer walks the whole run again on every call. - The clatter of an axle goes by the outer axle on its side of the vehicle, and not always by the same one, as the two can be on different tracks. - A vertex works out the amounts once, and the foot of the ballast not at all. --- tests/unit/test_track_uneven.cpp | 33 +++++++++++++++++++++++ vehicle/DynObj.cpp | 6 +++-- world/Segment.cpp | 45 +++++++++++++++++++++----------- world/Segment.h | 5 +++- world/Track.cpp | 16 +++++++----- world/TrackUneven.h | 25 ++++++++++++++++++ 6 files changed, 105 insertions(+), 25 deletions(-) diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index 247f9cbf..fd95cec9 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -194,6 +194,39 @@ TEST_CASE("uneven track: the attribute goes by the entry it's written into") CHECK(track_uneven::write_attribute(track_uneven::read_attribute("0;;;;6", false), false) == "uneven 0;;;;6 "); } +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})}; diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index af882828..3f633ccc 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3662,11 +3662,13 @@ bool TDynamicObject::Update(double dt, double dt1) ctOwner->Vehicle()->DirectionGet() == DirectionGet() ? 1 : -1 ) }; for( auto &axle : m_axlesounds ) { // on a track with dipping joints the clatter comes from them, elsewhere the axle counts a rail off itself - auto const railphase { Axle0.RailPhase( axle.offset - 0.5 * fAxleDist ) }; + // by the outer axle on its side of the vehicle: the two can be on different tracks + auto const &outer { axle.offset >= 0.0 ? Axle0 : Axle1 }; + auto const railphase { outer.RailPhase( axle.offset - std::copysign( 0.5 * fAxleDist, axle.offset ) ) }; auto crossed { false }; if( railphase >= 0.0 ) { // the rails under the axle, which aren't always those under the track the vehicle is on - crossed = axle.atjoints && std::abs( railphase - axle.distance ) > 0.5 * Axle0.GetTrack()->fSoundDistance; + crossed = axle.atjoints && std::abs( railphase - axle.distance ) > 0.5 * outer.GetTrack()->fSoundDistance; axle.distance = railphase; } else { diff --git a/world/Segment.cpp b/world/Segment.cpp index dd236c61..9c0e1077 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -385,13 +385,12 @@ void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm:: 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 += stray( position, fDistance ); + position += track_uneven::across_only( stray( position, amount, fDistance ), across ); } -glm::dvec3 TSegment::stray( glm::dvec3 const &Point, double const Distance ) const { - auto const amount { pOwner->uneven_at( Distance, m_path ) }; +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 }; + 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; @@ -401,13 +400,25 @@ glm::dvec3 TSegment::uneven_bend( double const Distance ) const { if( false == m_uneven ) { return glm::dvec3 { 0.0 }; } - // over about a bogie; FastGetTFromS extrapolates past the ends of the path + // over about a bogie auto const reach { 1.5 }; auto const strayed = [&]( double const At ) { - return stray( FastGetPoint( FastGetTFromS( At ) ), At ); }; + return stray( point_extended( At ), pOwner->uneven_at( At, m_path ), At ); }; return ( strayed( Distance + reach ) - 2.0 * strayed( Distance ) + strayed( Distance - reach ) ) / ( reach * reach ); } +glm::dvec3 TSegment::point_extended( double const Distance ) const { + auto const on { std::clamp( Distance, 0.0, fLength ) }; + auto const t { FastGetTFromS( on ) }; + auto const point { FastGetPoint( t ) }; + if( on == Distance ) { + return point; + } + auto const direction { FastGetDirection( t, 0.1 / fLength ) }; + auto const size { glm::length( direction ) }; + return size > 0.0 ? point + direction * ( ( Distance - on ) / size ) : point; +} + 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_uneven ) { return; @@ -416,8 +427,12 @@ void TSegment::settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origi 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 ) }; - Vertex.position.y -= static_cast( share * track_uneven::stray_per_mm * pOwner->uneven_at( Distance, m_path ).cross * track_uneven::level( Origin + glm::dvec3 { Position }, glm::dvec3 { Parallel } ) * across / gauge ); - Vertex.position += glm::vec3 { share * stray( Origin + glm::dvec3 { Vertex.position }, Distance ) }; + if( share <= 0.0 ) { + return; + } + auto const amount { pOwner->uneven_at( Distance, m_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::design_position(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const { @@ -446,15 +461,15 @@ void TSegment::design_position(double const fDistance, glm::dvec3 &position, glm }; 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 ); - auto const dir { FastGetDirection( t, 0.1 / fLength ) }; - Across = glm::dvec3{ -dir.z, 0.0, dir.x }; - if( glm::length2( Across ) == 0.0 ) { - auto const chord { FastGetPoint_1() - FastGetPoint_0() }; - Across = glm::dvec3{ -chord.z, 0.0, chord.x }; - } - Across = glm::normalize( Across ); + Across = track_uneven::across_of( FastGetDirection( t, 0.1 / fLength ), FastGetPoint_1() - FastGetPoint_0() ); } glm::dvec3 TSegment::FastGetPoint(double const t) const diff --git a/world/Segment.h b/world/Segment.h index 77e61f90..67e4ed05 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Classes.h" #include "rendering/geometrybank.h" +#include "world/TrackUneven.h" #include struct map_colored_paths { @@ -77,7 +78,9 @@ class TSegment // 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, double const Distance ) const; + glm::dvec3 stray( glm::dvec3 const &Point, track_uneven::amounts const &Amount, double const Distance ) const; + // a point of the path at a distance, past its ends straight on: the polynomial would run off on a short curved path + glm::dvec3 point_extended( double const Distance ) const; void design_position( double const fDistance, glm::dvec3 &position, glm::vec3 &rotation ) const; public: diff --git a/world/Track.cpp b/world/Track.cpp index 31b5f5c5..bdb1cfe9 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -908,6 +908,10 @@ void TTrack::settle_rail_run() const { 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::update_location() { @@ -1636,13 +1640,11 @@ void TTrack::share_joints() { closest = gap; joint.emplace( 0.5 * ( point + theirpoint ), glm::normalize( across + theiracross ) ); if( iCategoryFlag & 1 ) { - // the cant of a plain track is what was designed, a switch takes it; alike ones meet halfway - auto const own { segment->design_roll( end ) }; - auto const their { theirend == end ? -theirs->design_roll( theirend ) : theirs->design_roll( theirend ) }; - roll = ( - ( eType == tt_Switch ) == ( neighbour->eType == tt_Switch ) ? 0.5f * ( own + their ) : - eType == tt_Switch ? their : - own ); + roll = track_uneven::joint_roll( + segment->design_roll( end ), + theirend == end ? -theirs->design_roll( theirend ) : theirs->design_roll( theirend ), + eType == tt_Switch, + neighbour->eType == tt_Switch ); } } } diff --git a/world/TrackUneven.h b/world/TrackUneven.h index b46ebf7a..dbec7a24 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -224,6 +224,31 @@ inline double bed_share( double const Depth, double const Rail, double const Hei 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 ) { + + return Switch == Theirswitch ? 0.5f * ( Own + Their ) : Switch ? Their : Own; +} + // distance from the joint behind the point inline double rail_phase( double const Along, double const Rail ) { From 2b8d7d7cf3ab2b2df8fbe3e9c238d6cd96c83869 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 19:05:56 +0100 Subject: [PATCH 12/18] refactor(track): what goes with unevenness in one place, readable The rules stay in TrackUneven.h with no dependence on the engine, as they can be tested. Their use on the tracks moves out of Track.cpp to TrackUneven.cpp, split into steps: the group of switches, the values of its paths, the meeting of two ends. One set of helpers for the paths of a track takes the place of the same conditions written out in several places, and the value at the ends of a switch is worked out in one place. The clatter of an axle leaves the long Update of a vehicle. The fields in the editor say themselves when they show. No change in behaviour. --- CMakeLists.txt | 1 + application/editormodetrack.cpp | 13 +- tests/unit/test_track_uneven.cpp | 13 ++ vehicle/DynObj.cpp | 42 ++-- vehicle/DynObj.h | 2 + world/Segment.h | 3 + world/Track.cpp | 272 ------------------------- world/Track.h | 9 + world/TrackUneven.cpp | 336 +++++++++++++++++++++++++++++++ world/TrackUneven.h | 9 + 10 files changed, 405 insertions(+), 295 deletions(-) create mode 100644 world/TrackUneven.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 477f584e..eba44c4c 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/editormodetrack.cpp b/application/editormodetrack.cpp index 4e313bfd..49059c21 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1170,12 +1170,17 @@ void editor_mode::render_path_parameters(TTrack &Track) { char const *label; float *value; + bool shown; }; - for (auto const &part : {component{"Up and down (mm)", &track->m_uneven.up}, component{"Sideways (mm)", &track->m_uneven.side}, component{"Cross level (mm)", &track->m_uneven.cross}, 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}}) + auto const plain{track->eType == tt_Normal}; + for (auto const &part : {component{"Up and down (mm)", &track->m_uneven.up, true}, + component{"Sideways (mm)", &track->m_uneven.side, true}, + component{"Cross level (mm)", &track->m_uneven.cross, plain}, + component{"Diverging path (mm)", &track->m_uneven.branch, !plain}, + component{"Diverging, up and down (mm)", &track->m_uneven.branchup, !plain}, + component{"Diverging, sideways (mm)", &track->m_uneven.branchside, !plain}}) { - if (part.value == &track->m_uneven.cross && track->eType != tt_Normal) - continue; - if ((part.value == &track->m_uneven.branch || part.value == &track->m_uneven.branchup || part.value == &track->m_uneven.branchside) && track->eType != tt_Switch) + if (false == part.shown) continue; if (ImGui::InputFloat(STR_C(part.label), part.value, 0.0f, 0.0f, "%.1f")) *part.value = *part.value < 0.f ? -1.f : std::min(*part.value, track_uneven::most); diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index fd95cec9..65b7da6e 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -194,6 +194,19 @@ TEST_CASE("uneven track: the attribute goes by the entry it's written into") 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}; diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 3f633ccc..ac87f395 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3661,25 +3661,7 @@ bool TDynamicObject::Update(double dt, double dt1) 1 : ctOwner->Vehicle()->DirectionGet() == DirectionGet() ? 1 : -1 ) }; for( auto &axle : m_axlesounds ) { - // on a track with dipping joints the clatter comes from them, elsewhere the axle counts a rail off itself - // by the outer axle on its side of the vehicle: the two can be on different tracks - auto const &outer { axle.offset >= 0.0 ? Axle0 : Axle1 }; - auto const railphase { outer.RailPhase( axle.offset - std::copysign( 0.5 * fAxleDist, axle.offset ) ) }; - auto crossed { false }; - if( railphase >= 0.0 ) { - // the rails under the axle, which aren't always those under the track the vehicle is on - crossed = axle.atjoints && std::abs( railphase - axle.distance ) > 0.5 * outer.GetTrack()->fSoundDistance; - axle.distance = railphase; - } - else { - axle.distance += dDOMoveLen * directioninconsist; - crossed = axle.distance < 0 || axle.distance > dRailLength; - if( crossed ) { - axle.distance = clamp_circular( axle.distance, dRailLength ); - } - } - axle.atjoints = ( railphase >= 0.0 ); - if( crossed ) { + if( axle_passed_joint( axle, dDOMoveLen, directioninconsist ) ) { 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; @@ -8089,6 +8071,28 @@ TDynamicObject::update_shake( double const Timedelta ) { } } +bool +TDynamicObject::axle_passed_joint( axle_sounds &Axle, double const Move, int const Direction ) { + + // by the outer axle on its side of the vehicle: the two can be on different tracks + auto const &outer { Axle.offset >= 0.0 ? Axle0 : Axle1 }; + auto const railphase { outer.RailPhase( Axle.offset - std::copysign( 0.5 * fAxleDist, Axle.offset ) ) }; + if( 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 * outer.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 1bc50b1d..b903f087 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -404,6 +404,8 @@ private: 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 ); struct powertrain_sounds { sound_source inverter { sound_placement::engine }; diff --git a/world/Segment.h b/world/Segment.h index 67e4ed05..75e07015 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -127,6 +127,9 @@ public: set_uneven( bool const Uneven, int const Path = 0 ) { m_uneven = Uneven; m_path = Path; } + glm::dvec3 + end_point( int const End ) const { + return End == 0 ? Point1 : Point2; } // 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; diff --git a/world/Track.cpp b/world/Track.cpp index bdb1cfe9..49138b4e 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -49,9 +49,6 @@ TTrack::profiles_array TTrack::m_profiles; TTrack::profiles_map TTrack::m_profilesmap; int TTrack::s_layoutstamp { 0 }; -// how near an end of a path counts as being at a point, m -static double const switch_end_reach { 0.3 }; - TSwitchExtension::TSwitchExtension(TTrack *owner, int const what) { // na początku wszystko puste pNexts[0] = nullptr; // wskaźniki do kolejnych odcinków ruchu @@ -706,214 +703,6 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) // to a stale origin and the renderer would draw sleepers in the wrong place). } -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; -} - -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 ) const { - - auto const along = [ Distance ]( track_uneven::amounts const &Own, track_uneven::amounts const &Start, track_uneven::amounts const &End, double const Length ) -> track_uneven::amounts { - return { - track_uneven::along( Own.up, Start.up, End.up, Distance, Length ), - track_uneven::along( Own.side, Start.side, End.side, Distance, Length ), - track_uneven::along( Own.cross, Start.cross, End.cross, Distance, Length ) }; }; - if( eType == tt_Switch ) { - auto const &stray { switch_uneven() }; - return along( stray.paths[ Path ], stray.ends[ Path ][ 0 ], stray.ends[ Path ][ 1 ], SwitchExtension->Segments[ Path ]->GetLength() ); - } - auto const own { uneven_declared() }; - auto const atend = [&]( TTrack const *Other, glm::dvec3 const &Point ) -> track_uneven::amounts { - if( Other == nullptr ) { - return {}; - } - if( Other->eType == tt_Switch ) { - return Other->switch_uneven_at( Point ); - } - return track_uneven::least( own, Other->uneven_declared() ); }; - return along( own, atend( trPrev, Segment->FastGetPoint_0() ), atend( trNext, Segment->FastGetPoint_1() ), Length() ); -} - -TTrack::switch_stray const &TTrack::switch_uneven() const { - - if( m_switchstray.stamp == s_layoutstamp ) { - return m_switchstray; - } - std::vector group { this }; - std::array least { uneven_declared( 0 ), uneven_declared( 1 ) }; - for( std::size_t index = 0; index < group.size(); ++index ) { - auto const &extension { *group[ index ]->SwitchExtension }; - for( auto const *other : { extension.pPrevs[ 0 ], extension.pPrevs[ 1 ], extension.pNexts[ 0 ], extension.pNexts[ 1 ] } ) { - if( other == nullptr ) { - continue; - } - if( other->eType == tt_Switch && other->SwitchExtension ) { - least[ 0 ] = track_uneven::least( least[ 0 ], other->uneven_declared( 0 ) ); - least[ 1 ] = track_uneven::least( least[ 1 ], other->uneven_declared( 1 ) ); - if( std::find( group.begin(), group.end(), other ) == group.end() ) { - group.push_back( other ); - } - } - else { - // a track with none of a component keeps to its design in it, any other comes to the value of the switch - auto const theirs { other->uneven_declared() }; - for( auto &path : least ) { - if( theirs.up <= 0.0 ) { path.up = 0.0; } - if( theirs.side <= 0.0 ) { path.side = 0.0; } - } - } - } - } - for( auto const *track : group ) { - track->m_switchstray.paths = least; - track->m_switchstray.stamp = s_layoutstamp; - } - // the lowest of the paths of a switch ending at a point - auto const paths_at = []( TTrack const &Track, glm::dvec3 const &Point ) { - auto result { track_uneven::least( Track.m_switchstray.paths[ 0 ], Track.m_switchstray.paths[ 1 ] ) }; - auto found { false }; - track_uneven::amounts ending { track_uneven::most, track_uneven::most, track_uneven::most }; - for( int path = 0; path < 2; ++path ) { - auto const &segment { *Track.SwitchExtension->Segments[ path ] }; - if( glm::length( segment.FastGetPoint_0() - Point ) < switch_end_reach || glm::length( segment.FastGetPoint_1() - Point ) < switch_end_reach ) { - ending = track_uneven::least( ending, Track.m_switchstray.paths[ path ] ); - found = true; - } - } - return found ? ending : result; }; - for( auto const *track : group ) { - auto const &extension { *track->SwitchExtension }; - for( int path = 0; path < 2; ++path ) { - auto const &segment { *extension.Segments[ path ] }; - for( int end = 0; end < 2; ++end ) { - auto const point { end == 0 ? segment.FastGetPoint_0() : segment.FastGetPoint_1() }; - auto value { paths_at( *track, point ) }; - auto const *other { end == 0 ? extension.pPrevs[ path ] : extension.pNexts[ path ] }; - if( other != nullptr && other->eType == tt_Switch && other->SwitchExtension ) { - value = track_uneven::least( value, paths_at( *other, point ) ); - } - track->m_switchstray.ends[ path ][ end ] = value; - } - } - } - return m_switchstray; -} - -track_uneven::amounts TTrack::switch_uneven_at( glm::dvec3 const &Point ) const { - - auto const &stray { switch_uneven() }; - auto found { false }; - track_uneven::amounts result { track_uneven::most, track_uneven::most, track_uneven::most }; - for( int path = 0; path < 2; ++path ) { - auto const &segment { *SwitchExtension->Segments[ path ] }; - for( int end = 0; end < 2; ++end ) { - if( glm::length( ( end == 0 ? segment.FastGetPoint_0() : segment.FastGetPoint_1() ) - Point ) < switch_end_reach ) { - result = track_uneven::least( result, stray.ends[ path ][ end ] ); - found = true; - } - } - } - return found ? result : track_uneven::least( stray.paths[ 0 ], stray.paths[ 1 ] ); -} - -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 ) const { - - 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 = [&]( 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 ) const { - - 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 ); -} - -void TTrack::settle_rail_run() const { - - // 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 const * { - auto const *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 = [&]( int &End ) { - auto const *track { this }; - int joined { 0 }; - for( int step = 0; step < 1000000; ++step ) { - auto const *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 == 0 ? Track.Segment->FastGetPoint_0() : Track.Segment->FastGetPoint_1() }; - 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 const *track { walk( end ) }; - auto const *other { walk( otherend ) }; - if( 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::update_location() { location( ( @@ -1600,67 +1389,6 @@ void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { create_geometry( Bank ); } -void TTrack::share_joints() { - - int const paths { eType == tt_Switch ? 2 : 1 }; - auto const segment_of = []( TTrack const &Track, int const Path ) -> TSegment * { - return ( Track.eType == tt_Switch ? Track.SwitchExtension->Segments[ Path ].get() : Track.Segment.get() ); }; - for( int path = 0; path < paths; ++path ) { - auto *segment { segment_of( *this, path ) }; - if( segment == nullptr ) { - continue; - } - for( int end = 0; end < 2; ++end ) { - std::optional> joint; - auto roll { segment->design_roll( end ) }; - auto const *neighbour { eType == tt_Switch ? - ( end ? SwitchExtension->pNexts[ path ] : SwitchExtension->pPrevs[ path ] ) : - ( end ? trNext : trPrev ) }; - if( neighbour != nullptr && neighbour != this && ( neighbour->iCategoryFlag & 15 ) == ( iCategoryFlag & 15 ) ) { - glm::dvec3 point; - glm::dvec3 across; - segment->plan_frame( end ? segment->GetLength() : 0.0, point, across ); - auto closest { 0.2 }; - for( int theirpath = 0; theirpath < ( neighbour->eType == tt_Switch ? 2 : 1 ); ++theirpath ) { - auto const *theirs { segment_of( *neighbour, theirpath ) }; - if( theirs == nullptr ) { - continue; - } - for( int theirend = 0; theirend < 2; ++theirend ) { - glm::dvec3 theirpoint; - glm::dvec3 theiracross; - theirs->plan_frame( theirend ? theirs->GetLength() : 0.0, theirpoint, theiracross ); - if( theirend == end ) { - theiracross = -theiracross; - } - auto const gap { glm::length( theirpoint - point ) }; - if( gap >= closest || glm::dot( across, theiracross ) < 0.9 ) { - continue; - } - closest = gap; - joint.emplace( 0.5 * ( point + theirpoint ), glm::normalize( across + theiracross ) ); - if( iCategoryFlag & 1 ) { - roll = track_uneven::joint_roll( - segment->design_roll( end ), - theirend == end ? -theirs->design_roll( theirend ) : theirs->design_roll( theirend ), - eType == tt_Switch, - neighbour->eType == tt_Switch ); - } - } - } - } - segment->set_joint( end, joint, roll ); - } - } - if( eType != tt_Switch && Segment && ( iCategoryFlag & 1 ) ) { - float roll1, roll2; - Segment->GetRolls( roll1, roll2 ); - if( roll1 != 0.f || roll2 != 0.f ) { - iTrapezoid |= 1; - } - } -} - void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { gfx::userdata_array empty_userdata; share_joints(); diff --git a/world/Track.h b/world/Track.h index 7d07f434..f5bb8c1b 100644 --- a/world/Track.h +++ b/world/Track.h @@ -341,6 +341,10 @@ 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; @@ -402,6 +406,11 @@ private: }; mutable switch_stray m_switchstray; switch_stray const &switch_uneven() const; + void settle_switch_stray() const; + // 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 const *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 ) const; static int s_layoutstamp; // bumped when tracks are joined or changed, invalidates the two above diff --git a/world/TrackUneven.cpp b/world/TrackUneven.cpp new file mode 100644 index 00000000..152449ad --- /dev/null +++ b/world/TrackUneven.cpp @@ -0,0 +1,336 @@ +/* +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" + +// 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; +} + +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 const &Switch ) { + + std::vector group { &Switch }; + for( std::size_t index = 0; index < group.size(); ++index ) { + for( auto const *other : joined_to_switch( *group[ index ] ) ) { + if( is_switch( other ) && std::find( group.begin(), group.end(), other ) == group.end() ) { + group.push_back( other ); + } + } + } + return group; +} + +// 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::vector 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 || is_switch( other ) ) { + continue; + } + // any other comes to the value of the switch + auto const theirs { other->uneven_declared() }; + for( auto &path : values ) { + if( theirs.up <= 0.0 ) { path.up = 0.0; } + if( theirs.side <= 0.0 ) { path.side = 0.0; } + } + } + } + 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( Switch.path_segment( path )->end_point( 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; +} + +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 ) const { + + 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, Segment->end_point( 0 ) ), uneven_met( trNext, Segment->end_point( 1 ) ), Distance, Length() ); +} + +track_uneven::amounts TTrack::uneven_met( TTrack const *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() const { + + if( m_switchstray.stamp != s_layoutstamp ) { + settle_switch_stray(); + } + return m_switchstray; +} + +void TTrack::settle_switch_stray() const { + + auto const group { switch_group( *this ) }; + auto const values { group_path_values( group ) }; + for( auto const *track : group ) { + track->m_switchstray.paths = values; + track->m_switchstray.stamp = s_layoutstamp; + } + // at the ends of the paths, with the paths known in all of the group + for( auto const *track : group ) { + for( int path = 0; path < 2; ++path ) { + for( int end = 0; end < 2; ++end ) { + auto const point { track->path_segment( path )->end_point( end ) }; + auto value { track->switch_paths_at( point ) }; + auto const *other { track->path_neighbour( path, end ) }; + if( is_switch( other ) ) { + value = track_uneven::least( value, other->switch_paths_at( point ) ); + } + track->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 ] ), [ & ]( int const Path, int ) { return paths[ Path ]; } ); +} + +track_uneven::amounts TTrack::switch_uneven_at( glm::dvec3 const &Point ) const { + + auto const &stray { switch_uneven() }; + return least_at_ends( *this, Point, track_uneven::least( stray.paths[ 0 ], stray.paths[ 1 ] ), [ & ]( 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 ) const { + + 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 = [&]( 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 ) const { + + 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 ); +} + +void TTrack::settle_rail_run() const { + + // 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 const * { + auto const *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 = [&]( int &End ) { + auto const *track { this }; + int joined { 0 }; + for( int step = 0; step < 1000000; ++step ) { + auto const *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 { Track.Segment->end_point( 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 const *track { walk( end ) }; + auto const *other { walk( otherend ) }; + if( 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, roll2; + Segment->GetRolls( roll1, roll2 ); + if( roll1 != 0.f || roll2 != 0.f ) { + iTrapezoid |= 1; + } + } +} diff --git a/world/TrackUneven.h b/world/TrackUneven.h index dbec7a24..624e6db1 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -249,6 +249,15 @@ inline float joint_roll( float const Own, float const Their, bool const Switch, return Switch == Theirswitch ? 0.5f * ( Own + Their ) : 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 ) { From 702ab5422ef658ea600541ea8aeb72cd3d08a2f5 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 19:07:31 +0100 Subject: [PATCH 13/18] fix(tools): the grading tool reads files with grass in their name A plain file like baltyk/mod/grass.scm was taken for a plant and never read, which lost about a quarter of the plants on Baltyk, so overgrown tracks came out better than they are. The grading is also split into steps. --- tools/track_uneven_grade.py | 151 +++++++++++++++++++++++------------- 1 file changed, 95 insertions(+), 56 deletions(-) diff --git a/tools/track_uneven_grade.py b/tools/track_uneven_grade.py index eb39d7c6..d643f94f 100644 --- a/tools/track_uneven_grade.py +++ b/tools/track_uneven_grade.py @@ -114,10 +114,10 @@ class Scenery: def include(self, name, params, origin): path = self.root / name.replace("\\", "/") - if GRASS.search(name.lower()): - numbers = [float(w) for w in params if NUMBER.match(w)] - if len(numbers) >= 3: - self.plant(numbers[0] + origin[0], numbers[2] + origin[2]) + # 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]) return if not path.is_file(): self.notes["brak pliku " + name] += 1 @@ -355,47 +355,58 @@ def ceiling(t, product): return product / t["speed"] * (0.7 + 0.3 * min(1.0, max(0.0, (t["state"] - 0.5) / 0.7))) -def grade(scenery, options): - tracks = scenery.tracks - joints = Joints(tracks) - for t in tracks: - if t["kind"] == "switch": - radii = [min(plan_radius(p["line"]), p["radius"] if p["radius"] > 0 else math.inf) for p in t["paths"]] - t["bent"] = 1 if len(radii) < 2 or radii[1] <= radii[0] else 0 - settle_speeds(tracks, joints) - route = None - if options.route: - first, last = options.route - from_first, from_last = distances(tracks, joints, first), distances(tracks, joints, last) - ends = [from_last[n] for n in from_last if math.dist(n, first) < 300] - route = (from_first, from_last, min(ends) if ends else math.dist(first, last)) +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) - normal = [t for t in tracks if t["kind"] == "normal"] - for t in tracks: + for t in scenery.tracks: part = t["paths"][0] - radius = min(plan_radius(part["line"]), part["radius"] if part["radius"] > 0 else math.inf) + radius = path_radius(part) curve = 0.0 if radius >= 6000 else min(1.0, max(0.0, (1200 - radius) / 950)) - density = grass_on(part, scenery.plants) if t["kind"] == "normal" else 0.0 - crossing = t["kind"] == "normal" and hits_road(part, cells) - progress = 0.5 - if route: - shares = [] - for key in ("p1", "p2"): - node = (round(part[key][0], 1), round(part[key][2], 1)) - a, b = route[0].get(node), route[1].get(node) - if a is None or b is None: - a, b = math.dist((part[key][0], part[key][2]), options.route[0]), math.dist((part[key][0], part[key][2]), options.route[1]) - shares.append(min(1.0, max(0.0, (a - b + route[2]) / (2 * route[2])))) - progress = 0.5 * sum(shares) + 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) * options.gain, curve=curve, crossing=crossing, grass=density) - if t["kind"] == "normal": + 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)) - for t in normal: - t["heights"] = heights_per_mm(t) + + +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: @@ -404,43 +415,51 @@ def grade(scenery, options): low = middle else: high = middle - product = low for t in normal: - up = ceiling(t, product) + 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 curve_changes(t, joints): + part = t["paths"][0] + if abs(end_curve(part, "p1") - end_curve(part, "p2")) > CURVE_STEP: + return True + for key in ("p1", "p2"): + 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(end_curve(part, key) - (-their if side == key else their)) > CURVE_STEP: + return True + return False + + +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: - part = t["paths"][0] - change = abs(end_curve(part, "p1") - end_curve(part, "p2")) > CURVE_STEP - for key in ("p1", "p2"): - 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(end_curve(part, key) - (-their if side == key else their)) > CURVE_STEP: - change = True - if not (change or WOOD.search(t["ballast"] or "")): + 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) >= options.near: + 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 - 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 - # each path of a switch by the plain tracks it leads to, the worst of them; those only among switches from their neighbours - switches = [t for t in tracks if t["kind"] == "switch"] + return chosen + + +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"] = [None] * len(t["paths"]) for index, part in enumerate(t["paths"]): @@ -459,6 +478,26 @@ def grade(scenery, options): t["values"][index] = max(found) 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 From 56d389e2df05a8a2e5010ff0f1244d565f8c035c Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 19:07:31 +0100 Subject: [PATCH 14/18] docs(track): the documentation of track unevenness in English With a warning where values get chosen: at the limits of Id-14 a pantograph may not keep up at speed, which trips the main breaker. --- track_uneven_doc.md | 118 ++++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 58 deletions(-) diff --git a/track_uneven_doc.md b/track_uneven_doc.md index 325e8944..c7446e34 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -1,6 +1,6 @@ -# Nierówności toru +# Track unevenness -Opcjonalny atrybut wpisu toru, przed `endtrack`: +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 @@ -14,92 +14,94 @@ uneven 18;;;25;6 endtrack ``` -Liczby w mm, 0-100, rozdzielone średnikiem, bez spacji: +The numbers are in mm, 0-100, separated by semicolons with no spaces: -| pozycja | tor `normal` | rozjazd `switch` | -|---------|--------------------------------------------------|-----------------------------| -| 1 | największa odchyłka toku na cięciwie 10 m | to samo, droga p1-p2 | -| 2 | w pionie | droga p3-p4 | -| 3 | w poziomie | droga p1-p2, w pionie | -| 4 | przechyłka | droga p1-p2, w poziomie | -| 5 | zapadnięcie na styku, domyślnie 0,4 pionu | droga p3-p4, w pionie | -| 6 | | droga p3-p4, w poziomie | +| 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 | -Pozycję pominiętą zastępuje pierwsza liczba, a zapadnięcie liczy się z pionu. Końcowe można opuścić. Sam bok to `uneven 0;;30`, a same zapadnięcia na stykach to `uneven 0;;;;6`. Rozjazd bez składowych na drodze p3-p4 przyjmuje wartości z drogi p1-p2. Rozjazd na 100 km/h na wprost i 40 km/h na bok to na przykład `uneven 14;35`. +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`. -Wszystkie są też w edytorze, w panelu toru, w sekcji „Ride and sound”. Na drogach, rzekach i obrotnicach są ignorowane. +All of them are also in the editor, in the track panel, section "Ride and sound". On roads, rivers and turntables they are ignored. -## Jaką wartość wybrać +## Choosing a value -`uneven` odpowiada kolumnie „nierówności pionowe” z instrukcji PLK Id-14, załącznik 1 (klasa C): +`uneven` corresponds to the "vertical irregularities" column of the PLK Id-14 instruction, appendix 1 (class C): -| prędkość [km/h] | 20 | 40 | 60 | 80 | 100 | 120 | 160 | -|-----------------|----|----|----|----|-----|-----|-----| -| `uneven` [mm] | 50 | 35 | 25 | 18 | 14 | 10 | 6 | +| speed [km/h] | 20 | 40 | 60 | 80 | 100 | 120 | 160 | +|---------------|----|----|----|----|-----|-----|-----| +| `uneven` [mm] | 50 | 35 | 25 | 18 | 14 | 10 | 6 | -Tor w dobrym stanie to według instrukcji połowa tej wartości (klasa A), w dostatecznym 3/4 (klasa B). +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). -Tor z `uneven 18` mierzy się tak (`tests/unit/test_track_uneven.cpp`), w nawiasie limit Id-14 dla 80 km/h: +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. -| parametr | wynik | Id-14 | -|---------------------------|---------|---------| -| nierówności pionowe | 18 mm | 18 mm | -| nierówności poziome | 19 mm | 17 mm | -| wichrowatość na bazie 5 m | 15 mm | 16 mm | -| szerokość toru | ±10 mm | +10/-8 mm | -| przechyłka | 20 mm | 20 mm | -| wskaźnik J | 6,1 | 5,3 | +A track with `uneven 18` measures as follows (`tests/unit/test_track_uneven.cpp`), against the Id-14 limit for 80 km/h: -## Jak to działa +| 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 | -Nierówność to pole przesunięć zależne od położenia na mapie, złożone z fal o długości 5-42 m. W pionie krótkich fal jest mniej, żeby częściej dochodziły blisko wartości szczytowej, a do tego są fale 47-83 m: na cięciwie 10 m prawie się nie liczą, pudło unosi się na nich i opada, a pantograf za nimi nadąża. Nie ma w nim losowania: to samo miejsce wygina się tak samo przy każdym uruchomieniu. Przesunięty tor dostaje fale z nowego miejsca. +## How it works -Każda składowa łączy się osobno: +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. -- **Tor z torem:** na styku obowiązuje mniejsza z dwóch wartości, a każdy tor dochodzi do niej płynnie na 20 m. Wolny koniec toru leży w projekcie. -- **Rozjazd:** każda droga ma swoją wartość, a na wspólnym końcu obu obowiązuje mniejsza z nich. Rozjazdy złączone bezpośrednio przyjmują najmniejszą z tej samej drogi, a na styku dwóch dróg mniejszą z obu. Dołączony tor dochodzi do wartości rozjazdu na końcu, do którego dochodzi, także większej od własnej. Tor bez danej składowej trzyma w niej cały rozjazd w projekcie. -- **Styki:** zapadnięcia są co `fSoundDistance`, na długości 6 m, i biegną dalej przez kolejne tory o tym samym odstępie. Stukot osi wypada w miejscu zapadnięcia. Przy odstępie 6 m lub mniejszym oraz na rozjazdach zapadnięć nie ma. -- **Przechyłka:** dolicza się do projektowej. Jest pochyleniem w planie mapy, więc tory wpisane ku sobie spotykają się na tej samej przechyłce. Na rozjazdach tylko ta wynikająca z fal, bez osobnego siadania toków. +Each component joins separately: -Końce złączonych dróg spotykają się w środku między sobą, z jednym kierunkiem poprzecznym, jeśli są bliżej niż 20 cm, co zamyka pęknięcia z niezgodnych współrzędnych w danych, także na torach bez nierówności. Przechyłkę na takim styku rozjazd bierze od zwykłego toru, a dwa tory albo dwa rozjazdy spotykają się w połowie. To dotyczy też fizyki. +- **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. -Za nierównością idą szyny, wierzch podsypki, `sleepermodel`, osie, przechył pudła i bujanie kabiny. Skarpa podsypki przejmuje różnicę, a jej spód zostaje w projekcie, na terenie. +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. -Nie idą za nią modele z includów (np. napędy i podkłady rozjazdów), teren ani sieć trakcyjna, więc przy dużych wartościach szyna rozjeżdża się z takim modelem o kilka centymetrów. +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. -Na torze z nierównością kabinę buja sam tor. Odpada kołysanie wyliczane z przyspieszenia poprzecznego, czyli z promienia łuku, oraz losowe szarpnięcia przy większej prędkości. Na torach bez nierówności działają jak dotąd. +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. -Pantograf podnosi się z szybkością ciśnienia, ok. 0,14 m/s, a szczelina do drutu większa niż 1 cm odcina napięcie i wybija wyłącznik szybki. Nierówność dokłada się do tego przy dużej prędkości. Na torach Bałtyku symulacja daje szczelinę 0,6 cm, gdy składowa w pionie razy prędkość w km/h wynosi 925, czyli np. 8 mm przy 120 km/h, 13 mm przy 70 km/h i 23 mm przy 40 km/h. Wartości z tabeli Id-14 są od tego większe. Sam drut też to potrafi: na Bałtyku przy `tor0290` wznosi się o ok. 1%, więc pantograf nadąża tam do ok. 46 km/h, a powyżej wyłącznik wybija także na prostym torze. +On a track with unevenness the track itself shakes the cab. The sway calculated from the lateral acceleration, that is from the curve radius, and the random jolts at higher speed are off. On tracks with no unevenness they work as before. -Fizyka bierze promień łuku i pochylenie z projektu, więc nierówności nie zmieniają tych wartości, a do wykolejeń dokładają tylko przechyłkę, ok. 0,06 m/s² przy `uneven 18` wobec progu ok. 3,2 m/s². +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. -## Ocena torów dowolnej scenerii +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². -`tools/track_uneven_grade.py` wylicza wartości z tego, co widać w scenerii: z tekstur podsypki i szyn, trawy na torze, łuków, prędkości i przejazdów drogowych. Czyta plik `.scn`, `.scm` albo `.inc` razem z jego includami, oryginałów nie zmienia, a kopie plików z torami zapisuje do `--out`. Bez `--out` tylko wypisuje raport. +## 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`: tory pogarszają się od pierwszego punktu do drugiego, licząc po torach. -- `--gain`: mnożnik stanu torów, domyślnie 1. -- `--near`: jaka część torów na drewnie i przy zmianach krzywizny idzie bliżej granicy Id-14, domyślnie 0,6. Wybór jest stały. -- `--straight`: nazwy torów bez nierówności, po przecinku. -- `--root`: folder `scenery`, gdy podany plik leży głębiej. +- `--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. -Wartości trzymają się pod sufitem pantografu, liczonym symulacją na tych torach, ale bez wznosu samego drutu. Zużycie tekstur odczytywane jest z ich nazw, według tabel na początku skryptu, a nieznana nazwa dostaje 0,5. Tory z plików `.inc` z parametrami są pomijane, a bloki `rotate` nieuwzględniane. W obu przypadkach raport wypisuje uwagę. Tor, który ma już nierówność, zostaje bez zmian. +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. -## Wydajność +## Performance -Tor z nierównością ma geometrię co 2 m zamiast co 10 m na prostych. Rozjazdy zachowują swój podział. +A track with unevenness has geometry every 2 m instead of every 10 m on straights. Switches keep their division. -Zmierzone (kamera w ruchu lub kabina, 1920×1080): +Measured (camera moving or in the cab, 1920×1080): -| scena | bez flag | z flagami 18/35 | -|---------------------------------------|--------------------|--------------------| -| Bałtyk, 56 km torów: wierzchołki | 0,56 mln (ok. 27 MB) | 2,17 mln (ok. 104 MB) | -| Bałtyk: najdłuższa klatka w przelocie | 31,5 ms | 64 ms | -| `td.scn`, jazda 120 km/h: klatka | 2,73 ms | 2,93 ms | +| 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 | -Koszt rośnie z liczbą oflagowanych torów, a najdłuższe klatki pojawiają się przy budowaniu geometrii kolejnych sekcji. Sama jazda po takim torze praktycznie nie kosztuje: ok. 0,3 µs na oś w kroku fizyki. +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. From 9364b03e1ad9d9711942864c4d5ab17a00f487d3 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 20:17:47 +0100 Subject: [PATCH 15/18] fix(track): what Sonar reports on the unevenness The blockers are in the EP brake of a vehicle, older than this work: its arrays made with new[] are vectors now, which also removes the deletes. - The caches of the unevenness of a track are no longer mutable: what fills them isn't const any more. - TSegment gets back to RaPositionGet in one piece, with what the path has to do with the others in one struct. - Less nesting in the group of switches and in the fields of the editor, nested conditionals written out, and the attribute is read from a string_view. A test no longer counts in floating point. No change in behaviour. --- application/editormodetrack.cpp | 33 ++++----- tests/unit/test_track_uneven.cpp | 33 ++++++--- vehicle/DynObj.cpp | 73 +++++++++---------- vehicle/DynObj.h | 2 +- world/Segment.cpp | 77 +++++++++----------- world/Segment.h | 29 ++++---- world/Track.h | 21 +++--- world/TrackUneven.cpp | 117 ++++++++++++++++++------------- world/TrackUneven.h | 45 +++++++----- 9 files changed, 223 insertions(+), 207 deletions(-) diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 49059c21..0ae732d8 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1166,26 +1166,23 @@ void editor_mode::render_path_parameters(TTrack &Track) 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); - struct component - { - char const *label; - float *value; - bool shown; - }; - auto const plain{track->eType == tt_Normal}; - for (auto const &part : {component{"Up and down (mm)", &track->m_uneven.up, true}, - component{"Sideways (mm)", &track->m_uneven.side, true}, - component{"Cross level (mm)", &track->m_uneven.cross, plain}, - component{"Diverging path (mm)", &track->m_uneven.branch, !plain}, - component{"Diverging, up and down (mm)", &track->m_uneven.branchup, !plain}, - component{"Diverging, sideways (mm)", &track->m_uneven.branchside, !plain}}) - { - if (false == part.shown) - continue; - if (ImGui::InputFloat(STR_C(part.label), part.value, 0.0f, 0.0f, "%.1f")) - *part.value = *part.value < 0.f ? -1.f : std::min(*part.value, track_uneven::most); + 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) diff --git a/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index 65b7da6e..06f4663b 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -2,6 +2,8 @@ #include "doctest.h" #include "world/TrackUneven.h" +#include +#include #include #include @@ -12,19 +14,20 @@ double deviation(std::vector const &Values) auto mean{0.0}; for (auto const value : Values) mean += value; - mean /= Values.size(); + 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 / Values.size()); + return std::sqrt(sum / count); } double peak(std::vector Values) { for (auto &value : Values) value = std::abs(value); - std::sort(Values.begin(), Values.end()); - return Values[static_cast(0.999 * Values.size())]; + std::ranges::sort(Values); + return Values[Values.size() * 999 / 1000]; } void add_chord(std::vector &Output, std::vector const &Line, int const Half) @@ -44,14 +47,22 @@ TEST_CASE("uneven track: measures as the Id-14 limits for its value") int const steps{4000}; int const base{10}; // 5 m int const lines{36}; - double vertical{0.0}, horizontal{0.0}, twist{0.0}, wander{0.0}, level{0.0}, j{0.0}, away{0.0}; + 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::vector up[2], side[2], middle; + 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)}; @@ -64,7 +75,11 @@ TEST_CASE("uneven track: measures as the Id-14 limits for its value") } middle.push_back(track_uneven::field(point).y * amplitude); } - std::vector verticals, horizontals, twists, gauges, levels; + 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); @@ -269,9 +284,9 @@ TEST_CASE("uneven track: the value along a track meets its neighbours exactly") 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 (auto distance{0.25}; distance <= length; distance += 0.25) + for (int step = 1; step <= 2000; ++step) { - auto const value{track_uneven::along(18.0, 35.0, 0.0, distance, length)}; + 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; } diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index ac87f395..305e1202 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3382,14 +3382,12 @@ bool TDynamicObject::Update(double dt, double dt1) 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 +3546,6 @@ bool TDynamicObject::Update(double dt, double dt1) } - delete[] PrzekrF; - delete[] FzED; - delete[] FzEP; - delete[] FmaxEP; - MED_oldFED = FzadED; } @@ -3661,33 +3654,32 @@ bool TDynamicObject::Update(double dt, double dt1) 1 : ctOwner->Vehicle()->DirectionGet() == DirectionGet() ? 1 : -1 ) }; for( auto &axle : m_axlesounds ) { - if( axle_passed_joint( axle, dDOMoveLen, directioninconsist ) ) { - 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; } @@ -8072,12 +8064,11 @@ TDynamicObject::update_shake( double const Timedelta ) { } bool -TDynamicObject::axle_passed_joint( axle_sounds &Axle, double const Move, int const Direction ) { +TDynamicObject::axle_passed_joint( axle_sounds &Axle, double const Move, int const Direction ) const { // by the outer axle on its side of the vehicle: the two can be on different tracks auto const &outer { Axle.offset >= 0.0 ? Axle0 : Axle1 }; - auto const railphase { outer.RailPhase( Axle.offset - std::copysign( 0.5 * fAxleDist, Axle.offset ) ) }; - if( railphase >= 0.0 ) { + if( auto const railphase { outer.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 * outer.GetTrack()->fSoundDistance }; Axle.distance = railphase; diff --git a/vehicle/DynObj.h b/vehicle/DynObj.h index b903f087..4fee9778 100644 --- a/vehicle/DynObj.h +++ b/vehicle/DynObj.h @@ -405,7 +405,7 @@ private: 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 ); + 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 9c0e1077..1aff4ae4 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -69,10 +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_designrolls[ 0 ] = fRoll1; - m_designrolls[ 1 ] = fRoll2; - m_joints[ 0 ].reset(); - m_joints[ 1 ].reset(); + m_setting.designrolls = { fRoll1, fRoll2 }; + m_setting.joints = {}; bCurve = bIsCurve; if (Global.bRollFix) { // Ra: poprawianie przechyłki @@ -122,7 +120,7 @@ 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_uneven && pOwner->eType != tt_Switch ) { + 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 ); } @@ -373,21 +371,6 @@ Math3D::vector3 TSegment::GetPoint(double const fDistance) const }; */ -void TSegment::RaPositionGet(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const { - design_position( fDistance, position, rotation ); - if( false == m_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_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 ); -} - 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 }; @@ -397,30 +380,25 @@ glm::dvec3 TSegment::stray( glm::dvec3 const &Point, track_uneven::amounts const } glm::dvec3 TSegment::uneven_bend( double const Distance ) const { - if( false == m_uneven ) { + if( false == m_setting.uneven ) { return glm::dvec3 { 0.0 }; } // over about a bogie auto const reach { 1.5 }; - auto const strayed = [&]( double const At ) { - return stray( point_extended( At ), pOwner->uneven_at( At, m_path ), At ); }; + 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 ); } -glm::dvec3 TSegment::point_extended( double const Distance ) const { - auto const on { std::clamp( Distance, 0.0, fLength ) }; - auto const t { FastGetTFromS( on ) }; - auto const point { FastGetPoint( t ) }; - if( on == Distance ) { - return point; - } - auto const direction { FastGetDirection( t, 0.1 / fLength ) }; - auto const size { glm::length( direction ) }; - return size > 0.0 ? point + direction * ( ( Distance - on ) / size ) : point; -} - 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_uneven ) { + 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 @@ -430,12 +408,12 @@ void TSegment::settle_vertex( gfx::basic_vertex &Vertex, glm::dvec3 const &Origi if( share <= 0.0 ) { return; } - auto const amount { pOwner->uneven_at( Distance, m_path ) }; + 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::design_position(double const fDistance, glm::dvec3 &position, glm::vec3 &rotation) const { +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 auto const t = FastGetTFromS(fDistance); // aproksymacja dystansu na krzywej Beziera na parametr (t) @@ -458,6 +436,17 @@ void TSegment::design_position(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 { @@ -523,9 +512,9 @@ 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_joints[ 0 ] ) { - pos1 = glm::vec3{ m_joints[ 0 ]->first - Origin }; - parallel1 = glm::vec3{ m_joints[ 0 ]->second }; + 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; @@ -572,9 +561,9 @@ 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_joints[ 1 ] ) { - pos2 = glm::vec3{ m_joints[ 1 ]->first - Origin }; - parallel2 = glm::vec3{ m_joints[ 1 ]->second }; + 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 diff --git a/world/Segment.h b/world/Segment.h index 75e07015..099480e3 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -9,7 +9,9 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include #include +#include #include "utilities/Classes.h" #include "rendering/geometrybank.h" @@ -50,11 +52,14 @@ class TSegment glm::dvec3 Point2; float fRoll1 { 0.f }; float fRoll2 { 0.f }; // przechyłka na końcach - float m_designrolls[ 2 ] { 0.f, 0.f }; // as entered; the two above can differ at a joint - bool m_uneven { false }; // see TrackUneven.h - int m_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::optional> m_joints[ 2 ]; + // 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; + } 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 @@ -79,9 +84,6 @@ class TSegment 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; - // a point of the path at a distance, past its ends straight on: the polynomial would run off on a short curved path - glm::dvec3 point_extended( double const Distance ) const; - void design_position( double const fDistance, glm::dvec3 &position, glm::vec3 &rotation ) const; public: bool bCurve = false; @@ -125,20 +127,17 @@ public: // call before Init, it sets the density of the geometry void set_uneven( bool const Uneven, int const Path = 0 ) { - m_uneven = Uneven; - m_path = Path; } - glm::dvec3 - end_point( int const End ) const { - return End == 0 ? Point1 : Point2; } + 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_designrolls[ End ]; } + return m_setting.designrolls[ End ]; } void set_joint( int const End, std::optional> const &Joint, float const Roll ) { - m_joints[ End ] = Joint; + 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 diff --git a/world/Track.h b/world/Track.h index f5bb8c1b..72cbd884 100644 --- a/world/Track.h +++ b/world/Track.h @@ -349,15 +349,15 @@ public: track_uneven::amounts uneven_declared( int const Path = 0 ) const; bool is_uneven() const; // 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 ) const; + 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 ) const; // m + 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 ) const; + double rail_phase( double const Distance ); void ConnectionsLog(); bool DoubleSlip() const; static void fetch_default_profiles(); @@ -395,8 +395,8 @@ private: double way { 1.0 }; // direction of the path along the run int stamp { -1 }; }; - void settle_rail_run() const; - mutable rail_run m_railrun; + void settle_rail_run(); + rail_run m_railrun; // 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 { @@ -404,15 +404,16 @@ private: std::array, 2> ends; int stamp { -1 }; }; - mutable switch_stray m_switchstray; - switch_stray const &switch_uneven() const; - void settle_switch_stray() const; + 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 const *Other, glm::dvec3 const &Point ) const; + track_uneven::amounts uneven_met( TTrack *Other, glm::dvec3 const &Point ); // the value of a switch where a track joined to it meets it - track_uneven::amounts switch_uneven_at( glm::dvec3 const &Point ) const; + 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 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 index 152449ad..78505ac3 100644 --- a/world/TrackUneven.cpp +++ b/world/TrackUneven.cpp @@ -24,42 +24,55 @@ bool is_switch( TTrack const *Track ) { return Track != nullptr && Track->eType == tt_Switch && Track->SwitchExtension; } -std::array joined_to_switch( TTrack const &Switch ) { +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 const &Switch ) { +std::vector switch_group( TTrack &Switch ) { - std::vector group { &Switch }; - for( std::size_t index = 0; index < group.size(); ++index ) { - for( auto const *other : joined_to_switch( *group[ index ] ) ) { - if( is_switch( other ) && std::find( group.begin(), group.end(), other ) == group.end() ) { + 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::vector const &Group ) { +std::array group_path_values( std::vector 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 || is_switch( other ) ) { - continue; - } - // any other comes to the value of the switch - auto const theirs { other->uneven_declared() }; - for( auto &path : values ) { - if( theirs.up <= 0.0 ) { path.up = 0.0; } - if( theirs.side <= 0.0 ) { path.side = 0.0; } + if( other != nullptr && false == is_switch( other ) ) { + keep_to_plain_track( values, *other ); } } } @@ -74,7 +87,7 @@ track_uneven::amounts least_at_ends( TTrack const &Switch, glm::dvec3 const &Poi 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( Switch.path_segment( path )->end_point( end ) - Point ) < switch_end_reach ) { + if( glm::length( end_point( *Switch.path_segment( path ), end ) - Point ) < switch_end_reach ) { result = track_uneven::least( result, Value( path, end ) ); found = true; } @@ -165,16 +178,16 @@ 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 ) const { +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, Segment->end_point( 0 ) ), uneven_met( trNext, Segment->end_point( 1 ) ), Distance, Length() ); + 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 const *Other, glm::dvec3 const &Point ) const { +track_uneven::amounts TTrack::uneven_met( TTrack *Other, glm::dvec3 const &Point ) { if( Other == nullptr ) { return {}; @@ -182,7 +195,7 @@ track_uneven::amounts TTrack::uneven_met( TTrack const *Other, glm::dvec3 const 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() const { +TTrack::switch_stray const &TTrack::switch_uneven() { if( m_switchstray.stamp != s_layoutstamp ) { settle_switch_stray(); @@ -190,26 +203,30 @@ TTrack::switch_stray const &TTrack::switch_uneven() const { return m_switchstray; } -void TTrack::settle_switch_stray() const { +void TTrack::settle_switch_stray() { auto const group { switch_group( *this ) }; auto const values { group_path_values( group ) }; - for( auto const *track : group ) { + for( auto *track : group ) { track->m_switchstray.paths = values; track->m_switchstray.stamp = s_layoutstamp; } - // at the ends of the paths, with the paths known in all of the group - for( auto const *track : group ) { - for( int path = 0; path < 2; ++path ) { - for( int end = 0; end < 2; ++end ) { - auto const point { track->path_segment( path )->end_point( end ) }; - auto value { track->switch_paths_at( point ) }; - auto const *other { track->path_neighbour( path, end ) }; - if( is_switch( other ) ) { - value = track_uneven::least( value, other->switch_paths_at( point ) ); - } - track->m_switchstray.ends[ path ][ end ] = value; + // 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; } } } @@ -217,13 +234,13 @@ void TTrack::settle_switch_stray() const { 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 ] ), [ & ]( int const Path, int ) { return paths[ Path ]; } ); + 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 ) const { +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 ] ), [ & ]( int const Path, int const End ) { return stray.ends[ Path ][ End ]; } ); + 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 { @@ -234,7 +251,7 @@ double TTrack::joint_depth() const { return ( m_uneven.dip >= 0.f ? m_uneven.dip : track_uneven::dip_share * uneven_declared().up ); } -double TTrack::joint_dip( double const Distance ) const { +double TTrack::joint_dip( double const Distance ) { auto const phase { rail_phase( Distance ) }; if( phase < 0.0 ) { @@ -245,12 +262,12 @@ double TTrack::joint_dip( double const Distance ) const { return 0.0; } auto const depth { joint_depth() }; - auto const atend = [&]( TTrack const *Other ) { + 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 ) const { +double TTrack::rail_phase( double const Distance ) { if( joint_depth() <= 0.0 || fSoundDistance <= 2.0 * track_uneven::dip_reach ) { return -1.0; @@ -261,22 +278,22 @@ double TTrack::rail_phase( double const Distance ) const { return track_uneven::rail_phase( m_railrun.origin + m_railrun.way * Distance, fSoundDistance ); } -void TTrack::settle_rail_run() const { +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 const * { - auto const *other { End == 0 ? Track.trPrev : Track.trNext }; + 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 = [&]( int &End ) { - auto const *track { this }; + auto const walk = [ this, &onward ]( int &End ) { + auto *track { this }; int joined { 0 }; for( int step = 0; step < 1000000; ++step ) { - auto const *next { onward( *track, End, joined ) }; + auto *next { onward( *track, End, joined ) }; if( next == nullptr || next == this ) { break; } @@ -285,15 +302,14 @@ void TTrack::settle_rail_run() const { } return track; }; auto const place = []( TTrack const &Track, int const End ) { - auto const point { Track.Segment->end_point( 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 const *track { walk( end ) }; - auto const *other { walk( otherend ) }; - if( place( *other, otherend ) < place( *track, end ) ) { + auto *track { walk( end ) }; + if( auto *other { walk( otherend ) }; place( *other, otherend ) < place( *track, end ) ) { track = other; end = otherend; } @@ -327,7 +343,8 @@ void TTrack::share_joints() { } } if( eType != tt_Switch && Segment && ( iCategoryFlag & 1 ) ) { - float roll1, roll2; + 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 index 624e6db1..2c32ad66 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include @@ -51,18 +52,20 @@ struct wave { // the numbers of an entry like "uneven 14;35", negative where left out template -std::array read_fields( std::string const &Text ) { +std::array read_fields( std::string_view const Text ) { std::array result; result.fill( -1.f ); - std::size_t start { 0 }; - for( std::size_t index = 0; index < Count && start < Text.size(); ++index ) { - auto const end { std::min( Text.find( ';', start ), Text.size() ) }; - auto value { 0.f }; - if( end > start && std::from_chars( Text.data() + start, Text.data() + end, value ).ec == std::errc {} ) { - result[ index ] = std::clamp( value, 0.f, most ); + 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 ); } - start = end + 1; + if( end == rest.size() ) { + break; + } + rest.remove_prefix( end + 1 ); } return result; } @@ -100,7 +103,7 @@ struct attribute { }; // 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 const &Text, bool const Switch ) { +inline attribute read_attribute( std::string_view const Text, bool const Switch ) { auto const fields { read_fields<6>( Text ) }; attribute result; @@ -136,15 +139,15 @@ inline std::string write_attribute( attribute const &Values, bool const Switch ) // 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 { - Values.up >= 0.f ? Values.up : Values.value, - Values.side >= 0.f ? Values.side : Values.value, - false == Plain ? 0.f : Values.cross >= 0.f ? Values.cross : Values.value }; + given( Values.up, Values.value ), + given( Values.side, Values.value ), + Plain ? given( Values.cross, Values.value ) : 0.0 }; if( Path == 1 ) { - auto const branch = [ &Values ]( float const Part, double const Straight ) -> double { - return Part >= 0.f ? Part : Values.branch >= 0.f ? Values.branch : Straight; }; - result.up = branch( Values.branchup, result.up ); - result.side = branch( Values.branchside, result.side ); + result.up = given( Values.branchup, given( Values.branch, result.up ) ); + result.side = given( Values.branchside, given( Values.branch, result.side ) ); } return result; } @@ -160,8 +163,9 @@ wave_set make_waves( double const Turn, std::array const & wave_set result {}; for( std::size_t i = 0; i < Count; ++i ) { auto const number { glm::two_pi() / Lengths[ i ] }; - auto const heading { Turn + golden * i }; - result[ i ] = { number * std::cos( heading ), number * std::sin( heading ), golden * ( i + 1 ) * ( 1.0 + 3.0 * Turn ), Shares[ 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; } @@ -246,7 +250,10 @@ inline glm::dvec3 across_of( glm::dvec3 const &Direction, glm::dvec3 const &Chor // 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 ) { - return Switch == Theirswitch ? 0.5f * ( Own + Their ) : Switch ? Their : Own; + if( Switch == Theirswitch ) { + return 0.5f * ( Own + Their ); + } + return Switch ? Their : Own; } // each component the same way From 32fadd634d259595a961556fd5e6349895569dec Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 20:17:47 +0100 Subject: [PATCH 16/18] refactor(tools): the grading tool in smaller functions As Sonar asks, and a regular expression with no backtracking. The results stay the same byte for byte on baltyk_skm1, baltyk_cargo, td and lk357. --- tools/track_uneven_grade.py | 348 +++++++++++++++++++++--------------- 1 file changed, 208 insertions(+), 140 deletions(-) diff --git a/tools/track_uneven_grade.py b/tools/track_uneven_grade.py index d643f94f..1994559d 100644 --- a/tools/track_uneven_grade.py +++ b/tools/track_uneven_grade.py @@ -43,7 +43,7 @@ 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+([eE][-+]?\d+)?$") +NUMBER = re.compile(r"^[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?$") def limit(speed): @@ -79,38 +79,9 @@ class Scenery: def read(self, path, offset=(0.0, 0.0, 0.0)): path = path.resolve() - if path in self.seen: - return - self.seen.add(path) - text = path.read_bytes().decode("latin-1") - self.texts[path] = text - matches = [m for m in TOKEN.finditer(text) if not m.group(0).startswith("//")] - words = [m.group(0) for m in matches] - lower = [w.lower() for w in words] - origins = [offset] - i = 0 - while i < len(words): - word = lower[i] - if word == "include" and i + 1 < len(words): - end = i + 1 - while end < len(words) and lower[end] != "end": - end += 1 - self.include(words[i + 1], words[i + 2:end], origins[-1]) - i = end - elif word == "origin" and i + 3 < len(words) and all(NUMBER.match(w) for w in words[i + 1:i + 4]): - origins.append(tuple(o + float(w) for o, w in zip(origins[-1], words[i + 1:i + 4]))) - i += 3 - elif word == "endorigin" and len(origins) > 1: - origins.pop() - elif word == "rotate" and i + 3 < len(words) and any(w.strip("0.-") for w in words[i + 1:i + 4]): - self.notes["rotate"] += 1 - elif word == "node" and i + 4 < len(words): - kind = lower[i + 4] - if kind == "track": - i = self.track(path, matches, words, lower, i, origins[-1]) - elif kind == "model" and i + 9 < len(words) and GRASS.search(lower[i + 9]) and all(NUMBER.match(w) for w in words[i + 5:i + 8]): - self.plant(float(words[i + 5]) + origins[-1][0], float(words[i + 7]) + origins[-1][2]) - i += 1 + 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("\\", "/") @@ -118,21 +89,90 @@ class Scenery: 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]) - return - if not path.is_file(): + elif not path.is_file(): self.notes["brak pliku " + name] += 1 - return - if params: + 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 - if path.stat().st_size < 4000000 and re.search(rb"\btrack\s+(normal|switch)\b", path.read_bytes(), re.I): - self.notes["pliki z parametrami, tory pominiete"] += 1 - return - self.read(path, origin) + self.notes["pliki z parametrami, tory pominiete"] += 1 def plant(self, x, z): self.plants[(round(x / 10), round(z / 10))].append((x, z)) - def track(self, path, matches, words, lower, i, origin): + +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": @@ -142,33 +182,30 @@ class Scenery: kind = lower[at + 1] if kind not in ("normal", "switch", "road"): return end - cursor = at + 10 - rail = ballast = None - if lower[at + 9] == "vis": - rail, ballast = lower[cursor], lower[cursor + 2] - cursor += 6 - paths = [] - for _ in range(2 if kind == "switch" else 1): - values = words[cursor:cursor + 15] - if len(values) < 15 or not all(NUMBER.match(v) for v in values): - self.notes["tory z parametrami, pominiete"] += 1 - return end - n = [float(v) for v in values] - part = dict(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 - if kind == "road": - self.roads.append(paths[0]) - return end - block = lower[at:end] - taken = any(w in block for w in ("uneven", "unevenup", "unevenside", "unevencross", "jointdip")) - velocity = float(words[at + block.index("velocity") + 1]) if "velocity" in block and NUMBER.match(words[at + block.index("velocity") + 1]) else 0.0 - sound = float(words[at + 5]) if NUMBER.match(words[at + 5]) else -1.0 - self.tracks.append(dict(name=words[i + 3], kind=kind, rail=rail, ballast=ballast, sound=sound, paths=paths, speed=velocity if velocity > 0 else None, source="wpis" if velocity > 0 else None, file=path, offset=matches[end].start(), taken=taken)) + 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)] @@ -220,50 +257,67 @@ class Joints: 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 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: + for other, part, side in joints.at(reached): + if other is t or id(part) in seen: + continue + seen.add(id(part)) + if other["kind"] != "switch": + found.append(other) + elif other["paths"].index(part) != other["bent"]: + onward.append(part["p2" if side == "p1" else "p1"]) + 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 - heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"]) - while heads: - t = heads.popleft() - for key in ("p1", "p2"): - for other, _, _ in joints.at(t["paths"][0][key]): - if other is not t and other["kind"] == "normal" and other["speed"] is None: - other["speed"], other["source"] = t["speed"], "sasiad" - heads.append(other) - heads = collections.deque(t for t in tracks if t["kind"] == "normal" and t["speed"]) - while heads: - t = heads.popleft() - for key in ("p1", "p2"): - points, seen = [t["paths"][0][key]], set() - for _ in range(6): - onward = [] - for point in points: - 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": - if other["paths"].index(part) != other["bent"]: - onward.append(part["p2" if side == "p1" else "p1"]) - elif other["speed"] is None and other["paths"][0]["length"] < PIECE: - other["speed"], other["source"] = t["speed"], "sasiad" - heads.append(other) - points = onward + 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 t in tracks: - for part in t["paths"]: - for key in ("p1", "p2"): - gap = math.dist((part[key][0], part[key][2]), station) - node = (round(part[key][0], 1), round(part[key][2], 1)) - if gap < 300 and t["kind"] == "normal" and gap < best.get(node, math.inf): - best[node] = gap - heapq.heappush(heap, (gap, node, part[key])) + 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): @@ -293,30 +347,34 @@ def grass_on(part, plants): 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 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): - cells[(x, z)].append((a, b)) + 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): - for a, b in zip(part["line"], part["line"][1:]): - 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): - for c, d in cells.get((x, z), ()): - 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: - continue - t = ((c[0] - a[0]) * sz - (c[2] - a[2]) * sx) / k - u = ((c[0] - a[0]) * rz - (c[2] - a[2]) * rx) / k - if 0 <= t <= 1 and 0 <= u <= 1: - return True - return False + 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): @@ -423,17 +481,19 @@ def fit_under_pantograph(normal): 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] - if abs(end_curve(part, "p1") - end_curve(part, "p2")) > CURVE_STEP: - return True - for key in ("p1", "p2"): - 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(end_curve(part, key) - (-their if side == key else their)) > CURVE_STEP: - return True - return False + 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): @@ -458,24 +518,32 @@ def move_near_limit(normal, joints, share): 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"] = [None] * len(t["paths"]) - for index, part in enumerate(t["paths"]): - found = [o for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "normal" and theirs["length"] >= SHORT] - if found: - speed = max(o["speed"] for o in found) - if index == t["bent"]: - speed = min(speed, DIVERGING) - t["values"][index] = min(limit(speed) * max(o["state"] for o in found), SWITCH_MOST, product / speed) + 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: - for index, part in enumerate(t["paths"]): - if t["values"][index] is None: - found = [o["values"][i] for key in ("p1", "p2") for o, theirs, _ in joints.at(part[key]) if o["kind"] == "switch" and o is not t for i, q in enumerate(o["paths"]) if q is theirs and o["values"][i] is not None] - if found: - t["values"][index] = max(found) + 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"]] From dbc173ebeaf0d25c839e0e3df8bbe8dce76ef775 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 20:17:47 +0100 Subject: [PATCH 17/18] docs(track): what of the cab shake goes off on an uneven track A table of all the sources of the shake, and what happens to each of them. --- track_uneven_doc.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/track_uneven_doc.md b/track_uneven_doc.md index c7446e34..1c3163c5 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -69,7 +69,19 @@ The rails, the top of the ballast, `sleepermodel`, the axles, the roll of the bo 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. The sway calculated from the lateral acceleration, that is from the curve radius, and the random jolts at higher speed are off. On tracks with no unevenness they work as before. +On a track with unevenness the track itself shakes the cab, in place of two of the sources of shake that were there before. It goes by the track under the vehicle, and a track with only dips at its joints counts too. 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 | +| random jolts over 25 km/h | off | +| 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. From a68c535be9e3338306881576be4b115f5f87b0a6 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 1 Nov 2026 20:12:33 +0100 Subject: [PATCH 18/18] fix(track): the cab keeps the shake a track doesn't replace, and the editor redraws all that an edit changes The side sway from the lateral acceleration now goes off only on a track with a sideways value, and the random jolts on one with a vertical or a sideways value. Dips at the rail joints and cross level alone replace neither. On a switch it goes by the path set and what its joined tracks allow. An edit in the editor also rebuilds the other switches of a joined group with the tracks they meet, and the tracks of a rail run whose joints moved. Removing a track does the same. While dragging, the tracks further off wait until the drag ends. The clatter of a bogie's axles was right: the followers stand at the pivots and the offsets are the places of the wheel sets. The misleading name goes. Also what Sonar had left: a parameter as a span, a const method, a struct and its member apart, commented out code, and a function of the grading tool split, with the same output. --- application/editormode.h | 1 + application/editormodetrack.cpp | 6 ++++-- editor/editorTrack.cpp | 22 +++++++++++++++---- editor/editorTrack.hpp | 5 ++++- tests/unit/test_track_uneven.cpp | 22 +++++++++++++++++++ tools/track_uneven_grade.py | 25 ++++++++++++++------- track_uneven_doc.md | 6 +++--- vehicle/DynObj.cpp | 18 ++++++++-------- world/Segment.h | 3 ++- world/Track.cpp | 5 ++++- world/Track.h | 11 ++++++++-- world/TrackUneven.cpp | 37 ++++++++++++++++++++++++++++++-- world/TrackUneven.h | 12 +++++++++++ 13 files changed, 140 insertions(+), 33 deletions(-) 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 0ae732d8..2523929d 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; } diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index b7050174..8e633991 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -426,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; @@ -457,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) @@ -478,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(); @@ -2819,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 14f9e9a4..efb5fdac 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -93,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); @@ -350,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/tests/unit/test_track_uneven.cpp b/tests/unit/test_track_uneven.cpp index 06f4663b..b55de164 100644 --- a/tests/unit/test_track_uneven.cpp +++ b/tests/unit/test_track_uneven.cpp @@ -38,6 +38,28 @@ void add_chord(std::vector &Output, std::vector const &Line, int } // 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 diff --git a/tools/track_uneven_grade.py b/tools/track_uneven_grade.py index 1994559d..81df2ce2 100644 --- a/tools/track_uneven_grade.py +++ b/tools/track_uneven_grade.py @@ -261,20 +261,29 @@ 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: - for other, part, side in joints.at(reached): - if other is t or id(part) in seen: - continue - seen.add(id(part)) - if other["kind"] != "switch": - found.append(other) - elif other["paths"].index(part) != other["bent"]: - onward.append(part["p2" if side == "p1" else "p1"]) + plain, further = joined_at(t, reached, joints, seen) + found += plain + onward += further points = onward return found diff --git a/track_uneven_doc.md b/track_uneven_doc.md index 1c3163c5..e4822b97 100644 --- a/track_uneven_doc.md +++ b/track_uneven_doc.md @@ -69,12 +69,12 @@ The rails, the top of the ballast, `sleepermodel`, the axles, the roll of the bo 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. It goes by the track under the vehicle, and a track with only dips at its joints counts too. On tracks with no unevenness everything works as before. +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 | -| random jolts over 25 km/h | off | +| 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 | diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 305e1202..c4838395 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -3381,7 +3381,6 @@ 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; std::vector PrzekrF( np ); float nPrzekrF = 0; bool test = true; @@ -8019,12 +8018,12 @@ 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 - auto const uneven { MyTrack != nullptr && MyTrack->is_uneven() }; + // 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( - uneven ? 0.0 : -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 @@ -8037,7 +8036,7 @@ TDynamicObject::update_shake( double const Timedelta ) { auto shake { 1.25 * ShakeSpring.ComputateForces( shakevector, ShakeState.offset ) }; - if( false == uneven && LocalRandom( iVel ) > 25.0 ) { + if( false == replaced.jolts && LocalRandom( iVel ) > 25.0 ) { // extra shake at increased velocity shake += ShakeSpring.ComputateForces( glm::dvec3( @@ -8066,11 +8065,12 @@ TDynamicObject::update_shake( double const Timedelta ) { bool TDynamicObject::axle_passed_joint( axle_sounds &Axle, double const Move, int const Direction ) const { - // by the outer axle on its side of the vehicle: the two can be on different tracks - auto const &outer { Axle.offset >= 0.0 ? Axle0 : Axle1 }; - if( auto const railphase { outer.RailPhase( Axle.offset - std::copysign( 0.5 * fAxleDist, Axle.offset ) ) }; railphase >= 0.0 ) { + // 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 * outer.GetTrack()->fSoundDistance }; + auto const passed { Axle.atjoints && std::abs( railphase - Axle.distance ) > 0.5 * follower.GetTrack()->fSoundDistance }; Axle.distance = railphase; Axle.atjoints = true; return passed; diff --git a/world/Segment.h b/world/Segment.h index 099480e3..05522e60 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -59,7 +59,8 @@ class TSegment 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; - } m_setting; + }; + 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 diff --git a/world/Track.cpp b/world/Track.cpp index 49138b4e..c9c7ae36 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -1375,7 +1375,7 @@ glm::vec3 TTrack::get_nearest_point(const glm::dvec3 &point) const // wypełnianie tablic VBO void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { - ++s_layoutstamp; + m_drawnrun = {}; Geometry1.clear(); Geometry2.clear(); @@ -1392,6 +1392,9 @@ 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() ) { diff --git a/world/Track.h b/world/Track.h index 72cbd884..dc6be741 100644 --- a/world/Track.h +++ b/world/Track.h @@ -348,6 +348,8 @@ public: // 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 @@ -358,6 +360,10 @@ public: 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(); @@ -397,6 +403,7 @@ private: }; 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 { @@ -411,10 +418,10 @@ private: // 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 ); + 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 + 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 index 78505ac3..10bf48d6 100644 --- a/world/TrackUneven.cpp +++ b/world/TrackUneven.cpp @@ -10,6 +10,8 @@ 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. @@ -64,7 +66,7 @@ void keep_to_plain_track( std::array &Values, TTrack c } // 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::vector const &Group ) { +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 ) { @@ -173,6 +175,11 @@ bool TTrack::is_uneven() const { 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 ); @@ -187,7 +194,7 @@ track_uneven::amounts TTrack::uneven_at( double const Distance, int const Path ) 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 ) { +track_uneven::amounts TTrack::uneven_met( TTrack *Other, glm::dvec3 const &Point ) const { if( Other == nullptr ) { return {}; @@ -278,6 +285,32 @@ double TTrack::rail_phase( double const Distance ) { 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 diff --git a/world/TrackUneven.h b/world/TrackUneven.h index 2c32ad66..8e0f0775 100644 --- a/world/TrackUneven.h +++ b/world/TrackUneven.h @@ -43,6 +43,18 @@ 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;