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