From 3dbe8a75f80fd109ccd0dab5bccf96b23faf33d8 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 31 Oct 2026 18:27:19 +0100 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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ł.