From 16672e81cc8b62d2c0e88855ef34b64e68ee0b04 Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 13 Oct 2026 18:30:16 +0200 Subject: [PATCH] feat(track): rails are cut where they cross other rails: a switch with the key frogs draws its frog, wing rails and check rails from its rails, tracks crossing other tracks get acute frogs with wing rails and check rails, obtuse points, guard rails along the quadrilateral of a crossing between 20 and 45 degrees and broken rails with guard rails at wider angles, the track going through chosen by name; sizes come from the rail profile, only for the profiles default, s60, uic60 and s39, others are laid whole; rail ends left open are closed with their cross-section; switches of older sceneries, with check rails in their includes and no key, are drawn as before --- CMakeLists.txt | 1 + world/Segment.cpp | 152 +++++ world/Segment.h | 11 + world/Track.cpp | 236 ++++++-- world/Track.h | 93 ++- world/TrackRails.cpp | 1280 ++++++++++++++++++++++++++++++++++++++++++ world/TrackRails.h | 37 ++ 7 files changed, 1766 insertions(+), 44 deletions(-) create mode 100644 world/TrackRails.cpp create mode 100644 world/TrackRails.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 62b41aa5..e9682362 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,7 @@ set(SOURCES "model/Texture.cpp" "utilities/Timer.cpp" "world/Track.cpp" +"world/TrackRails.cpp" "world/Road.cpp" "world/Sweep.cpp" "world/RoadPoint.cpp" diff --git a/world/Segment.cpp b/world/Segment.cpp index 69886f42..4f755209 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -441,6 +441,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 )); @@ -486,6 +490,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 ) { @@ -560,6 +568,150 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, return true; }; +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 ); +} + +bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Origin, gfx::vertex_array const &ShapePoints, int const Side, float const Halfwidth, double const fTextureLength, double const From, double const To, std::vector const &Offsets, std::vector const &Extents, float const Lift, std::vector const &Inner, gfx::userdata_array *Userdata, std::vector const &Ramp, double const Rampwidth ) const { + + if( fTsBuffer.empty() || To - From < 0.001 || ShapePoints.empty() ) + return false; + + auto const sample = []( std::vector const &Knots, double const Distance, double const Default ) { + if( Knots.empty() ) + return Default; + if( Distance <= Knots.front().x ) + return Knots.front().y; + for( std::size_t i = 1; i < Knots.size(); ++i ) { + if( Distance <= Knots[ i ].x ) { + return glm::mix( Knots[ i - 1 ].y, Knots[ i ].y, ( Distance - Knots[ i - 1 ].x ) / std::max( Knots[ i ].x - Knots[ i - 1 ].x, 1e-6 ) ); + } + } + return Knots.back().y; }; + + std::vector stations { From, To }; + auto const step { std::max( fStep, 0.2 ) }; + for( auto distance { std::ceil( From / step ) * step }; distance < To; distance += step ) { + stations.push_back( distance ); + } + for( auto const *knots : { &Offsets, &Extents, &Inner } ) { + for( auto const &knot : *knots ) { + if( knot.x > From && knot.x < To ) { + stations.push_back( knot.x ); + } + } + } + for( auto const *knots : { &Extents, &Inner } ) { + for( std::size_t i = 1; i < knots->size(); ++i ) { + auto const &a { ( *knots )[ i - 1 ] }; + auto const &b { ( *knots )[ i ] }; + if( std::abs( b.y - a.y ) < 1e-9 ) { + continue; + } + for( auto const &point : ShapePoints ) { + auto const reach { static_cast( Side * point.position.x - Halfwidth ) }; + auto const t { ( reach - a.y ) / ( b.y - a.y ) }; + auto const distance { glm::mix( a.x, b.x, t ) }; + if( t > 0.0 && t < 1.0 && distance > From && distance < To ) { + stations.push_back( distance ); + } + } + } + } + std::sort( stations.begin(), stations.end() ); + stations.erase( std::unique( stations.begin(), stations.end(), []( double const A, double const B ) { return B - A < 0.001; } ), stations.end() ); + + auto const ring = [&]( double const Distance, std::vector &Ring, std::vector &Wear ) { + glm::dvec3 point; + glm::dvec3 across; + plan_frame( Distance, point, across ); + if( auto const &joint { m_joints[ Distance < 0.001 ? 0 : 1 ] }; joint && ( Distance < 0.001 || Distance > fLength - 0.001 ) ) { + point = joint->first; + across = joint->second; + } + auto const position { glm::vec3{ point - Origin } }; + auto const parallel { glm::vec3{ across } }; + auto const offset { sample( Offsets, Distance, 0.0 ) }; + auto const extent { sample( Extents, Distance, 1e6 ) }; + auto const innerextent { sample( Inner, Distance, -1e6 ) }; + auto const worn { sample( Ramp, Distance, -1e6 ) }; + Ring.clear(); + Wear.clear(); + auto const count { ShapePoints.size() }; + auto const reach = [&]( std::size_t const Index ) { + return static_cast( Side * ShapePoints[ Index ].position.x - Halfwidth ); }; + for( std::size_t j = 0; j < count; ++j ) { + auto shape { ShapePoints[ j ] }; + auto outside { reach( j ) }; + if( outside > extent ) { + for( auto const neighbour : { j - 1, j + 1 } ) { + if( neighbour >= count || reach( neighbour ) > extent ) { + continue; + } + auto const &inner { ShapePoints[ neighbour ] }; + auto const t { static_cast( ( extent - reach( neighbour ) ) / std::max( outside - reach( neighbour ), 1e-6 ) ) }; + shape.position.y = glm::mix( inner.position.y, shape.position.y, t ); + shape.texture.x = glm::mix( inner.texture.x, shape.texture.x, t ); + shape.normal = inner.normal; + break; + } + outside = extent; + } + else if( outside < innerextent ) { + for( auto const neighbour : { j - 1, j + 1 } ) { + if( neighbour >= count || reach( neighbour ) < innerextent ) { + continue; + } + auto const &other { ShapePoints[ neighbour ] }; + auto const t { static_cast( ( innerextent - reach( neighbour ) ) / std::min( outside - reach( neighbour ), -1e-6 ) ) }; + shape.position.y = glm::mix( other.position.y, shape.position.y, t ); + shape.texture.x = glm::mix( other.texture.x, shape.texture.x, t ); + shape.normal = other.normal; + break; + } + outside = innerextent; + } + auto pt { parallel * static_cast( Side * ( Halfwidth + outside + offset ) ) + position }; + pt.y += shape.position.y + Lift; + auto norm { shape.normal.x * parallel }; + norm.y += shape.normal.y; + Ring.emplace_back( pt, glm::normalize( norm ), glm::vec2{ shape.texture.x, 0.f } ); + Wear.push_back( Rampwidth > 0.0 ? static_cast( std::clamp( ( outside - worn + Rampwidth ) / Rampwidth, 0.0, 1.0 ) ) : 1.f ); + } + }; + + auto const texturelength { static_cast( fTextureLength ) }; + std::vector ring1, ring2; + std::vector wear1, wear2; + ring( stations.front(), ring1, wear1 ); + float tv1 { clamp_circular( 1.f - static_cast( std::fmod( From, fTextureLength ) / fTextureLength ), 1.0f ) }; + for( std::size_t i = 1; i < stations.size(); ++i ) { + ring( stations[ i ], ring2, wear2 ); + tv1 = clamp_circular( tv1, 1.0f ); + auto const tv2 { tv1 - static_cast( stations[ i ] - stations[ i - 1 ] ) / texturelength }; + for( std::size_t j = 0; j < ring1.size(); ++j ) { + Output.emplace_back( ring1[ j ].position, ring1[ j ].normal, glm::vec2{ ring1[ j ].texture.x, tv1 } ); + Output.emplace_back( ring2[ j ].position, ring2[ j ].normal, glm::vec2{ ring2[ j ].texture.x, tv2 } ); + if( Userdata != nullptr ) { + Userdata->push_back( { glm::vec4{ wear1[ j ], 0.f, 0.f, 0.f } } ); + Userdata->push_back( { glm::vec4{ wear2[ j ], 0.f, 0.f, 0.f } } ); + } + } + std::swap( ring1, ring2 ); + std::swap( wear1, wear2 ); + tv1 = tv2; + } + return true; +} + void TSegment::render_lines(std::vector &out, float quality) const { float step = 1.0f / iSegCount / quality; diff --git a/world/Segment.h b/world/Segment.h index a3c83ade..68b0a93f 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Classes.h" #include "rendering/geometrybank.h" #include +#include struct map_colored_paths { std::vector switches; @@ -58,6 +59,7 @@ class TSegment glm::dvec3 vB; glm::dvec3 vC; // współczynniki wielomianów trzeciego stopnia vD==Point1 TTrack *pOwner = nullptr; // wskaźnik na właściciela + std::optional> m_joints[ 2 ]; glm::dvec3 GetFirstDerivative(double const fTime) const; double RombergIntegral(double const fA, double const fB) const; @@ -128,6 +130,15 @@ public: bool RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, gfx::vertex_array const &ShapePoints, bool const Transition, double fTextureLength, double Texturescale = 1.0, int iSkip = 0, int iEnd = 0, std::pair fOffsetX = {0.f, 0.f}, glm::vec3 **p = nullptr, bool bRender = true ); + // the profile lofted from distance From to To, moved outwards by Offsets and cut on its outer side to Extents beyond the gauge line, both as (distance, value) knots + bool + RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Origin, gfx::vertex_array const &ShapePoints, int const Side, float const Halfwidth, double const fTextureLength, double const From, double const To, std::vector const &Offsets, std::vector const &Extents = {}, float const Lift = 0.f, std::vector const &Inner = {}, gfx::userdata_array *Userdata = nullptr, std::vector const &Ramp = {}, double const Rampwidth = 0.0 ) const; + // point of the path and the unit vector across it towards the right rail profile, at a distance from Point1 + void + plan_frame( double const Distance, glm::dvec3 &Point, glm::dvec3 &Across ) const; + void + set_joint( int const End, std::optional> const &Joint ) { + m_joints[ End ] = Joint; } /* void Render(); diff --git a/world/Track.cpp b/world/Track.cpp index f1a29798..6a6d42c4 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -15,7 +15,9 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "world/Track.h" +#include "world/TrackRails.h" #include "simulation/simulation.h" +#include "scene/scene.h" #include "utilities/Globals.h" #include "world/Event.h" #include "world/MemCell.h" @@ -633,6 +635,12 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) m_profile1 = fetch_track_rail_profile( railprofile ); } } + else if( str == "frogs" ) { + // frog and check rails made from the rails of the switch + if( eType == tt_Switch ) { + SwitchExtension->m_frogs = true; + } + } else if( str == "friction" ) { // memory cell holding friction value modifiers m_friction.first = parser->getToken(); @@ -1353,6 +1361,8 @@ void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { Geometry1.clear(); Geometry2.clear(); + GeometryCut1.clear(); + GeometryCut2.clear(); if( SwitchExtension ) { SwitchExtension->Geometry3 = gfx::geometry_handle{}; } @@ -1363,8 +1373,50 @@ void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { create_geometry( Bank ); } +void TTrack::share_joints() { + + for( int path = 0; path < ( eType == tt_Switch ? 2 : 1 ); ++path ) { + auto *segment { eType == tt_Switch ? SwitchExtension->Segments[ path ].get() : Segment.get() }; + if( segment == nullptr ) { + continue; + } + for( int end = 0; end < 2; ++end ) { + std::optional> joint; + auto *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 { neighbour->eType == tt_Switch ? neighbour->SwitchExtension->Segments[ theirpath ].get() : neighbour->Segment.get() }; + 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 ) ); + } + } + } + segment->set_joint( end, joint ); + } + } +} + 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() ) { @@ -1377,6 +1429,16 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { // zwykla szyna: //Ra: czemu główki są asymetryczne na wysokości 0.140? gfx::vertex_array rpts1, rpts2; create_track_rail_profile( rpts1, rpts2 ); + auto const drawing { make_rail_drawing( Bank ) }; + auto const cut_faces = [&]( gfx::vertex_array const &Strip, std::size_t const Ring, bool const Start, bool const End, geometryhandle_sequence &Geometry ) { + track_rails::cut_faces( Strip, Ring, Start, End, Bank, Geometry ); }; + m_materialcut1 = track_rails::cut_material( m_material1 ); + auto const layout { plan_rails( Bank != 0 ) }; + if( Bank != 0 ) { + m_crossings = layout.crossed; + } + auto const lay_rail = [&]( int const Path, gfx::vertex_array const &Profile, int const Side, float const Texturelength, double const From, bool const Openstart, bool const Openend, geometryhandle_sequence &Geometry, geometryhandle_sequence &Cuts ) { + return this->lay_rail( layout, drawing, Path, Profile, Side, Texturelength, From, Openstart, Openend, Geometry, Cuts ); }; switch (eType) // dalej zależnie od typu { case tt_Table: // obrotnica jak zwykły tor, tylko animacja dochodzi @@ -1409,12 +1471,20 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { auto const nnumPts { track_rail_profile( m_profile1.second ).size() / 2 }; auto const texturelength { texture_length( m_material1 ) }; gfx::vertex_array vertices; + auto const open { eType == tt_Normal && ( trPrev == nullptr || trNext == nullptr ) }; if( Bank != 0 && true == Geometry1.empty() ) { - Segment->RenderLoft( vertices, m_origin, rpts1, iTrapezoid > 0, texturelength ); - Geometry1.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); - vertices.clear(); // reuse the scratchpad - Segment->RenderLoft( vertices, m_origin, rpts2, iTrapezoid > 0, texturelength ); - Geometry1.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); + for( auto const side : { 1, -1 } ) { + auto const &profile { side > 0 ? rpts1 : rpts2 }; + if( eType == tt_Normal && iTrapezoid == 0 && lay_rail( 0, profile, side, texturelength, 0.0, trPrev == nullptr, trNext == nullptr, Geometry1, GeometryCut1 ) ) { + continue; + } + vertices.clear(); // reuse the scratchpad + Segment->RenderLoft( vertices, m_origin, profile, iTrapezoid > 0, texturelength ); + if( open ) { + cut_faces( vertices, nnumPts, trPrev == nullptr, trNext == nullptr, GeometryCut1 ); + } + Geometry1.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); + } } if( Bank == 0 && false == Geometry1.empty() ) { // special variant, replace existing data for a turntable track @@ -1433,11 +1503,28 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { create_track_blade_profile( rpts3, rpts4 ); // TODO, TBD: change all track geometry to triangles, to allow packing data in less, larger buffers auto const nnumPts { track_rail_profile( m_profile1.second ).size() / 2 }; - auto const bladelength { static_cast( std::ceil( SwitchExtension->Segments[ 0 ]->RaSegCount() * 0.65 ) ) }; + auto const bladelength { track_rails::blade_steps( *this ) }; // positive jointlength: the switch is typically used along the main track, negative: along the diverging track // TODO: determine this from names of textures assigned to the tracks // auto const jointlength { static_cast( std::ceil( SwitchExtension->Segments[ 0 ]->RaSegCount() * 0.15 ) ) }; auto const jointlength { 0 }; // temporary until implementation of the above + m_materialcut2 = track_rails::cut_material( m_material2 ); + // rail behind the blade + auto const fixed_rail = [&]( int const Path, gfx::vertex_array const &Profile, int const Side, float const Texturelength, geometryhandle_sequence &Geometry, geometryhandle_sequence &Cuts ) { + if( false == lay_rail( Path, Profile, Side, Texturelength, track_rails::blade_length( *this, Path ), false, false, Geometry, Cuts ) ) { + gfx::vertex_array vertices; + SwitchExtension->Segments[ Path ]->RenderLoft( vertices, m_origin, Profile, false, Texturelength, 1.0, bladelength ); + Geometry.emplace_back( GfxRenderer->Insert( vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP ) ); + } + }; + // rail along the whole path + auto const whole_rail = [&]( int const Path, gfx::vertex_array const &Profile, int const Side, float const Texturelength, geometryhandle_sequence &Geometry, geometryhandle_sequence &Cuts ) { + if( false == lay_rail( Path, Profile, Side, Texturelength, 0.0, false, false, Geometry, Cuts ) ) { + gfx::vertex_array vertices; + SwitchExtension->Segments[ Path ]->RenderLoft( vertices, m_origin, Profile, false, Texturelength, 1.0, jointlength ); + Geometry.emplace_back( GfxRenderer->Insert( vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP ) ); + } + }; if (SwitchExtension->RightSwitch) { // nowa wersja z SPKS, ale odwrotnie lewa/prawa gfx::vertex_array vertices; @@ -1447,12 +1534,11 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { // composed from two parts: transition from blade to regular rail, and regular rail SwitchExtension->Segments[ 0 ]->RenderLoft( vertices, m_origin, rpts3, true, texturelength, 1.0, 0, bladelength / 2, { SwitchExtension->fOffset2, SwitchExtension->fOffset2 / 2 } ); SwitchExtension->Segments[ 0 ]->RenderLoft( vertices, m_origin, rpts1, false, texturelength, 1.0, bladelength / 2, bladelength, { SwitchExtension->fOffset2 / 2, 0.f } ); + cut_faces( vertices, nnumPts, true, false, GeometryCut1 ); Geometry1.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); vertices.clear(); // fixed parts - SwitchExtension->Segments[ 0 ]->RenderLoft( vertices, m_origin, rpts1, false, texturelength, 1.0, bladelength ); - Geometry1.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); - vertices.clear(); + fixed_rail( 0, rpts1, 1, texturelength, Geometry1, GeometryCut1 ); if( jointlength > 0 ) { // part of the diverging rail touched by wheels of vehicle going straight SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rpts1, false, texturelength, 1.0, 0, jointlength ); @@ -1460,9 +1546,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { vertices.clear(); } // other rail, full length - SwitchExtension->Segments[ 0 ]->RenderLoft( vertices, m_origin, rpts2, false, texturelength ); - Geometry1.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); - vertices.clear(); + whole_rail( 0, rpts2, -1, texturelength, Geometry1, GeometryCut1 ); } if( m_material2 ) { auto const texturelength { texture_length( m_material2 ) }; @@ -1470,16 +1554,13 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { // composed from two parts: transition from blade to regular rail, and regular rail SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rpts4, true, texturelength, 1.0, 0, bladelength / 2, { -fMaxOffset + SwitchExtension->fOffset1, ( -fMaxOffset + SwitchExtension->fOffset1 ) / 2 } ); SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rpts2, false, texturelength, 1.0, bladelength / 2, bladelength, { ( -fMaxOffset + SwitchExtension->fOffset1 ) / 2, 0.f } ); + cut_faces( vertices, nnumPts, true, false, GeometryCut2 ); Geometry2.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); vertices.clear(); // fixed parts - SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rpts2, false, texturelength, 1.0, bladelength ); - Geometry2.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); - vertices.clear(); + fixed_rail( 1, rpts2, -1, texturelength, Geometry2, GeometryCut2 ); // diverging rail, potentially minus part touched by wheels of vehicle going straight - SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rpts1, false, texturelength, 1.0, jointlength ); - Geometry2.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); - vertices.clear(); + whole_rail( 1, rpts1, 1, texturelength, Geometry2, GeometryCut2 ); } } else @@ -1491,13 +1572,12 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { // composed from two parts: transition from blade to regular rail, and regular rail SwitchExtension->Segments[ 0 ]->RenderLoft( vertices, m_origin, rpts4, true, texturelength, 1.0, 0, bladelength / 2, { -SwitchExtension->fOffset2, -SwitchExtension->fOffset2 / 2 } ); SwitchExtension->Segments[ 0 ]->RenderLoft( vertices, m_origin, rpts2, false, texturelength, 1.0, bladelength / 2, bladelength, { -SwitchExtension->fOffset2 / 2, 0.f } ); + cut_faces( vertices, nnumPts, true, false, GeometryCut1 ); Geometry1.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); vertices.clear(); // fixed parts // prawa szyna za iglicą - SwitchExtension->Segments[ 0 ]->RenderLoft( vertices, m_origin, rpts2, false, texturelength, 1.0, bladelength ); - Geometry1.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); - vertices.clear(); + fixed_rail( 0, rpts2, -1, texturelength, Geometry1, GeometryCut1 ); if( jointlength > 0 ) { // part of the diverging rail touched by wheels of vehicle going straight SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rpts2, false, texturelength, 1.0, 0, jointlength ); @@ -1505,9 +1585,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { vertices.clear(); } // other rail, full length - SwitchExtension->Segments[ 0 ]->RenderLoft( vertices, m_origin, rpts1, false, texturelength ); - Geometry1.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); - vertices.clear(); + whole_rail( 0, rpts1, 1, texturelength, Geometry1, GeometryCut1 ); } if( m_material2 ) { auto const texturelength { texture_length( m_material2 ) }; @@ -1515,17 +1593,14 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { // composed from two parts: transition from blade to regular rail, and regular rail SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rpts3, true, texturelength, 1.0, 0, bladelength / 2, { fMaxOffset - SwitchExtension->fOffset1, ( fMaxOffset - SwitchExtension->fOffset1 ) / 2 } ); SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rpts1, false, texturelength, 1.0, bladelength / 2, bladelength, { ( fMaxOffset - SwitchExtension->fOffset1 ) / 2, 0.f } ); + cut_faces( vertices, nnumPts, true, false, GeometryCut2 ); Geometry2.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); vertices.clear(); // fixed parts // lewa szyna za iglicą - SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rpts1, false, texturelength, 1.0, bladelength ); - Geometry2.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); - vertices.clear(); + fixed_rail( 1, rpts1, 1, texturelength, Geometry2, GeometryCut2 ); // diverging rail, potentially minus part touched by wheels of vehicle going straight - SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rpts2, false, texturelength, 1.0, jointlength ); - Geometry2.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP)); - vertices.clear(); + whole_rail( 1, rpts2, -1, texturelength, Geometry2, GeometryCut2 ); } } } @@ -2063,7 +2138,7 @@ TTrack * TTrack::RaAnimate() gfx::vertex_array rpts3, rpts4; create_track_blade_profile( rpts3, rpts4 ); gfx::vertex_array vertices; - auto const bladelength { static_cast( std::ceil( SwitchExtension->Segments[ 0 ]->RaSegCount() * 0.65 ) ) }; + auto const bladelength { track_rails::blade_steps( *this ) }; auto const nnumPts { track_rail_profile( m_profile1.second ).size() / 2 }; if (SwitchExtension->RightSwitch) { // nowa wersja z SPKS, ale odwrotnie lewa/prawa @@ -2073,6 +2148,12 @@ TTrack * TTrack::RaAnimate() // composed from two parts: transition from blade to regular rail, and regular rail SwitchExtension->Segments[ 0 ]->RenderLoft( vertices, m_origin, rpts3, true, texturelength, 1.0, 0, bladelength / 2, { SwitchExtension->fOffset2, SwitchExtension->fOffset2 / 2 } ); SwitchExtension->Segments[ 0 ]->RenderLoft( vertices, m_origin, rpts1, false, texturelength, 1.0, bladelength / 2, bladelength, { SwitchExtension->fOffset2 / 2, 0.f } ); + if( false == GeometryCut1.empty() ) { + gfx::vertex_array triangles; + track_rails::cap_loft( vertices, nnumPts, true, false, triangles ); + auto userdata { track_rails::worn_userdata( triangles.size() ) }; + GfxRenderer->Replace(triangles, userdata, GeometryCut1[0], GL_TRIANGLES); + } GfxRenderer->Replace(vertices, empty_userdata, Geometry1[0], GL_TRIANGLE_STRIP); vertices.clear(); } @@ -2082,6 +2163,12 @@ TTrack * TTrack::RaAnimate() // composed from two parts: transition from blade to regular rail, and regular rail SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rpts4, true, texturelength, 1.0, 0, bladelength / 2, { -fMaxOffset + SwitchExtension->fOffset1, ( -fMaxOffset + SwitchExtension->fOffset1 ) / 2 } ); SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rpts2, false, texturelength, 1.0, bladelength / 2, bladelength, { ( -fMaxOffset + SwitchExtension->fOffset1 ) / 2, 0.f } ); + if( false == GeometryCut2.empty() ) { + gfx::vertex_array triangles; + track_rails::cap_loft( vertices, nnumPts, true, false, triangles ); + auto userdata { track_rails::worn_userdata( triangles.size() ) }; + GfxRenderer->Replace(triangles, userdata, GeometryCut2[0], GL_TRIANGLES); + } GfxRenderer->Replace(vertices, empty_userdata, Geometry2[0], GL_TRIANGLE_STRIP); vertices.clear(); } @@ -2093,6 +2180,12 @@ TTrack * TTrack::RaAnimate() // composed from two parts: transition from blade to regular rail, and regular rail SwitchExtension->Segments[ 0 ]->RenderLoft( vertices, m_origin, rpts4, true, texturelength, 1.0, 0, bladelength / 2, { -SwitchExtension->fOffset2, -SwitchExtension->fOffset2 / 2 } ); SwitchExtension->Segments[ 0 ]->RenderLoft( vertices, m_origin, rpts2, false, texturelength, 1.0, bladelength / 2, bladelength, { -SwitchExtension->fOffset2 / 2, 0.f } ); + if( false == GeometryCut1.empty() ) { + gfx::vertex_array triangles; + track_rails::cap_loft( vertices, nnumPts, true, false, triangles ); + auto userdata { track_rails::worn_userdata( triangles.size() ) }; + GfxRenderer->Replace(triangles, userdata, GeometryCut1[0], GL_TRIANGLES); + } GfxRenderer->Replace(vertices, empty_userdata, Geometry1[0], GL_TRIANGLE_STRIP); vertices.clear(); } @@ -2102,6 +2195,12 @@ TTrack * TTrack::RaAnimate() // composed from two parts: transition from blade to regular rail, and regular rail SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rpts3, true, texturelength, 1.0, 0, bladelength / 2, { fMaxOffset - SwitchExtension->fOffset1, ( fMaxOffset - SwitchExtension->fOffset1 ) / 2 } ); SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rpts1, false, texturelength, 1.0, bladelength / 2, bladelength, { ( fMaxOffset - SwitchExtension->fOffset1 ) / 2, 0.f } ); + if( false == GeometryCut2.empty() ) { + gfx::vertex_array triangles; + track_rails::cap_loft( vertices, nnumPts, true, false, triangles ); + auto userdata { track_rails::worn_userdata( triangles.size() ) }; + GfxRenderer->Replace(triangles, userdata, GeometryCut2[0], GL_TRIANGLES); + } GfxRenderer->Replace(vertices, empty_userdata, Geometry2[0], GL_TRIANGLE_STRIP); vertices.clear(); } @@ -2441,6 +2540,10 @@ TTrack::export_as_text_( std::ostream &Output ) const { } Output << "trackbed " << texturefile << ' '; } + if( eType == tt_Switch + && SwitchExtension->m_frogs ) { + Output << "frogs "; + } if( iCategoryFlag == 1 && false == m_profile1.first.empty() ) { Output << "railprofile " << m_profile1.first << ' '; @@ -2579,6 +2682,40 @@ TTrack::track_rail_profile( int const Profile ) { return m_profiles[ Profile ]; } +TTrack::rail_size +TTrack::rail_dimensions() const { + + rail_size result; + if( m_profiles.empty() ) { + return result; + } + auto const &profile { track_rail_profile( m_profile1.second ) }; + auto const count { profile.size() / 2 }; + if( count < 2 ) { + return result; + } + auto top { -1e9 }; + auto bottom { 1e9 }; + result.low = 1e9; + result.high = -1e9; + for( std::size_t i = 0; i < count; ++i ) { + auto const &point { profile[ i ].position }; + top = std::max( top, static_cast( point.y ) ); + bottom = std::min( bottom, static_cast( point.y ) ); + result.low = std::min( result.low, static_cast( point.x ) ); + result.high = std::max( result.high, static_cast( point.x ) ); + } + result.height = top - bottom; + result.head = 0.0; + for( std::size_t i = 0; i < count; ++i ) { + auto const &point { profile[ i ].position }; + if( point.y > top - 0.3 * result.height ) { + result.head = std::max( result.head, static_cast( point.x ) ); + } + } + return result; +} + float TTrack::texture_length( material_handle const Material ) { @@ -2710,7 +2847,7 @@ TTrack * TTrack::Connected(int s, double &d) const // creates rail profile data for current track void -TTrack::create_track_rail_profile( gfx::vertex_array &Right, gfx::vertex_array &Left ) { +TTrack::create_track_rail_profile( gfx::vertex_array &Right, gfx::vertex_array &Left, gfx::vertex_array const *Shape ) { auto const fHTW { 0.5f * std::abs( fTrackWidth ) }; float const fHTW2{((iTrapezoid & 2) != 0 ? // ten bit oznacza, że istnieje odpowiednie pNext @@ -2720,9 +2857,7 @@ TTrack::create_track_rail_profile( gfx::vertex_array &Right, gfx::vertex_array & roll1{ 0.f }, roll2{ 0.f }; - if( Segment ) { - Segment->GetRolls( roll1, roll2 ); - } + profile_rolls( roll1, roll2 ); float const sin1 { std::sin( roll1 ) }, @@ -2730,9 +2865,9 @@ TTrack::create_track_rail_profile( gfx::vertex_array &Right, gfx::vertex_array & sin2 { std::sin( roll2 ) }, cos2 { std::cos( roll2 ) }; - auto const &railprofile { track_rail_profile( m_profile1.second ) }; + auto const &railprofile { Shape != nullptr ? *Shape : track_rail_profile( m_profile1.second ) }; // NOTE: rail profile defines both regular rail and switch blade profiles, so we halve total point count - auto const pointcount { railprofile.size() / 2 }; + auto const pointcount { Shape != nullptr ? railprofile.size() : railprofile.size() / 2 }; Right.resize( pointcount * ( iTrapezoid == 0 ? 1 : 2 ) ); Left.resize( pointcount * ( iTrapezoid == 0 ? 1 : 2 ) ); @@ -2797,6 +2932,25 @@ TTrack::create_track_rail_profile( gfx::vertex_array &Right, gfx::vertex_array & } } +void +TTrack::profile_rolls( float &Roll1, float &Roll2 ) const { + + Roll1 = Roll2 = 0.f; + if( eType == tt_Switch && SwitchExtension ) { + auto sum { 0.f }; + for( int path = 0; path < 2; ++path ) { + float roll1, roll2; + SwitchExtension->Segments[ path ]->GetRolls( roll1, roll2 ); + sum += roll1 + roll2; + } + Roll1 = Roll2 = 0.25f * sum; + return; + } + if( Segment ) { + Segment->GetRolls( Roll1, Roll2 ); + } +} + // creates switch blades profile data for current track void TTrack::create_track_blade_profile( gfx::vertex_array &Right, gfx::vertex_array &Left ) { @@ -2811,9 +2965,7 @@ TTrack::create_track_blade_profile( gfx::vertex_array &Right, gfx::vertex_array roll1 { 0.f }, roll2 { 0.f }; - if( Segment ) { - Segment->GetRolls( roll1, roll2 ); - } + profile_rolls( roll1, roll2 ); float const sin1 { std::sin( roll1 ) }, @@ -2921,9 +3073,7 @@ TTrack::create_track_bed_profile( gfx::vertex_array &Output, TTrack const *Previ roll1{ 0.f }, roll2{ 0.f }; - if( Segment ) { - Segment->GetRolls( roll1, roll2 ); - } + profile_rolls( roll1, roll2 ); float const sin1 { std::sin( roll1 ) }, diff --git a/world/Track.h b/world/Track.h index 72a6b7ff..39834daf 100644 --- a/world/Track.h +++ b/world/Track.h @@ -99,6 +99,7 @@ class TSwitchExtension glm::dvec3 vTrans; // docelowa translacja przesuwnicy material_handle m_material3 = 0; // texture of auto generated switch trackbed gfx::geometry_handle Geometry3; // geometry of auto generated switch trackbed + bool m_frogs { false }; // the frog and check rails are made from the rails, for switches with no model of them in their includes gfx::geometrybank_handle map_geometry[6]; // geometry for map visualisation }; @@ -184,9 +185,14 @@ public: using geometryhandle_sequence = std::vector; geometryhandle_sequence Geometry1; // geometry chunks textured with texture 1 geometryhandle_sequence Geometry2; // geometry chunks textured with texture 2 + material_handle m_materialcut1 = 0; // matte variant of texture 1, for the cut faces of rails + material_handle m_materialcut2 = 0; // matte variant of texture 2, for the cut faces of rails of the other switch path + geometryhandle_sequence GeometryCut1; // cut faces of rails textured with texture 1, the blade tip first for a switch + geometryhandle_sequence GeometryCut2; // cut faces of rails textured with texture 2, the blade tip first std::vector m_paths; // source data for owned paths bool m_editorremoved { false }; + std::vector m_crossings; // tracks whose rails cross the rails of this one, found when its geometry was made scene::basic_node *m_road { nullptr }; // road or road junction which generated this path. such path is one-way, and isn't saved nor edited on its own std::array m_routeweights { 0.f, 0.f, 0.f }; // odds of each route through a crossroads being picked by a driver, by the number of the route. all zero: even odds int iterate_stamp = 0; @@ -286,6 +292,80 @@ public: int RouteDraw() const; void Load(cParser *parser, glm::dvec3 const &pOrigin); void init_segments( bool const Initial ); + // where the inner rails of the two paths of a switch cross, found from their gauge lines behind the blades + struct switch_frog { + bool valid { false }; + int side[ 2 ] {}; // rail profile of the crossing rail of each path, 1 right, -1 left + double at[ 2 ] {}; // distance of the crossing along each path + double blades[ 2 ] {}; // distance the blades take along each path + double angle { 0.0 }; + }; + switch_frog frog() const; + // where a rail of this track crosses a rail of another one, found from their gauge lines + struct rail_crossing { + TTrack *track { nullptr }; + int path { 0 }; + int side { 0 }; // rail profile of the crossing rail, 1 right, -1 left + double at { 0.0 }; // distance of the crossing along the path, past the ends where the track goes on + double angle { 0.0 }; + double into { 0.0 }; // the path direction against the gauge normal of the other rail, above zero: going into the other track + double facing { 0.0 }; // the gauge normals of both rails against each other, below zero: an acute frog + bool nose { false }; // at an acute frog the rail goes on as the nose rail, the other one as the rail beside it + int otherpath { 0 }; + int otherside { 0 }; // rail profile of the other rail, 1 right, -1 left + double otherat { 0.0 }; // distance of the crossing along the other path + bool aligned { true }; // both paths run the same way at the crossing + }; + std::vector rail_crossings( bool const Joined = true ); + // the size of the rail profile, out from the gauge face: positive outwards, the side of the head away from the track + struct rail_size { + double head { 0.0735 }; // how far the head reaches out + double height { 0.172 }; + double low { -0.0385 }; // the reach of the profile inwards + double high { 0.1115 }; // the reach of the profile outwards + }; + rail_size rail_dimensions() const; + // a check rail beside a rail, how far its ends turn away from the running rail + struct check_rail { + double from { 0.0 }; + double to { 0.0 }; + double flarefrom { 0.0 }; + double flareto { 0.0 }; + }; + using check_rail_set = std::array, 2>, 2>; // path, right side + // the check rails beside the rails of each path, kept clear of the blades and the rails of other paths running inside them + check_rail_set check_rails( std::vector const &Crossings ); + // a rail broken where another one crosses it: the part coming from below ends at end, the part going on starts at start + struct rail_cut { + double at { 0.0 }; + double end { 0.0 }; + double start { 0.0 }; + std::vector endoffsets, endextents, startoffsets, startextents; + std::vector endrolled, startrolled; // how far out from the gauge line the wheels roll on the head, of the part coming from below and of the part going on + glm::dvec2 endmiter { 0.0 }; + glm::dvec2 startmiter { 0.0 }; + }; + struct rail_plan { + std::vector cuts; + }; + // guard rails of a crossing at a wide angle: the rail profile laid inside the running rail across the flangeway, broken like the rails + struct guard_rail { + double from { 0.0 }; + double to { 0.0 }; + rail_plan plan; + bool flarefrom { true }; + bool flareto { true }; + glm::dvec2 frommiter { 0.0 }; + glm::dvec2 tomiter { 0.0 }; + }; + struct rail_layout { + rail_plan plans[ 2 ][ 2 ]; // path, right side + std::vector guards[ 2 ][ 2 ]; + std::vector wingends[ 2 ][ 2 ]; // ends of the wing rails of other rails running along each rail, and which way they run there + check_rail_set checks; + std::vector crossed; // tracks whose rails cross the rails of this one + }; + rail_layout plan_rails( bool const Crossings ); void update_location(); bool AssignEvents(); bool AssignForcedEvents(basic_event *NewEventPlus, basic_event *NewEventMinus); @@ -367,8 +447,10 @@ private: float texture_length( material_handle const Material ); // creates profile for a part of current path void create_switch_trackbed( gfx::vertex_array &Output ); - void create_track_rail_profile( gfx::vertex_array &Right, gfx::vertex_array &Left ); + void create_track_rail_profile( gfx::vertex_array &Right, gfx::vertex_array &Left, gfx::vertex_array const *Shape = nullptr ); void create_track_blade_profile( gfx::vertex_array &Right, gfx::vertex_array &Left ); + void profile_rolls( float &Roll1, float &Roll2 ) const; + void share_joints(); void create_track_bed_profile( gfx::vertex_array &Output, TTrack const *Previous, TTrack const *Next ); 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 ); @@ -378,6 +460,15 @@ private: /// Safe to call multiple times; clears any previously cached transforms first. /// void build_sleeper_transforms(); + struct rail_drawing { + gfx::geometrybank_handle bank; + rail_size size; + float halfwidth { 0.f }; + gfx::vertex_array checkshapes[ 2 ]; + }; + rail_drawing make_rail_drawing( gfx::geometrybank_handle const &Bank ); + // a rail along a path from a distance on, in parts where other rails cross it, and the check rails beside it. false: no crossing, the rail is laid whole as before + bool lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, int const Path, gfx::vertex_array const &Profile, int const Side, float const Texturelength, double const From, bool const Openstart, bool const Openend, geometryhandle_sequence &Geometry, geometryhandle_sequence &Cuts ); // members static profiles_array m_profiles; // shared database of path element profiles static profiles_map m_profilesmap; diff --git a/world/TrackRails.cpp b/world/TrackRails.cpp new file mode 100644 index 00000000..a6ddd8d0 --- /dev/null +++ b/world/TrackRails.cpp @@ -0,0 +1,1280 @@ +/* +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/TrackRails.h" +#include "world/Segment.h" +#include "simulation/simulation.h" +#include "scene/scene.h" +#include "rendering/renderer.h" + +namespace { + +// gauge line of a rail: the running edge of its head, along a path and on past its ends along the tangents +struct gauge_line { + TSegment const *segment { nullptr }; + double offset { 0.0 }; // across the path, to the running edge + double from { 0.0 }; + std::vector points; + std::vector distances; + glm::dvec2 low { 0.0 }; + glm::dvec2 high { 0.0 }; + + void at( double const Distance, glm::dvec3 &Point, glm::dvec3 &Tangent ) const { + auto const length { segment->GetLength() }; + auto const local = [&]( double const D ) { + glm::dvec3 point, across; + segment->plan_frame( D, point, across ); + return point + across * offset; }; + auto const inside { std::clamp( Distance, 0.0, length ) }; + auto const delta { std::min( 0.05, 0.5 * length ) }; + auto const a { std::max( inside - delta, 0.0 ) }; + auto const b { std::min( inside + delta, length ) }; + Tangent = local( b ) - local( a ); + Tangent = ( glm::length2( Tangent ) > 0.0 ? glm::normalize( Tangent ) : glm::dvec3{ 1.0, 0.0, 0.0 } ); + Point = local( inside ) + Tangent * ( Distance - inside ); + } + glm::dvec3 normal( double const Distance, int const Side ) const { + glm::dvec3 point, across; + segment->plan_frame( std::clamp( Distance, 0.0, segment->GetLength() ), point, across ); + return across * static_cast( -Side ); + } +}; + +gauge_line make_gauge_line( TSegment const &Segment, double const Offset, double const From ) { + + gauge_line line; + line.segment = &Segment; + line.offset = Offset; + line.from = From; + auto const length { Segment.GetLength() }; + auto const add = [&]( double const Distance ) { + glm::dvec3 point, tangent; + line.at( Distance, point, tangent ); + line.points.push_back( point ); + line.distances.push_back( Distance ); }; + auto const count { std::max( { 1, Segment.RaSegCount(), static_cast( std::ceil( ( length - From ) / 5.0 ) ) } ) }; + for( int i = 0; i <= count; ++i ) { + add( From + ( length - From ) * i / count ); + } + line.low = line.high = glm::dvec2{ line.points.front().x, line.points.front().z }; + for( auto const &point : line.points ) { + line.low = glm::min( line.low, glm::dvec2{ point.x, point.z } ); + line.high = glm::max( line.high, glm::dvec2{ point.x, point.z } ); + } + return line; +} + +// rails of the profiles the frogs, check rails and guard rails are made for; others are laid whole +bool cut_profile( std::string Profile ) { + + std::transform( Profile.begin(), Profile.end(), Profile.begin(), []( unsigned char const Char ) { return static_cast( std::tolower( Char ) ); } ); + return Profile.empty() || Profile == "default" || Profile == "s60" || Profile == "uic60" || Profile == "s39"; +} + +bool joined( TTrack const &Track, TTrack const &Other ) { + + auto const next = []( TTrack const &From, TTrack const &To ) { + if( From.trPrev == &To || From.trNext == &To ) { + return true; + } + if( From.eType == tt_Switch && From.SwitchExtension ) { + for( int i = 0; i < 2; ++i ) { + if( From.SwitchExtension->pPrevs[ i ] == &To || From.SwitchExtension->pNexts[ i ] == &To ) { + return true; + } + } + } + return false; }; + return next( Track, Other ) || next( Other, Track ); +} + +TSegment const &path_segment( TTrack const &Track, int const Path ) { + + return ( Track.eType == tt_Switch ? *Track.SwitchExtension->Segments[ Path ] : *Track.Segment ); +} + +// a check rail starting right beside the end of a wing rail running along the same rail, which the wing rail goes on in place of; the far end of it +bool replaced_check( std::vector const &Checks, double const Wingend, double const Direction, double &Far ) { + + for( auto const &check : Checks ) { + auto const nearend { Direction > 0.0 ? check.from : check.to }; + auto const farend { Direction > 0.0 ? check.to : check.from }; + auto const gap { ( nearend - Wingend ) * Direction }; + if( gap > -0.5 && gap < 1.0 && ( farend - Wingend ) * Direction > 0.5 ) { + Far = farend; + return true; + } + } + return false; +} + +} // namespace + +namespace track_rails { + +double wing_reach( double const Slope ) { + + return 0.15 / Slope + 0.3; +} + +double check_length( double const Slope ) { + + return std::clamp( 0.5 / Slope, 3.6, 9.0 ); +} + +// the rails of the track are cut where they cross other rails: plain tracks, and switches whose frog is made from the rails +bool cut_rails( TTrack const &Track ) { + + return ( Track.iCategoryFlag & 15 ) == 1 + && Track.m_material1 != null_handle + && false == Track.m_editorremoved + && cut_profile( Track.m_profile1.first ) + && ( Track.eType == tt_Normal ? Track.Segment != nullptr : + Track.eType == tt_Switch && Track.SwitchExtension && Track.SwitchExtension->m_frogs && Track.SwitchExtension->Segments[ 0 ] && Track.SwitchExtension->Segments[ 1 ] ); +} + +int blade_steps( TTrack const &Track ) { + + return static_cast( std::ceil( Track.SwitchExtension->Segments[ 0 ]->RaSegCount() * 0.65 ) ); +} + +double blade_length( TTrack const &Track, int const Path ) { + + auto const &segment { *Track.SwitchExtension->Segments[ Path ] }; + return segment.GetLength() * blade_steps( Track ) / segment.RaSegCount(); +} + +material_handle cut_material( material_handle const Material ) { + + static auto const unrolled { std::getenv( "EU07_EDITOR_UNROLLED" ) != nullptr }; + return unrolled ? GfxRenderer->Fetch_Material( "editor_unrolled" ) : GfxRenderer->Matte_Material( Material ); +} + +// in the colour of the top of the rail foot +void cap_loft( gfx::vertex_array const &Strip, std::size_t const Ring, bool const Start, bool const End, gfx::vertex_array &Triangles ) { + + if( Ring < 3 || Strip.size() < 2 * Ring ) { + return; + } + std::vector, glm::vec3>> caps; + if( Start ) { + std::vector ring; + for( std::size_t j = 0; j < Ring; ++j ) { + ring.emplace_back( Strip[ 2 * j ] ); + } + caps.emplace_back( ring, Strip[ 0 ].position - Strip[ 1 ].position ); + } + if( End ) { + auto const base { Strip.size() - 2 * Ring }; + std::vector ring; + for( std::size_t j = 0; j < Ring; ++j ) { + ring.emplace_back( Strip[ base + 2 * j + 1 ] ); + } + caps.emplace_back( ring, Strip[ base + 1 ].position - Strip[ base ].position ); + } + for( auto const &cap : caps ) { + auto const &ring { cap.first }; + if( glm::length2( cap.second ) == 0.f ) { + continue; + } + auto const outward { glm::normalize( cap.second ) }; + auto across { glm::vec3{ -outward.z, 0.f, outward.x } }; + if( glm::length2( across ) == 0.f ) { + continue; + } + across = glm::normalize( across ); + std::vector plan; + auto bottom { 0.0 }, top { 0.0 }; + for( auto const &vertex : ring ) { + auto const offset { vertex.position - ring[ 0 ].position }; + plan.emplace_back( glm::dot( offset, across ), offset.y ); + bottom = std::min( bottom, offset.y ); + top = std::max( top, offset.y ); + } + auto web { ring[ 0 ].texture }; + auto nearest { std::numeric_limits::max() }; + for( std::size_t j = 0; j < Ring / 2; ++j ) { + auto const distance { std::abs( plan[ j ].y - 0.5 * ( bottom + top ) ) }; + if( distance < nearest ) { + nearest = distance; + web = ring[ j ].texture; + } + } + auto lowest { std::numeric_limits::max() }; + for( std::size_t j = 0; j < Ring / 2; ++j ) { + if( ring[ j ].normal.y > 0.5f && plan[ j ].y < lowest ) { + lowest = plan[ j ].y; + web = ring[ j ].texture; + } + } + auto const cross = []( glm::dvec2 const &A, glm::dvec2 const &B, glm::dvec2 const &C ) { + return ( B.x - A.x ) * ( C.y - A.y ) - ( B.y - A.y ) * ( C.x - A.x ); }; + double area { 0.0 }; + for( std::size_t j = 0; j < plan.size(); ++j ) { + auto const &a { plan[ j ] }; + auto const &b { plan[ ( j + 1 ) % plan.size() ] }; + area += a.x * b.y - b.x * a.y; + } + auto const orientation { area < 0.0 ? -1.0 : 1.0 }; + std::vector left( Ring ); + for( std::size_t j = 0; j < Ring; ++j ) { + left[ j ] = j; + } + std::vector> triangles; + while( left.size() > 3 ) { + auto ear { left.size() }; + for( std::size_t j = 0; j < left.size() && ear == left.size(); ++j ) { + auto const &a { plan[ left[ ( j + left.size() - 1 ) % left.size() ] ] }; + auto const &b { plan[ left[ j ] ] }; + auto const &c { plan[ left[ ( j + 1 ) % left.size() ] ] }; + auto const turn { cross( a, b, c ) * orientation }; + if( turn < -1e-12 ) { + continue; + } + auto inside { false }; + if( turn > 1e-12 ) { + for( auto const k : left ) { + auto const &p { plan[ k ] }; + if( p == a || p == b || p == c ) { + continue; + } + if( cross( a, b, p ) * orientation > 1e-12 && cross( b, c, p ) * orientation > 1e-12 && cross( c, a, p ) * orientation > 1e-12 ) { + inside = true; + break; + } + } + } + if( false == inside ) { + ear = j; + } + } + if( ear == left.size() ) { + ear = 0; + } + triangles.push_back( { left[ ( ear + left.size() - 1 ) % left.size() ], left[ ear ], left[ ( ear + 1 ) % left.size() ] } ); + left.erase( left.begin() + ear ); + } + triangles.push_back( { left[ 0 ], left[ 1 ], left[ 2 ] } ); + for( auto const &triangle : triangles ) { + for( auto const &order : { std::array{ 0, 1, 2 }, std::array{ 0, 2, 1 } } ) { + for( auto const k : order ) { + auto const index { triangle[ k ] }; + Triangles.emplace_back( ring[ index ].position, outward, web ); + } + } + } + } +} + +gfx::userdata_array worn_userdata( std::size_t const Count ) { + + return gfx::userdata_array( Count, gfx::vertex_userdata{ glm::vec4{ 1.f, 0.f, 0.f, 0.f } } ); +} + +void cut_faces( gfx::vertex_array const &Strip, std::size_t const Ring, bool const Start, bool const End, gfx::geometrybank_handle const &Bank, TTrack::geometryhandle_sequence &Geometry ) { + + gfx::vertex_array triangles; + cap_loft( Strip, Ring, Start, End, triangles ); + if( false == triangles.empty() ) { + auto userdata { worn_userdata( triangles.size() ) }; + Geometry.emplace_back( GfxRenderer->Insert( triangles, userdata, Bank, GL_TRIANGLES ) ); + } +} + +} // namespace track_rails + +using namespace track_rails; + +std::vector TTrack::rail_crossings( bool const Joined ) { + + std::vector result; + if( false == cut_rails( *this ) || simulation::Region == nullptr ) { + return result; + } + auto const extension { 10.0 }; + auto const halfwidth { 0.5 * std::abs( fTrackWidth ) }; + auto const paths { eType == tt_Switch ? 2 : 1 }; + gauge_line ours[ 2 ][ 2 ]; + for( int path = 0; path < paths; ++path ) { + for( int side = 0; side < 2; ++side ) { + ours[ path ][ side ] = make_gauge_line( path_segment( *this, path ), ( side ? 1.0 : -1.0 ) * halfwidth, 0.0 ); + } + } + auto const sections { simulation::Region->sections( location(), static_cast( radius() + 50.0 ) ) }; + std::vector others; + for( auto *section : sections ) { + for( auto const &cell : section->m_cells ) { + if( glm::length( cell.area().center - location() ) > cell.area().radius + radius() + 1.0 ) { + continue; + } + for( auto *other : cell.m_directories.paths ) { + if( other == nullptr || other == this || false == cut_rails( *other ) ) { + continue; + } + if( glm::length( other->location() - location() ) > other->radius() + radius() + 1.0 ) { + continue; + } + if( std::find( others.begin(), others.end(), other ) == others.end() ) { + others.push_back( other ); + } + } + } + } + auto const cross = []( glm::dvec2 const &A, glm::dvec2 const &B ) { + return A.x * B.y - A.y * B.x; }; + auto const plan = []( glm::dvec3 const &V ) { + return glm::dvec2{ V.x, V.z }; }; + auto const bend = [&]( gauge_line const &Line, double const At ) { + glm::dvec3 point, before, after; + Line.at( At - 1.0, point, before ); + Line.at( At + 1.0, point, after ); + return std::abs( cross( plan( before ), plan( after ) ) ); }; + auto const heading = []( glm::dvec2 const &Tangent ) { + auto const angle { std::atan2( Tangent.y, Tangent.x ) }; + return ( angle < 0.0 ? angle + M_PI : angle ); }; + auto const overlap = []( gauge_line const &A, gauge_line const &B ) { + return A.low.x <= B.high.x && B.low.x <= A.high.x && A.low.y <= B.high.y && B.low.y <= A.high.y; }; + for( auto *other : others ) { + auto const otherwidth { 0.5 * std::abs( other->fTrackWidth ) }; + auto const otherpaths { other->eType == tt_Switch ? 2 : 1 }; + for( int otherpath = 0; otherpath < otherpaths; ++otherpath ) { + auto const &othersegment { path_segment( *other, otherpath ) }; + auto const otherfrom { other->eType == tt_Switch ? blade_length( *other, otherpath ) : 0.0 }; + auto const otherlength { othersegment.GetLength() }; + for( int otherside = 0; otherside < 2; ++otherside ) { + auto const theirs { make_gauge_line( othersegment, ( otherside ? 1.0 : -1.0 ) * otherwidth, otherfrom ) }; + for( int path = 0; path < paths; ++path ) { + auto const length { path_segment( *this, path ).GetLength() }; + for( int side = 0; side < 2; ++side ) { + auto const &line { ours[ path ][ side ] }; + if( false == overlap( line, theirs ) ) { + continue; + } + for( std::size_t i = 1; i < line.points.size(); ++i ) { + auto const a0 { plan( line.points[ i - 1 ] ) }; + auto const a { plan( line.points[ i ] ) - a0 }; + for( std::size_t j = 1; j < theirs.points.size(); ++j ) { + auto const b0 { plan( theirs.points[ j - 1 ] ) }; + auto const b { plan( theirs.points[ j ] ) - b0 }; + auto const denominator { cross( a, b ) }; + if( std::abs( denominator ) < 1e-12 ) { + continue; + } + auto const u { cross( b0 - a0, b ) / denominator }; + auto const v { cross( b0 - a0, a ) / denominator }; + if( u < -0.01 || u > 1.01 || v < -0.01 || v > 1.01 ) { + continue; + } + auto s { glm::mix( line.distances[ i - 1 ], line.distances[ i ], u ) }; + auto t { glm::mix( theirs.distances[ j - 1 ], theirs.distances[ j ], v ) }; + glm::dvec3 p, tp, q, tq; + for( int step = 0; step < 6; ++step ) { + line.at( s, p, tp ); + theirs.at( t, q, tq ); + auto const d { cross( plan( tp ), plan( tq ) ) }; + if( std::abs( d ) < 1e-9 ) { + break; + } + auto const offset { plan( q ) - plan( p ) }; + s += cross( offset, plan( tq ) ) / d; + t += cross( offset, plan( tp ) ) / d; + } + line.at( s, p, tp ); + theirs.at( t, q, tq ); + if( glm::length( plan( p ) - plan( q ) ) > 0.01 + || s < 0.0 || s > length + || t < otherfrom || t > otherlength ) { + continue; + } + if( std::abs( p.y - q.y ) > 0.5 ) { + continue; + } + if( ( s < 0.5 || s > length - 0.5 ) && ( t < 0.5 || t > otherlength - 0.5 ) && joined( *this, *other ) ) { + continue; + } + auto const tangent { glm::normalize( plan( tp ) ) }; + auto const othertangent { glm::normalize( plan( tq ) ) }; + auto const angle { std::acos( std::min( 1.0, std::abs( glm::dot( tangent, othertangent ) ) ) ) }; + if( angle < 0.03 ) { + continue; + } + auto const sidevalue { side ? 1 : -1 }; + if( std::any_of( result.begin(), result.end(), [&]( rail_crossing const &Crossing ) { + return Crossing.path == path && Crossing.side == sidevalue && std::abs( Crossing.at - s ) < 0.05; } ) ) { + continue; + } + auto const normal { glm::normalize( plan( line.normal( s, sidevalue ) ) ) }; + auto const othernormal { glm::normalize( plan( theirs.normal( t, otherside ? 1 : -1 ) ) ) }; + rail_crossing crossing; + crossing.track = other; + crossing.path = path; + crossing.side = sidevalue; + crossing.at = s; + crossing.angle = angle; + crossing.into = glm::dot( tangent, othernormal ); + crossing.facing = glm::dot( normal, othernormal ); + auto const ourbend { bend( line, s ) }; + auto const theirbend { bend( theirs, t ) }; + crossing.nose = ( std::abs( ourbend - theirbend ) > 1e-4 ? ourbend < theirbend : heading( tangent ) < heading( othertangent ) ); + crossing.otherpath = otherpath; + crossing.otherside = ( otherside ? 1 : -1 ); + crossing.otherat = t; + crossing.aligned = glm::dot( tangent, othertangent ) > 0.0; + result.push_back( crossing ); + } + } + } + } + } + } + } + if( false == Joined ) { + return result; + } + // crossings of the tracks going on from the ends, near enough for their check rails and wings to reach this one + auto const owncount { result.size() }; + for( int path = 0; path < paths; ++path ) { + auto const &segment { path_segment( *this, path ) }; + auto const length { segment.GetLength() }; + for( int end = 0; end < 2; ++end ) { + auto *neighbour { eType == tt_Switch ? ( end ? SwitchExtension->pNexts[ path ] : SwitchExtension->pPrevs[ path ] ) : ( end ? trNext : trPrev ) }; + if( neighbour == nullptr || neighbour == this || false == cut_rails( *neighbour ) ) { + continue; + } + auto const joint { end ? segment.FastGetPoint_1() : segment.FastGetPoint_0() }; + auto const theirs { neighbour->rail_crossings( false ) }; + for( int neighbourpath = 0; neighbourpath < ( neighbour->eType == tt_Switch ? 2 : 1 ); ++neighbourpath ) { + auto const &neighboursegment { path_segment( *neighbour, neighbourpath ) }; + auto const neighbourlength { neighboursegment.GetLength() }; + auto const atstart { glm::length( neighboursegment.FastGetPoint_0() - joint ) < 0.5 }; + if( false == atstart && glm::length( neighboursegment.FastGetPoint_1() - joint ) >= 0.5 ) { + continue; + } + auto const same { atstart == ( end == 1 ) }; + for( auto const &crossing : theirs ) { + if( crossing.path != neighbourpath || crossing.track == this ) { + continue; + } + auto const along { atstart ? crossing.at : neighbourlength - crossing.at }; + if( along > extension ) { + continue; + } + auto mapped { crossing }; + mapped.path = path; + mapped.at = ( end ? length + along : -along ); + if( false == same ) { + mapped.side = -crossing.side; + mapped.into = -crossing.into; + mapped.aligned = !crossing.aligned; + } + if( std::none_of( result.begin(), result.begin() + owncount, [&]( rail_crossing const &Crossing ) { + return Crossing.path == mapped.path && Crossing.side == mapped.side && std::abs( Crossing.at - mapped.at ) < 0.05; } ) ) { + result.push_back( mapped ); + } + } + } + } + } + return result; +} + +TTrack::check_rail_set TTrack::check_rails( std::vector const &Crossings ) { + + struct planned { + double from { 0.0 }; + double to { 0.0 }; + bool flarefrom { true }; + bool flareto { true }; + double shortest { 1.5 }; + double flare { 0.02 }; + }; + std::vector plans[ 2 ][ 2 ]; + check_rail_set result; + auto const size { rail_dimensions() }; + auto const acute = [&]( std::vector &Other, double const At, double const Toward, double const Slope, double const Flare ) { + auto const half { 0.5 * check_length( Slope ) }; + Other.push_back( { At + Toward * 0.25 - half, At + Toward * 0.25 + half, true, true, 1.5, Flare } ); + }; + for( auto const &crossing : Crossings ) { + if( crossing.angle > middle_angle ) { + continue; + } + if( crossing.facing < 0.0 ) { + acute( plans[ crossing.path ][ crossing.side < 0 ], crossing.at, crossing.into > 0.0 ? 1.0 : -1.0, std::tan( crossing.angle ), 0.02 ); + } + else if( crossing.into > 0.0 ) { + plans[ crossing.path ][ crossing.side > 0 ].push_back( { crossing.at - 0.03, crossing.at + 1.2, false, true, 0.5, 0.02 } ); + } + else { + plans[ crossing.path ][ crossing.side > 0 ].push_back( { crossing.at - 1.2, crossing.at + 0.03, true, false, 0.5, 0.02 } ); + } + } + if( eType == tt_Switch ) { + auto const frog { this->frog() }; + if( frog.valid ) { + for( int path = 0; path < 2; ++path ) { + acute( plans[ path ][ frog.side[ path ] < 0 ], frog.at[ path ], -1.0, std::tan( frog.angle ), 0.04 ); + } + } + } + auto const halfwidth { 0.5 * std::abs( fTrackWidth ) }; + auto const clearance { 0.046 + 0.04 * size.head / 0.0735 + size.head }; + auto const flat = []( glm::dvec3 const &V ) { return glm::dvec2{ V.x, V.z }; }; + for( int path = 0; path < ( eType == tt_Switch ? 2 : 1 ); ++path ) { + auto const &segment { path_segment( *this, path ) }; + auto const length { segment.GetLength() }; + for( int right = 0; right < 2; ++right ) { + auto const side { right ? 1 : -1 }; + auto &checks { plans[ path ][ right ] }; + if( checks.empty() ) { + continue; + } + std::sort( checks.begin(), checks.end(), []( planned const &A, planned const &B ) { return A.from < B.from; } ); + for( std::size_t i = 1; i < checks.size(); ++i ) { + if( checks[ i ].from < checks[ i - 1 ].to + 2.0 ) { + if( checks[ i ].to > checks[ i - 1 ].to ) { + checks[ i - 1 ].to = checks[ i ].to; + checks[ i - 1 ].flareto = checks[ i ].flareto; + } + checks[ i - 1 ].shortest = std::min( checks[ i - 1 ].shortest, checks[ i ].shortest ); + checks[ i - 1 ].flare = std::max( checks[ i - 1 ].flare, checks[ i ].flare ); + checks.erase( checks.begin() + i-- ); + } + } + // check rails keep clear of the blades and the rails of the other path of a switch running inside the rail, here and in a switch joined at an end, and of the blade tips + std::vector blocks; + auto const start { flat( segment.FastGetPoint_0() ) }; + auto const end { flat( segment.FastGetPoint_1() ) }; + std::vector others; + auto const add_path = [&]( TSegment const &Other ) { + others.push_back( make_gauge_line( Other, -halfwidth, 0.0 ) ); + others.push_back( make_gauge_line( Other, halfwidth, 0.0 ) ); }; + if( eType == tt_Switch ) { + blocks.emplace_back( -1e9, 0.3 ); + add_path( path_segment( *this, 1 - path ) ); + } + for( auto const *neighbour : { trPrev, trNext } ) { + if( neighbour == nullptr || neighbour == this || neighbour->eType != tt_Switch || neighbour->SwitchExtension == nullptr ) { + continue; + } + auto const tip { flat( neighbour->SwitchExtension->Segments[ 0 ]->FastGetPoint_0() ) }; + if( glm::distance( start, tip ) < 0.1 ) { + blocks.emplace_back( -1e9, 0.3 ); + continue; + } + if( glm::distance( end, tip ) < 0.1 ) { + blocks.emplace_back( length - 0.3, 1e9 ); + continue; + } + for( int otherpath = 0; otherpath < 2; ++otherpath ) { + auto const &other { *neighbour->SwitchExtension->Segments[ otherpath ] }; + auto const continues = [&]( glm::dvec2 const &Point ) { + return glm::distance( Point, flat( other.FastGetPoint_0() ) ) < 0.1 || glm::distance( Point, flat( other.FastGetPoint_1() ) ) < 0.1; }; + if( false == continues( start ) && false == continues( end ) ) { + add_path( other ); + } + } + } + if( false == others.empty() ) { + auto const ours { make_gauge_line( segment, side * halfwidth, 0.0 ) }; + for( auto const &check : checks ) { + for( auto s { check.from }; s <= check.to; s += 0.1 ) { + glm::dvec3 point, tangent; + ours.at( s, point, tangent ); + auto const gauge { flat( point ) }; + auto const inward { flat( ours.normal( s, side ) ) }; + auto blocked { false }; + for( auto const &other : others ) { + for( std::size_t i = 1; i < other.points.size() && false == blocked; ++i ) { + auto const a { flat( other.points[ i - 1 ] ) }; + auto const edge { flat( other.points[ i ] ) - a }; + auto const determinant { inward.x * edge.y - inward.y * edge.x }; + if( std::abs( determinant ) < 1e-9 ) { + continue; + } + auto const offset { a - gauge }; + auto const t { ( offset.x * edge.y - offset.y * edge.x ) / determinant }; + auto const u { ( offset.x * inward.y - offset.y * inward.x ) / determinant }; + blocked = ( u > -1e-6 && u < 1.0 + 1e-6 && t > -0.05 && t < clearance ); + } + } + if( blocked ) { + blocks.emplace_back( s - 0.3, s + 0.3 ); + } + } + } + } + std::sort( blocks.begin(), blocks.end(), []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; } ); + for( auto const &check : checks ) { + auto const add = [&]( double const From, double const To ) { + result[ path ][ right ].push_back( { + From, To, + ( From > check.from || check.flarefrom ) ? check.flare : 0.0, + ( To < check.to || check.flareto ) ? check.flare : 0.0 } ); }; + auto from { check.from }; + for( auto const &block : blocks ) { + if( block.y <= from || block.x >= check.to ) { + continue; + } + if( block.x - from >= check.shortest ) { + add( from, block.x ); + } + from = std::max( from, block.y ); + } + if( check.to - from >= check.shortest ) { + add( from, check.to ); + } + } + } + } + return result; +} + +TTrack::switch_frog TTrack::frog() const { + + switch_frog result; + if( eType != tt_Switch || false == cut_rails( *this ) ) { + return result; + } + result.side[ 0 ] = ( SwitchExtension->RightSwitch ? 1 : -1 ); + result.side[ 1 ] = -result.side[ 0 ]; + auto const halfwidth { 0.5 * std::abs( fTrackWidth ) }; + std::vector lines[ 2 ]; + std::vector distances[ 2 ]; + for( int path = 0; path < 2; ++path ) { + auto const &segment { *SwitchExtension->Segments[ path ] }; + auto const length { segment.GetLength() }; + result.blades[ path ] = blade_length( *this, path ); + auto const count { std::max( 1, static_cast( std::ceil( length / 0.25 ) ) ) }; + for( int i = 0; i <= count; ++i ) { + auto const distance { length * i / count }; + glm::dvec3 point, across; + segment.plan_frame( distance, point, across ); + auto const gauge { point + across * ( result.side[ path ] * halfwidth ) }; + lines[ path ].emplace_back( gauge.x, gauge.z ); + distances[ path ].push_back( distance ); + } + } + auto const cross = []( glm::dvec2 const &A, glm::dvec2 const &B ) { + return A.x * B.y - A.y * B.x; }; + for( std::size_t i = 1; i < lines[ 0 ].size() && false == result.valid; ++i ) { + if( distances[ 0 ][ i ] <= result.blades[ 0 ] ) { + continue; + } + auto const a0 { lines[ 0 ][ i - 1 ] }; + auto const a { lines[ 0 ][ i ] - a0 }; + for( std::size_t j = 1; j < lines[ 1 ].size(); ++j ) { + if( distances[ 1 ][ j ] <= result.blades[ 1 ] ) { + continue; + } + auto const b0 { lines[ 1 ][ j - 1 ] }; + auto const b { lines[ 1 ][ j ] - b0 }; + auto const denominator { cross( a, b ) }; + if( std::abs( denominator ) < 1e-12 ) { + continue; + } + auto const u { cross( b0 - a0, b ) / denominator }; + auto const v { cross( b0 - a0, a ) / denominator }; + if( u < 0.0 || u > 1.0 || v < 0.0 || v > 1.0 ) { + continue; + } + result.at[ 0 ] = glm::mix( distances[ 0 ][ i - 1 ], distances[ 0 ][ i ], u ); + result.at[ 1 ] = glm::mix( distances[ 1 ][ j - 1 ], distances[ 1 ][ j ], v ); + result.valid = true; + break; + } + } + if( false == result.valid ) { + return result; + } + glm::dvec3 point, across0, across1; + SwitchExtension->Segments[ 0 ]->plan_frame( result.at[ 0 ], point, across0 ); + SwitchExtension->Segments[ 1 ]->plan_frame( result.at[ 1 ], point, across1 ); + result.angle = std::acos( std::min( 1.0, std::abs( glm::dot( across0, across1 ) ) ) ); + if( result.angle < 0.01 || result.angle > 0.5 ) { + result.valid = false; + return result; + } + for( int path = 0; path < 2; ++path ) { + auto const throat { result.at[ path ] - flangeway / std::tan( result.angle ) }; + if( throat <= result.blades[ path ] || result.at[ path ] >= SwitchExtension->Segments[ path ]->GetLength() ) { + result.valid = false; + } + } + return result; +} + +namespace { + +glm::dvec2 plan( glm::dvec3 const &V ) { + + return glm::dvec2{ V.x, V.z }; +} + +// a wing rail ending right beside a check rail of the rail it runs along goes on in its place up to the far end of it, past the end of the wing +std::vector wing_extension( TTrack &Track, TTrack::check_rail_set const &Checks, int const Path, int const Side, double const At, double const Toward, double const Slope, TTrack &Partner, int const Partnerpath, int const Partnerside, double const Partnerat, double const Partnerdirection ) { + + std::vector knots; + auto const partnerchecks { &Partner == &Track ? Checks : Partner.check_rails( Partner.rail_crossings() ) }; + auto const wingend { Partnerat + Partnerdirection * wing_reach( Slope ) }; + auto farend { 0.0 }; + if( false == replaced_check( partnerchecks[ Partnerpath ][ Partnerside > 0 ], wingend, Partnerdirection, farend ) ) { + return knots; + } + auto const theirs { make_gauge_line( path_segment( Partner, Partnerpath ), Partnerside * ( 0.5 * std::abs( Partner.fTrackWidth ) - flangeway ), 0.0 ) }; + auto const ours { make_gauge_line( path_segment( Track, Path ), Side * ( 0.5f * std::abs( Track.fTrackWidth ) ), 0.0 ) }; + auto s { At - Toward * wing_reach( Slope ) }; + auto const count { std::max( 1, static_cast( std::ceil( std::abs( farend - wingend ) / 0.5 ) ) ) }; + for( int i = 1; i <= count; ++i ) { + glm::dvec3 target, tangent, point; + theirs.at( glm::mix( wingend, farend, static_cast( i ) / count ), target, tangent ); + for( int step = 0; step < 4; ++step ) { + ours.at( s, point, tangent ); + s += glm::dot( plan( target - point ), glm::normalize( plan( tangent ) ) ); + } + ours.at( s, point, tangent ); + knots.emplace_back( s, -glm::dot( plan( target - point ), plan( ours.normal( s, Side ) ) ) ); + } + return knots; +} + +// acute frog: the rail coming from the throat bends out as a wing rail, the rail going on behind the gap is a leg of the nose +void acute( TTrack::rail_plan &Plan, double const At, double const Toward, double const Slope, bool const Nose, std::vector const &Extension, TTrack::rail_size const &Size ) { + + auto const railhead { Size.head }; + auto const apart { Size.high + 0.1 }; + auto const throat { At + Toward * flangeway / Slope }; + auto const flare { At - Toward * wing_reach( Slope ) }; + auto const tip { Nose ? nose_tip : railhead + 0.002 }; + auto const nose { At - Toward * tip / Slope }; + auto const blunt { At - Toward * apart / Slope }; + auto const reach = [&]( double const Apart ) { + return Nose ? Apart : Apart - railhead + 0.002; }; + std::vector wing { { throat, 0.0 }, { flare, std::abs( flare - throat ) * Slope } }; + wing.insert( wing.end(), Extension.begin(), Extension.end() ); + auto const wingend { Extension.empty() ? flare : Extension.back().x }; + std::vector rolled; + for( auto const &knot : wing ) { + rolled.emplace_back( knot.x, rolled_band - knot.y ); + } + std::sort( rolled.begin(), rolled.end(), []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; } ); + TTrack::rail_cut cut; + cut.at = At; + if( Toward < 0.0 ) { + cut.end = wingend; + cut.endoffsets = wing; + cut.endrolled = rolled; + cut.start = nose; + cut.startextents = { { nose, reach( tip ) }, { blunt, reach( apart ) } }; + } + else { + cut.end = nose; + cut.endextents = { { blunt, reach( apart ) }, { nose, reach( tip ) } }; + cut.start = wingend; + cut.startoffsets = wing; + cut.startrolled = rolled; + } + Plan.cuts.push_back( cut ); +} + +// obtuse crossing: the rail ends where it meets the other one and starts again pointed past its flangeway, a check rail beside the point guards the gap +void obtuse( TTrack::rail_plan &Plan, double const At, double const Into, TTrack::rail_size const &Size ) { + + auto const gap { flangeway / std::abs( Into ) }; + auto const full { Size.high + 0.0135 }; + auto const taper { ( flangeway + full ) / std::abs( Into ) }; + TTrack::rail_cut cut; + cut.at = At; + if( Into > 0.0 ) { + cut.end = At; + cut.start = At + gap; + cut.startextents = { { At + gap, 0.006 }, { At + taper, full } }; + } + else { + cut.end = At - gap; + cut.endextents = { { At - taper, full }, { At - gap, 0.006 } }; + cut.start = At; + } + Plan.cuts.push_back( cut ); +} + +// crossing at a wide angle: running rails are broken for the flanges of the other track, the ones of the track not going through also where the head of the other rail lies; guard rails give way to the running rails and flangeways of the other track, the ones of the track not going through also to its guard rails +void wide( TTrack::rail_plan &Plan, double const At, double const Into, double const Facing, double const From, double const To, double const Inner, double const Outer ) { + + auto const low { ( From + std::min( Inner * Facing, Outer * Facing ) - 0.005 ) / Into }; + auto const high { ( To + std::max( Inner * Facing, Outer * Facing ) + 0.005 ) / Into }; + TTrack::rail_cut cut; + cut.at = At; + cut.end = At + std::min( low, high ); + cut.start = At + std::max( low, high ); + cut.endmiter = { At + ( Into > 0.0 ? From - 0.005 : To + 0.005 ) / Into, Facing / Into }; + cut.startmiter = { At + ( Into > 0.0 ? To + 0.005 : From - 0.005 ) / Into, Facing / Into }; + Plan.cuts.push_back( cut ); +} + +bool goes_through( TTrack const &Track, TTrack const &Other ) { + + if( Track.name() != Other.name() ) { + return Track.name() < Other.name(); + } + auto const here { Track.location() }; + auto const there { Other.location() }; + return ( here.x != there.x ? here.x < there.x : here.z < there.z ); +} + +// crossings at a middle angle: guard rails along the sides of the quadrilateral between the tracks, glued at its corners to the guard rails of the other track +struct frame_corner { + TTrack *track { nullptr }; + int path { 0 }; + int side { 0 }; + int otherpath { 0 }; + double at { 0.0 }; + double into { 0.0 }; + double facing { 0.0 }; +}; + +} // namespace + +TTrack::rail_layout TTrack::plan_rails( bool const Crossings ) { + + rail_layout layout; + auto const size { rail_dimensions() }; + auto const railhead { size.head }; + std::vector crossings; + if( Crossings ) { + crossings = rail_crossings(); + } + layout.checks = check_rails( crossings ); + auto const &checks { layout.checks }; + auto &plans { layout.plans }; + auto &guards { layout.guards }; + auto &wingends { layout.wingends }; + auto const guardoffset { -( flangeway + railhead ) }; + std::vector corners; + if( Crossings ) { + for( auto const &crossing : crossings ) { + auto &plan { plans[ crossing.path ][ crossing.side > 0 ] }; + if( crossing.angle > middle_angle && crossing.angle <= wide_angle ) { + corners.push_back( { crossing.track, crossing.path, crossing.side, crossing.otherpath, crossing.at, crossing.into, crossing.facing } ); + } + if( crossing.angle > wide_angle ) { + auto const other { crossing.track->rail_dimensions() }; + auto const through { goes_through( *this, *crossing.track ) }; + wide( plan, crossing.at, crossing.into, crossing.facing, through ? 0.0 : -other.head, flangeway, 0.0, railhead ); + auto const reach { ( flangeway + railhead + 0.3 ) / std::abs( crossing.into ) }; + auto &spans { guards[ crossing.path ][ crossing.side > 0 ] }; + TTrack::guard_rail guard { crossing.at - reach, crossing.at + reach }; + wide( guard.plan, crossing.at, crossing.into, crossing.facing, -other.head, through ? flangeway : flangeway + other.head, guardoffset, guardoffset + railhead ); + auto merged { false }; + for( auto &span : spans ) { + if( guard.from < span.to + 2.0 && guard.to > span.from - 2.0 ) { + span.from = std::min( span.from, guard.from ); + span.to = std::max( span.to, guard.to ); + span.plan.cuts.insert( span.plan.cuts.end(), guard.plan.cuts.begin(), guard.plan.cuts.end() ); + merged = true; + break; + } + } + if( false == merged ) { + spans.push_back( guard ); + } + } + else if( crossing.facing < 0.0 ) { + auto const toward { crossing.into > 0.0 ? 1.0 : -1.0 }; + auto const slope { std::tan( crossing.angle ) }; + wingends[ crossing.path ][ crossing.side > 0 ].emplace_back( crossing.at - toward * wing_reach( slope ), -toward ); + acute( plan, crossing.at, toward, slope, crossing.nose, + wing_extension( *this, checks, crossing.path, crossing.side, crossing.at, toward, slope, *crossing.track, crossing.otherpath, crossing.otherside, crossing.otherat, crossing.aligned ? -toward : toward ), size ); + } + else { + obtuse( plan, crossing.at, crossing.into, size ); + } + if( std::find( layout.crossed.begin(), layout.crossed.end(), crossing.track ) == layout.crossed.end() ) { + layout.crossed.push_back( crossing.track ); + } + } + for( auto &pathguards : guards ) { + std::vector spans[ 2 ]; + for( int side = 0; side < 2; ++side ) { + for( auto const &guard : pathguards[ side ] ) { + spans[ side ].emplace_back( guard.from, guard.to ); + } + } + for( int side = 0; side < 2; ++side ) { + for( auto &guard : pathguards[ side ] ) { + for( auto const &opposite : spans[ 1 - side ] ) { + if( opposite.x < guard.to && opposite.y > guard.from ) { + guard.from = std::min( guard.from, opposite.x ); + guard.to = std::max( guard.to, opposite.y ); + } + } + } + } + } + std::sort( corners.begin(), corners.end(), []( frame_corner const &A, frame_corner const &B ) { return A.at < B.at; } ); + auto const glued = [&]( frame_corner const &Corner ) { + return Corner.at + ( flangeway + std::max( guardoffset * Corner.facing, ( guardoffset + railhead ) * Corner.facing ) ) / Corner.into; }; + auto const gluedmiter = [&]( frame_corner const &Corner ) { + return glm::dvec2 { Corner.at + flangeway / Corner.into, Corner.facing / Corner.into }; }; + for( std::size_t i = 0; i < corners.size(); ++i ) { + auto const &start { corners[ i ] }; + if( start.into <= 0.0 ) { + continue; + } + for( std::size_t j = i + 1; j < corners.size(); ++j ) { + auto const &end { corners[ j ] }; + if( end.track != start.track || end.path != start.path || end.side != start.side || end.otherpath != start.otherpath ) { + continue; + } + if( end.into < 0.0 ) { + TTrack::guard_rail guard { glued( start ), glued( end ) }; + guard.flarefrom = false; + guard.flareto = false; + guard.frommiter = gluedmiter( start ); + guard.tomiter = gluedmiter( end ); + for( auto const *corner : { &start, &end } ) { + if( corner->facing < 0.0 ) { + continue; + } + auto const forward { corner == &end }; + auto reach { forward ? guard.to : guard.from }; + for( auto const &opposite : corners ) { + if( opposite.track != start.track || opposite.path != start.path || opposite.side != -start.side || opposite.otherpath != start.otherpath ) { + continue; + } + auto const margin { ( flangeway + railhead + 0.3 ) / std::abs( opposite.into ) }; + reach = forward ? std::max( reach, opposite.at + margin ) : std::min( reach, opposite.at - margin ); + } + if( reach == ( forward ? guard.to : guard.from ) ) { + continue; + } + wide( guard.plan, corner->at, corner->into, corner->facing, -corner->track->rail_dimensions().head, flangeway, guardoffset, guardoffset + railhead ); + auto &cut { guard.plan.cuts.back() }; + if( forward ) { + cut.end = guard.to; + cut.endmiter = guard.tomiter; + guard.to = reach; + guard.tomiter = {}; + guard.flareto = true; + } + else { + cut.start = guard.from; + cut.startmiter = guard.frommiter; + guard.from = reach; + guard.frommiter = {}; + guard.flarefrom = true; + } + } + if( guard.to - guard.from > 0.1 ) { + guards[ start.path ][ start.side > 0 ].push_back( guard ); + } + } + break; + } + } + } + if( eType == tt_Switch ) { + auto const frog { this->frog() }; + if( frog.valid ) { + auto const slope { std::tan( frog.angle ) }; + for( int path = 0; path < 2; ++path ) { + wingends[ path ][ frog.side[ path ] > 0 ].emplace_back( frog.at[ path ] + wing_reach( slope ), 1.0 ); + acute( plans[ path ][ frog.side[ path ] > 0 ], frog.at[ path ], -1.0, slope, path == 0, + wing_extension( *this, checks, path, frog.side[ path ], frog.at[ path ], -1.0, slope, *this, 1 - path, frog.side[ 1 - path ], frog.at[ 1 - path ], 1.0 ), size ); + } + } + } + return layout; +} + +TTrack::rail_drawing TTrack::make_rail_drawing( gfx::geometrybank_handle const &Bank ) { + + rail_drawing drawing; + drawing.bank = Bank; + drawing.size = rail_dimensions(); + drawing.halfwidth = 0.5f * std::abs( fTrackWidth ); + // check rail: a narrow head facing the running rail across the flangeway, on a thin web with a foot turned away from it, sized after the rail + gfx::vertex_array checkshape { + {{ -0.041f, -0.170f, 0.f }, { 1.000f, 0.000f, 0.f }, { 0.15f, 0.f }}, + {{ -0.041f, -0.010f, 0.f }, { 0.707f, 0.707f, 0.f }, { 0.40f, 0.f }}, + {{ -0.051f, -0.000f, 0.f }, { 0.000f, 1.000f, 0.f }, { 0.45f, 0.f }}, + {{ -0.071f, -0.000f, 0.f }, { 0.000f, 1.000f, 0.f }, { 0.55f, 0.f }}, + {{ -0.081f, -0.010f, 0.f }, { -0.707f, 0.707f, 0.f }, { 0.60f, 0.f }}, + {{ -0.081f, -0.045f, 0.f }, { -0.707f, -0.707f, 0.f }, { 0.65f, 0.f }}, + {{ -0.066f, -0.060f, 0.f }, { -0.707f, -0.707f, 0.f }, { 0.75f, 0.f }}, + {{ -0.066f, -0.150f, 0.f }, { -0.707f, 0.707f, 0.f }, { 0.85f, 0.f }}, + {{ -0.101f, -0.170f, 0.f }, { -1.000f, 0.000f, 0.f }, { 1.00f, 0.f }} }; + for( auto &point : checkshape ) { + point.position.x = -0.041f + ( point.position.x + 0.041f ) * static_cast( drawing.size.head / 0.0735 ); + point.position.y *= static_cast( drawing.size.height / 0.172 ); + } + create_track_rail_profile( drawing.checkshapes[ 0 ], drawing.checkshapes[ 1 ], &checkshape ); + for( auto &shape : drawing.checkshapes ) { + shape.resize( checkshape.size() ); + } + return drawing; +} + +bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, int const Path, gfx::vertex_array const &Profile, int const Side, float const Texturelength, double const From, bool const Openstart, bool const Openend, geometryhandle_sequence &Geometry, geometryhandle_sequence &Cuts ) { + + gfx::userdata_array empty_userdata; + auto const &size { Drawing.size }; + auto const halfwidth { Drawing.halfwidth }; + auto const railhead { size.head }; + auto const guardoffset { -( flangeway + railhead ) }; + auto const &segment { path_segment( *this, Path ) }; + auto const &plan { Layout.plans[ Path ][ Side > 0 ] }; + auto const length { segment.GetLength() }; + // parts no wheel rolls on go in the matte material of the cut faces + auto const piece = [&]( double const Start, double const End, std::vector const &Offsets, std::vector const &Extents, bool const Capstart, bool const Capend, gfx::vertex_array const &Shape, bool const Rolled, std::vector const &Inner, std::vector const &Ramp = {} ) { + gfx::vertex_array vertices; + gfx::userdata_array userdata; + if( Shape.size() > 1 && segment.RenderLoftSpan( vertices, m_origin, Shape, Side, halfwidth, Texturelength, Start, End, Offsets, Extents, 0.f, Inner, Rolled ? nullptr : &userdata, Ramp, Ramp.empty() ? 0.0 : blend ) ) { + cut_faces( vertices, Shape.size(), Capstart, Capend, Drawing.bank, Cuts ); + gfx::vertex_array triangles; + gfx::userdata_array triangleuserdata; + auto const ring { 2 * Shape.size() }; + for( std::size_t block = 0; block + ring <= vertices.size(); block += ring ) { + for( std::size_t j = 0; j + 1 < Shape.size(); ++j ) { + for( auto const index : { 0, 1, 2, 1, 3, 2 } ) { + auto const vertex { block + 2 * j + index }; + triangles.push_back( vertices[ vertex ] ); + if( false == Rolled ) { + triangleuserdata.push_back( userdata[ vertex ] ); + } + } + } + } + ( Rolled ? Geometry : Cuts ).emplace_back( GfxRenderer->Insert( triangles, Rolled ? empty_userdata : triangleuserdata, Drawing.bank, GL_TRIANGLES ) ); + } + }; + auto const level = []( std::vector const &Knots, double const Distance ) { + if( Distance <= Knots.front().x ) { + return Knots.front().y; + } + for( std::size_t i = 1; i < Knots.size(); ++i ) { + if( Distance <= Knots[ i ].x ) { + return glm::mix( Knots[ i - 1 ].y, Knots[ i ].y, ( Distance - Knots[ i - 1 ].x ) / std::max( Knots[ i ].x - Knots[ i - 1 ].x, 1e-6 ) ); + } + } + return Knots.back().y; }; + auto const lowest = [&]( std::vector const &A, std::vector const &B ) { + if( A.empty() || B.empty() ) { + return ( A.empty() ? B : A ); + } + std::vector result; + for( auto const *knots : { &A, &B } ) { + for( auto const &knot : *knots ) { + result.emplace_back( knot.x, std::min( level( A, knot.x ), level( B, knot.x ) ) ); + } + } + std::sort( result.begin(), result.end(), []( glm::dvec2 const &L, glm::dvec2 const &R ) { return L.x < R.x; } ); + return result; }; + // the running surface on top of the head is split along the edge of the wheel treads, the part of it out of their reach goes matte; the rest of the profile and the end faces are laid whole + std::size_t topfirst { Profile.size() }; + std::size_t toplast { 0 }; + for( std::size_t j = 0; j < Profile.size(); ++j ) { + if( Profile[ j ].position.y > static_cast( -0.087 * size.height ) ) { + topfirst = std::min( topfirst, j ); + toplast = std::max( toplast, j ); + } + } + auto const strip = [&]( std::size_t const First, std::size_t const Last ) { + return gfx::vertex_array( Profile.begin() + First, Profile.begin() + Last + 1 ); }; + auto const running = [&]( double const Start, double const End, std::vector const &Offsets, std::vector const &Extents, std::vector const &Rolled, bool const Capstart, bool const Capend ) { + gfx::vertex_array vertices; + if( segment.RenderLoftSpan( vertices, m_origin, Profile, Side, halfwidth, Texturelength, Start, End, Offsets, Extents ) ) { + cut_faces( vertices, Profile.size(), Capstart, Capend, Drawing.bank, Cuts ); + } + piece( Start, End, Offsets, Extents, false, false, strip( 0, topfirst ), true, {} ); + piece( Start, End, Offsets, Extents, false, false, strip( toplast, Profile.size() - 1 ), true, {} ); + auto const top { strip( topfirst, toplast ) }; + auto low { 1e9 }; + auto high { -1e9 }; + for( auto const &point : top ) { + low = std::min( low, static_cast( Side * point.position.x - halfwidth ) ); + high = std::max( high, static_cast( Side * point.position.x - halfwidth ) ); + } + std::vector inner; + for( auto const &knot : Rolled ) { + inner.emplace_back( knot.x, knot.y - blend ); + } + std::vector splits { Start, End }; + for( auto const edge : { low, high, low + blend, high + blend } ) { + for( std::size_t i = 1; i < Rolled.size(); ++i ) { + auto const &a { Rolled[ i - 1 ] }; + auto const &b { Rolled[ i ] }; + if( ( a.y - edge ) * ( b.y - edge ) < 0.0 ) { + auto const distance { a.x + ( edge - a.y ) / ( b.y - a.y ) * ( b.x - a.x ) }; + if( distance > Start && distance < End ) { + splits.push_back( distance ); + } + } + } + } + std::sort( splits.begin(), splits.end() ); + auto const shiny { lowest( Extents, inner ) }; + auto const blended { lowest( Extents, Rolled ) }; + for( std::size_t i = 1; i < splits.size(); ++i ) { + auto const reach { level( Rolled, 0.5 * ( splits[ i - 1 ] + splits[ i ] ) ) }; + if( reach - blend > low ) { + piece( splits[ i - 1 ], splits[ i ], Offsets, shiny, false, false, top, true, {} ); + } + if( reach > low && reach - blend < high ) { + piece( splits[ i - 1 ], splits[ i ], Offsets, blended, false, false, top, false, inner, Rolled ); + } + if( reach < high ) { + piece( splits[ i - 1 ], splits[ i ], Offsets, Extents, false, false, top, false, Rolled ); + } + } + }; + for( auto const &guard : Layout.guards[ Path ][ Side > 0 ] ) { + auto const flare { std::min( 0.3, 0.25 * ( guard.to - guard.from ) ) }; + std::vector offsets; + if( guard.flarefrom ) { + offsets.insert( offsets.end(), { { guard.from, guardoffset - 0.02 }, { guard.from + flare, guardoffset } } ); + } + else { + offsets.push_back( { guard.from, guardoffset } ); + } + if( guard.flareto ) { + offsets.insert( offsets.end(), { { guard.to - flare, guardoffset }, { guard.to, guardoffset - 0.02 } } ); + } + else { + offsets.push_back( { guard.to, guardoffset } ); + } + auto const top { flangeway + railhead + size.low - 0.002 }; + auto const mitered = [&]( glm::dvec2 const &Miter, double const Square, bool const Latest ) { + if( Miter.y == 0.0 ) { + return Square; + } + auto const inside { Miter.x + Miter.y * ( size.low + guardoffset ) }; + auto const outside { Miter.x + Miter.y * ( top + guardoffset ) }; + return Latest ? std::max( inside, outside ) : std::min( inside, outside ); }; + auto const envelope = [&]( std::vector const &Lines, bool const Upper, double const A, double const B ) { + std::vector knots; + if( Lines.empty() ) { + return knots; + } + std::vector stations { A, B }; + for( std::size_t i = 0; i < Lines.size(); ++i ) { + for( std::size_t j = i + 1; j < Lines.size(); ++j ) { + if( std::abs( Lines[ i ].x - Lines[ j ].x ) > 1e-12 ) { + auto const d { ( Lines[ j ].y - Lines[ i ].y ) / ( Lines[ i ].x - Lines[ j ].x ) }; + if( d > A && d < B ) { + stations.push_back( d ); + } + } + } + } + std::sort( stations.begin(), stations.end() ); + for( auto const d : stations ) { + auto value { Upper ? 1e6 : -1e6 }; + for( auto const &line : Lines ) { + value = Upper ? std::min( value, line.x * d + line.y ) : std::max( value, line.x * d + line.y ); + } + knots.emplace_back( d, value ); + } + return knots; }; + auto cuts { guard.plan.cuts }; + std::sort( cuts.begin(), cuts.end(), []( rail_cut const &A, rail_cut const &B ) { return A.at < B.at; } ); + auto start { guard.from }; + auto startmiter { guard.frommiter }; + for( std::size_t i = 0; i <= cuts.size(); ++i ) { + auto const end { i < cuts.size() ? cuts[ i ].end : guard.to }; + auto const endmiter { i < cuts.size() ? cuts[ i ].endmiter : guard.tomiter }; + auto const a { std::max( { mitered( startmiter, start, false ), From, 0.0 } ) }; + auto const b { std::min( { mitered( endmiter, end, true ), length, mitered( guard.tomiter, guard.to, true ) } ) }; + if( b - a > 0.01 ) { + std::vector uppers { { 0.0, top } }; + std::vector lowers; + for( auto const &[ miter, ending ] : { std::pair{ startmiter, false }, std::pair{ endmiter, true } } ) { + if( miter.y != 0.0 ) { + ( ending == ( miter.y < 0.0 ) ? uppers : lowers ).emplace_back( 1.0 / miter.y, -miter.x / miter.y - guardoffset ); + } + } + piece( a, b, offsets, envelope( uppers, true, a, b ), true, true, Profile, false, envelope( lowers, false, a, b ) ); + } + if( i < cuts.size() && cuts[ i ].start > start ) { + start = cuts[ i ].start; + startmiter = cuts[ i ].startmiter; + } + } + } + auto const &shape { Drawing.checkshapes[ Side > 0 ? 0 : 1 ] }; + for( auto const &check : Layout.checks[ Path ][ Side > 0 ] ) { + auto const replaced { std::any_of( Layout.wingends[ Path ][ Side > 0 ].begin(), Layout.wingends[ Path ][ Side > 0 ].end(), [&]( glm::dvec2 const &Wing ) { + auto farend { 0.0 }; + return replaced_check( { check }, Wing.x, Wing.y, farend ); } ) }; + auto const from { check.from }; + auto const to { check.to }; + auto const a { std::max( from, From ) }; + auto const b { std::min( to, length ) }; + if( replaced || b - a <= 0.01 ) { + continue; + } + auto const flare { std::min( 0.6, 0.4 * ( to - from ) ) }; + std::vector offsets; + if( check.flarefrom > 0.0 ) { + offsets.insert( offsets.end(), { { from, -check.flarefrom }, { from + flare, 0.0 } } ); + } + else { + offsets.push_back( { from, 0.0 } ); + } + if( check.flareto > 0.0 ) { + offsets.insert( offsets.end(), { { to - flare, 0.0 }, { to, -check.flareto } } ); + } + else { + offsets.push_back( { to, 0.0 } ); + } + piece( a, b, offsets, {}, a > 0.0 || from >= 0.0, to <= length, shape, false, {} ); + } + if( plan.cuts.empty() ) { + return false; + } + auto cuts { plan.cuts }; + std::sort( cuts.begin(), cuts.end(), []( rail_cut const &A, rail_cut const &B ) { return A.at < B.at; } ); + auto const byposition = []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; }; + auto start { -1e9 }; + std::vector rolledfrom; + std::vector offsets, extents; + for( std::size_t i = 0; i <= cuts.size(); ++i ) { + auto const end { i < cuts.size() ? cuts[ i ].end : 1e9 }; + auto pieceoffsets { offsets }; + auto pieceextents { extents }; + if( i < cuts.size() ) { + pieceoffsets.insert( pieceoffsets.end(), cuts[ i ].endoffsets.begin(), cuts[ i ].endoffsets.end() ); + pieceextents.insert( pieceextents.end(), cuts[ i ].endextents.begin(), cuts[ i ].endextents.end() ); + } + std::sort( pieceoffsets.begin(), pieceoffsets.end(), byposition ); + std::sort( pieceextents.begin(), pieceextents.end(), byposition ); + auto const a { std::max( start, From ) }; + auto const b { std::min( end, length ) }; + auto const rolled { i < cuts.size() ? lowest( rolledfrom, cuts[ i ].endrolled ) : rolledfrom }; + auto const capstart { start > From ? true : Openstart }; + auto const capend { end < length ? true : Openend }; + if( b - a > 0.01 ) { + if( rolled.empty() || topfirst >= toplast ) { + piece( a, b, pieceoffsets, pieceextents, capstart, capend, Profile, true, {} ); + } + else { + running( a, b, pieceoffsets, pieceextents, rolled, capstart, capend ); + } + } + if( i < cuts.size() ) { + rolledfrom = cuts[ i ].startrolled; + start = cuts[ i ].start; + offsets = cuts[ i ].startoffsets; + extents = cuts[ i ].startextents; + } + } + return true; +} diff --git a/world/TrackRails.h b/world/TrackRails.h new file mode 100644 index 00000000..fb2f39b9 --- /dev/null +++ b/world/TrackRails.h @@ -0,0 +1,37 @@ +/* +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 "world/Track.h" + +namespace track_rails { + +double constexpr flangeway { 0.045 }; +double constexpr blend { 0.015 }; +double constexpr rolled_band { 0.1 }; +double constexpr nose_tip { 0.012 }; +double constexpr middle_angle { 0.35 }; +double constexpr wide_angle { 0.785 }; + +// how far a wing rail reaches past the crossing point of the rails +double wing_reach( double const Slope ); +double check_length( double const Slope ); +// the rails of the track are cut where they cross other rails: plain tracks, and switches whose frog is made from the rails +bool cut_rails( TTrack const &Track ); +int blade_steps( TTrack const &Track ); +double blade_length( TTrack const &Track, int const Path ); +material_handle cut_material( material_handle const Material ); +// triangles closing the first and/or the last cross-section of a lofted strip, made of pairs of points for each of Ring profile points, both sides of each +void cap_loft( gfx::vertex_array const &Strip, std::size_t const Ring, bool const Start, bool const End, gfx::vertex_array &Triangles ); +gfx::userdata_array worn_userdata( std::size_t const Count ); +// the ends of rails left open: at track ends with nothing joined, blade tips, the gap of a frog +void cut_faces( gfx::vertex_array const &Strip, std::size_t const Ring, bool const Start, bool const End, gfx::geometrybank_handle const &Bank, TTrack::geometryhandle_sequence &Geometry ); + +} // namespace track_rails