diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 94aa77bd..64de97c1 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -2486,6 +2486,73 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co editor_mode::selftest_result editor_mode::selftest_rails(std::string const &command, std::istringstream &words) { + if (command == "blade") + { + std::string name; + words >> name; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->SwitchExtension == nullptr) + continue; + if (false == name.empty() && track->name() != name) + continue; + auto const vertices = [&]() { + return std::make_pair(track->Geometry1.empty() ? std::size_t{0} : GfxRenderer->Vertices(track->Geometry1[0]).size(), track->Geometry2.empty() ? std::size_t{0} : GfxRenderer->Vertices(track->Geometry2[0]).size()); + }; + auto const set = [&](int const Path) { + track->Switch(Path); + track->SwitchExtension->fOffset = track->SwitchExtension->fDesiredOffset; + track->RaAnimate(); + }; + set(0); + auto const straight{vertices()}; + set(1); + auto const diverging{vertices()}; + set(0); + auto const frog{track->frog()}; + auto const radius{std::abs(track->m_paths.size() > 1 ? track->m_paths[1].radius : 0.f)}; + for (int path = 0; path < 2; ++path) + { + auto const &shape{track->blade(path)}; + auto const &segment{*track->SwitchExtension->Segments[path]}; + auto const old{segment.GetLength() * track_rails::blade_steps(*track, path) / segment.RaSegCount()}; + auto const throat{frog.valid ? frog.at[path] - track_rails::flangeway / std::tan(frog.angle) : 0.0}; + auto const at = [&](double const Width) { + for (std::size_t i = 1; i < shape.gauge.size(); ++i) + if (shape.gauge[i].y >= Width) + return glm::mix(shape.gauge[i - 1].x, shape.gauge[i].x, (Width - shape.gauge[i - 1].y) / std::max(shape.gauge[i].y - shape.gauge[i - 1].y, 1e-9)); + return -1.0; + }; + auto const gauge = [&](double const Distance) { + for (std::size_t i = 1; i < shape.gauge.size(); ++i) + if (shape.gauge[i].x >= Distance) + return glm::mix(shape.gauge[i - 1].y, shape.gauge[i].y, (Distance - shape.gauge[i - 1].x) / std::max(shape.gauge[i].x - shape.gauge[i - 1].x, 1e-9)); + return shape.gauge.empty() ? 0.0 : shape.gauge.back().y; + }; + std::string samples; + auto worst{0.0}; + for (auto const distance : {1.0, 2.0, 4.0, 6.0, 8.0, 10.0}) + { + auto const analytic{radius > 0.f ? distance * distance / (2.0 * radius) : 0.0}; + samples += format(" %.0f:%.4f/%.4f", distance, gauge(distance), analytic); + if (shape.valid && distance <= shape.heel) + worst = std::max(worst, std::abs(gauge(distance) - analytic)); + } + std::string faults; + if (track_rails::cut_rails(*track) && false == shape.valid) + faults += " FAULT blade not cut from the rails"; + if (shape.valid && shape.opening < track_rails::blade_clearance - 1e-4) + faults += " FAULT flangeway"; + if (shape.valid && frog.valid && shape.heel >= throat) + faults += " FAULT heel past the throat"; + if (straight != diverging) + faults += " FAULT vertex count changes"; + WriteLog(format("SELFTEST blade %s path %d: %s, R %.0f, tip %.3f, 50mm %.3f, full head %.3f, heel %.3f (old %.3f), drop %.4f, opening %.4f at %.3f, throat %.3f, d(x)/x^2/2R%s, worst %.4f, vertices %zu/%zu %zu/%zu%s", track->name().c_str(), path, shape.valid ? (shape.rigid ? "geometric rigid" : "geometric") : "old", radius, shape.tip, at(track_rails::blade_lowered + track_rails::blade_gap), shape.full, shape.heel, old, shape.drop, shape.opening, shape.openingat, + throat, samples.c_str(), worst, straight.first, diverging.first, straight.second, diverging.second, faults.c_str())); + } + } + return selftest_result::handled; + } if (command == "frog") { std::string name; @@ -2801,6 +2868,7 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm // wait // camera from the first point, looking at the second // select +// blade [name] logs the blades of each switch cut from the rails: tip, 50 mm head, full head, heel, drop of the head, open flangeway, the gauge lines apart against x^2/2R, vertices in both positions // frog [name] logs where the inner rails of each switch cross: distance on both paths, 1:n, throat, nose, check rails // guards [name] logs each guard rail and what each of its ends and breaks is cut along; a fault when a cut is not along a rail or a glued end not on the guard rail of the other track // railtime lays out the rails of every track, as when its geometry is made, and logs how long it took diff --git a/world/Segment.cpp b/world/Segment.cpp index f4e27b01..1b335ddc 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -645,7 +645,7 @@ void TSegment::plan_frame( double const Distance, glm::dvec3 &Point, glm::dvec3 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, bool const Walls, float const Wallu, glm::dvec2 const &Bends ) const { +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, bool const Walls, float const Wallu, glm::dvec2 const &Bends, std::vector const &Drops, glm::dvec2 const &Head ) const { if( fTsBuffer.empty() || To - From < 0.001 || ShapePoints.empty() ) return false; @@ -667,7 +667,10 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig 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 distance { std::ceil( From / 0.5 ) * 0.5 }; distance < std::min( To, m_dense ); distance += 0.5 ) { + stations.push_back( distance ); + } + for( auto const *knots : { &Offsets, &Extents, &Inner, &Drops } ) { for( auto const &knot : *knots ) { if( knot.x > From && knot.x < To ) { stations.push_back( knot.x ); @@ -733,6 +736,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig innerwall = glm::vec3{ glm::normalize( tangent * slope( Inner, -1e6 ) - outward ) }; } auto const worn { sample( Ramp, Distance, -1e6 ) }; + auto const drop { sample( Drops, Distance, 0.0 ) }; auto const bend { Distance <= From ? Bends.x : Distance >= To ? Bends.y : 0.0 }; auto along { glm::vec3 { 0.f } }; if( bend != 0.0 ) { @@ -823,6 +827,9 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig wall = ( side > 0 ? outerwall : innerwall ); } } + if( drop > 0.0 && shape.position.y > Head.x && Head.y > Head.x ) { + shape.position.y -= static_cast( drop * ( shape.position.y - Head.x ) / ( Head.y - Head.x ) ); + } auto pt { parallel * static_cast( Side * ( Halfwidth + outside + offset ) ) + position + along * static_cast( outside + offset ) }; pt.y += shape.position.y + Lift; auto norm { shape.normal.x * parallel }; diff --git a/world/Segment.h b/world/Segment.h index b739cfe3..b8a947ee 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -75,6 +75,7 @@ class TSegment std::vector> m_pieces; // parts of a path made of several curves, parametrized by distance std::vector m_starts; // distance from Point1 to the start of each part std::optional> m_joints[ 2 ]; + double m_dense { 0.0 }; glm::dvec3 GetFirstDerivative(double const fTime) const; double RombergIntegral(double const fA, double const fB) const; @@ -156,13 +157,16 @@ public: 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, bool const Walls = false, float const Wallu = -1.f, glm::dvec2 const &Bends = glm::dvec2 { 0.0 } ) const; + 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, bool const Walls = false, float const Wallu = -1.f, glm::dvec2 const &Bends = glm::dvec2 { 0.0 }, std::vector const &Drops = {}, glm::dvec2 const &Head = glm::dvec2 { 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 + set_dense( double const Distance ) { + m_dense = Distance; } /* void Render(); diff --git a/world/Track.cpp b/world/Track.cpp index cf78ccb7..0fbbb693 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -1588,7 +1588,9 @@ 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 { track_rails::blade_steps( *this ) }; + auto const dense { std::max( blade( 0 ).valid ? blade( 0 ).heel : 0.0, blade( 1 ).valid ? blade( 1 ).heel : 0.0 ) }; + SwitchExtension->Segments[ 0 ]->set_dense( dense ); + SwitchExtension->Segments[ 1 ]->set_dense( dense ); // 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 ) ) }; @@ -1598,7 +1600,12 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { 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 ); + if( auto const &shape { blade( Path ) }; shape.valid ) { + SwitchExtension->Segments[ Path ]->RenderLoftSpan( vertices, m_origin, Profile, Side, 0.5f * std::abs( fTrackWidth ), Texturelength, shape.heel, SwitchExtension->Segments[ Path ]->GetLength(), {} ); + } + else { + SwitchExtension->Segments[ Path ]->RenderLoft( vertices, m_origin, Profile, false, Texturelength, 1.0, track_rails::blade_steps( *this, Path ) ); + } Geometry.emplace_back( GfxRenderer->Insert( vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP ) ); } }; @@ -2141,8 +2148,19 @@ int TTrack::blade_side( int const Path ) const { void TTrack::loft_blade( int const Path, gfx::vertex_array const &Rail, gfx::vertex_array const &Blade, float const Texturelength, gfx::vertex_array &Vertices ) const { - auto const offset { blade_side( Path ) * ( Path == 0 ? SwitchExtension->fOffset2 : fMaxOffset - SwitchExtension->fOffset1 ) }; - auto const bladelength { track_rails::blade_steps( *this ) }; + auto const opening { Path == 0 ? SwitchExtension->fOffset2 : fMaxOffset - SwitchExtension->fOffset1 }; + auto const offset { blade_side( Path ) * opening }; + if( auto const &shape { blade( Path ) }; shape.valid ) { + auto const thrown { track_rails::blade_throw * std::clamp( static_cast( opening ) / ( fMaxOffset + SwitchExtension->fOffsetDelay ), 0.0, 1.0 ) }; + std::vector offsets; + for( auto const &bend : shape.bends ) { + offsets.emplace_back( bend.x, -thrown * bend.y ); + } + auto const size { rail_dimensions() }; + SwitchExtension->Segments[ Path ]->RenderLoftSpan( Vertices, m_origin, Rail, blade_side( Path ), 0.5f * std::abs( fTrackWidth ), Texturelength, shape.tip, shape.heel, offsets, shape.extents, 0.f, {}, nullptr, {}, 0.0, true, -1.f, glm::dvec2 { 0.0 }, shape.drops, { size.neck, size.crest } ); + return; + } + auto const bladelength { track_rails::blade_steps( *this, Path ) }; // composed from two parts: transition from blade to regular rail, and regular rail SwitchExtension->Segments[ Path ]->RenderLoft( Vertices, m_origin, Blade, true, Texturelength, 1.0, 0, bladelength / 2, { offset, offset / 2 } ); SwitchExtension->Segments[ Path ]->RenderLoft( Vertices, m_origin, Rail, false, Texturelength, 1.0, bladelength / 2, bladelength, { offset / 2, 0.f } ); diff --git a/world/Track.h b/world/Track.h index 9f15fab4..01e94044 100644 --- a/world/Track.h +++ b/world/Track.h @@ -308,6 +308,21 @@ public: double angle { 0.0 }; }; switch_frog frog() const; + struct switch_blade { + bool valid { false }; + bool rigid { false }; + double tip { 0.0 }; + double full { 0.0 }; + double heel { 0.0 }; + double drop { 0.0 }; + double opening { 0.0 }; + double openingat { 0.0 }; + std::vector gauge; + std::vector extents; + std::vector drops; + std::vector bends; + }; + switch_blade const &blade( int const Path ) const; // where a rail of this track crosses a rail of another one, found from their gauge lines struct rail_crossing { TTrack *track { nullptr }; diff --git a/world/TrackRails.cpp b/world/TrackRails.cpp index 7dd82d02..31f7b262 100644 --- a/world/TrackRails.cpp +++ b/world/TrackRails.cpp @@ -147,6 +147,7 @@ struct rails_memo { std::optional> own; std::optional> joined; std::optional checks; + std::optional blades[ 2 ]; }; std::unordered_map memos; unsigned int memostamp { 0 }; @@ -192,15 +193,18 @@ bool cut_rails( TTrack const &Track ) { Track.eType == tt_Switch && Track.SwitchExtension && Track.SwitchExtension->m_frogs && Track.SwitchExtension->Segments[ 0 ] && Track.SwitchExtension->Segments[ 1 ] ); } -int blade_steps( TTrack const &Track ) { +int blade_steps( TTrack const &Track, int const Path ) { - return static_cast( std::ceil( Track.SwitchExtension->Segments[ 0 ]->RaSegCount() * 0.65 ) ); + return static_cast( std::ceil( Track.SwitchExtension->Segments[ Path ]->RaSegCount() * 0.65 ) ); } double blade_length( TTrack const &Track, int const Path ) { + if( auto const &shape { Track.blade( Path ) }; shape.valid ) { + return shape.heel; + } auto const &segment { *Track.SwitchExtension->Segments[ Path ] }; - return segment.GetLength() * blade_steps( Track ) / segment.RaSegCount(); + return segment.GetLength() * blade_steps( Track, Path ) / segment.RaSegCount(); } material_handle cut_material( material_handle const Material ) { @@ -1212,6 +1216,129 @@ std::vector nose_extents( TTrack const &Track, int const Path, int c } // namespace +TTrack::switch_blade const &TTrack::blade( int const Path ) const { + + auto &cached { memo_of( *this ).blades[ Path ] }; + if( cached.has_value() ) { + return *cached; + } + cached = switch_blade {}; + auto &result { *cached }; + if( eType != tt_Switch || false == cut_rails( *this ) || m_profiles.empty() ) { + return result; + } + auto const side { blade_side( Path ) }; + auto const &ours { rail_line( *this, Path, side > 0, false ) }; + auto const &theirs { rail_line( *this, 1 - Path, side > 0, false ) }; + auto const size { rail_dimensions() }; + auto const length { std::min( SwitchExtension->Segments[ Path ]->GetLength(), SwitchExtension->Segments[ 1 - Path ]->GetLength() ) }; + auto const count { std::max( 2, static_cast( std::ceil( length / 0.1 ) ) ) }; + auto &gauge { result.gauge }; + glm::dvec3 point, tangent; + ours.at( 0.0, point, tangent ); + auto otherat { nearest_distance( theirs, point ) }; + for( int i = 0; i <= count; ++i ) { + auto const distance { length * i / count }; + ours.at( distance, point, tangent ); + lateral( theirs, side, point, otherat ); + glm::dvec3 line; + theirs.at( otherat, line, tangent ); + auto at { distance }; + auto const gap { lateral( ours, side, line, at ) }; + gauge.emplace_back( at, gap ); + } + std::sort( gauge.begin(), gauge.end(), []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; } ); + if( std::abs( gauge.front().y ) > 0.002 ) { + return result; + } + auto const reach = [&]( double const Width ) { + for( std::size_t i = 1; i < gauge.size(); ++i ) { + if( gauge[ i ].y >= Width ) { + auto const &a { gauge[ i - 1 ] }; + auto const &b { gauge[ i ] }; + return glm::mix( a.x, b.x, std::clamp( ( Width - a.y ) / std::max( b.y - a.y, 1e-9 ), 0.0, 1.0 ) ); + } + } + return -1.0; }; + result.tip = reach( blade_point ); + result.full = reach( size.head + blade_gap ); + auto const clear { reach( size.head + blade_clearance ) }; + auto const foot { reach( size.high + blade_gap ) }; + auto const limit { 0.9 * length }; + if( result.tip < 0.0 || result.full < 0.0 || clear < 0.0 || clear > limit ) { + return result; + } + auto const bend = [&]( double const Distance, double const Heel ) { + auto const u { std::clamp( ( Heel - Distance ) / std::max( Heel - result.tip, 1e-6 ), 0.0, 1.0 ) }; + return ( result.rigid ? u : u * u * ( 3.0 - u ) * 0.5 ); }; + auto const opening = [&]( double const Heel, double &At ) { + auto least { 1e9 }; + for( auto const &sample : gauge ) { + if( sample.x < result.tip || sample.x > Heel ) { + continue; + } + auto const gap { std::max( 0.0, sample.y - size.head ) + blade_throw * bend( sample.x, Heel ) }; + if( gap < least ) { + least = gap; + At = sample.x; + } + } + return least; }; + auto at { 0.0 }; + if( opening( limit, at ) < blade_clearance ) { + result.rigid = true; + if( opening( limit, at ) < blade_clearance ) { + return result; + } + } + auto low { clear }; + auto high { limit }; + for( int step = 0; step < 40; ++step ) { + auto const middle { 0.5 * ( low + high ) }; + ( opening( middle, at ) >= blade_clearance ? high : low ) = middle; + } + result.heel = high; + result.opening = opening( result.heel, result.openingat ); + for( std::size_t i = 1; i < gauge.size() && gauge[ i ].x <= result.heel; ++i ) { + if( gauge[ i ].y < gauge[ i - 1 ].y - 1e-4 ) { + return result; + } + } + auto const &profile { track_rail_profile( m_profile1.second ) }; + auto const half { profile.size() / 2 }; + auto bladetop { -1e9 }; + for( auto i { half }; i < profile.size(); ++i ) { + bladetop = std::max( bladetop, static_cast( profile[ i ].position.y ) ); + } + result.drop = ( half > 0 ? std::max( 0.0, size.crest - bladetop ) : 0.0 ); + auto const level = [&]( double const Distance ) { + for( std::size_t i = 1; i < gauge.size(); ++i ) { + if( gauge[ i ].x >= Distance ) { + auto const &a { gauge[ i - 1 ] }; + auto const &b { gauge[ i ] }; + return glm::mix( a.y, b.y, std::clamp( ( Distance - a.x ) / std::max( b.x - a.x, 1e-9 ), 0.0, 1.0 ) ); + } + } + return gauge.back().y; }; + auto const lowered { reach( blade_lowered + blade_gap ) }; + auto const cutto { std::min( result.heel, foot >= 0.0 ? foot + blade_spacing : result.heel ) }; + for( auto distance { result.tip }; distance < cutto + 0.5 * blade_spacing; distance += blade_spacing ) { + auto const at { std::min( distance, cutto ) }; + auto const width { level( at ) - blade_gap }; + result.extents.emplace_back( at, width ); + if( result.drop > 0.0 && ( lowered < 0.0 || at <= lowered + blade_spacing ) ) { + result.drops.emplace_back( at, result.drop * std::clamp( 1.0 - width / blade_lowered, 0.0, 1.0 ) ); + } + } + auto const steps { std::max( 1, static_cast( std::ceil( ( result.heel - result.tip ) / blade_spacing ) ) ) }; + for( int i = 0; i <= steps; ++i ) { + auto const distance { glm::mix( result.tip, result.heel, static_cast( i ) / steps ) }; + result.bends.emplace_back( distance, bend( distance, result.heel ) ); + } + result.valid = true; + return result; +} + material_handle TTrack::rail_material( int const Path ) const { return ( Path == 1 && eType == tt_Switch ? m_material2 : m_material1 ); diff --git a/world/TrackRails.h b/world/TrackRails.h index be54bb86..62f97b3f 100644 --- a/world/TrackRails.h +++ b/world/TrackRails.h @@ -22,6 +22,12 @@ double constexpr rolled_band { wheel_width - ( 1.435 - back_to_back ) / 2.0 }; double constexpr nose_tip { 0.012 }; double constexpr middle_angle { 0.35 }; double constexpr wide_angle { 0.785 }; +double constexpr blade_throw { 0.160 }; +double constexpr blade_clearance { 0.058 }; +double constexpr blade_point { 0.003 }; +double constexpr blade_gap { 0.0005 }; +double constexpr blade_lowered { 0.050 }; +double constexpr blade_spacing { 0.5 }; void forget(); // how far a wing rail reaches past the crossing point of the rails @@ -29,7 +35,7 @@ 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 ); +int blade_steps( TTrack const &Track, int const Path ); double blade_length( TTrack const &Track, int const Path ); material_handle cut_material( material_handle const Material ); float tread_brightness( material_handle const Material, glm::vec2 const &Band );