From 280497846477aea53d5790ac7fbc8c198677fd93 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 03:09:08 +0200 Subject: [PATCH 01/19] feat(track): at a crossing at a wide angle the rails and guard rails ending against the side of another rail or guard rail are glued to it, their ends fitted to its profile height by height instead of square ends with caps and a gap of 5 mm, so the corners of the crossing are closed; the ends at the flangeways stay open for the flanges; a loft span can shift its first and last cross-section along the track by a fit given per height of the profile, and the rail profile gives the outlines of its field and gauge sides --- world/Segment.cpp | 30 ++++++++++++++++++------ world/Segment.h | 2 +- world/Track.cpp | 30 ++++++++++++++++++++++++ world/Track.h | 2 ++ world/TrackRails.cpp | 56 ++++++++++++++++++++++++++++++++++++++------ 5 files changed, 105 insertions(+), 15 deletions(-) diff --git a/world/Segment.cpp b/world/Segment.cpp index bcef534f..59bbbac8 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -580,7 +580,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 &Fitfrom, std::vector const &Fitto ) const { if( fTsBuffer.empty() || To - From < 0.001 || ShapePoints.empty() ) return false; @@ -628,14 +628,26 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig } 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() ); - if( Bends.x != 0.0 || Bends.y != 0.0 ) { + if( Bends.x != 0.0 || Bends.y != 0.0 || false == Fitfrom.empty() || false == Fitto.empty() ) { auto widest { 0.0 }; for( auto const &point : ShapePoints ) { widest = std::max( widest, std::abs( static_cast( Side * point.position.x - Halfwidth ) ) ); } + auto offsetmost { 0.0 }; + for( auto const &knot : Offsets ) { + offsetmost = std::max( offsetmost, std::abs( knot.y ) ); + } + auto const margin = [&]( double const Bend, std::vector const &Fit ) { + auto fitmost { 0.0 }; + for( auto const &knot : Fit ) { + fitmost = std::max( fitmost, std::abs( knot.y ) ); + } + return std::abs( Bend ) * ( Fit.empty() ? widest : widest + offsetmost ) + fitmost + 0.001; }; + auto const frommargin { margin( Bends.x, Fitfrom ) }; + auto const tomargin { margin( Bends.y, Fitto ) }; stations.erase( std::remove_if( stations.begin(), stations.end(), [&]( double const Distance ) { - return ( Distance > From && Distance < From + std::abs( Bends.x ) * widest + 0.001 ) - || ( Distance < To && Distance > To - std::abs( Bends.y ) * widest - 0.001 ); } ), stations.end() ); + return ( Distance > From && Distance < To ) + && ( Distance < From + frommargin || Distance > To - tomargin ); } ), stations.end() ); } auto const ring = [&]( double const Distance, double const Middle, std::vector &Ring, std::vector &Wear ) { @@ -669,14 +681,18 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig } auto const worn { sample( Ramp, Distance, -1e6 ) }; auto const bend { Distance <= From ? Bends.x : Distance >= To ? Bends.y : 0.0 }; + static std::vector const unfitted; + auto const &fit { Distance <= From ? Fitfrom : Distance >= To ? Fitto : unfitted }; auto along { glm::vec3 { 0.f } }; - if( bend != 0.0 ) { + auto forward { glm::vec3 { 0.f } }; + if( bend != 0.0 || false == fit.empty() ) { glm::dvec3 ahead, behind, unused; plan_frame( std::min( Distance + 0.001, static_cast( fLength ) ), ahead, unused ); plan_frame( std::max( Distance - 0.001, 0.0 ), behind, unused ); auto const tangent { glm::dvec3 { ahead.x - behind.x, 0.0, ahead.z - behind.z } }; if( glm::length2( tangent ) > 0.0 ) { - along = glm::vec3 { glm::normalize( tangent ) * bend }; + forward = glm::vec3 { glm::normalize( tangent ) }; + along = forward * static_cast( bend ); } } Ring.clear(); @@ -758,7 +774,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig wall = ( side > 0 ? outerwall : innerwall ); } } - auto pt { parallel * static_cast( Side * ( Halfwidth + outside + offset ) ) + position + along * static_cast( outside + offset ) }; + auto pt { parallel * static_cast( Side * ( Halfwidth + outside + offset ) ) + position + along * static_cast( outside + offset ) + forward * static_cast( sample( fit, shape.position.y, 0.0 ) ) }; pt.y += shape.position.y + Lift; auto norm { shape.normal.x * parallel }; norm.y += shape.normal.y; diff --git a/world/Segment.h b/world/Segment.h index 1ab6f317..1e9f4270 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -132,7 +132,7 @@ 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 &Fitfrom = {}, std::vector const &Fitto = {} ) 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; diff --git a/world/Track.cpp b/world/Track.cpp index 40c6ec5b..f701c5dd 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -2635,6 +2635,36 @@ TTrack::rail_dimensions() const { } result.height = top - bottom; result.crest = top; + std::size_t firsttop { count }; + std::size_t lasttop { 0 }; + for( std::size_t i = 0; i < count; ++i ) { + if( profile[ i ].position.y > top - 1e-4 ) { + firsttop = std::min( firsttop, i ); + lasttop = std::max( lasttop, i ); + } + } + std::vector sides[ 2 ]; + for( std::size_t i = 0; i < count; ++i ) { + auto const &point { profile[ i ].position }; + if( i <= firsttop ) { + sides[ 0 ].emplace_back( point.y, point.x ); + } + if( i >= lasttop ) { + sides[ 1 ].emplace_back( point.y, point.x ); + } + } + for( auto &side : sides ) { + std::sort( side.begin(), side.end(), []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; } ); + } + auto const mean = []( std::vector const &Side ) { + auto sum { 0.0 }; + for( auto const &knot : Side ) { + sum += knot.y; + } + return sum / std::max( Side.size(), 1 ); }; + auto const fieldfirst { mean( sides[ 0 ] ) > mean( sides[ 1 ] ) }; + result.field = sides[ fieldfirst ? 0 : 1 ]; + result.gauge = sides[ fieldfirst ? 1 : 0 ]; auto const middle { top - 0.5 * result.height }; auto webfrom { 1e9 }; auto webto { -1e9 }; diff --git a/world/Track.h b/world/Track.h index 9f15fab4..c415bf00 100644 --- a/world/Track.h +++ b/world/Track.h @@ -342,6 +342,7 @@ public: double ledge { 0.0 }; double web { 0.0365 }; double neck { -0.0467 }; + std::vector field, gauge; }; rail_size rail_dimensions() const; // a check rail beside a rail, how far its ends turn away from the running rail @@ -367,6 +368,7 @@ public: glm::dvec2 startmiter { 0.0 }; double endbend { 0.0 }; double startbend { 0.0 }; + std::vector endfit, startfit; std::vector endinner, startinner; bool endcap { true }; bool startcap { true }; diff --git a/world/TrackRails.cpp b/world/TrackRails.cpp index 7dd82d02..80dec144 100644 --- a/world/TrackRails.cpp +++ b/world/TrackRails.cpp @@ -1021,7 +1021,7 @@ void obtuse( TTrack::rail_plan &Plan, double const At, double const Into, TTrack } // 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 ) { +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, std::vector const &Fromline = {}, std::vector const &Toline = {} ) { 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 }; @@ -1031,9 +1031,31 @@ void wide( TTrack::rail_plan &Plan, double const At, double const Into, double c 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 }; + auto const glue = [&]( std::vector const &Line, double const Edge, double &Position, glm::dvec2 &Miter, double &Bend, std::vector &Fit, bool &Cap ) { + if( Line.empty() ) { + return; + } + Position = At + Edge / Into; + Miter = {}; + Bend = Facing / Into; + for( auto const &knot : Line ) { + Fit.emplace_back( knot.x, ( knot.y - Edge ) / Into ); + } + Cap = false; }; + glue( Into > 0.0 ? Fromline : Toline, Into > 0.0 ? From : To, cut.end, cut.endmiter, cut.endbend, cut.endfit, cut.endcap ); + glue( Into > 0.0 ? Toline : Fromline, Into > 0.0 ? To : From, cut.start, cut.startmiter, cut.startbend, cut.startfit, cut.startcap ); Plan.cuts.push_back( cut ); } +std::vector glue_line( std::vector const &Outline, double const Base ) { + + std::vector line; + for( auto const &knot : Outline ) { + line.emplace_back( knot.x, Base - knot.y ); + } + return line; +} + TTrack::rail_fill guard_fill( TTrack::guard_rail const &Guard, TTrack::rail_size const &Size ) { auto const offset { Size.web - ( flangeway + Size.head ) }; @@ -1338,7 +1360,7 @@ TTrack::rail_layout TTrack::plan_rails( bool const Crossings ) { 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 ); + wide( plan, crossing.at, crossing.into, crossing.facing, through ? 0.0 : -other.head, flangeway, 0.0, railhead, through ? std::vector{} : glue_line( other.field, 0.0 ) ); if( through ) { tread( crossing.path, crossing.side, across( crossing, other ), *crossing.track, crossing.otherpath ); } @@ -1346,7 +1368,7 @@ TTrack::rail_layout TTrack::plan_rails( bool const Crossings ) { auto &spans { guards[ crossing.path ][ crossing.side > 0 ] }; TTrack::guard_rail guard { crossing.at - reach, crossing.at + reach }; guard.through = through; - wide( guard.plan, crossing.at, crossing.into, crossing.facing, -other.head, through ? flangeway : flangeway + other.head, guardoffset, guardoffset + railhead ); + wide( guard.plan, crossing.at, crossing.into, crossing.facing, -other.head, through ? flangeway : flangeway + other.head, guardoffset, guardoffset + railhead, glue_line( other.field, 0.0 ), through ? std::vector{} : glue_line( other.gauge, flangeway + other.head ) ); auto merged { false }; for( auto &span : spans ) { if( guard.from < span.to + 2.0 && guard.to > span.from - 2.0 ) { @@ -1652,10 +1674,14 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i return glm::dvec2 { std::abs( Start - bendfrom.x ) < 1e-6 ? bendfrom.y : 0.0, std::abs( End - bendto.x ) < 1e-6 ? bendto.y : 0.0 }; }; + std::pair> fitfrom, fitto; + std::vector const unfitted; + auto const fitted = [&]( std::pair> const &Fit, double const Distance ) -> std::vector const & { + return ( std::abs( Distance - Fit.first ) < 1e-6 ? Fit.second : unfitted ); }; 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 = {}, bool const Walls = false, float const Lift = 0.f, float const Wallu = -1.f, double const Fade = blend ) { 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, Lift, Inner, Rolled ? nullptr : &userdata, Ramp, Ramp.empty() ? 0.0 : Fade, Walls, Wallu, bends( Start, End ) ) ) { + if( Shape.size() > 1 && segment.RenderLoftSpan( vertices, m_origin, Shape, Side, halfwidth, texturelength(), Start, End, Offsets, Extents, Lift, Inner, Rolled ? nullptr : &userdata, Ramp, Ramp.empty() ? 0.0 : Fade, Walls, Wallu, bends( Start, End ), fitted( fitfrom, Start ), fitted( fitto, End ) ) ) { cut_faces( vertices, Shape.size(), Capstart, Capend, Drawing.bank, sink( true ) ); gfx::vertex_array triangles; gfx::userdata_array triangleuserdata; @@ -1751,7 +1777,7 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i }; auto const running = [&]( double const Start, double const End, std::vector const &Offsets, std::vector const &Extents, std::vector const &Inner, 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, 0.f, Inner, nullptr, {}, 0.0, false, -1.f, bends( Start, End ) ) ) { + if( segment.RenderLoftSpan( vertices, m_origin, Profile, Side, halfwidth, texturelength(), Start, End, Offsets, Extents, 0.f, Inner, nullptr, {}, 0.0, false, -1.f, bends( Start, End ), fitted( fitfrom, Start ), fitted( fitto, End ) ) ) { cut_faces( vertices, Profile.size(), Capstart, Capend, Drawing.bank, sink( true ) ); } piece( Start, End, Offsets, Extents, false, false, strip( 0, topfirst ), true, Inner ); @@ -1801,7 +1827,7 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i auto const treaded = [&]( double const Start, double const End, std::vector const &Offsets, std::vector const &Extents, std::vector const &Inner, bool const Capstart, bool const Capend, rail_tread const &Span ) { current = null_handle; gfx::vertex_array vertices; - if( segment.RenderLoftSpan( vertices, m_origin, Profile, Side, halfwidth, texturelength(), Start, End, Offsets, Extents, 0.f, Inner, nullptr, {}, 0.0, false, -1.f, bends( Start, End ) ) ) { + if( segment.RenderLoftSpan( vertices, m_origin, Profile, Side, halfwidth, texturelength(), Start, End, Offsets, Extents, 0.f, Inner, nullptr, {}, 0.0, false, -1.f, bends( Start, End ), fitted( fitfrom, Start ), fitted( fitto, End ) ) ) { cut_faces( vertices, Profile.size(), Capstart, Capend, Drawing.bank, sink( true ) ); } piece( Start, End, Offsets, Extents, false, false, strip( 0, topfirst ), true, Inner ); @@ -1946,9 +1972,11 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i 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 }; + rail_cut startcut; 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 endcut { i < cuts.size() ? cuts[ i ] : rail_cut {} }; 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 ) { @@ -1959,11 +1987,20 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i ( 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 ) ); + auto const glued { a == start && false == startcut.startfit.empty() }; + auto const gluedend { b == end && false == endcut.endfit.empty() }; + bendfrom = { a, glued ? startcut.startbend : 0.0 }; + bendto = { b, gluedend ? endcut.endbend : 0.0 }; + fitfrom = { a, glued ? startcut.startfit : unfitted }; + fitto = { b, gluedend ? endcut.endfit : unfitted }; + piece( a, b, offsets, envelope( uppers, true, a, b ), false == glued, false == gluedend, Profile, false, envelope( lowers, false, a, b ) ); + bendfrom = bendto = glm::dvec2 { 0.0 }; + fitfrom = fitto = {}; } if( i < cuts.size() && cuts[ i ].start > start ) { start = cuts[ i ].start; startmiter = cuts[ i ].startmiter; + startcut = cuts[ i ]; } } } @@ -2052,6 +2089,7 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i auto start { -1e9 }; auto startcap { true }; auto startbend { 0.0 }; + std::vector startfit; std::vector rolledfrom; std::vector offsets, extents, inner; for( std::size_t i = 0; i <= cuts.size(); ++i ) { @@ -2074,6 +2112,8 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i auto const capend { end < length ? cuts[ i ].endcap : Openend }; bendfrom = { a, start > From ? startbend : 0.0 }; bendto = { b, end < length ? cuts[ i ].endbend : 0.0 }; + fitfrom = { a, start > From ? startfit : unfitted }; + fitto = { b, end < length ? cuts[ i ].endfit : unfitted }; if( b - a > 0.01 ) { std::vector active; for( auto const &span : Spans ) { @@ -2112,11 +2152,13 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i current = null_handle; } bendfrom = bendto = glm::dvec2 { 0.0 }; + fitfrom = fitto = {}; if( i < cuts.size() ) { rolledfrom = cuts[ i ].startrolled; start = cuts[ i ].start; startcap = cuts[ i ].startcap; startbend = cuts[ i ].startbend; + startfit = cuts[ i ].startfit; offsets = cuts[ i ].startoffsets; extents = cuts[ i ].startextents; inner = cuts[ i ].startinner; From 38c8adaec3a1cd7b446cc3bdb8acf526340c7417 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 09:08:55 +0200 Subject: [PATCH 02/19] test(editor): geomhash also logs an exact hash of the bits of every vertex value, the userdata and the names of the tread materials, so a refactor can be checked to change nothing --- application/editormodeselftest.cpp | 31 +++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index ed079b9b..0d6830d1 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -51,7 +51,7 @@ http://mozilla.org/MPL/2.0/. // switches <0|1> sets every switch to the path and moves its blades there at once // crossings [name] logs where rails of tracks cross rails of other tracks and of what kind (acute, obtuse, middle acute or obtuse, wide), checks that each acute crossing has one partner and one nose // frogkey name [on|off] sets or clears the frogs key of a switch (one undo step); logs the key, whether it is written, the frog and the crossings -// geomhash [name] logs the number of vertices and a hash of the drawn geometry of each rail track (rails, trackbed, cut faces), to compare builds +// geomhash [name] logs the number of vertices and a hash of the drawn geometry of each rail track (rails, trackbed, cut faces), to compare builds; exact: the bits of every value, userdata and tread materials too // sweep =... model, params (separated by commas), lateral, height, side (origin, near, far, // centre), top (origin, bottom, top, edge), mode (bend, repeat), step, // length (original, all, or metres), from, to, widen (0, 1), face (0, 1), tilt (0, 1) @@ -184,7 +184,17 @@ void editor_mode::selftest_step() auto const mix = [](std::uint64_t &Hash, float const Value) { Hash = (Hash ^ static_cast(static_cast(std::llround(Value * 1e4)))) * 1099511628211ull; }; + auto const exact = [](std::uint64_t &Hash, float const Value) { + std::uint32_t bits; + std::memcpy(&bits, &Value, sizeof(bits)); + Hash = (Hash ^ bits) * 1099511628211ull; + }; + auto const named = [](std::uint64_t &Hash, material_handle const Material) { + for (auto const letter : (Material != null_handle ? GfxRenderer->Material(Material)->GetName() : std::string{}) + '|') + Hash = (Hash ^ static_cast(letter)) * 1099511628211ull; + }; std::uint64_t total{1469598103934665603ull}; + std::uint64_t exacttotal{1469598103934665603ull}; std::size_t vertices{0}; int tracks{0}; for (auto *track : simulation::Paths.sequence()) @@ -194,6 +204,7 @@ void editor_mode::selftest_step() if (false == name.empty() && track->name() != name) continue; std::uint64_t hash{1469598103934665603ull}; + std::uint64_t exacthash{1469598103934665603ull}; std::size_t count{0}; int chunks{0}; std::vector sequences{&track->Geometry1, &track->Geometry2, &track->GeometryCut1, &track->GeometryCut2}; @@ -201,6 +212,8 @@ void editor_mode::selftest_step() { sequences.push_back(&tread.geometry); sequences.push_back(&tread.cuts); + named(exacthash, tread.material); + named(exacthash, tread.matte); } for (auto const *sequence : sequences) { @@ -213,20 +226,32 @@ void editor_mode::selftest_step() { mix(hash, vertex.position[i]); mix(hash, vertex.normal[i]); + exact(exacthash, vertex.position[i]); + exact(exacthash, vertex.normal[i]); } mix(hash, vertex.texture.x); mix(hash, vertex.texture.y); + exact(exacthash, vertex.texture.x); + exact(exacthash, vertex.texture.y); ++count; } + for (auto const &userdata : GfxRenderer->UserData(geometry)) + { + for (int i = 0; i < 4; ++i) + exact(exacthash, userdata.data[i]); + } + exact(exacthash, -2.f); } mix(hash, -1.f); + exact(exacthash, -1.f); } - WriteLog(format("SELFTEST geom %s: chunks %d, vertices %zu, hash %016llx", track->name().c_str(), chunks, count, static_cast(hash))); + WriteLog(format("SELFTEST geom %s: chunks %d, vertices %zu, hash %016llx, exact %016llx", track->name().c_str(), chunks, count, static_cast(hash), static_cast(exacthash))); total = (total ^ hash) * 1099511628211ull; + exacttotal = (exacttotal ^ exacthash) * 1099511628211ull; vertices += count; ++tracks; } - WriteLog(format("SELFTEST geom total: tracks %d, vertices %zu, hash %016llx", tracks, vertices, static_cast(total))); + WriteLog(format("SELFTEST geom total: tracks %d, vertices %zu, hash %016llx, exact %016llx", tracks, vertices, static_cast(total), static_cast(exacttotal))); } else if (command == "frogkey") { From 2143cfc04323428ea21de9a99cda269629eda767 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 09:16:37 +0200 Subject: [PATCH 03/19] refactor(track): a lofted rail span is described by one loft_span value instead of twenty arguments, and the knot line helpers (sample, bound, meetings, middle, shifted) live once in world/Knots.h instead of in lambdas of the segment and of lay_rail --- world/Knots.h | 110 +++++++++++++++++++++++++++++ world/Segment.cpp | 113 ++++++++++++++---------------- world/Segment.h | 27 +++++++- world/TrackRails.cpp | 160 +++++++++++++++---------------------------- 4 files changed, 243 insertions(+), 167 deletions(-) create mode 100644 world/Knots.h diff --git a/world/Knots.h b/world/Knots.h new file mode 100644 index 00000000..fa78aad1 --- /dev/null +++ b/world/Knots.h @@ -0,0 +1,110 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include + +namespace knots { + +using line = std::vector; + +inline bool by_position( glm::dvec2 const &A, glm::dvec2 const &B ) { + + return A.x < B.x; +} + +inline void sort( line &Line ) { + + std::sort( Line.begin(), Line.end(), by_position ); +} + +inline double sample( line const &Line, double const Position, double const Default ) { + + if( Line.empty() ) { + return Default; + } + if( Position <= Line.front().x ) { + return Line.front().y; + } + for( std::size_t i = 1; i < Line.size(); ++i ) { + if( Position <= Line[ i ].x ) { + return glm::mix( Line[ i - 1 ].y, Line[ i ].y, ( Position - Line[ i - 1 ].x ) / std::max( Line[ i ].x - Line[ i - 1 ].x, 1e-6 ) ); + } + } + return Line.back().y; +} + +inline line shifted( line Line, double const Value ) { + + for( auto &knot : Line ) { + knot.y += Value; + } + return Line; +} + +inline std::vector stations( line const &A, line const &B ) { + + std::vector result; + for( auto const *each : { &A, &B } ) { + for( auto const &knot : *each ) { + result.push_back( knot.x ); + } + } + std::sort( result.begin(), result.end() ); + return result; +} + +inline std::vector meetings( line const &A, line const &B ) { + + std::vector result; + if( A.empty() || B.empty() ) { + return result; + } + auto const all { stations( A, B ) }; + for( std::size_t i = 1; i < all.size(); ++i ) { + auto const before { sample( A, all[ i - 1 ], 0.0 ) - sample( B, all[ i - 1 ], 0.0 ) }; + auto const after { sample( A, all[ i ], 0.0 ) - sample( B, all[ i ], 0.0 ) }; + if( before * after < 0.0 ) { + result.push_back( all[ i - 1 ] + ( all[ i ] - all[ i - 1 ] ) * before / ( before - after ) ); + } + } + return result; +} + +inline line bound( line const &A, line const &B, bool const Lower ) { + + if( A.empty() || B.empty() ) { + return ( A.empty() ? B : A ); + } + auto all { stations( A, B ) }; + auto const crossed { meetings( A, B ) }; + all.insert( all.end(), crossed.begin(), crossed.end() ); + std::sort( all.begin(), all.end() ); + line result; + for( auto const position : all ) { + auto const a { sample( A, position, 0.0 ) }; + auto const b { sample( B, position, 0.0 ) }; + result.emplace_back( position, Lower ? std::min( a, b ) : std::max( a, b ) ); + } + return result; +} + +inline line middle( line const &A, line const &B ) { + + line result; + for( auto const position : stations( A, B ) ) { + result.emplace_back( position, 0.5 * ( sample( A, position, 0.0 ) + sample( B, position, 0.0 ) ) ); + } + return result; +} + +} // namespace knots diff --git a/world/Segment.cpp b/world/Segment.cpp index 59bbbac8..d52113c7 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -580,47 +580,35 @@ 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, std::vector const &Fitfrom, std::vector const &Fitto ) const { +bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Origin, gfx::vertex_array const &ShapePoints, loft_span const &Span ) const { - if( fTsBuffer.empty() || To - From < 0.001 || ShapePoints.empty() ) + if( fTsBuffer.empty() || Span.to - Span.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 }; + std::vector stations { Span.from, Span.to }; auto const step { std::max( fStep, 0.2 ) }; - for( auto distance { std::ceil( From / step ) * step }; distance < To; distance += step ) { + for( auto distance { std::ceil( Span.from / step ) * step }; distance < Span.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 ) { + for( auto const *line : { &Span.offsets, &Span.extents, &Span.inner } ) { + for( auto const &knot : *line ) { + if( knot.x > Span.from && knot.x < Span.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 ] }; + for( auto const *line : { &Span.extents, &Span.inner } ) { + for( std::size_t i = 1; i < line->size(); ++i ) { + auto const &a { ( *line )[ i - 1 ] }; + auto const &b { ( *line )[ 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 reach { static_cast( Span.side * point.position.x - Span.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 ) { + if( t > 0.0 && t < 1.0 && distance > Span.from && distance < Span.to ) { stations.push_back( distance ); } } @@ -628,26 +616,26 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig } 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() ); - if( Bends.x != 0.0 || Bends.y != 0.0 || false == Fitfrom.empty() || false == Fitto.empty() ) { + if( Span.fromend.bend != 0.0 || Span.toend.bend != 0.0 || false == Span.fromend.fit.empty() || false == Span.toend.fit.empty() ) { auto widest { 0.0 }; for( auto const &point : ShapePoints ) { - widest = std::max( widest, std::abs( static_cast( Side * point.position.x - Halfwidth ) ) ); + widest = std::max( widest, std::abs( static_cast( Span.side * point.position.x - Span.halfwidth ) ) ); } auto offsetmost { 0.0 }; - for( auto const &knot : Offsets ) { + for( auto const &knot : Span.offsets ) { offsetmost = std::max( offsetmost, std::abs( knot.y ) ); } - auto const margin = [&]( double const Bend, std::vector const &Fit ) { + auto const margin = [&]( loft_end const &End ) { auto fitmost { 0.0 }; - for( auto const &knot : Fit ) { + for( auto const &knot : End.fit ) { fitmost = std::max( fitmost, std::abs( knot.y ) ); } - return std::abs( Bend ) * ( Fit.empty() ? widest : widest + offsetmost ) + fitmost + 0.001; }; - auto const frommargin { margin( Bends.x, Fitfrom ) }; - auto const tomargin { margin( Bends.y, Fitto ) }; + return std::abs( End.bend ) * ( End.fit.empty() ? widest : widest + offsetmost ) + fitmost + 0.001; }; + auto const frommargin { margin( Span.fromend ) }; + auto const tomargin { margin( Span.toend ) }; stations.erase( std::remove_if( stations.begin(), stations.end(), [&]( double const Distance ) { - return ( Distance > From && Distance < To ) - && ( Distance < From + frommargin || Distance > To - tomargin ); } ), stations.end() ); + return ( Distance > Span.from && Distance < Span.to ) + && ( Distance < Span.from + frommargin || Distance > Span.to - tomargin ); } ), stations.end() ); } auto const ring = [&]( double const Distance, double const Middle, std::vector &Ring, std::vector &Wear ) { @@ -660,29 +648,30 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig } 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 middleextent { sample( Extents, Middle, 1e6 ) }; - auto const middleinner { sample( Inner, Middle, -1e6 ) }; + auto const offset { knots::sample( Span.offsets, Distance, 0.0 ) }; + auto const extent { knots::sample( Span.extents, Distance, 1e6 ) }; + auto const innerextent { knots::sample( Span.inner, Distance, -1e6 ) }; + auto const middleextent { knots::sample( Span.extents, Middle, 1e6 ) }; + auto const middleinner { knots::sample( Span.inner, Middle, -1e6 ) }; glm::vec3 outerwall { 0.f }, innerwall { 0.f }; - if( Walls ) { + if( Span.walls ) { auto const delta { 0.001 }; glm::dvec3 ahead, behind, unused; plan_frame( std::min( Distance + delta, static_cast( fLength ) ), ahead, unused ); plan_frame( std::max( Distance - delta, 0.0 ), behind, unused ); auto tangent { glm::dvec3{ ahead.x - behind.x, 0.0, ahead.z - behind.z } }; tangent = ( glm::length2( tangent ) > 0.0 ? glm::normalize( tangent ) : glm::dvec3{ 0.0 } ); - auto const outward { glm::dvec3{ across } * static_cast( Side ) }; - auto const slope = [&]( std::vector const &Knots, double const Default ) { - return ( sample( Knots, Distance + delta, Default ) - sample( Knots, Distance - delta, Default ) + sample( Offsets, Distance + delta, 0.0 ) - sample( Offsets, Distance - delta, 0.0 ) ) / ( 2.0 * delta ); }; - outerwall = glm::vec3{ glm::normalize( outward - tangent * slope( Extents, 1e6 ) ) }; - innerwall = glm::vec3{ glm::normalize( tangent * slope( Inner, -1e6 ) - outward ) }; + auto const outward { glm::dvec3{ across } * static_cast( Span.side ) }; + auto const slope = [&]( knots::line const &Knots, double const Default ) { + return ( knots::sample( Knots, Distance + delta, Default ) - knots::sample( Knots, Distance - delta, Default ) + knots::sample( Span.offsets, Distance + delta, 0.0 ) - knots::sample( Span.offsets, Distance - delta, 0.0 ) ) / ( 2.0 * delta ); }; + outerwall = glm::vec3{ glm::normalize( outward - tangent * slope( Span.extents, 1e6 ) ) }; + innerwall = glm::vec3{ glm::normalize( tangent * slope( Span.inner, -1e6 ) - outward ) }; } - auto const worn { sample( Ramp, Distance, -1e6 ) }; - auto const bend { Distance <= From ? Bends.x : Distance >= To ? Bends.y : 0.0 }; - static std::vector const unfitted; - auto const &fit { Distance <= From ? Fitfrom : Distance >= To ? Fitto : unfitted }; + auto const worn { knots::sample( Span.ramp, Distance, -1e6 ) }; + static loft_end const unshaped; + auto const &shaped { Distance <= Span.from ? Span.fromend : Distance >= Span.to ? Span.toend : unshaped }; + auto const bend { shaped.bend }; + auto const &fit { shaped.fit }; auto along { glm::vec3 { 0.f } }; auto forward { glm::vec3 { 0.f } }; if( bend != 0.0 || false == fit.empty() ) { @@ -699,7 +688,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig Wear.clear(); auto const count { ShapePoints.size() }; auto const reach = [&]( std::size_t const Index ) { - return static_cast( Side * ShapePoints[ Index ].position.x - Halfwidth ); }; + return static_cast( Span.side * ShapePoints[ Index ].position.x - Span.halfwidth ); }; auto const twin = [&]( std::size_t const A, std::size_t const B ) { return ShapePoints[ A ].position.x == ShapePoints[ B ].position.x && ShapePoints[ A ].position.y == ShapePoints[ B ].position.y; }; auto const clamped = [&]( std::size_t const Index ) { @@ -716,7 +705,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig return Index < count && ( side > 0 ? reach( Index ) <= middleextent : reach( Index ) >= middleinner ); }; auto first { j }; auto last { j }; - if( Walls ) { + if( Span.walls ) { while( first > 0 && twin( first - 1, j ) ) { --first; } @@ -774,25 +763,25 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig wall = ( side > 0 ? outerwall : innerwall ); } } - auto pt { parallel * static_cast( Side * ( Halfwidth + outside + offset ) ) + position + along * static_cast( outside + offset ) + forward * static_cast( sample( fit, shape.position.y, 0.0 ) ) }; - pt.y += shape.position.y + Lift; + auto pt { parallel * static_cast( Span.side * ( Span.halfwidth + outside + offset ) ) + position + along * static_cast( outside + offset ) + forward * static_cast( knots::sample( fit, shape.position.y, 0.0 ) ) }; + pt.y += shape.position.y + Span.lift; auto norm { shape.normal.x * parallel }; norm.y += shape.normal.y; if( glm::length2( wall ) > 0.f ) { norm = wall; - if( Wallu >= 0.f ) { - shape.texture.x = Wallu; + if( Span.wallu >= 0.f ) { + shape.texture.x = Span.wallu; } } Ring.emplace_back( pt, glm::normalize( norm ), glm::vec2{ shape.texture.x, 0.f } ); - Wear.push_back( Rampwidth != 0.0 ? static_cast( std::clamp( ( Rampwidth > 0.0 ? outside - worn : worn - outside ) / std::abs( Rampwidth ) + 1.0, 0.0, 1.0 ) ) : 1.f ); + Wear.push_back( Span.rampwidth != 0.0 ? static_cast( std::clamp( ( Span.rampwidth > 0.0 ? outside - worn : worn - outside ) / std::abs( Span.rampwidth ) + 1.0, 0.0, 1.0 ) ) : 1.f ); } }; - auto const texturelength { static_cast( fTextureLength ) }; + auto const texturelength { static_cast( Span.texturelength ) }; std::vector ring1, ring2; std::vector wear1, wear2; - float tv1 { clamp_circular( 1.f - static_cast( std::fmod( From, fTextureLength ) / fTextureLength ), 1.0f ) }; + float tv1 { clamp_circular( 1.f - static_cast( std::fmod( Span.from, Span.texturelength ) / Span.texturelength ), 1.0f ) }; for( std::size_t i = 1; i < stations.size(); ++i ) { auto const middle { 0.5 * ( stations[ i - 1 ] + stations[ i ] ) }; ring( stations[ i - 1 ], middle, ring1, wear1 ); @@ -802,9 +791,9 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig 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 } } ); + if( Span.userdata != nullptr ) { + Span.userdata->push_back( { glm::vec4{ wear1[ j ], 0.f, 0.f, 0.f } } ); + Span.userdata->push_back( { glm::vec4{ wear2[ j ], 0.f, 0.f, 0.f } } ); } } tv1 = tv2; diff --git a/world/Segment.h b/world/Segment.h index 1e9f4270..971794c3 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/geometrybank.h" #include #include +#include "world/Knots.h" struct map_colored_paths { std::vector switches; @@ -21,6 +22,30 @@ struct map_colored_paths { std::vector highlighted; }; +struct loft_end { + double bend { 0.0 }; + knots::line fit; +}; + +struct loft_span { + int side { 1 }; + float halfwidth { 0.f }; + double texturelength { 1.0 }; + double from { 0.0 }; + double to { 0.0 }; + knots::line offsets; + knots::line extents; + knots::line inner; + float lift { 0.f }; + gfx::userdata_array *userdata { nullptr }; + knots::line ramp; + double rampwidth { 0.0 }; + bool walls { false }; + float wallu { -1.f }; + loft_end fromend; + loft_end toend; +}; + struct segment_data { // types enum point { @@ -132,7 +157,7 @@ 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 }, std::vector const &Fitfrom = {}, std::vector const &Fitto = {} ) const; + RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Origin, gfx::vertex_array const &ShapePoints, loft_span const &Span ) 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; diff --git a/world/TrackRails.cpp b/world/TrackRails.cpp index 80dec144..37bdf527 100644 --- a/world/TrackRails.cpp +++ b/world/TrackRails.cpp @@ -1678,10 +1678,31 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i std::vector const unfitted; auto const fitted = [&]( std::pair> const &Fit, double const Distance ) -> std::vector const & { return ( std::abs( Distance - Fit.first ) < 1e-6 ? Fit.second : unfitted ); }; + auto const span = [&]( double const Start, double const End, knots::line const &Offsets, knots::line const &Extents, knots::line const &Inner ) { + loft_span result; + result.side = Side; + result.halfwidth = halfwidth; + result.texturelength = texturelength(); + result.from = Start; + result.to = End; + result.offsets = Offsets; + result.extents = Extents; + result.inner = Inner; + auto const bent { bends( Start, End ) }; + result.fromend = { bent.x, fitted( fitfrom, Start ) }; + result.toend = { bent.y, fitted( fitto, End ) }; + return result; }; 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 = {}, bool const Walls = false, float const Lift = 0.f, float const Wallu = -1.f, double const Fade = blend ) { 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, Lift, Inner, Rolled ? nullptr : &userdata, Ramp, Ramp.empty() ? 0.0 : Fade, Walls, Wallu, bends( Start, End ), fitted( fitfrom, Start ), fitted( fitto, End ) ) ) { + auto loft { span( Start, End, Offsets, Extents, Inner ) }; + loft.lift = Lift; + loft.userdata = ( Rolled ? nullptr : &userdata ); + loft.ramp = Ramp; + loft.rampwidth = ( Ramp.empty() ? 0.0 : Fade ); + loft.walls = Walls; + loft.wallu = Wallu; + if( Shape.size() > 1 && segment.RenderLoftSpan( vertices, m_origin, Shape, loft ) ) { cut_faces( vertices, Shape.size(), Capstart, Capend, Drawing.bank, sink( true ) ); gfx::vertex_array triangles; gfx::userdata_array triangleuserdata; @@ -1700,43 +1721,6 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i sink( false == Rolled ).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 bound = [&]( std::vector const &A, std::vector const &B, bool const Lower ) { - if( A.empty() || B.empty() ) { - return ( A.empty() ? B : A ); - } - std::vector stations; - for( auto const *knots : { &A, &B } ) { - for( auto const &knot : *knots ) { - stations.push_back( knot.x ); - } - } - std::sort( stations.begin(), stations.end() ); - auto const count { stations.size() }; - for( std::size_t i = 1; i < count; ++i ) { - auto const a { stations[ i - 1 ] }; - auto const b { stations[ i ] }; - auto const before { level( A, a ) - level( B, a ) }; - auto const after { level( A, b ) - level( B, b ) }; - if( before * after < 0.0 ) { - stations.push_back( a + ( b - a ) * before / ( before - after ) ); - } - } - std::sort( stations.begin(), stations.end() ); - std::vector result; - for( auto const station : stations ) { - result.emplace_back( station, Lower ? std::min( level( A, station ), level( B, station ) ) : std::max( level( A, station ), level( B, station ) ) ); - } - 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 }; @@ -1748,36 +1732,16 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i } 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 shifted = []( std::vector Knots, double const Lateral ) { - for( auto &knot : Knots ) { - knot.y += Lateral; - } - return Knots; }; - auto const crossings = [&]( std::vector const &A, std::vector const &B, double const Start, double const End, double const Margin, std::vector &Splits ) { - if( A.empty() || B.empty() ) { - return; - } - std::vector stations; - for( auto const *knots : { &A, &B } ) { - for( auto const &knot : *knots ) { - stations.push_back( knot.x ); - } - } - std::sort( stations.begin(), stations.end() ); - for( std::size_t i = 1; i < stations.size(); ++i ) { - auto const before { level( A, stations[ i - 1 ] ) - level( B, stations[ i - 1 ] ) }; - auto const after { level( A, stations[ i ] ) - level( B, stations[ i ] ) }; - if( before * after < 0.0 ) { - auto const distance { stations[ i - 1 ] + ( stations[ i ] - stations[ i - 1 ] ) * before / ( before - after ) }; - if( distance > Start + Margin && distance < End - Margin ) { - Splits.push_back( distance ); - } + auto const crossings = []( knots::line const &A, knots::line const &B, double const Start, double const End, double const Margin, std::vector &Splits ) { + for( auto const distance : knots::meetings( A, B ) ) { + if( distance > Start + Margin && distance < End - Margin ) { + Splits.push_back( distance ); } } }; auto const running = [&]( double const Start, double const End, std::vector const &Offsets, std::vector const &Extents, std::vector const &Inner, 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, 0.f, Inner, nullptr, {}, 0.0, false, -1.f, bends( Start, End ), fitted( fitfrom, Start ), fitted( fitto, End ) ) ) { + if( segment.RenderLoftSpan( vertices, m_origin, Profile, span( Start, End, Offsets, Extents, Inner ) ) ) { cut_faces( vertices, Profile.size(), Capstart, Capend, Drawing.bank, sink( true ) ); } piece( Start, End, Offsets, Extents, false, false, strip( 0, topfirst ), true, Inner ); @@ -1789,7 +1753,7 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i low = std::min( low, static_cast( Side * point.position.x - halfwidth ) ); high = std::max( high, static_cast( Side * point.position.x - halfwidth ) ); } - auto const inner { shifted( Rolled, -blend ) }; + auto const inner { knots::shifted( Rolled, -blend ) }; std::vector splits { Start, End }; std::vector const lowline { { Start, low }, { End, low } }; std::vector const highline { { Start, high }, { End, high } }; @@ -1799,13 +1763,13 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i } } std::sort( splits.begin(), splits.end() ); - auto const shiny { bound( Extents, inner, true ) }; - auto const blended { bound( Extents, Rolled, true ) }; + auto const shiny { knots::bound( Extents, inner, true ) }; + auto const blended { knots::bound( Extents, Rolled, true ) }; for( std::size_t i = 1; i < splits.size(); ++i ) { auto const middle { 0.5 * ( splits[ i - 1 ] + splits[ i ] ) }; - auto const reach { level( Rolled, middle ) }; - auto const least { std::max( low, Inner.empty() ? low : level( Inner, middle ) ) }; - auto const most { std::min( high, Extents.empty() ? high : level( Extents, middle ) ) }; + auto const reach { knots::sample( Rolled, middle, 0.0 ) }; + auto const least { std::max( low, Inner.empty() ? low : knots::sample( Inner, middle, 0.0 ) ) }; + auto const most { std::min( high, Extents.empty() ? high : knots::sample( Extents, middle, 0.0 ) ) }; auto const shown = [&]( double const Lowest, double const Highest ) { return Highest > Lowest + 1e-6 && Highest > least && Lowest < most; }; if( shown( -1e9, reach - blend ) ) { @@ -1815,11 +1779,11 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i auto const own { current == null_handle ? rail_material( Path ) : current }; if( shown( reach - blend, reach ) ) { current = ( marked ? paint( own, { 1.f, 1.f, 0.f } ) : plain ); - piece( splits[ i - 1 ], splits[ i ], Offsets, blended, false, false, top, false, bound( inner, Inner, false ), marked ? unramped : Rolled ); + piece( splits[ i - 1 ], splits[ i ], Offsets, blended, false, false, top, false, knots::bound( inner, Inner, false ), marked ? unramped : Rolled ); } if( shown( reach, 1e9 ) ) { current = ( marked ? paint( own, { 1.f, 0.f, 1.f } ) : plain ); - piece( splits[ i - 1 ], splits[ i ], Offsets, Extents, false, false, top, false, bound( Rolled, Inner, false ) ); + piece( splits[ i - 1 ], splits[ i ], Offsets, Extents, false, false, top, false, knots::bound( Rolled, Inner, false ) ); } current = plain; } @@ -1827,7 +1791,7 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i auto const treaded = [&]( double const Start, double const End, std::vector const &Offsets, std::vector const &Extents, std::vector const &Inner, bool const Capstart, bool const Capend, rail_tread const &Span ) { current = null_handle; gfx::vertex_array vertices; - if( segment.RenderLoftSpan( vertices, m_origin, Profile, Side, halfwidth, texturelength(), Start, End, Offsets, Extents, 0.f, Inner, nullptr, {}, 0.0, false, -1.f, bends( Start, End ), fitted( fitfrom, Start ), fitted( fitto, End ) ) ) { + if( segment.RenderLoftSpan( vertices, m_origin, Profile, span( Start, End, Offsets, Extents, Inner ) ) ) { cut_faces( vertices, Profile.size(), Capstart, Capend, Drawing.bank, sink( true ) ); } piece( Start, End, Offsets, Extents, false, false, strip( 0, topfirst ), true, Inner ); @@ -1850,23 +1814,11 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i auto const shared { marked ? tint( Span.material, { 0.f, 0.f, 1.f } ) : Span.material }; auto const ramp = [&]( std::vector const &Knots ) -> std::vector const & { return marked ? unramped : Knots; }; - std::vector centre; - { - std::vector stations; - for( auto const *knots : { &lower, &upper } ) { - for( auto const &knot : *knots ) { - stations.push_back( knot.x ); - } - } - std::sort( stations.begin(), stations.end() ); - for( auto const station : stations ) { - centre.emplace_back( station, 0.5 * ( level( lower, station ) + level( upper, station ) ) ); - } - } - auto const beneath { shifted( lower, -tread_fade ) }; - auto const beyond { shifted( upper, tread_fade ) }; - auto const lowfade { bound( shifted( lower, tread_fade ), centre, true ) }; - auto const highfade { bound( shifted( upper, -tread_fade ), centre, false ) }; + auto const centre { knots::middle( lower, upper ) }; + auto const beneath { knots::shifted( lower, -tread_fade ) }; + auto const beyond { knots::shifted( upper, tread_fade ) }; + auto const lowfade { knots::bound( knots::shifted( lower, tread_fade ), centre, true ) }; + auto const highfade { knots::bound( knots::shifted( upper, -tread_fade ), centre, false ) }; std::vector splits { Start, End }; std::vector const lowline { { Start, low }, { End, low } }; std::vector const highline { { Start, high }, { End, high } }; @@ -1875,47 +1827,47 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i crossings( *knots, *limit, Start, End, 0.001, splits ); } } - crossings( shifted( lower, 2.0 * tread_fade ), upper, Start, End, 0.001, splits ); + crossings( knots::shifted( lower, 2.0 * tread_fade ), upper, Start, End, 0.001, splits ); std::sort( splits.begin(), splits.end() ); for( std::size_t i = 1; i < splits.size(); ++i ) { auto const a { splits[ i - 1 ] }; auto const b { splits[ i ] }; auto const middle { 0.5 * ( a + b ) }; - auto const from { level( lower, middle ) }; - auto const to { level( upper, middle ) }; - auto const inner { level( lowfade, middle ) }; - auto const outer { level( highfade, middle ) }; - auto const least { std::max( low, Inner.empty() ? low : level( Inner, middle ) ) }; - auto const most { std::min( high, Extents.empty() ? high : level( Extents, middle ) ) }; + auto const from { knots::sample( lower, middle, 0.0 ) }; + auto const to { knots::sample( upper, middle, 0.0 ) }; + auto const inner { knots::sample( lowfade, middle, 0.0 ) }; + auto const outer { knots::sample( highfade, middle, 0.0 ) }; + auto const least { std::max( low, Inner.empty() ? low : knots::sample( Inner, middle, 0.0 ) ) }; + auto const most { std::min( high, Extents.empty() ? high : knots::sample( Extents, middle, 0.0 ) ) }; auto const shown = [&]( double const Lowest, double const Highest ) { return Highest > Lowest + 1e-6 && Highest > least && Lowest < most; }; if( shown( -1e9, from - tread_fade ) ) { current = null_handle; - piece( a, b, Offsets, bound( Extents, beneath, true ), false, false, top, true, Inner ); + piece( a, b, Offsets, knots::bound( Extents, beneath, true ), false, false, top, true, Inner ); } if( shown( from - tread_fade, from ) ) { current = owntint; - piece( a, b, Offsets, bound( Extents, lower, true ), false, false, top, false, bound( Inner, beneath, false ), ramp( lower ), false, 0.f, -1.f, tread_fade ); + piece( a, b, Offsets, knots::bound( Extents, lower, true ), false, false, top, false, knots::bound( Inner, beneath, false ), ramp( lower ), false, 0.f, -1.f, tread_fade ); } if( shown( from, inner ) ) { current = sharedtint; - piece( a, b, Offsets, bound( Extents, lowfade, true ), false, false, top, false, bound( Inner, lower, false ), ramp( lower ), false, 0.f, -1.f, -tread_fade ); + piece( a, b, Offsets, knots::bound( Extents, lowfade, true ), false, false, top, false, knots::bound( Inner, lower, false ), ramp( lower ), false, 0.f, -1.f, -tread_fade ); } if( shown( inner, outer ) ) { current = shared; - piece( a, b, Offsets, bound( Extents, highfade, true ), false, false, top, false == marked, bound( Inner, lowfade, false ) ); + piece( a, b, Offsets, knots::bound( Extents, highfade, true ), false, false, top, false == marked, knots::bound( Inner, lowfade, false ) ); } if( shown( outer, to ) ) { current = sharedtint; - piece( a, b, Offsets, bound( Extents, upper, true ), false, false, top, false, bound( Inner, highfade, false ), ramp( upper ), false, 0.f, -1.f, tread_fade ); + piece( a, b, Offsets, knots::bound( Extents, upper, true ), false, false, top, false, knots::bound( Inner, highfade, false ), ramp( upper ), false, 0.f, -1.f, tread_fade ); } if( shown( to, to + tread_fade ) ) { current = owntint; - piece( a, b, Offsets, bound( Extents, beyond, true ), false, false, top, false, bound( Inner, upper, false ), ramp( upper ), false, 0.f, -1.f, -tread_fade ); + piece( a, b, Offsets, knots::bound( Extents, beyond, true ), false, false, top, false, knots::bound( Inner, upper, false ), ramp( upper ), false, 0.f, -1.f, -tread_fade ); } if( shown( to + tread_fade, 1e9 ) ) { current = null_handle; - piece( a, b, Offsets, Extents, false, false, top, true, bound( Inner, beyond, false ) ); + piece( a, b, Offsets, Extents, false, false, top, true, knots::bound( Inner, beyond, false ) ); } } current = null_handle; @@ -2107,7 +2059,7 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i std::sort( pieceinner.begin(), pieceinner.end(), byposition ); auto const a { std::max( start, From ) }; auto const b { std::min( end, length ) }; - auto const rolled { i < cuts.size() ? bound( rolledfrom, cuts[ i ].endrolled, true ) : rolledfrom }; + auto const rolled { i < cuts.size() ? knots::bound( rolledfrom, cuts[ i ].endrolled, true ) : rolledfrom }; auto const capstart { start > From ? startcap : Openstart }; auto const capend { end < length ? cuts[ i ].endcap : Openend }; bendfrom = { a, start > From ? startbend : 0.0 }; From a88774d279c98fc36769ec5b5def4febeb9860b4 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 09:21:19 +0200 Subject: [PATCH 04/19] refactor(track): the rail layout value types (crossings, sizes, check and guard rails, cuts, bands, fills, treads, layout) and the wheel and rail constants live in world/RailLayout.h in track_rails instead of inside TTrack --- application/editormodeselftest.cpp | 2 +- world/RailLayout.h | 153 +++++++++++++++++++++++++++++ world/Track.cpp | 4 +- world/Track.h | 135 ++----------------------- world/TrackRails.cpp | 50 +++++----- world/TrackRails.h | 10 -- 6 files changed, 189 insertions(+), 165 deletions(-) create mode 100644 world/RailLayout.h diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 0d6830d1..02891d31 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -444,7 +444,7 @@ void editor_mode::selftest_step() WriteLog(format("SELFTEST crossing %s path %d side %d at %.3f m: %s with %s, 1:%.2f, into %.3f, facing %.3f, nose %d", track->name().c_str(), crossing.path, crossing.side, crossing.at, kind, crossing.track->name().c_str(), 1.0 / std::tan(crossing.angle), crossing.into, crossing.facing, crossing.nose ? 1 : 0)); } - auto const gauge = [](TTrack *Track, TTrack::rail_crossing const &Crossing) { + auto const gauge = [](TTrack *Track, track_rails::rail_crossing const &Crossing) { auto const &segment{Track->eType == tt_Switch ? *Track->SwitchExtension->Segments[Crossing.path] : *Track->Segment}; glm::dvec3 point, across; segment.plan_frame(Crossing.at, point, across); diff --git a/world/RailLayout.h b/world/RailLayout.h new file mode 100644 index 00000000..b56859c7 --- /dev/null +++ b/world/RailLayout.h @@ -0,0 +1,153 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include "utilities/Classes.h" +#include "world/Knots.h" + +class TTrack; + +namespace track_rails { + +double constexpr flangeway { 0.045 }; +double constexpr blend { 0.015 }; +double constexpr tread_fade { 0.02 }; +double constexpr wheel_width { 0.14 }; +double constexpr back_to_back { 1.36 }; +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 }; + +// 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 }; +}; +// 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 + TTrack *owner { nullptr }; +}; +// 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 + bool grooved { false }; + double groove { 0.0 }; + double lip { 0.0 }; + double rim { 0.0 }; + double crown { 0.0 }; + double liptop { 0.0 }; + double crest { 0.0 }; + double tread { 0.0735 }; + double ledge { 0.0 }; + double web { 0.0365 }; + double neck { -0.0467 }; + knots::line field, gauge; +}; +// 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 }; + double frombend { 0.0 }; + double tobend { 0.0 }; +}; +using check_rail_set = std::array, 2>, 2>; // path, right side +// 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 }; + knots::line endoffsets, endextents, startoffsets, startextents; + knots::line 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 }; + double endbend { 0.0 }; + double startbend { 0.0 }; + knots::line endfit, startfit; + knots::line endinner, startinner; + bool endcap { true }; + bool startcap { true }; +}; +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 }; + bool through { true }; +}; +struct rail_band { + double at { 0.0 }; + double into { 0.0 }; + double facing { 0.0 }; + double from { 0.0 }; + double to { 0.0 }; + bool whole { true }; +}; +struct rail_fill { + double from { 0.0 }; + double to { 0.0 }; + knots::line inner, outer; + bool capfrom { false }; + bool capto { false }; + double lift { 0.0 }; + bool web { false }; +}; +struct rail_tread { + double from { 0.0 }; + double to { 0.0 }; + material_handle material { 0 }; + int anchor { 0 }; + knots::line edge; + knots::line gauge; + bool below { false }; +}; +struct rail_layout { + rail_plan plans[ 2 ][ 2 ]; // path, right side + std::vector bands[ 2 ][ 2 ]; + std::vector fills[ 2 ][ 2 ]; + knots::line blocks[ 2 ][ 2 ]; + std::vector treads[ 2 ][ 2 ]; + std::vector guards[ 2 ][ 2 ]; + knots::line 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 +}; + +} // namespace track_rails diff --git a/world/Track.cpp b/world/Track.cpp index f701c5dd..05aeb220 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -2610,10 +2610,10 @@ TTrack::track_rail_profile( int const Profile ) { return m_profiles[ Profile ]; } -TTrack::rail_size +track_rails::rail_size TTrack::rail_dimensions() const { - rail_size result; + track_rails::rail_size result; if( m_profiles.empty() ) { return result; } diff --git a/world/Track.h b/world/Track.h index c415bf00..8d740251 100644 --- a/world/Track.h +++ b/world/Track.h @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Classes.h" #include "world/Segment.h" +#include "world/RailLayout.h" #include "model/material.h" #include "scene/scenenode.h" #include "utilities/Names.h" @@ -299,132 +300,12 @@ 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 - TTrack *owner { nullptr }; - }; - 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 - bool grooved { false }; - double groove { 0.0 }; - double lip { 0.0 }; - double rim { 0.0 }; - double crown { 0.0 }; - double liptop { 0.0 }; - double crest { 0.0 }; - double tread { 0.0735 }; - double ledge { 0.0 }; - double web { 0.0365 }; - double neck { -0.0467 }; - std::vector field, gauge; - }; - 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 }; - double frombend { 0.0 }; - double tobend { 0.0 }; - }; - using check_rail_set = std::array, 2>, 2>; // path, right side + track_rails::switch_frog frog() const; + std::vector rail_crossings( bool const Joined = true ); + track_rails::rail_size rail_dimensions() const; // 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 }; - double endbend { 0.0 }; - double startbend { 0.0 }; - std::vector endfit, startfit; - std::vector endinner, startinner; - bool endcap { true }; - bool startcap { true }; - }; - 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 }; - bool through { true }; - }; - struct rail_band { - double at { 0.0 }; - double into { 0.0 }; - double facing { 0.0 }; - double from { 0.0 }; - double to { 0.0 }; - bool whole { true }; - }; - struct rail_fill { - double from { 0.0 }; - double to { 0.0 }; - std::vector inner, outer; - bool capfrom { false }; - bool capto { false }; - double lift { 0.0 }; - bool web { false }; - }; - struct rail_tread { - double from { 0.0 }; - double to { 0.0 }; - material_handle material { 0 }; - int anchor { 0 }; - std::vector edge; - std::vector gauge; - bool below { false }; - }; - struct rail_layout { - rail_plan plans[ 2 ][ 2 ]; // path, right side - std::vector bands[ 2 ][ 2 ]; - std::vector fills[ 2 ][ 2 ]; - std::vector blocks[ 2 ][ 2 ]; - std::vector treads[ 2 ][ 2 ]; - 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 ); + track_rails::check_rail_set check_rails( std::vector const &Crossings ); + track_rails::rail_layout plan_rails( bool const Crossings ); void update_location(); bool AssignEvents(); bool AssignForcedEvents(basic_event *NewEventPlus, basic_event *NewEventMinus); @@ -521,7 +402,7 @@ private: void build_sleeper_transforms(); struct rail_drawing { gfx::geometrybank_handle bank; - rail_size size; + track_rails::rail_size size; float halfwidth { 0.f }; gfx::vertex_array checkshapes[ 2 ]; gfx::vertex_array plates[ 2 ]; @@ -530,7 +411,7 @@ private: }; 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 ); + bool lay_rail( track_rails::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 ); int blade_side( int const Path ) const; material_handle rail_material( int const Path ) const; glm::vec2 tread_band() const; diff --git a/world/TrackRails.cpp b/world/TrackRails.cpp index 37bdf527..f64a7ee6 100644 --- a/world/TrackRails.cpp +++ b/world/TrackRails.cpp @@ -128,7 +128,7 @@ double knuckle_bend( double const Into ) { } // 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 ) { +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 }; @@ -144,9 +144,9 @@ bool replaced_check( std::vector const &Checks, double const struct rails_memo { std::optional lines[ 2 ][ 2 ][ 2 ]; // path, right side, from the blades - std::optional> own; - std::optional> joined; - std::optional checks; + std::optional> own; + std::optional> joined; + std::optional checks; }; std::unordered_map memos; unsigned int memostamp { 0 }; @@ -494,7 +494,7 @@ gauge_line const &rail_line( TTrack const &Track, int const Path, int const Side return *line; } -TTrack::check_rail_set const &rail_checks( TTrack &Track ) { +track_rails::check_rail_set const &rail_checks( TTrack &Track ) { auto &checks { memo_of( Track ).checks }; if( false == checks.has_value() ) { @@ -505,7 +505,7 @@ TTrack::check_rail_set const &rail_checks( TTrack &Track ) { } // namespace -std::vector TTrack::rail_crossings( bool const Joined ) { +std::vector TTrack::rail_crossings( bool const Joined ) { std::vector result; if( false == cut_rails( *this ) || simulation::Region == nullptr ) { @@ -708,7 +708,7 @@ std::vector TTrack::rail_crossings( bool const Joined ) { return result; } -TTrack::check_rail_set TTrack::check_rails( std::vector const &Crossings ) { +track_rails::check_rail_set TTrack::check_rails( std::vector const &Crossings ) { struct planned { double from { 0.0 }; @@ -864,7 +864,7 @@ TTrack::check_rail_set TTrack::check_rails( std::vector const &Cr return result; } -TTrack::switch_frog TTrack::frog() const { +track_rails::switch_frog TTrack::frog() const { switch_frog result; if( eType != tt_Switch || false == cut_rails( *this ) ) { @@ -937,7 +937,7 @@ TTrack::switch_frog TTrack::frog() const { namespace { // 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 wing_extension( TTrack &Track, track_rails::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 : rail_checks( Partner ) }; @@ -964,7 +964,7 @@ std::vector wing_extension( TTrack &Track, TTrack::check_rail_set co } // 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, std::vector const &Extension, std::vector const &Legs ) { +void acute( track_rails::rail_plan &Plan, double const At, double const Toward, double const Slope, std::vector const &Extension, std::vector const &Legs ) { auto const throat { At + Toward * flangeway / Slope }; auto const flare { At - Toward * wing_reach( Slope ) }; @@ -977,7 +977,7 @@ void acute( TTrack::rail_plan &Plan, double const At, double const Toward, doubl 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; + track_rails::rail_cut cut; cut.at = At; if( Toward < 0.0 ) { cut.end = wingend; @@ -997,13 +997,13 @@ void acute( TTrack::rail_plan &Plan, double const At, double const Toward, doubl } // 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 ) { +void obtuse( track_rails::rail_plan &Plan, double const At, double const Into, track_rails::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 ) }; auto const bend { knuckle_bend( Into ) }; - TTrack::rail_cut cut; + track_rails::rail_cut cut; cut.at = At; if( Into > 0.0 ) { cut.end = At; @@ -1021,11 +1021,11 @@ void obtuse( TTrack::rail_plan &Plan, double const At, double const Into, TTrack } // 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, std::vector const &Fromline = {}, std::vector const &Toline = {} ) { +void wide( track_rails::rail_plan &Plan, double const At, double const Into, double const Facing, double const From, double const To, double const Inner, double const Outer, std::vector const &Fromline = {}, std::vector const &Toline = {} ) { 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; + track_rails::rail_cut cut; cut.at = At; cut.end = At + std::min( low, high ); cut.start = At + std::max( low, high ); @@ -1056,11 +1056,11 @@ std::vector glue_line( std::vector const &Outline, doubl return line; } -TTrack::rail_fill guard_fill( TTrack::guard_rail const &Guard, TTrack::rail_size const &Size ) { +track_rails::rail_fill guard_fill( track_rails::guard_rail const &Guard, track_rails::rail_size const &Size ) { auto const offset { Size.web - ( flangeway + Size.head ) }; auto const flare { std::min( 0.3, 0.25 * ( Guard.to - Guard.from ) ) }; - TTrack::rail_fill fill; + track_rails::rail_fill fill; fill.web = true; fill.from = Guard.from; fill.to = Guard.to; @@ -1082,17 +1082,17 @@ bool goes_through( TTrack const &Track, TTrack const &Other ) { return ( here.x != there.x ? here.x < there.x : here.z < there.z ); } -TTrack::rail_band groove_band( double const At, double const Into, double const Facing, bool const Through, TTrack::rail_size const &Other ) { +track_rails::rail_band groove_band( double const At, double const Into, double const Facing, bool const Through, track_rails::rail_size const &Other ) { auto const seam { 0.001 }; - TTrack::rail_band band { At, Into, Facing }; + track_rails::rail_band band { At, Into, Facing }; band.whole = ( false == Through ); band.from = ( Through ? 0.0 : seam - Other.tread ); band.to = ( Through ? ( Other.grooved ? -Other.lip : flangeway ) : -Other.ledge - seam ); return band; } -void vee_fill( std::vector &Fills, double const At, double const Into, double const Facing, TTrack::rail_size const &Own, TTrack::rail_size const &Other, bool const Outwards ) { +void vee_fill( std::vector &Fills, double const At, double const Into, double const Facing, track_rails::rail_size const &Own, track_rails::rail_size const &Other, bool const Outwards ) { auto const seam { 0.001 }; auto const sign { Outwards ? 1.0 : -1.0 }; @@ -1110,7 +1110,7 @@ void vee_fill( std::vector &Fills, double const At, double co auto const apex { meets( 0.0 ) }; auto const end { meets( gap ) }; auto const reach { seam + rounded / slant }; - TTrack::rail_fill fill; + track_rails::rail_fill fill; fill.from = std::min( apex, end ); fill.to = std::max( apex, end ); fill.capfrom = ( end < apex ); @@ -1194,7 +1194,7 @@ std::vector tread_edge( TTrack const &Track, int const Path, int con return knots; } -std::vector nose_extents( TTrack const &Track, int const Path, int const Side, TTrack const &Other, int const Otherpath, int const Otherside, double const At, double const Toward, double const Slope, TTrack::rail_size const &Size ) { +std::vector nose_extents( TTrack const &Track, int const Path, int const Side, TTrack const &Other, int const Otherpath, int const Otherside, double const At, double const Toward, double const Slope, track_rails::rail_size const &Size ) { auto const half { Slope / ( 1.0 + std::sqrt( 1.0 + Slope * Slope ) ) }; auto const tip { 0.5 * nose_tip }; @@ -1275,7 +1275,7 @@ material_handle TTrack::shared_tread( int const Path, TTrack const &Other, int c return ( track_rails::tread_brightness( theirs, Other.tread_band() ) > track_rails::tread_brightness( own, tread_band() ) ? theirs : own ); } -TTrack::rail_layout TTrack::plan_rails( bool const Crossings ) { +track_rails::rail_layout TTrack::plan_rails( bool const Crossings ) { rail_layout layout; auto const size { rail_dimensions() }; @@ -1366,7 +1366,7 @@ TTrack::rail_layout TTrack::plan_rails( bool const Crossings ) { } 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 }; + track_rails::guard_rail guard { crossing.at - reach, crossing.at + reach }; guard.through = through; wide( guard.plan, crossing.at, crossing.into, crossing.facing, -other.head, through ? flangeway : flangeway + other.head, guardoffset, guardoffset + railhead, glue_line( other.field, 0.0 ), through ? std::vector{} : glue_line( other.gauge, flangeway + other.head ) ); auto merged { false }; @@ -1442,7 +1442,7 @@ TTrack::rail_layout TTrack::plan_rails( bool const Crossings ) { continue; } if( end.into < 0.0 ) { - TTrack::guard_rail guard { glued( start ), glued( end ) }; + track_rails::guard_rail guard { glued( start ), glued( end ) }; guard.flarefrom = false; guard.flareto = false; guard.frommiter = gluedmiter( start ); diff --git a/world/TrackRails.h b/world/TrackRails.h index be54bb86..feb35a45 100644 --- a/world/TrackRails.h +++ b/world/TrackRails.h @@ -13,16 +13,6 @@ http://mozilla.org/MPL/2.0/. namespace track_rails { -double constexpr flangeway { 0.045 }; -double constexpr blend { 0.015 }; -double constexpr tread_fade { 0.02 }; -double constexpr wheel_width { 0.14 }; -double constexpr back_to_back { 1.36 }; -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 }; - void forget(); // how far a wing rail reaches past the crossing point of the rails double wing_reach( double const Slope ); From f67380f84317ce23cdc5f030055d1957f6b0f50d Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 09:24:52 +0200 Subject: [PATCH 05/19] refactor(track): the rail code is split by concern into gauge lines, crossings and check rails, rail materials, rail profile measuring, planning the cuts and laying the geometry, instead of one TrackRails.cpp; rail_offset reuses nearest_distance and lateral; TTrack::rail_dimensions measures the profile through track_rails::measure_rail --- CMakeLists.txt | 9 +- world/GaugeLine.cpp | 65 ++ world/GaugeLine.h | 72 ++ world/RailCrossings.cpp | 593 ++++++++++ world/RailLayer.cpp | 929 ++++++++++++++++ world/RailMaterials.cpp | 173 +++ world/RailPlan.cpp | 548 ++++++++++ world/RailProfile.cpp | 158 +++ world/Track.cpp | 142 +-- world/TrackRails.cpp | 2313 --------------------------------------- world/TrackRails.h | 7 + 11 files changed, 2554 insertions(+), 2455 deletions(-) create mode 100644 world/GaugeLine.cpp create mode 100644 world/GaugeLine.h create mode 100644 world/RailCrossings.cpp create mode 100644 world/RailLayer.cpp create mode 100644 world/RailMaterials.cpp create mode 100644 world/RailPlan.cpp create mode 100644 world/RailProfile.cpp delete mode 100644 world/TrackRails.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 87d53795..67ce285d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,7 +96,12 @@ set(SOURCES "model/Texture.cpp" "utilities/Timer.cpp" "world/Track.cpp" -"world/TrackRails.cpp" +"world/GaugeLine.cpp" +"world/RailCrossings.cpp" +"world/RailMaterials.cpp" +"world/RailProfile.cpp" +"world/RailPlan.cpp" +"world/RailLayer.cpp" "world/Road.cpp" "world/Sweep.cpp" "world/RoadPoint.cpp" @@ -736,4 +741,4 @@ if(WITH_TESTS) # Visual Studio builds rerun them with the next change of the game) set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY LINK_DEPENDS $ ${UNIT_TEST_DATA} "${CMAKE_CURRENT_SOURCE_DIR}/tests/unit/run_tests.cmake") -endif() \ No newline at end of file +endif() diff --git a/world/GaugeLine.cpp b/world/GaugeLine.cpp new file mode 100644 index 00000000..27740f06 --- /dev/null +++ b/world/GaugeLine.cpp @@ -0,0 +1,65 @@ +/* +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/GaugeLine.h" + +namespace track_rails { + +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 = plan( line.points.front() ); + for( auto const &point : line.points ) { + line.low = glm::min( line.low, plan( point ) ); + line.high = glm::max( line.high, plan( point ) ); + } + return line; +} + +double lateral( gauge_line const &Line, int const Side, glm::dvec3 const &Point, double &Distance ) { + + glm::dvec3 point, tangent; + for( int step = 0; step < 8; ++step ) { + Line.at( Distance, point, tangent ); + Distance += glm::dot( plan( Point - point ), glm::normalize( plan( tangent ) ) ); + } + Line.at( Distance, point, tangent ); + return -glm::dot( plan( Point - point ), plan( Line.normal( Distance, Side ) ) ); +} + +double nearest_distance( gauge_line const &Line, glm::dvec3 const &Point ) { + + auto result { Line.distances.front() }; + auto nearest { std::numeric_limits::max() }; + for( std::size_t i = 0; i < Line.points.size(); ++i ) { + auto const gap { glm::length( plan( Line.points[ i ] - Point ) ) }; + if( gap < nearest ) { + nearest = gap; + result = Line.distances[ i ]; + } + } + return result; +} + +} // namespace track_rails diff --git a/world/GaugeLine.h b/world/GaugeLine.h new file mode 100644 index 00000000..8f6f72ee --- /dev/null +++ b/world/GaugeLine.h @@ -0,0 +1,72 @@ +/* +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/Segment.h" + +namespace track_rails { + +inline glm::dvec2 plan( glm::dvec3 const &V ) { + + return glm::dvec2{ V.x, V.z }; +} + +inline double cross( glm::dvec2 const &A, glm::dvec2 const &B ) { + + return A.x * B.y - A.y * B.x; +} + +inline bool meet( glm::dvec2 const &A0, glm::dvec2 const &A, glm::dvec2 const &B0, glm::dvec2 const &B, double const Parallel, double &U, double &V ) { + + auto const denominator { cross( A, B ) }; + if( std::abs( denominator ) < Parallel ) { + return false; + } + U = cross( B0 - A0, B ) / denominator; + V = cross( B0 - A0, A ) / denominator; + return true; +} + +// 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 ); +double lateral( gauge_line const &Line, int const Side, glm::dvec3 const &Point, double &Distance ); +double nearest_distance( gauge_line const &Line, glm::dvec3 const &Point ); + +} // namespace track_rails diff --git a/world/RailCrossings.cpp b/world/RailCrossings.cpp new file mode 100644 index 00000000..4fce7bde --- /dev/null +++ b/world/RailCrossings.cpp @@ -0,0 +1,593 @@ +/* +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 "simulation/simulation.h" +#include "scene/scene.h" + +using namespace track_rails; + +namespace { + +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 ); +} + +struct rails_memo { + std::optional lines[ 2 ][ 2 ][ 2 ]; // path, right side, from the blades + std::optional> own; + std::optional> joined; + std::optional checks; +}; +std::unordered_map memos; +unsigned int memostamp { 0 }; +unsigned int stamp { 1 }; + +rails_memo &memo_of( TTrack const &Track ) { + + if( memostamp != stamp ) { + memos.clear(); + memostamp = stamp; + } + return memos[ &Track ]; +} + +} // namespace + +namespace track_rails { + +void forget() { + + ++stamp; +} + + +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(); +} + +TSegment const &path_segment( TTrack const &Track, int const Path ) { + + return ( Track.eType == tt_Switch ? *Track.SwitchExtension->Segments[ Path ] : *Track.Segment ); +} + +double knuckle_bend( double const Into ) { + + return std::abs( Into ) / ( 1.0 + std::sqrt( std::max( 0.0, 1.0 - Into * Into ) ) ); +} + +// 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; +} + +// gauge line of a rail of a track, along a path of a switch from past its blades when Blades +gauge_line const &rail_line( TTrack const &Track, int const Path, int const Side, bool const Blades ) { + + auto const blades { Blades && Track.eType == tt_Switch }; + auto &line { memo_of( Track ).lines[ Path ][ Side ][ blades ] }; + if( false == line.has_value() ) { + line = make_gauge_line( path_segment( Track, Path ), ( Side ? 1.0 : -1.0 ) * ( 0.5 * std::abs( Track.fTrackWidth ) ), blades ? blade_length( Track, Path ) : 0.0 ); + } + return *line; +} + +track_rails::check_rail_set const &rail_checks( TTrack &Track ) { + + auto &checks { memo_of( Track ).checks }; + if( false == checks.has_value() ) { + checks = Track.check_rails( Track.rail_crossings() ); + } + return *checks; +} + +glm::dvec3 rail_point( TTrack const &Track, int const Path, int const Side, double const Distance, double const Lateral ) { + + auto const line { make_gauge_line( path_segment( Track, Path ), Side * 0.5 * std::abs( Track.fTrackWidth ), 0.0 ) }; + glm::dvec3 point, tangent; + line.at( Distance, point, tangent ); + return point - line.normal( Distance, Side ) * Lateral; +} + +void rail_offset( TTrack const &Track, int const Path, int const Side, glm::dvec3 const &Point, double &Distance, double &Lateral ) { + + auto const line { make_gauge_line( path_segment( Track, Path ), Side * 0.5 * std::abs( Track.fTrackWidth ), 0.0 ) }; + Distance = nearest_distance( line, Point ); + Lateral = lateral( line, Side, Point, Distance ); +} + +} // 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 &memo { memo_of( *this ) }; + if( Joined && memo.joined.has_value() ) { + return *memo.joined; + } + auto const extension { 10.0 }; + auto const paths { eType == tt_Switch ? 2 : 1 }; + if( memo.own.has_value() ) { + result = *memo.own; + } + else { + 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 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 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 { rail_line( *other, otherpath, otherside, true ) }; + 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 { rail_line( *this, path, side, false ) }; + 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 }; + auto const alow { glm::min( a0, a0 + a ) - 0.02 * glm::length( a ) - 1e-6 }; + auto const ahigh { glm::max( a0, a0 + a ) + 0.02 * glm::length( a ) + 1e-6 }; + if( alow.x > theirs.high.x + 1.0 || theirs.low.x - 1.0 > ahigh.x || alow.y > theirs.high.y + 1.0 || theirs.low.y - 1.0 > ahigh.y ) { + continue; + } + 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 reach { 0.02 * glm::length( b ) }; + if( alow.x > std::max( b0.x, b0.x + b.x ) + reach || std::min( b0.x, b0.x + b.x ) - reach > ahigh.x + || alow.y > std::max( b0.y, b0.y + b.y ) + reach || std::min( b0.y, b0.y + b.y ) - reach > ahigh.y ) { + continue; + } + double u, v; + if( false == meet( a0, a, b0, b, 1e-12, u, v ) ) { + continue; + } + 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 ); + } + } + } + } + } + } + } + memo.own = result; + } + 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 tostart { glm::length( neighboursegment.FastGetPoint_0() - joint ) }; + auto const toend { glm::length( neighboursegment.FastGetPoint_1() - joint ) }; + if( std::min( tostart, toend ) > 0.05 ) { + continue; + } + auto const atstart { tostart <= toend }; + 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 ); + mapped.owner = neighbour; + 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 ); + } + } + } + } + } + memo.joined = result; + return result; +} + +track_rails::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 }; + double frombend { 0.0 }; + double tobend { 0.0 }; + }; + std::vector plans[ 2 ][ 2 ]; + check_rail_set result; + auto const size { rail_dimensions() }; + if( size.grooved ) { + return result; + } + 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 || crossing.track->rail_dimensions().grooved ) { + 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, crossing.at + 1.2, false, true, 0.5, 0.02, -knuckle_bend( crossing.into ), 0.0 } ); + } + else { + plans[ crossing.path ][ crossing.side > 0 ].push_back( { crossing.at - 1.2, crossing.at, true, false, 0.5, 0.02, 0.0, knuckle_bend( crossing.into ) } ); + } + } + 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 }; + 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 ].tobend = checks[ i ].tobend; + } + 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 { plan( segment.FastGetPoint_0() ) }; + auto const end { plan( 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 { plan( 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, plan( other.FastGetPoint_0() ) ) < 0.1 || glm::distance( Point, plan( 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 { plan( point ) }; + auto const inward { plan( 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 { plan( other.points[ i - 1 ] ) }; + auto const edge { plan( other.points[ i ] ) - a }; + double t, u; + if( false == meet( gauge, inward, a, edge, 1e-9, t, u ) ) { + continue; + } + 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, + From > check.from ? 0.0 : check.frombend, + To < check.to ? 0.0 : check.tobend } ); }; + 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; +} + +track_rails::switch_frog TTrack::frog() const { + + switch_frog result; + if( eType != tt_Switch || false == cut_rails( *this ) ) { + return result; + } + result.side[ 0 ] = blade_side( 0 ); + 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 ); + } + } + 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 }; + double u, v; + if( false == meet( a0, a, b0, b, 1e-12, u, v ) ) { + continue; + } + 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; +} diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp new file mode 100644 index 00000000..1e19cb39 --- /dev/null +++ b/world/RailLayer.cpp @@ -0,0 +1,929 @@ +/* +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 "rendering/renderer.h" + +namespace track_rails { + +// 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; + +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() ); + } + if( false == m_profiles.empty() ) { + auto const &outline { track_rail_profile( m_profile1.second ) }; + auto texture { 0.f }; + auto highest { -1e9f }; + for( std::size_t i = 0; i < outline.size() / 2; ++i ) { + if( outline[ i ].position.y > highest ) { + highest = outline[ i ].position.y; + texture = outline[ i ].texture.s; + } + } + if( drawing.size.grooved ) { + auto sum { 0.f }; + auto points { 0 }; + for( std::size_t i = 0; i < outline.size() / 2; ++i ) { + if( outline[ i ].position.y > highest - 0.0005f && outline[ i ].position.x > -0.02f ) { + sum += outline[ i ].texture.s; + ++points; + } + } + texture = sum / std::max( points, 1 ); + } + auto const top { static_cast( ( drawing.size.grooved ? drawing.size.liptop : highest ) - 0.0005 ) }; + auto const bottom { static_cast( -drawing.size.height ) }; + gfx::vertex_array plate { + {{ 1.f, bottom, 0.f }, { 1.f, 0.f, 0.f }, { texture, 0.f }}, + {{ 1.f, top, 0.f }, { 1.f, 0.f, 0.f }, { texture, 0.f }}, + {{ 1.f, top, 0.f }, { 0.f, 1.f, 0.f }, { texture, 0.f }}, + {{ -1.f, top, 0.f }, { 0.f, 1.f, 0.f }, { texture, 0.f }}, + {{ -1.f, top, 0.f }, { -1.f, 0.f, 0.f }, { texture, 0.f }}, + {{ -1.f, bottom, 0.f }, { -1.f, 0.f, 0.f }, { texture, 0.f }} }; + drawing.platetexture = texture; + create_track_rail_profile( drawing.plates[ 0 ], drawing.plates[ 1 ], &plate ); + for( auto &shape : drawing.plates ) { + shape.resize( plate.size() ); + } + auto const neck { static_cast( drawing.size.neck ) }; + auto webtexture { texture }; + for( std::size_t i = 0; i < outline.size() / 2; ++i ) { + auto const &point { outline[ i ] }; + if( point.position.y < neck && point.normal.y > 0.9f ) { + webtexture = point.texture.s; + break; + } + } + gfx::vertex_array filler { + {{ 1.f, bottom, 0.f }, { 1.f, 0.f, 0.f }, { webtexture, 0.f }}, + {{ 1.f, neck, 0.f }, { 1.f, 0.f, 0.f }, { webtexture, 0.f }}, + {{ 1.f, neck, 0.f }, { 0.f, 1.f, 0.f }, { webtexture, 0.f }}, + {{ -1.f, neck, 0.f }, { 0.f, 1.f, 0.f }, { webtexture, 0.f }}, + {{ -1.f, neck, 0.f }, { -1.f, 0.f, 0.f }, { webtexture, 0.f }}, + {{ -1.f, bottom, 0.f }, { -1.f, 0.f, 0.f }, { webtexture, 0.f }} }; + create_track_rail_profile( drawing.fillers[ 0 ], drawing.fillers[ 1 ], &filler ); + for( auto &shape : drawing.fillers ) { + shape.resize( filler.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() }; + auto const &treads { Layout.treads[ Path ][ Side > 0 ] }; + material_handle current { null_handle }; + std::vector tints; + static auto const marked { std::getenv( "EU07_EDITOR_TREADS" ) != nullptr }; + auto const paint = [&]( material_handle const Material, glm::vec3 const &Colour ) { + auto const tinted { tint_material( Material, Colour ) }; + if( tinted != Material ) { + tints.push_back( tinted ); + } + return tinted; }; + std::vector const unramped; + auto const sink = [&]( bool const Matte ) -> geometryhandle_sequence & { + if( current == null_handle ) { + return Matte ? Cuts : Geometry; + } + if( current == m_material1 ) { + return Matte ? GeometryCut1 : Geometry1; + } + if( eType == tt_Switch && current == m_material2 ) { + return Matte ? GeometryCut2 : Geometry2; + } + auto const tinted { std::find( tints.begin(), tints.end(), current ) != tints.end() }; + auto found { std::find_if( GeometryTreads.begin(), GeometryTreads.end(), [&]( tread_geometry const &Tread ) { return Tread.material == current; } ) }; + if( found == GeometryTreads.end() ) { + found = GeometryTreads.insert( GeometryTreads.end(), { current, tinted ? current : cut_material( current ), {}, {} } ); + } + return Matte && false == tinted ? found->cuts : found->geometry; }; + auto const texturelength = [&]() { + return ( current == null_handle ? Texturelength : texture_length( current ) ); }; + auto const aim = [&]( std::vector const &Spans, double const Middle ) -> rail_tread const * { + current = null_handle; + for( auto const &span : Spans ) { + if( Middle > span.from && Middle < span.to ) { + current = span.material; + return &span; + } + } + return nullptr; + }; + // parts no wheel rolls on go in the matte material of the cut faces + glm::dvec2 bendfrom { 0.0 }; + glm::dvec2 bendto { 0.0 }; + auto const bends = [&]( double const Start, double const End ) { + return glm::dvec2 { + std::abs( Start - bendfrom.x ) < 1e-6 ? bendfrom.y : 0.0, + std::abs( End - bendto.x ) < 1e-6 ? bendto.y : 0.0 }; }; + std::pair> fitfrom, fitto; + std::vector const unfitted; + auto const fitted = [&]( std::pair> const &Fit, double const Distance ) -> std::vector const & { + return ( std::abs( Distance - Fit.first ) < 1e-6 ? Fit.second : unfitted ); }; + auto const span = [&]( double const Start, double const End, knots::line const &Offsets, knots::line const &Extents, knots::line const &Inner ) { + loft_span result; + result.side = Side; + result.halfwidth = halfwidth; + result.texturelength = texturelength(); + result.from = Start; + result.to = End; + result.offsets = Offsets; + result.extents = Extents; + result.inner = Inner; + auto const bent { bends( Start, End ) }; + result.fromend = { bent.x, fitted( fitfrom, Start ) }; + result.toend = { bent.y, fitted( fitto, End ) }; + return result; }; + 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 = {}, bool const Walls = false, float const Lift = 0.f, float const Wallu = -1.f, double const Fade = blend ) { + gfx::vertex_array vertices; + gfx::userdata_array userdata; + auto loft { span( Start, End, Offsets, Extents, Inner ) }; + loft.lift = Lift; + loft.userdata = ( Rolled ? nullptr : &userdata ); + loft.ramp = Ramp; + loft.rampwidth = ( Ramp.empty() ? 0.0 : Fade ); + loft.walls = Walls; + loft.wallu = Wallu; + if( Shape.size() > 1 && segment.RenderLoftSpan( vertices, m_origin, Shape, loft ) ) { + cut_faces( vertices, Shape.size(), Capstart, Capend, Drawing.bank, sink( true ) ); + 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 ] ); + } + } + } + } + sink( false == Rolled ).emplace_back( GfxRenderer->Insert( triangles, Rolled ? empty_userdata : triangleuserdata, Drawing.bank, GL_TRIANGLES ) ); + } + }; + // 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 crossings = []( knots::line const &A, knots::line const &B, double const Start, double const End, double const Margin, std::vector &Splits ) { + for( auto const distance : knots::meetings( A, B ) ) { + if( distance > Start + Margin && distance < End - Margin ) { + Splits.push_back( distance ); + } + } + }; + auto const running = [&]( double const Start, double const End, std::vector const &Offsets, std::vector const &Extents, std::vector const &Inner, std::vector const &Rolled, bool const Capstart, bool const Capend ) { + gfx::vertex_array vertices; + if( segment.RenderLoftSpan( vertices, m_origin, Profile, span( Start, End, Offsets, Extents, Inner ) ) ) { + cut_faces( vertices, Profile.size(), Capstart, Capend, Drawing.bank, sink( true ) ); + } + piece( Start, End, Offsets, Extents, false, false, strip( 0, topfirst ), true, Inner ); + piece( Start, End, Offsets, Extents, false, false, strip( toplast, Profile.size() - 1 ), true, Inner ); + 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 ) ); + } + auto const inner { knots::shifted( Rolled, -blend ) }; + std::vector splits { Start, End }; + std::vector const lowline { { Start, low }, { End, low } }; + std::vector const highline { { Start, high }, { End, high } }; + for( auto const *knots : { &Rolled, &inner } ) { + for( auto const *limit : { &lowline, &highline, &Extents, &Inner } ) { + crossings( *knots, *limit, Start, End, 0.0, splits ); + } + } + std::sort( splits.begin(), splits.end() ); + auto const shiny { knots::bound( Extents, inner, true ) }; + auto const blended { knots::bound( Extents, Rolled, true ) }; + for( std::size_t i = 1; i < splits.size(); ++i ) { + auto const middle { 0.5 * ( splits[ i - 1 ] + splits[ i ] ) }; + auto const reach { knots::sample( Rolled, middle, 0.0 ) }; + auto const least { std::max( low, Inner.empty() ? low : knots::sample( Inner, middle, 0.0 ) ) }; + auto const most { std::min( high, Extents.empty() ? high : knots::sample( Extents, middle, 0.0 ) ) }; + auto const shown = [&]( double const Lowest, double const Highest ) { + return Highest > Lowest + 1e-6 && Highest > least && Lowest < most; }; + if( shown( -1e9, reach - blend ) ) { + piece( splits[ i - 1 ], splits[ i ], Offsets, shiny, false, false, top, true, Inner ); + } + auto const plain { current }; + auto const own { current == null_handle ? rail_material( Path ) : current }; + if( shown( reach - blend, reach ) ) { + current = ( marked ? paint( own, { 1.f, 1.f, 0.f } ) : plain ); + piece( splits[ i - 1 ], splits[ i ], Offsets, blended, false, false, top, false, knots::bound( inner, Inner, false ), marked ? unramped : Rolled ); + } + if( shown( reach, 1e9 ) ) { + current = ( marked ? paint( own, { 1.f, 0.f, 1.f } ) : plain ); + piece( splits[ i - 1 ], splits[ i ], Offsets, Extents, false, false, top, false, knots::bound( Rolled, Inner, false ) ); + } + current = plain; + } + }; + auto const treaded = [&]( double const Start, double const End, std::vector const &Offsets, std::vector const &Extents, std::vector const &Inner, bool const Capstart, bool const Capend, rail_tread const &Span ) { + current = null_handle; + gfx::vertex_array vertices; + if( segment.RenderLoftSpan( vertices, m_origin, Profile, span( Start, End, Offsets, Extents, Inner ) ) ) { + cut_faces( vertices, Profile.size(), Capstart, Capend, Drawing.bank, sink( true ) ); + } + piece( Start, End, Offsets, Extents, false, false, strip( 0, topfirst ), true, Inner ); + piece( Start, End, Offsets, Extents, false, false, strip( toplast, Profile.size() - 1 ), true, Inner ); + 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 ) ); + } + auto const &lower { Span.below ? Span.gauge : Span.edge }; + auto const &upper { Span.below ? Span.edge : Span.gauge }; + auto const own { rail_material( Path ) }; + auto const colour { 0.5f * ( tread_colour( Span.material, tread_band() ) + tread_colour( own, tread_band() ) ) }; + auto const tint = [&]( material_handle const Material, glm::vec3 const &Mark ) { + return paint( Material, marked ? Mark : colour ); }; + auto const owntint { tint( own, { 1.f, 0.f, 0.f } ) }; + auto const sharedtint { tint( Span.material, { 0.f, 1.f, 0.f } ) }; + auto const shared { marked ? tint( Span.material, { 0.f, 0.f, 1.f } ) : Span.material }; + auto const ramp = [&]( std::vector const &Knots ) -> std::vector const & { + return marked ? unramped : Knots; }; + auto const centre { knots::middle( lower, upper ) }; + auto const beneath { knots::shifted( lower, -tread_fade ) }; + auto const beyond { knots::shifted( upper, tread_fade ) }; + auto const lowfade { knots::bound( knots::shifted( lower, tread_fade ), centre, true ) }; + auto const highfade { knots::bound( knots::shifted( upper, -tread_fade ), centre, false ) }; + std::vector splits { Start, End }; + std::vector const lowline { { Start, low }, { End, low } }; + std::vector const highline { { Start, high }, { End, high } }; + for( auto const *knots : { &lower, &upper, &lowfade, &highfade, &beneath, &beyond } ) { + for( auto const *limit : { &lowline, &highline, &Extents, &Inner } ) { + crossings( *knots, *limit, Start, End, 0.001, splits ); + } + } + crossings( knots::shifted( lower, 2.0 * tread_fade ), upper, Start, End, 0.001, splits ); + std::sort( splits.begin(), splits.end() ); + for( std::size_t i = 1; i < splits.size(); ++i ) { + auto const a { splits[ i - 1 ] }; + auto const b { splits[ i ] }; + auto const middle { 0.5 * ( a + b ) }; + auto const from { knots::sample( lower, middle, 0.0 ) }; + auto const to { knots::sample( upper, middle, 0.0 ) }; + auto const inner { knots::sample( lowfade, middle, 0.0 ) }; + auto const outer { knots::sample( highfade, middle, 0.0 ) }; + auto const least { std::max( low, Inner.empty() ? low : knots::sample( Inner, middle, 0.0 ) ) }; + auto const most { std::min( high, Extents.empty() ? high : knots::sample( Extents, middle, 0.0 ) ) }; + auto const shown = [&]( double const Lowest, double const Highest ) { + return Highest > Lowest + 1e-6 && Highest > least && Lowest < most; }; + if( shown( -1e9, from - tread_fade ) ) { + current = null_handle; + piece( a, b, Offsets, knots::bound( Extents, beneath, true ), false, false, top, true, Inner ); + } + if( shown( from - tread_fade, from ) ) { + current = owntint; + piece( a, b, Offsets, knots::bound( Extents, lower, true ), false, false, top, false, knots::bound( Inner, beneath, false ), ramp( lower ), false, 0.f, -1.f, tread_fade ); + } + if( shown( from, inner ) ) { + current = sharedtint; + piece( a, b, Offsets, knots::bound( Extents, lowfade, true ), false, false, top, false, knots::bound( Inner, lower, false ), ramp( lower ), false, 0.f, -1.f, -tread_fade ); + } + if( shown( inner, outer ) ) { + current = shared; + piece( a, b, Offsets, knots::bound( Extents, highfade, true ), false, false, top, false == marked, knots::bound( Inner, lowfade, false ) ); + } + if( shown( outer, to ) ) { + current = sharedtint; + piece( a, b, Offsets, knots::bound( Extents, upper, true ), false, false, top, false, knots::bound( Inner, highfade, false ), ramp( upper ), false, 0.f, -1.f, tread_fade ); + } + if( shown( to, to + tread_fade ) ) { + current = owntint; + piece( a, b, Offsets, knots::bound( Extents, beyond, true ), false, false, top, false, knots::bound( Inner, upper, false ), ramp( upper ), false, 0.f, -1.f, -tread_fade ); + } + if( shown( to + tread_fade, 1e9 ) ) { + current = null_handle; + piece( a, b, Offsets, Extents, false, false, top, true, knots::bound( Inner, beyond, false ) ); + } + } + current = null_handle; + }; + 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 }; + rail_cut startcut; + 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 endcut { i < cuts.size() ? cuts[ i ] : rail_cut {} }; + 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 ); + } + } + auto const glued { a == start && false == startcut.startfit.empty() }; + auto const gluedend { b == end && false == endcut.endfit.empty() }; + bendfrom = { a, glued ? startcut.startbend : 0.0 }; + bendto = { b, gluedend ? endcut.endbend : 0.0 }; + fitfrom = { a, glued ? startcut.startfit : unfitted }; + fitto = { b, gluedend ? endcut.endfit : unfitted }; + piece( a, b, offsets, envelope( uppers, true, a, b ), false == glued, false == gluedend, Profile, false, envelope( lowers, false, a, b ) ); + bendfrom = bendto = glm::dvec2 { 0.0 }; + fitfrom = fitto = {}; + } + if( i < cuts.size() && cuts[ i ].start > start ) { + start = cuts[ i ].start; + startmiter = cuts[ i ].startmiter; + startcut = cuts[ i ]; + } + } + } + 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 } ); + } + bendfrom = { a, a == from ? check.frombend : 0.0 }; + bendto = { b, b == to ? check.tobend : 0.0 }; + piece( a, b, offsets, {}, a > 0.0 || from >= 0.0, to <= length, shape, false, {} ); + bendfrom = bendto = glm::dvec2 { 0.0 }; + } + for( auto const &fill : Layout.fills[ Path ][ Side > 0 ] ) { + auto const a { std::max( fill.from, From ) }; + auto const b { std::min( fill.to, length ) }; + if( b - a > 0.01 ) { + current = null_handle; + if( false == fill.web ) { + aim( treads, 0.5 * ( a + b ) ); + } + auto const &shapes { fill.web ? Drawing.fillers : Drawing.plates }; + piece( a, b, {}, fill.outer, a == fill.from && fill.capfrom, b == fill.to && fill.capto, shapes[ Side > 0 ? 0 : 1 ], true, fill.inner, {}, true, static_cast( fill.lift ) ); + current = null_handle; + } + } + auto const &bands { Layout.bands[ Path ][ Side > 0 ] }; + if( plan.cuts.empty() && bands.empty() && treads.empty() ) { + return false; + } + auto const byposition = []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; }; + auto const reaches = [&]( gfx::vertex_array const &Shape ) { + glm::dvec2 result { 1e9, -1e9 }; + for( auto const &point : Shape ) { + auto const reach { static_cast( Side * point.position.x - halfwidth ) }; + result = { std::min( result.x, reach ), std::max( result.y, reach ) }; + } + return result; }; + auto const band_cut = [&]( rail_band const &Band, glm::dvec2 const &Reach ) { + rail_cut cut; + cut.at = Band.at; + auto const before { Band.into > 0.0 ? Band.from : Band.to }; + auto const after { Band.into > 0.0 ? Band.to : Band.from }; + auto const meets = [&]( double const Edge, double const Reach ) { + return glm::dvec2{ Band.at + ( Edge + Band.facing * Reach ) / Band.into, Reach }; }; + if( std::abs( Band.facing ) < 1e-3 ) { + cut.end = meets( before, 0.0 ).x; + cut.start = meets( after, 0.0 ).x; + return cut; + } + std::vector endknots { meets( before, Reach.x ), meets( before, Reach.y ) }; + std::vector startknots { meets( after, Reach.x ), meets( after, Reach.y ) }; + std::sort( endknots.begin(), endknots.end(), byposition ); + std::sort( startknots.begin(), startknots.end(), byposition ); + cut.end = endknots.back().x; + cut.start = startknots.front().x; + cut.endcap = false; + cut.startcap = false; + auto const outwards { Band.facing / Band.into > 0.0 }; + ( outwards ? cut.endinner : cut.endextents ) = endknots; + ( outwards ? cut.startextents : cut.startinner ) = startknots; + return cut; }; + auto const lay = [&]( gfx::vertex_array const &Shape, std::vector cuts, bool const Wheels, std::vector const &Spans, float const Wallu = -1.f ) { + std::sort( cuts.begin(), cuts.end(), []( rail_cut const &A, rail_cut const &B ) { return A.at < B.at; } ); + auto start { -1e9 }; + auto startcap { true }; + auto startbend { 0.0 }; + std::vector startfit; + std::vector rolledfrom; + std::vector offsets, extents, inner; + 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 }; + auto pieceinner { inner }; + 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() ); + pieceinner.insert( pieceinner.end(), cuts[ i ].endinner.begin(), cuts[ i ].endinner.end() ); + } + std::sort( pieceoffsets.begin(), pieceoffsets.end(), byposition ); + std::sort( pieceextents.begin(), pieceextents.end(), byposition ); + std::sort( pieceinner.begin(), pieceinner.end(), byposition ); + auto const a { std::max( start, From ) }; + auto const b { std::min( end, length ) }; + auto const rolled { i < cuts.size() ? knots::bound( rolledfrom, cuts[ i ].endrolled, true ) : rolledfrom }; + auto const capstart { start > From ? startcap : Openstart }; + auto const capend { end < length ? cuts[ i ].endcap : Openend }; + bendfrom = { a, start > From ? startbend : 0.0 }; + bendto = { b, end < length ? cuts[ i ].endbend : 0.0 }; + fitfrom = { a, start > From ? startfit : unfitted }; + fitto = { b, end < length ? cuts[ i ].endfit : unfitted }; + if( b - a > 0.01 ) { + std::vector active; + for( auto const &span : Spans ) { + if( span.anchor == 0 + || ( span.anchor > 0 && b > span.from + 0.01 && b < span.to + 0.01 ) + || ( span.anchor < 0 && a > span.from - 0.01 && a < span.to - 0.01 ) ) { + active.push_back( span ); + } + } + std::vector splits; + for( auto const &span : active ) { + for( auto const edge : { span.from, span.to } ) { + if( edge > a + 0.01 && edge < b - 0.01 ) { + splits.push_back( edge ); + } + } + } + std::sort( splits.begin(), splits.end() ); + splits.erase( std::unique( splits.begin(), splits.end(), []( double const A, double const B ) { return B - A < 0.002; } ), splits.end() ); + splits.insert( splits.begin(), a ); + splits.push_back( b ); + for( std::size_t k = 1; k < splits.size(); ++k ) { + auto const first { k == 1 }; + auto const last { k + 1 == splits.size() }; + auto const *span { aim( active, 0.5 * ( splits[ k - 1 ] + splits[ k ] ) ) }; + if( Wheels && span != nullptr && false == span->edge.empty() && topfirst < toplast ) { + treaded( splits[ k - 1 ], splits[ k ], pieceoffsets, pieceextents, pieceinner, first && capstart, last && capend, *span ); + } + else if( false == Wheels || rolled.empty() || topfirst >= toplast ) { + piece( splits[ k - 1 ], splits[ k ], pieceoffsets, pieceextents, first && capstart, last && capend, Shape, true, pieceinner, {}, false == Wheels, 0.f, Wallu ); + } + else { + running( splits[ k - 1 ], splits[ k ], pieceoffsets, pieceextents, pieceinner, rolled, first && capstart, last && capend ); + } + } + current = null_handle; + } + bendfrom = bendto = glm::dvec2 { 0.0 }; + fitfrom = fitto = {}; + if( i < cuts.size() ) { + rolledfrom = cuts[ i ].startrolled; + start = cuts[ i ].start; + startcap = cuts[ i ].startcap; + startbend = cuts[ i ].startbend; + startfit = cuts[ i ].startfit; + offsets = cuts[ i ].startoffsets; + extents = cuts[ i ].startextents; + inner = cuts[ i ].startinner; + } + } + }; + auto const &outline { track_rail_profile( m_profile1.second ) }; + auto const count { Profile.size() }; + if( false == size.grooved || outline.size() != 2 * count ) { + auto cuts { plan.cuts }; + auto const reach { reaches( Profile ) }; + for( auto const &band : bands ) { + cuts.push_back( band_cut( band, reach ) ); + } + lay( Profile, cuts, true, treads ); + return true; + } + auto const height = [&]( std::size_t const Index ) { + return static_cast( outline[ Side > 0 ? Index : count - 1 - Index ].position.y ) - size.groove; }; + auto const above = [&]( std::size_t const Index ) { + return height( Index ) > 1e-4; }; + auto const floorpoint = [&]( std::size_t const A, std::size_t const B ) { + auto const t { static_cast( height( A ) / ( height( A ) - height( B ) ) ) }; + auto point { Profile[ above( A ) ? A : B ] }; + point.position = glm::mix( Profile[ A ].position, Profile[ B ].position, t ); + point.texture = glm::mix( Profile[ A ].texture, Profile[ B ].texture, t ); + return point; }; + auto const flattened = [&]( std::size_t const Index ) { + auto point { Profile[ Index ] }; + auto const &file { outline[ Side > 0 ? Index : count - 1 - Index ].position }; + if( file.y > size.crest - 0.0005 || ( file.x < -0.02 && file.y > size.liptop - 0.0005 ) ) { + point.normal = { 0.f, 1.f, 0.f }; + point.texture.x = Drawing.platetexture; + } + return point; }; + std::vector tops, flats; + gfx::vertex_array body; + std::vector floors; + auto floortexture { 0.f }; + auto floortextures { 0 }; + for( std::size_t i = 0; i < count; ++i ) { + if( false == above( i ) ) { + if( height( i ) > -1e-4 ) { + floors.push_back( body.size() ); + floortexture += Profile[ i ].texture.x; + ++floortextures; + } + body.push_back( Profile[ i ] ); + continue; + } + auto const first { i }; + while( i + 1 < count && above( i + 1 ) ) { + ++i; + } + gfx::vertex_array top, flat; + if( first > 0 ) { + if( height( first - 1 ) > -1e-4 ) { + top.push_back( Profile[ first - 1 ] ); + flat.push_back( flattened( first - 1 ) ); + } + else { + top.push_back( floorpoint( first - 1, first ) ); + flat.push_back( top.back() ); + body.push_back( top.back() ); + floors.push_back( body.size() ); + body.push_back( top.back() ); + } + } + for( auto k { first }; k <= i; ++k ) { + top.push_back( Profile[ k ] ); + flat.push_back( flattened( k ) ); + } + if( i + 1 < count ) { + if( height( i + 1 ) > -1e-4 ) { + top.push_back( Profile[ i + 1 ] ); + flat.push_back( flattened( i + 1 ) ); + } + else { + top.push_back( floorpoint( i, i + 1 ) ); + flat.push_back( top.back() ); + floors.push_back( body.size() ); + body.push_back( top.back() ); + body.push_back( top.back() ); + } + } + for( auto *strip : { &top, &flat } ) { + gfx::vertex_array doubled { strip->front(), strip->front() }; + for( std::size_t k = 1; k + 1 < strip->size(); ++k ) { + doubled.insert( doubled.end(), 2, ( *strip )[ k ] ); + } + doubled.insert( doubled.end(), 2, strip->back() ); + *strip = doubled; + } + tops.push_back( top ); + flats.push_back( flat ); + } + if( floors.size() > 1 ) { + auto const across { body[ floors.back() ].position - body[ floors.front() ].position }; + auto up { glm::normalize( glm::vec3{ -across.y, across.x, 0.f } ) }; + if( up.y < 0.f ) { + up = -up; + } + for( auto const index : floors ) { + body[ index ].normal = up; + if( floortextures > 0 ) { + body[ index ].texture.x = floortexture / floortextures; + } + } + } + std::vector wholecuts { plan.cuts }; + auto const bodyreach { reaches( body ) }; + for( auto const &band : bands ) { + if( band.whole ) { + wholecuts.push_back( band_cut( band, bodyreach ) ); + } + } + lay( body, wholecuts, false, {} ); + auto const wallu { floortextures > 0 ? floortexture / floortextures : -1.f }; + for( std::size_t k = 0; k < tops.size(); ++k ) { + auto const reach { reaches( tops[ k ] ) }; + auto const head { reach.x + reach.y > 0.0 }; + auto edge { head ? 1e9 : -1e9 }; + for( auto const &point : tops[ k ] ) { + if( point.position.y > ( head ? size.crest : size.liptop ) - 0.0005 ) { + auto const across { static_cast( Side * point.position.x - halfwidth ) }; + edge = ( head ? std::min( edge, across ) : std::max( edge, across ) ); + } + } + std::vector cuts { plan.cuts }; + for( auto const &band : bands ) { + auto cut { band_cut( band, reach ) }; + if( false == band.whole && std::abs( band.facing ) >= 1e-3 && std::abs( edge ) < 1.0 ) { + auto const blunt { edge + ( head ? nose_tip : -nose_tip ) }; + if( head == ( band.facing / band.into > 0.0 ) ) { + cut.start = std::max( cut.start, band.at + ( ( band.into > 0.0 ? band.to : band.from ) + band.facing * blunt ) / band.into ); + cut.startcap = true; + } + else { + cut.end = std::min( cut.end, band.at + ( ( band.into > 0.0 ? band.from : band.to ) + band.facing * blunt ) / band.into ); + cut.endcap = true; + } + } + cuts.push_back( cut ); + } + std::vector spans { Layout.blocks[ Path ][ Side > 0 ] }; + for( auto const &cut : cuts ) { + for( auto &span : spans ) { + if( cut.at > span.x && cut.at < span.y ) { + span = { std::min( span.x, cut.end ), std::max( span.y, cut.start ) }; + } + } + } + std::sort( spans.begin(), spans.end(), byposition ); + std::vector merged; + for( auto const &span : spans ) { + if( false == merged.empty() && span.x <= merged.back().y ) { + merged.back().y = std::max( merged.back().y, span.y ); + } + else { + merged.push_back( span ); + } + } + if( merged.empty() ) { + lay( tops[ k ], cuts, false, treads, wallu ); + continue; + } + std::vector blocktreads; + for( auto const &span : merged ) { + for( auto const &tread : treads ) { + if( tread.from < span.y && tread.to > span.x ) { + blocktreads.push_back( { span.x, span.y, tread.material } ); + break; + } + } + } + auto const seam = []( double const At, double const End, double const Start ) { + rail_cut cut; + cut.at = At; + cut.end = End; + cut.start = Start; + cut.endcap = false; + cut.startcap = false; + return cut; }; + std::vector outside, inside; + for( auto const &cut : cuts ) { + auto const within { std::any_of( merged.begin(), merged.end(), [&]( glm::dvec2 const &Span ) { return cut.at > Span.x && cut.at < Span.y; } ) }; + ( within ? inside : outside ).push_back( cut ); + } + inside.push_back( seam( merged.front().x - 0.001, -1e9, merged.front().x ) ); + for( std::size_t m = 0; m < merged.size(); ++m ) { + outside.push_back( seam( 0.5 * ( merged[ m ].x + merged[ m ].y ), merged[ m ].x, merged[ m ].y ) ); + inside.push_back( m + 1 < merged.size() ? + seam( 0.5 * ( merged[ m ].y + merged[ m + 1 ].x ), merged[ m ].y, merged[ m + 1 ].x ) : + seam( merged[ m ].y + 0.001, merged[ m ].y, 1e9 ) ); + } + lay( tops[ k ], outside, false, treads, wallu ); + lay( flats[ k ], inside, false, blocktreads, wallu ); + } + return true; +} + diff --git a/world/RailMaterials.cpp b/world/RailMaterials.cpp new file mode 100644 index 00000000..50e402e5 --- /dev/null +++ b/world/RailMaterials.cpp @@ -0,0 +1,173 @@ +/* +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 "rendering/renderer.h" +#include "stb/stb_image.h" + +namespace track_rails { + +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 ); +} + +material_handle tint_material( material_handle const Material, glm::vec3 const &Colour ) { + + return GfxRenderer->Tinted_Material( Material, Colour ); +} + +std::pair const &tread_sample( material_handle const Material, glm::vec2 const &Band ) { + + static std::map, std::pair> samples; + auto const key { std::make_tuple( Material, Band.x, Band.y ) }; + if( auto const lookup { samples.find( key ) }; lookup != samples.end() ) { + return lookup->second; + } + auto &result { samples[ key ] }; + result = { -1.f, glm::vec3{ 0.f } }; + if( Material == null_handle ) { + return result; + } + auto const texture { GfxRenderer->Material( Material )->GetTexture( 0 ) }; + if( texture == null_handle ) { + return result; + } + auto const &image { GfxRenderer->Texture( texture ) }; + auto const type { std::string( image.get_type() ) }; + auto const file { std::string( image.get_name() ) + type }; + auto const columns = [&]( int const Width ) { + auto const first { std::clamp( static_cast( std::floor( std::min( Band.x, Band.y ) * Width ) ), 0, Width - 1 ) }; + auto const last { std::clamp( static_cast( std::ceil( std::max( Band.x, Band.y ) * Width ) ) - 1, first, Width - 1 ) }; + return std::pair{ first, last }; }; + auto sum { 0.0 }; + glm::dvec3 colour { 0.0 }; + std::size_t count { 0 }; + auto const add = [&]( float const Red, float const Green, float const Blue ) { + sum += 0.2126 * Red + 0.7152 * Green + 0.0722 * Blue; + colour += glm::pow( glm::dvec3{ Red, Green, Blue } / 255.0, glm::dvec3{ 2.2 } ); + ++count; }; + if( type == ".dds" ) { + std::ifstream input( file, std::ios::binary ); + std::array header {}; + if( input.read( reinterpret_cast( header.data() ), header.size() ) && std::equal( header.begin(), header.begin() + 4, "DDS " ) ) { + auto const word = [&]( std::size_t const Offset ) { + return static_cast( header[ Offset ] ) | static_cast( header[ Offset + 1 ] ) << 8 | static_cast( header[ Offset + 2 ] ) << 16 | static_cast( header[ Offset + 3 ] ) << 24; }; + auto const height { static_cast( word( 12 ) ) }; + auto const width { static_cast( word( 16 ) ) }; + auto const fourcc { word( 84 ) }; + auto const blocksize { fourcc == 0x31545844u ? 8 : ( fourcc == 0x33545844u || fourcc == 0x35545844u ) ? 16 : 0 }; + if( blocksize > 0 && width > 0 && height > 0 ) { + auto const wide { ( width + 3 ) / 4 }; + auto const high { ( height + 3 ) / 4 }; + std::vector data( static_cast( wide ) * high * blocksize ); + if( input.read( reinterpret_cast( data.data() ), data.size() ) ) { + auto const [ first, last ] { columns( width ) }; + auto const unpack = []( unsigned const Color ) { + return glm::vec3{ ( ( Color >> 11 ) & 31 ) * 255.f / 31.f, ( ( Color >> 5 ) & 63 ) * 255.f / 63.f, ( Color & 31 ) * 255.f / 31.f }; }; + for( int row = 0; row < high; ++row ) { + for( int column = first / 4; column <= last / 4; ++column ) { + auto const *block { &data[ ( static_cast( row ) * wide + column ) * blocksize + ( blocksize - 8 ) ] }; + auto const low { static_cast( block[ 0 ] | block[ 1 ] << 8 ) }; + auto const top { static_cast( block[ 2 ] | block[ 3 ] << 8 ) }; + auto const opaque { blocksize == 16 || low > top }; + glm::vec3 const colors[ 4 ] { + unpack( low ), + unpack( top ), + opaque ? ( 2.f * unpack( low ) + unpack( top ) ) / 3.f : 0.5f * ( unpack( low ) + unpack( top ) ), + opaque ? ( unpack( low ) + 2.f * unpack( top ) ) / 3.f : glm::vec3{ 0.f } }; + auto const bits { static_cast( block[ 4 ] ) | static_cast( block[ 5 ] ) << 8 | static_cast( block[ 6 ] ) << 16 | static_cast( block[ 7 ] ) << 24 }; + for( int texel = 0; texel < 16; ++texel ) { + auto const x { column * 4 + texel % 4 }; + auto const index { ( bits >> ( 2 * texel ) ) & 3 }; + if( x < first || x > last || ( false == opaque && index == 3 ) ) { + continue; + } + add( colors[ index ].r, colors[ index ].g, colors[ index ].b ); + } + } + } + } + } + } + } + else { + int width, height, components; + if( auto *pixels { stbi_load( file.c_str(), &width, &height, &components, 4 ) } ) { + auto const [ first, last ] { columns( width ) }; + for( int y = 0; y < height; ++y ) { + for( int x = first; x <= last; ++x ) { + auto const *pixel { &pixels[ ( static_cast( y ) * width + x ) * 4 ] }; + add( pixel[ 0 ], pixel[ 1 ], pixel[ 2 ] ); + } + } + stbi_image_free( pixels ); + } + } + if( count > 0 ) { + result = { static_cast( sum / count ), glm::vec3{ colour / static_cast( count ) } }; + } + return result; +} + +float tread_brightness( material_handle const Material, glm::vec2 const &Band ) { + + return tread_sample( Material, Band ).first; +} + +glm::vec3 tread_colour( material_handle const Material, glm::vec2 const &Band ) { + + return tread_sample( Material, Band ).second; +} + +} // namespace track_rails + +material_handle TTrack::rail_material( int const Path ) const { + + return ( Path == 1 && eType == tt_Switch ? m_material2 : m_material1 ); +} + +glm::vec2 TTrack::tread_band() const { + + glm::vec2 band { 0.45f, 0.55f }; + if( m_profiles.empty() ) { + return band; + } + auto const &outline { track_rail_profile( m_profile1.second ) }; + auto const count { outline.size() / 2 }; + auto top { -1e9f }; + for( std::size_t i = 0; i < count; ++i ) { + top = std::max( top, outline[ i ].position.y ); + } + glm::vec2 found { 1e9f, -1e9f }; + for( std::size_t i = 0; i < count; ++i ) { + if( outline[ i ].position.y > top - 0.0005f ) { + found = { std::min( found.x, outline[ i ].texture.s ), std::max( found.y, outline[ i ].texture.s ) }; + } + } + if( found.x > found.y ) { + return band; + } + auto const middle { 0.5f * ( found.x + found.y ) }; + auto const half { std::max( 0.5f * ( found.y - found.x ), 0.01f ) }; + return { middle - half, middle + half }; +} + +material_handle TTrack::shared_tread( int const Path, TTrack const &Other, int const Otherpath ) const { + + auto const own { rail_material( Path ) }; + auto const theirs { Other.rail_material( Otherpath ) }; + if( theirs == null_handle || theirs == own ) { + return own; + } + return ( track_rails::tread_brightness( theirs, Other.tread_band() ) > track_rails::tread_brightness( own, tread_band() ) ? theirs : own ); +} diff --git a/world/RailPlan.cpp b/world/RailPlan.cpp new file mode 100644 index 00000000..cb5a4fbf --- /dev/null +++ b/world/RailPlan.cpp @@ -0,0 +1,548 @@ +/* +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" + +using namespace track_rails; + +namespace { + +// 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, track_rails::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 : rail_checks( Partner ) }; + 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( track_rails::rail_plan &Plan, double const At, double const Toward, double const Slope, std::vector const &Extension, std::vector const &Legs ) { + + auto const throat { At + Toward * flangeway / Slope }; + auto const flare { At - Toward * wing_reach( Slope ) }; + auto const nose { Toward < 0.0 ? Legs.front().x : Legs.back().x }; + 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; } ); + track_rails::rail_cut cut; + cut.at = At; + if( Toward < 0.0 ) { + cut.end = wingend; + cut.endoffsets = wing; + cut.endrolled = rolled; + cut.start = nose; + cut.startextents = Legs; + } + else { + cut.end = nose; + cut.endextents = Legs; + 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( track_rails::rail_plan &Plan, double const At, double const Into, track_rails::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 ) }; + auto const bend { knuckle_bend( Into ) }; + track_rails::rail_cut cut; + cut.at = At; + if( Into > 0.0 ) { + cut.end = At; + cut.endbend = -bend; + 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; + cut.startbend = bend; + } + 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( track_rails::rail_plan &Plan, double const At, double const Into, double const Facing, double const From, double const To, double const Inner, double const Outer, std::vector const &Fromline = {}, std::vector const &Toline = {} ) { + + 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 }; + track_rails::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 }; + auto const glue = [&]( std::vector const &Line, double const Edge, double &Position, glm::dvec2 &Miter, double &Bend, std::vector &Fit, bool &Cap ) { + if( Line.empty() ) { + return; + } + Position = At + Edge / Into; + Miter = {}; + Bend = Facing / Into; + for( auto const &knot : Line ) { + Fit.emplace_back( knot.x, ( knot.y - Edge ) / Into ); + } + Cap = false; }; + glue( Into > 0.0 ? Fromline : Toline, Into > 0.0 ? From : To, cut.end, cut.endmiter, cut.endbend, cut.endfit, cut.endcap ); + glue( Into > 0.0 ? Toline : Fromline, Into > 0.0 ? To : From, cut.start, cut.startmiter, cut.startbend, cut.startfit, cut.startcap ); + Plan.cuts.push_back( cut ); +} + +std::vector glue_line( std::vector const &Outline, double const Base ) { + + std::vector line; + for( auto const &knot : Outline ) { + line.emplace_back( knot.x, Base - knot.y ); + } + return line; +} + +track_rails::rail_fill guard_fill( track_rails::guard_rail const &Guard, track_rails::rail_size const &Size ) { + + auto const offset { Size.web - ( flangeway + Size.head ) }; + auto const flare { std::min( 0.3, 0.25 * ( Guard.to - Guard.from ) ) }; + track_rails::rail_fill fill; + fill.web = true; + fill.from = Guard.from; + fill.to = Guard.to; + fill.capfrom = true; + fill.capto = true; + fill.lift = ( Guard.through ? 0.0 : -0.001 ); + fill.inner = { { Guard.from, offset - ( Guard.flarefrom ? 0.02 : 0.0 ) }, { Guard.from + flare, offset }, { Guard.to - flare, offset }, { Guard.to, offset - ( Guard.flareto ? 0.02 : 0.0 ) } }; + fill.outer = { { Guard.from, Size.web }, { Guard.to, Size.web } }; + return fill; +} + +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 ); +} + +track_rails::rail_band groove_band( double const At, double const Into, double const Facing, bool const Through, track_rails::rail_size const &Other ) { + + auto const seam { 0.001 }; + track_rails::rail_band band { At, Into, Facing }; + band.whole = ( false == Through ); + band.from = ( Through ? 0.0 : seam - Other.tread ); + band.to = ( Through ? ( Other.grooved ? -Other.lip : flangeway ) : -Other.ledge - seam ); + return band; +} + +void vee_fill( std::vector &Fills, double const At, double const Into, double const Facing, track_rails::rail_size const &Own, track_rails::rail_size const &Other, bool const Outwards ) { + + auto const seam { 0.001 }; + auto const sign { Outwards ? 1.0 : -1.0 }; + auto const edge { Outwards ? std::max( Own.head, Own.crown ) : Own.rim }; + auto const flat { Outwards ? Own.tread : Own.ledge }; + auto const inside { Outwards == ( Facing > 0.0 ) }; + auto const closest { inside ? -Other.rim : -std::max( Other.head, Other.crown ) }; + auto const rounded { inside ? Other.ledge - Other.rim : std::max( Other.head, Other.crown ) - Other.tread }; + auto const own { Outwards ? Own.high - edge : edge - Own.low }; + auto const other { inside ? Other.rim - Other.low : Other.high - std::max( Other.head, Other.crown ) }; + auto const slant { std::max( std::abs( Facing ), 0.1 ) }; + auto const gap { own + other / slant }; + auto const meets = [&]( double const Width ) { + return At + ( closest + Facing * ( edge + sign * Width ) ) / Into; }; + auto const apex { meets( 0.0 ) }; + auto const end { meets( gap ) }; + auto const reach { seam + rounded / slant }; + track_rails::rail_fill fill; + fill.from = std::min( apex, end ); + fill.to = std::max( apex, end ); + fill.capfrom = ( end < apex ); + fill.capto = ( end > apex ); + if( Own.grooved ) { + fill.lift = std::min( Outwards ? Own.crest : Own.liptop, inside ? Other.liptop : Other.crest ) - Own.liptop; + } + std::vector line { { apex, edge + sign * reach }, { end, edge + sign * ( gap + reach ) } }; + std::sort( line.begin(), line.end(), []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; } ); + std::vector fixed { { fill.from, flat - sign * seam }, { fill.to, flat - sign * seam } }; + fill.outer = ( Outwards ? line : fixed ); + fill.inner = ( Outwards ? fixed : line ); + Fills.push_back( fill ); +} + +// 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 }; +}; + +std::vector tread_edge( TTrack const &Track, int const Path, int const Side, TTrack const &Other, int const Otherpath, int const Otherside, double const From, double const To, bool &Below, std::vector &Gauge ) { + + auto const &ours { rail_line( Track, Path, Side > 0, false ) }; + auto const &theirs { rail_line( Other, Otherpath, Otherside > 0, false ) }; + glm::dvec3 point, tangent; + ours.at( From, point, tangent ); + auto otherat { nearest_distance( theirs, point ) }; + std::vector knots; + auto below { 0 }; + auto const count { std::max( 2, static_cast( std::ceil( ( To - From ) / 0.1 ) ) + 1 ) }; + for( int i = 0; i < count; ++i ) { + auto distance { glm::mix( From, To, static_cast( i ) / ( count - 1 ) ) }; + ours.at( distance, point, tangent ); + lateral( theirs, Otherside, point, otherat ); + glm::dvec3 line; + theirs.at( otherat, line, tangent ); + auto const normal { theirs.normal( otherat, Otherside ) }; + auto at { distance }; + auto const gauge { lateral( ours, Side, line, at ) }; + Gauge.emplace_back( at, gauge ); + at = distance; + auto const edge { lateral( ours, Side, line - normal * rolled_band, at ) }; + below += ( gauge < edge ? 1 : -1 ); + knots.emplace_back( at, edge ); + } + auto const byposition = []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; }; + std::sort( knots.begin(), knots.end(), byposition ); + std::sort( Gauge.begin(), Gauge.end(), byposition ); + Below = ( below > 0 ); + return knots; +} + +std::vector nose_extents( TTrack const &Track, int const Path, int const Side, TTrack const &Other, int const Otherpath, int const Otherside, double const At, double const Toward, double const Slope, track_rails::rail_size const &Size ) { + + auto const half { Slope / ( 1.0 + std::sqrt( 1.0 + Slope * Slope ) ) }; + auto const tip { 0.5 * nose_tip }; + auto const apart { Size.high + 0.01 }; + auto const nose { At - Toward * tip / half }; + auto const blunt { At - Toward * apart / half }; + std::vector straight { { std::min( nose, blunt ), Toward < 0.0 ? tip : apart }, { std::max( nose, blunt ), Toward < 0.0 ? apart : tip } }; + auto const &ours { rail_line( Track, Path, Side > 0, false ) }; + auto const &theirs { rail_line( Other, Otherpath, Otherside > 0, false ) }; + glm::dvec3 point, tangent; + ours.at( nose, point, tangent ); + auto otherat { nearest_distance( theirs, point ) }; + auto extent { tip }; + auto const step { 0.1 }; + auto const limit { static_cast( std::ceil( 3.0 * std::abs( blunt - nose ) / step ) ) }; + std::vector knots; + for( int i = 0; i <= limit; ++i ) { + auto const distance { nose - Toward * step * i }; + ours.at( distance, point, tangent ); + auto const normal { ours.normal( distance, Side ) }; + for( int iteration = 0; iteration < 6; ++iteration ) { + extent += 0.5 * ( lateral( theirs, Otherside, point - normal * extent, otherat ) - extent ); + } + if( extent >= apart && false == knots.empty() ) { + auto const &last { knots.back() }; + knots.emplace_back( glm::mix( last.x, distance, ( apart - last.y ) / ( extent - last.y ) ), apart ); + break; + } + knots.emplace_back( distance, std::max( extent, 0.0 ) ); + } + if( knots.size() < 2 || knots.back().y < apart ) { + return straight; + } + std::sort( knots.begin(), knots.end(), []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; } ); + return knots; +} + +} // namespace + +track_rails::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 = ( Crossings ? rail_checks( *this ) : 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 ) }; + auto const tread = [&]( int const Path, int const Side, glm::dvec2 const &Span, TTrack const &Other, int const Otherpath, int const Anchor = 0, int const Otherside = 0 ) { + auto const material { shared_tread( Path, Other, Otherpath ) }; + if( material == rail_material( Path ) || Span.y <= Span.x ) { + return; + } + if( Otherside == 0 ) { + layout.treads[ Path ][ Side > 0 ].push_back( { Span.x, Span.y, material, Anchor } ); + return; + } + rail_tread part { Span.x, Span.y, material, Anchor }; + part.edge = tread_edge( *this, Path, Side, Other, Otherpath, Otherside, Span.x, Span.y, part.below, part.gauge ); + auto from { 1e9 }; + auto to { -1e9 }; + for( std::size_t i = 0; i < part.edge.size() && i < part.gauge.size(); ++i ) { + auto const low { std::min( part.edge[ i ].y, part.gauge[ i ].y ) }; + auto const high { std::max( part.edge[ i ].y, part.gauge[ i ].y ) }; + if( low - tread_fade < size.head && high + tread_fade > 0.0 ) { + from = std::min( from, part.edge[ i ].x ); + to = std::max( to, part.edge[ i ].x ); + } + } + part.from = std::max( Span.x, from - 0.1 ); + part.to = std::min( Span.y, to + 0.1 ); + if( part.to > part.from ) { + layout.treads[ Path ][ Side > 0 ].push_back( part ); + } + }; + auto const across = [&]( rail_crossing const &Crossing, rail_size const &Other ) { + glm::dvec2 span { 1e9, -1e9 }; + for( auto const theirs : { Other.grooved ? Other.ledge : 0.0, Other.grooved ? Other.tread : Other.head } ) { + for( auto const ours : { size.grooved ? size.ledge : 0.0, size.grooved ? size.tread : size.head } ) { + auto const distance { Crossing.at + ( Crossing.facing * ours - theirs ) / Crossing.into }; + span = { std::min( span.x, distance ), std::max( span.y, distance ) }; + } + } + return span; }; + std::vector corners; + if( Crossings ) { + for( auto const &crossing : crossings ) { + auto &plan { plans[ crossing.path ][ crossing.side > 0 ] }; + if( std::find( layout.crossed.begin(), layout.crossed.end(), crossing.track ) == layout.crossed.end() ) { + layout.crossed.push_back( crossing.track ); + } + auto const theirs { crossing.track->rail_dimensions() }; + if( size.grooved || theirs.grooved ) { + auto const through { size.grooved != theirs.grooved ? size.grooved : goes_through( crossing.owner ? *crossing.owner : *this, *crossing.track ) }; + layout.bands[ crossing.path ][ crossing.side > 0 ].push_back( groove_band( crossing.at, crossing.into, crossing.facing, through, theirs ) ); + if( size.grooved && theirs.grooved && crossing.angle <= middle_angle ) { + std::vector vee; + vee_fill( vee, crossing.at, crossing.into, crossing.facing, size, theirs, true ); + vee_fill( vee, crossing.at, crossing.into, crossing.facing, size, theirs, false ); + glm::dvec2 const block { std::min( vee[ 0 ].from, vee[ 1 ].from ), std::max( vee[ 0 ].to, vee[ 1 ].to ) }; + layout.blocks[ crossing.path ][ crossing.side > 0 ].push_back( block ); + tread( crossing.path, crossing.side, block, *crossing.track, crossing.otherpath ); + if( through ) { + auto &fills { layout.fills[ crossing.path ][ crossing.side > 0 ] }; + fills.insert( fills.end(), vee.begin(), vee.end() ); + } + } + else if( through ) { + tread( crossing.path, crossing.side, across( crossing, theirs ), *crossing.track, crossing.otherpath ); + } + continue; + } + 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, through ? std::vector{} : glue_line( other.field, 0.0 ) ); + if( through ) { + tread( crossing.path, crossing.side, across( crossing, other ), *crossing.track, crossing.otherpath ); + } + auto const reach { ( flangeway + railhead + 0.3 ) / std::abs( crossing.into ) }; + auto &spans { guards[ crossing.path ][ crossing.side > 0 ] }; + track_rails::guard_rail guard { crossing.at - reach, crossing.at + reach }; + guard.through = through; + wide( guard.plan, crossing.at, crossing.into, crossing.facing, -other.head, through ? flangeway : flangeway + other.head, guardoffset, guardoffset + railhead, glue_line( other.field, 0.0 ), through ? std::vector{} : glue_line( other.gauge, flangeway + other.head ) ); + 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() ); + span.through = ( span.through && guard.through ); + 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, + wing_extension( *this, checks, crossing.path, crossing.side, crossing.at, toward, slope, *crossing.track, crossing.otherpath, crossing.otherside, crossing.otherat, crossing.aligned ? -toward : toward ), + nose_extents( *this, crossing.path, crossing.side, *crossing.track, crossing.otherpath, crossing.otherside, crossing.at, toward, slope, size ) ); + auto const nose { toward < 0.0 ? plan.cuts.back().start : plan.cuts.back().end }; + auto const shared { crossing.at - toward * ( rolled_band + size.head + tread_fade ) / slope }; + tread( crossing.path, crossing.side, { std::min( nose, shared ), std::max( nose, shared ) }, *crossing.track, crossing.otherpath, toward > 0.0 ? 1 : -1, crossing.otherside ); + } + else { + obtuse( plan, crossing.at, crossing.into, size ); + auto const reach { ( rolled_band + size.head + tread_fade ) / std::max( std::tan( crossing.angle ), 0.01 ) + 0.2 }; + tread( crossing.path, crossing.side, { crossing.at - reach, crossing.at + reach }, *crossing.track, crossing.otherpath, 0, crossing.otherside ); + } + } + 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 ); + } + } + } + } + } + for( int path = 0; path < 2; ++path ) { + for( int side = 0; side < 2; ++side ) { + for( auto const &guard : guards[ path ][ side ] ) { + layout.fills[ path ][ side ].push_back( guard_fill( guard, size ) ); + } + } + } + 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 ) { + track_rails::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 && cut_rails( *this ) ) { + for( int path = 0; path < 2; ++path ) { + tread( path, -blade_side( path ), { 0.0, blade_length( *this, 1 - path ) }, *this, 1 - path, 0, -blade_side( path ) ); + } + } + if( eType == tt_Switch ) { + auto const frog { this->frog() }; + if( frog.valid ) { + glm::dvec3 point, tangents[ 2 ]; + glm::dvec2 normals[ 2 ]; + for( int path = 0; path < 2; ++path ) { + auto const &line { rail_line( *this, path, frog.side[ path ] > 0, false ) }; + line.at( frog.at[ path ], point, tangents[ path ] ); + normals[ path ] = glm::normalize( plan( line.normal( frog.at[ path ], frog.side[ path ] ) ) ); + } + auto const facing { glm::dot( normals[ 0 ], normals[ 1 ] ) }; + auto const slope { std::tan( frog.angle ) }; + for( int path = 0; path < 2; ++path ) { + auto const into { glm::dot( glm::normalize( plan( tangents[ path ] ) ), normals[ 1 - path ] ) }; + auto &fills { layout.fills[ path ][ frog.side[ path ] > 0 ] }; + if( size.grooved ) { + layout.bands[ path ][ frog.side[ path ] > 0 ].push_back( groove_band( frog.at[ path ], into, facing, path == 0, size ) ); + std::vector vee; + vee_fill( vee, frog.at[ path ], into, facing, size, size, true ); + vee_fill( vee, frog.at[ path ], into, facing, size, size, false ); + glm::dvec2 const block { std::min( vee[ 0 ].from, vee[ 1 ].from ), std::max( vee[ 0 ].to, vee[ 1 ].to ) }; + layout.blocks[ path ][ frog.side[ path ] > 0 ].push_back( block ); + tread( path, frog.side[ path ], block, *this, 1 - path ); + if( path == 0 ) { + fills.insert( fills.end(), vee.begin(), vee.end() ); + } + continue; + } + 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, + 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 ), + nose_extents( *this, path, frog.side[ path ], *this, 1 - path, frog.side[ 1 - path ], frog.at[ path ], -1.0, slope, size ) ); + auto const nose { plans[ path ][ frog.side[ path ] > 0 ].cuts.back().start }; + auto const shared { frog.at[ path ] + ( rolled_band + size.head + tread_fade ) / slope }; + tread( path, frog.side[ path ], { std::min( nose, shared ), std::max( nose, shared ) }, *this, 1 - path, -1, frog.side[ 1 - path ] ); + } + } + } + return layout; +} diff --git a/world/RailProfile.cpp b/world/RailProfile.cpp new file mode 100644 index 00000000..c3a01cfd --- /dev/null +++ b/world/RailProfile.cpp @@ -0,0 +1,158 @@ +/* +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" + +namespace track_rails { + +rail_size measure_rail( gfx::vertex_array const &Profile ) { + + rail_size result; + 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.crest = top; + std::size_t firsttop { count }; + std::size_t lasttop { 0 }; + for( std::size_t i = 0; i < count; ++i ) { + if( Profile[ i ].position.y > top - 1e-4 ) { + firsttop = std::min( firsttop, i ); + lasttop = std::max( lasttop, i ); + } + } + knots::line sides[ 2 ]; + for( std::size_t i = 0; i < count; ++i ) { + auto const &point { Profile[ i ].position }; + if( i <= firsttop ) { + sides[ 0 ].emplace_back( point.y, point.x ); + } + if( i >= lasttop ) { + sides[ 1 ].emplace_back( point.y, point.x ); + } + } + for( auto &side : sides ) { + knots::sort( side ); + } + auto const mean = []( knots::line const &Side ) { + auto sum { 0.0 }; + for( auto const &knot : Side ) { + sum += knot.y; + } + return sum / std::max( Side.size(), 1 ); }; + auto const fieldfirst { mean( sides[ 0 ] ) > mean( sides[ 1 ] ) }; + result.field = sides[ fieldfirst ? 0 : 1 ]; + result.gauge = sides[ fieldfirst ? 1 : 0 ]; + auto const middle { top - 0.5 * result.height }; + auto webfrom { 1e9 }; + auto webto { -1e9 }; + for( std::size_t i = 0; i + 1 < count; ++i ) { + auto const &a { Profile[ i ].position }; + auto const &b { Profile[ i + 1 ].position }; + if( a.y != b.y && ( a.y - middle ) * ( b.y - middle ) <= 0.0 ) { + auto const x { a.x + ( b.x - a.x ) * ( middle - a.y ) / ( b.y - a.y ) }; + webfrom = std::min( webfrom, x ); + webto = std::max( webto, x ); + } + } + if( webfrom < webto ) { + result.web = 0.5 * ( webfrom + webto ); + result.neck = top; + for( std::size_t i = 0; i < count; ++i ) { + auto const &point { Profile[ i ].position }; + if( point.y > middle && ( point.x < webfrom - 0.0005 || point.x > webto + 0.0005 ) ) { + result.neck = std::min( result.neck, static_cast( point.y ) ); + } + } + } + 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 ) ); + } + } + result.tread = result.head; + std::vector heads, lips; + for( std::size_t i = 0; i < count; ++i ) { + auto const &point { Profile[ i ].position }; + if( point.y > top - 0.015 ) { + ( point.x < -0.02 ? lips : heads ).push_back( i ); + } + } + if( heads.empty() || lips.empty() ) { + return result; + } + auto const first { heads.back() < lips.front() ? heads.back() : lips.back() }; + auto const last { heads.back() < lips.front() ? lips.front() : heads.front() }; + if( first >= last || ( heads.back() > lips.front() && lips.back() > heads.front() ) ) { + return result; + } + auto groove { 1e9 }; + for( auto i { first + 1 }; i < last; ++i ) { + groove = std::min( groove, static_cast( Profile[ i ].position.y ) ); + } + if( groove > top ) { + return result; + } + result.grooved = true; + result.groove = groove; + result.lip = -1e9; + result.liptop = -1e9; + for( auto const i : lips ) { + result.lip = std::max( result.lip, static_cast( Profile[ i ].position.x ) ); + result.liptop = std::max( result.liptop, static_cast( Profile[ i ].position.y ) ); + } + result.tread = -1e9; + for( auto const i : heads ) { + if( Profile[ i ].position.y > top - 0.0005 ) { + result.tread = std::max( result.tread, static_cast( Profile[ i ].position.x ) ); + } + } + result.ledge = 1e9; + for( auto const i : lips ) { + if( Profile[ i ].position.y > result.liptop - 0.0005 ) { + result.ledge = std::min( result.ledge, static_cast( Profile[ i ].position.x ) ); + } + } + result.rim = 1e9; + result.crown = -1e9; + auto const reach = [&]( double const X ) { + result.rim = std::min( result.rim, X ); + result.crown = std::max( result.crown, X ); }; + for( std::size_t i = 0; i < count; ++i ) { + auto const &point { Profile[ i ].position }; + if( point.y > groove + 1e-4 ) { + reach( point.x ); + } + if( i + 1 < count ) { + auto const &next { Profile[ i + 1 ].position }; + if( ( point.y - groove ) * ( next.y - groove ) < 0.0 ) { + reach( point.x + ( next.x - point.x ) * ( groove - point.y ) / ( next.y - point.y ) ); + } + } + } + return result; +} + +} // namespace track_rails diff --git a/world/Track.cpp b/world/Track.cpp index 05aeb220..20ee978f 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -2613,148 +2613,10 @@ TTrack::track_rail_profile( int const Profile ) { track_rails::rail_size TTrack::rail_dimensions() const { - track_rails::rail_size result; if( m_profiles.empty() ) { - return result; + return {}; } - 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.crest = top; - std::size_t firsttop { count }; - std::size_t lasttop { 0 }; - for( std::size_t i = 0; i < count; ++i ) { - if( profile[ i ].position.y > top - 1e-4 ) { - firsttop = std::min( firsttop, i ); - lasttop = std::max( lasttop, i ); - } - } - std::vector sides[ 2 ]; - for( std::size_t i = 0; i < count; ++i ) { - auto const &point { profile[ i ].position }; - if( i <= firsttop ) { - sides[ 0 ].emplace_back( point.y, point.x ); - } - if( i >= lasttop ) { - sides[ 1 ].emplace_back( point.y, point.x ); - } - } - for( auto &side : sides ) { - std::sort( side.begin(), side.end(), []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; } ); - } - auto const mean = []( std::vector const &Side ) { - auto sum { 0.0 }; - for( auto const &knot : Side ) { - sum += knot.y; - } - return sum / std::max( Side.size(), 1 ); }; - auto const fieldfirst { mean( sides[ 0 ] ) > mean( sides[ 1 ] ) }; - result.field = sides[ fieldfirst ? 0 : 1 ]; - result.gauge = sides[ fieldfirst ? 1 : 0 ]; - auto const middle { top - 0.5 * result.height }; - auto webfrom { 1e9 }; - auto webto { -1e9 }; - for( std::size_t i = 0; i + 1 < count; ++i ) { - auto const &a { profile[ i ].position }; - auto const &b { profile[ i + 1 ].position }; - if( a.y != b.y && ( a.y - middle ) * ( b.y - middle ) <= 0.0 ) { - auto const x { a.x + ( b.x - a.x ) * ( middle - a.y ) / ( b.y - a.y ) }; - webfrom = std::min( webfrom, x ); - webto = std::max( webto, x ); - } - } - if( webfrom < webto ) { - result.web = 0.5 * ( webfrom + webto ); - result.neck = top; - for( std::size_t i = 0; i < count; ++i ) { - auto const &point { profile[ i ].position }; - if( point.y > middle && ( point.x < webfrom - 0.0005 || point.x > webto + 0.0005 ) ) { - result.neck = std::min( result.neck, static_cast( point.y ) ); - } - } - } - 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 ) ); - } - } - result.tread = result.head; - std::vector heads, lips; - for( std::size_t i = 0; i < count; ++i ) { - auto const &point { profile[ i ].position }; - if( point.y > top - 0.015 ) { - ( point.x < -0.02 ? lips : heads ).push_back( i ); - } - } - if( heads.empty() || lips.empty() ) { - return result; - } - auto const first { heads.back() < lips.front() ? heads.back() : lips.back() }; - auto const last { heads.back() < lips.front() ? lips.front() : heads.front() }; - if( first >= last || ( heads.back() > lips.front() && lips.back() > heads.front() ) ) { - return result; - } - auto groove { 1e9 }; - for( auto i { first + 1 }; i < last; ++i ) { - groove = std::min( groove, static_cast( profile[ i ].position.y ) ); - } - if( groove > top ) { - return result; - } - result.grooved = true; - result.groove = groove; - result.lip = -1e9; - result.liptop = -1e9; - for( auto const i : lips ) { - result.lip = std::max( result.lip, static_cast( profile[ i ].position.x ) ); - result.liptop = std::max( result.liptop, static_cast( profile[ i ].position.y ) ); - } - result.tread = -1e9; - for( auto const i : heads ) { - if( profile[ i ].position.y > top - 0.0005 ) { - result.tread = std::max( result.tread, static_cast( profile[ i ].position.x ) ); - } - } - result.ledge = 1e9; - for( auto const i : lips ) { - if( profile[ i ].position.y > result.liptop - 0.0005 ) { - result.ledge = std::min( result.ledge, static_cast( profile[ i ].position.x ) ); - } - } - result.rim = 1e9; - result.crown = -1e9; - auto const reach = [&]( double const X ) { - result.rim = std::min( result.rim, X ); - result.crown = std::max( result.crown, X ); }; - for( std::size_t i = 0; i < count; ++i ) { - auto const &point { profile[ i ].position }; - if( point.y > groove + 1e-4 ) { - reach( point.x ); - } - if( i + 1 < count ) { - auto const &next { profile[ i + 1 ].position }; - if( ( point.y - groove ) * ( next.y - groove ) < 0.0 ) { - reach( point.x + ( next.x - point.x ) * ( groove - point.y ) / ( next.y - point.y ) ); - } - } - } - return result; + return track_rails::measure_rail( track_rail_profile( m_profile1.second ) ); } float diff --git a/world/TrackRails.cpp b/world/TrackRails.cpp deleted file mode 100644 index f64a7ee6..00000000 --- a/world/TrackRails.cpp +++ /dev/null @@ -1,2313 +0,0 @@ -/* -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" -#include "stb/stb_image.h" - -namespace { - -glm::dvec2 plan( glm::dvec3 const &V ) { - - return glm::dvec2{ V.x, V.z }; -} - -double cross( glm::dvec2 const &A, glm::dvec2 const &B ) { - - return A.x * B.y - A.y * B.x; -} - -bool meet( glm::dvec2 const &A0, glm::dvec2 const &A, glm::dvec2 const &B0, glm::dvec2 const &B, double const Parallel, double &U, double &V ) { - - auto const denominator { cross( A, B ) }; - if( std::abs( denominator ) < Parallel ) { - return false; - } - U = cross( B0 - A0, B ) / denominator; - V = cross( B0 - A0, A ) / denominator; - return true; -} - -// 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 = plan( line.points.front() ); - for( auto const &point : line.points ) { - line.low = glm::min( line.low, plan( point ) ); - line.high = glm::max( line.high, plan( point ) ); - } - return line; -} - -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 ); -} - -double knuckle_bend( double const Into ) { - - return std::abs( Into ) / ( 1.0 + std::sqrt( std::max( 0.0, 1.0 - Into * Into ) ) ); -} - -// 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; -} - -struct rails_memo { - std::optional lines[ 2 ][ 2 ][ 2 ]; // path, right side, from the blades - std::optional> own; - std::optional> joined; - std::optional checks; -}; -std::unordered_map memos; -unsigned int memostamp { 0 }; -unsigned int stamp { 1 }; - -rails_memo &memo_of( TTrack const &Track ) { - - if( memostamp != stamp ) { - memos.clear(); - memostamp = stamp; - } - return memos[ &Track ]; -} - -} // namespace - -namespace track_rails { - -void forget() { - - ++stamp; -} - - -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 ); -} - -material_handle tint_material( material_handle const Material, glm::vec3 const &Colour ) { - - return GfxRenderer->Tinted_Material( Material, Colour ); -} - -std::pair const &tread_sample( material_handle const Material, glm::vec2 const &Band ) { - - static std::map, std::pair> samples; - auto const key { std::make_tuple( Material, Band.x, Band.y ) }; - if( auto const lookup { samples.find( key ) }; lookup != samples.end() ) { - return lookup->second; - } - auto &result { samples[ key ] }; - result = { -1.f, glm::vec3{ 0.f } }; - if( Material == null_handle ) { - return result; - } - auto const texture { GfxRenderer->Material( Material )->GetTexture( 0 ) }; - if( texture == null_handle ) { - return result; - } - auto const &image { GfxRenderer->Texture( texture ) }; - auto const type { std::string( image.get_type() ) }; - auto const file { std::string( image.get_name() ) + type }; - auto const columns = [&]( int const Width ) { - auto const first { std::clamp( static_cast( std::floor( std::min( Band.x, Band.y ) * Width ) ), 0, Width - 1 ) }; - auto const last { std::clamp( static_cast( std::ceil( std::max( Band.x, Band.y ) * Width ) ) - 1, first, Width - 1 ) }; - return std::pair{ first, last }; }; - auto sum { 0.0 }; - glm::dvec3 colour { 0.0 }; - std::size_t count { 0 }; - auto const add = [&]( float const Red, float const Green, float const Blue ) { - sum += 0.2126 * Red + 0.7152 * Green + 0.0722 * Blue; - colour += glm::pow( glm::dvec3{ Red, Green, Blue } / 255.0, glm::dvec3{ 2.2 } ); - ++count; }; - if( type == ".dds" ) { - std::ifstream input( file, std::ios::binary ); - std::array header {}; - if( input.read( reinterpret_cast( header.data() ), header.size() ) && std::equal( header.begin(), header.begin() + 4, "DDS " ) ) { - auto const word = [&]( std::size_t const Offset ) { - return static_cast( header[ Offset ] ) | static_cast( header[ Offset + 1 ] ) << 8 | static_cast( header[ Offset + 2 ] ) << 16 | static_cast( header[ Offset + 3 ] ) << 24; }; - auto const height { static_cast( word( 12 ) ) }; - auto const width { static_cast( word( 16 ) ) }; - auto const fourcc { word( 84 ) }; - auto const blocksize { fourcc == 0x31545844u ? 8 : ( fourcc == 0x33545844u || fourcc == 0x35545844u ) ? 16 : 0 }; - if( blocksize > 0 && width > 0 && height > 0 ) { - auto const wide { ( width + 3 ) / 4 }; - auto const high { ( height + 3 ) / 4 }; - std::vector data( static_cast( wide ) * high * blocksize ); - if( input.read( reinterpret_cast( data.data() ), data.size() ) ) { - auto const [ first, last ] { columns( width ) }; - auto const unpack = []( unsigned const Color ) { - return glm::vec3{ ( ( Color >> 11 ) & 31 ) * 255.f / 31.f, ( ( Color >> 5 ) & 63 ) * 255.f / 63.f, ( Color & 31 ) * 255.f / 31.f }; }; - for( int row = 0; row < high; ++row ) { - for( int column = first / 4; column <= last / 4; ++column ) { - auto const *block { &data[ ( static_cast( row ) * wide + column ) * blocksize + ( blocksize - 8 ) ] }; - auto const low { static_cast( block[ 0 ] | block[ 1 ] << 8 ) }; - auto const top { static_cast( block[ 2 ] | block[ 3 ] << 8 ) }; - auto const opaque { blocksize == 16 || low > top }; - glm::vec3 const colors[ 4 ] { - unpack( low ), - unpack( top ), - opaque ? ( 2.f * unpack( low ) + unpack( top ) ) / 3.f : 0.5f * ( unpack( low ) + unpack( top ) ), - opaque ? ( unpack( low ) + 2.f * unpack( top ) ) / 3.f : glm::vec3{ 0.f } }; - auto const bits { static_cast( block[ 4 ] ) | static_cast( block[ 5 ] ) << 8 | static_cast( block[ 6 ] ) << 16 | static_cast( block[ 7 ] ) << 24 }; - for( int texel = 0; texel < 16; ++texel ) { - auto const x { column * 4 + texel % 4 }; - auto const index { ( bits >> ( 2 * texel ) ) & 3 }; - if( x < first || x > last || ( false == opaque && index == 3 ) ) { - continue; - } - add( colors[ index ].r, colors[ index ].g, colors[ index ].b ); - } - } - } - } - } - } - } - else { - int width, height, components; - if( auto *pixels { stbi_load( file.c_str(), &width, &height, &components, 4 ) } ) { - auto const [ first, last ] { columns( width ) }; - for( int y = 0; y < height; ++y ) { - for( int x = first; x <= last; ++x ) { - auto const *pixel { &pixels[ ( static_cast( y ) * width + x ) * 4 ] }; - add( pixel[ 0 ], pixel[ 1 ], pixel[ 2 ] ); - } - } - stbi_image_free( pixels ); - } - } - if( count > 0 ) { - result = { static_cast( sum / count ), glm::vec3{ colour / static_cast( count ) } }; - } - return result; -} - -float tread_brightness( material_handle const Material, glm::vec2 const &Band ) { - - return tread_sample( Material, Band ).first; -} - -glm::vec3 tread_colour( material_handle const Material, glm::vec2 const &Band ) { - - return tread_sample( Material, Band ).second; -} - -// 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 ) ); - } -} - -glm::dvec3 rail_point( TTrack const &Track, int const Path, int const Side, double const Distance, double const Lateral ) { - - auto const line { make_gauge_line( path_segment( Track, Path ), Side * 0.5 * std::abs( Track.fTrackWidth ), 0.0 ) }; - glm::dvec3 point, tangent; - line.at( Distance, point, tangent ); - return point - line.normal( Distance, Side ) * Lateral; -} - -void rail_offset( TTrack const &Track, int const Path, int const Side, glm::dvec3 const &Point, double &Distance, double &Lateral ) { - - auto const line { make_gauge_line( path_segment( Track, Path ), Side * 0.5 * std::abs( Track.fTrackWidth ), 0.0 ) }; - Distance = line.distances.front(); - auto nearest { std::numeric_limits::max() }; - for( std::size_t i = 0; i < line.points.size(); ++i ) { - auto const gap { glm::length( plan( line.points[ i ] - Point ) ) }; - if( gap < nearest ) { - nearest = gap; - Distance = line.distances[ i ]; - } - } - glm::dvec3 point, tangent; - for( int step = 0; step < 8; ++step ) { - line.at( Distance, point, tangent ); - Distance += glm::dot( plan( Point - point ), glm::normalize( plan( tangent ) ) ); - } - line.at( Distance, point, tangent ); - Lateral = -glm::dot( plan( Point - point ), plan( line.normal( Distance, Side ) ) ); -} - -} // namespace track_rails - -using namespace track_rails; - -namespace { - -// gauge line of a rail of a track, along a path of a switch from past its blades when Blades -gauge_line const &rail_line( TTrack const &Track, int const Path, int const Side, bool const Blades ) { - - auto const blades { Blades && Track.eType == tt_Switch }; - auto &line { memo_of( Track ).lines[ Path ][ Side ][ blades ] }; - if( false == line.has_value() ) { - line = make_gauge_line( path_segment( Track, Path ), ( Side ? 1.0 : -1.0 ) * ( 0.5 * std::abs( Track.fTrackWidth ) ), blades ? blade_length( Track, Path ) : 0.0 ); - } - return *line; -} - -track_rails::check_rail_set const &rail_checks( TTrack &Track ) { - - auto &checks { memo_of( Track ).checks }; - if( false == checks.has_value() ) { - checks = Track.check_rails( Track.rail_crossings() ); - } - return *checks; -} - -} // namespace - -std::vector TTrack::rail_crossings( bool const Joined ) { - - std::vector result; - if( false == cut_rails( *this ) || simulation::Region == nullptr ) { - return result; - } - auto &memo { memo_of( *this ) }; - if( Joined && memo.joined.has_value() ) { - return *memo.joined; - } - auto const extension { 10.0 }; - auto const paths { eType == tt_Switch ? 2 : 1 }; - if( memo.own.has_value() ) { - result = *memo.own; - } - else { - 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 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 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 { rail_line( *other, otherpath, otherside, true ) }; - 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 { rail_line( *this, path, side, false ) }; - 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 }; - auto const alow { glm::min( a0, a0 + a ) - 0.02 * glm::length( a ) - 1e-6 }; - auto const ahigh { glm::max( a0, a0 + a ) + 0.02 * glm::length( a ) + 1e-6 }; - if( alow.x > theirs.high.x + 1.0 || theirs.low.x - 1.0 > ahigh.x || alow.y > theirs.high.y + 1.0 || theirs.low.y - 1.0 > ahigh.y ) { - continue; - } - 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 reach { 0.02 * glm::length( b ) }; - if( alow.x > std::max( b0.x, b0.x + b.x ) + reach || std::min( b0.x, b0.x + b.x ) - reach > ahigh.x - || alow.y > std::max( b0.y, b0.y + b.y ) + reach || std::min( b0.y, b0.y + b.y ) - reach > ahigh.y ) { - continue; - } - double u, v; - if( false == meet( a0, a, b0, b, 1e-12, u, v ) ) { - continue; - } - 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 ); - } - } - } - } - } - } - } - memo.own = result; - } - 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 tostart { glm::length( neighboursegment.FastGetPoint_0() - joint ) }; - auto const toend { glm::length( neighboursegment.FastGetPoint_1() - joint ) }; - if( std::min( tostart, toend ) > 0.05 ) { - continue; - } - auto const atstart { tostart <= toend }; - 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 ); - mapped.owner = neighbour; - 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 ); - } - } - } - } - } - memo.joined = result; - return result; -} - -track_rails::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 }; - double frombend { 0.0 }; - double tobend { 0.0 }; - }; - std::vector plans[ 2 ][ 2 ]; - check_rail_set result; - auto const size { rail_dimensions() }; - if( size.grooved ) { - return result; - } - 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 || crossing.track->rail_dimensions().grooved ) { - 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, crossing.at + 1.2, false, true, 0.5, 0.02, -knuckle_bend( crossing.into ), 0.0 } ); - } - else { - plans[ crossing.path ][ crossing.side > 0 ].push_back( { crossing.at - 1.2, crossing.at, true, false, 0.5, 0.02, 0.0, knuckle_bend( crossing.into ) } ); - } - } - 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 }; - 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 ].tobend = checks[ i ].tobend; - } - 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 { plan( segment.FastGetPoint_0() ) }; - auto const end { plan( 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 { plan( 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, plan( other.FastGetPoint_0() ) ) < 0.1 || glm::distance( Point, plan( 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 { plan( point ) }; - auto const inward { plan( 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 { plan( other.points[ i - 1 ] ) }; - auto const edge { plan( other.points[ i ] ) - a }; - double t, u; - if( false == meet( gauge, inward, a, edge, 1e-9, t, u ) ) { - continue; - } - 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, - From > check.from ? 0.0 : check.frombend, - To < check.to ? 0.0 : check.tobend } ); }; - 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; -} - -track_rails::switch_frog TTrack::frog() const { - - switch_frog result; - if( eType != tt_Switch || false == cut_rails( *this ) ) { - return result; - } - result.side[ 0 ] = blade_side( 0 ); - 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 ); - } - } - 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 }; - double u, v; - if( false == meet( a0, a, b0, b, 1e-12, u, v ) ) { - continue; - } - 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 { - -// 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, track_rails::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 : rail_checks( Partner ) }; - 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( track_rails::rail_plan &Plan, double const At, double const Toward, double const Slope, std::vector const &Extension, std::vector const &Legs ) { - - auto const throat { At + Toward * flangeway / Slope }; - auto const flare { At - Toward * wing_reach( Slope ) }; - auto const nose { Toward < 0.0 ? Legs.front().x : Legs.back().x }; - 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; } ); - track_rails::rail_cut cut; - cut.at = At; - if( Toward < 0.0 ) { - cut.end = wingend; - cut.endoffsets = wing; - cut.endrolled = rolled; - cut.start = nose; - cut.startextents = Legs; - } - else { - cut.end = nose; - cut.endextents = Legs; - 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( track_rails::rail_plan &Plan, double const At, double const Into, track_rails::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 ) }; - auto const bend { knuckle_bend( Into ) }; - track_rails::rail_cut cut; - cut.at = At; - if( Into > 0.0 ) { - cut.end = At; - cut.endbend = -bend; - 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; - cut.startbend = bend; - } - 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( track_rails::rail_plan &Plan, double const At, double const Into, double const Facing, double const From, double const To, double const Inner, double const Outer, std::vector const &Fromline = {}, std::vector const &Toline = {} ) { - - 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 }; - track_rails::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 }; - auto const glue = [&]( std::vector const &Line, double const Edge, double &Position, glm::dvec2 &Miter, double &Bend, std::vector &Fit, bool &Cap ) { - if( Line.empty() ) { - return; - } - Position = At + Edge / Into; - Miter = {}; - Bend = Facing / Into; - for( auto const &knot : Line ) { - Fit.emplace_back( knot.x, ( knot.y - Edge ) / Into ); - } - Cap = false; }; - glue( Into > 0.0 ? Fromline : Toline, Into > 0.0 ? From : To, cut.end, cut.endmiter, cut.endbend, cut.endfit, cut.endcap ); - glue( Into > 0.0 ? Toline : Fromline, Into > 0.0 ? To : From, cut.start, cut.startmiter, cut.startbend, cut.startfit, cut.startcap ); - Plan.cuts.push_back( cut ); -} - -std::vector glue_line( std::vector const &Outline, double const Base ) { - - std::vector line; - for( auto const &knot : Outline ) { - line.emplace_back( knot.x, Base - knot.y ); - } - return line; -} - -track_rails::rail_fill guard_fill( track_rails::guard_rail const &Guard, track_rails::rail_size const &Size ) { - - auto const offset { Size.web - ( flangeway + Size.head ) }; - auto const flare { std::min( 0.3, 0.25 * ( Guard.to - Guard.from ) ) }; - track_rails::rail_fill fill; - fill.web = true; - fill.from = Guard.from; - fill.to = Guard.to; - fill.capfrom = true; - fill.capto = true; - fill.lift = ( Guard.through ? 0.0 : -0.001 ); - fill.inner = { { Guard.from, offset - ( Guard.flarefrom ? 0.02 : 0.0 ) }, { Guard.from + flare, offset }, { Guard.to - flare, offset }, { Guard.to, offset - ( Guard.flareto ? 0.02 : 0.0 ) } }; - fill.outer = { { Guard.from, Size.web }, { Guard.to, Size.web } }; - return fill; -} - -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 ); -} - -track_rails::rail_band groove_band( double const At, double const Into, double const Facing, bool const Through, track_rails::rail_size const &Other ) { - - auto const seam { 0.001 }; - track_rails::rail_band band { At, Into, Facing }; - band.whole = ( false == Through ); - band.from = ( Through ? 0.0 : seam - Other.tread ); - band.to = ( Through ? ( Other.grooved ? -Other.lip : flangeway ) : -Other.ledge - seam ); - return band; -} - -void vee_fill( std::vector &Fills, double const At, double const Into, double const Facing, track_rails::rail_size const &Own, track_rails::rail_size const &Other, bool const Outwards ) { - - auto const seam { 0.001 }; - auto const sign { Outwards ? 1.0 : -1.0 }; - auto const edge { Outwards ? std::max( Own.head, Own.crown ) : Own.rim }; - auto const flat { Outwards ? Own.tread : Own.ledge }; - auto const inside { Outwards == ( Facing > 0.0 ) }; - auto const closest { inside ? -Other.rim : -std::max( Other.head, Other.crown ) }; - auto const rounded { inside ? Other.ledge - Other.rim : std::max( Other.head, Other.crown ) - Other.tread }; - auto const own { Outwards ? Own.high - edge : edge - Own.low }; - auto const other { inside ? Other.rim - Other.low : Other.high - std::max( Other.head, Other.crown ) }; - auto const slant { std::max( std::abs( Facing ), 0.1 ) }; - auto const gap { own + other / slant }; - auto const meets = [&]( double const Width ) { - return At + ( closest + Facing * ( edge + sign * Width ) ) / Into; }; - auto const apex { meets( 0.0 ) }; - auto const end { meets( gap ) }; - auto const reach { seam + rounded / slant }; - track_rails::rail_fill fill; - fill.from = std::min( apex, end ); - fill.to = std::max( apex, end ); - fill.capfrom = ( end < apex ); - fill.capto = ( end > apex ); - if( Own.grooved ) { - fill.lift = std::min( Outwards ? Own.crest : Own.liptop, inside ? Other.liptop : Other.crest ) - Own.liptop; - } - std::vector line { { apex, edge + sign * reach }, { end, edge + sign * ( gap + reach ) } }; - std::sort( line.begin(), line.end(), []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; } ); - std::vector fixed { { fill.from, flat - sign * seam }, { fill.to, flat - sign * seam } }; - fill.outer = ( Outwards ? line : fixed ); - fill.inner = ( Outwards ? fixed : line ); - Fills.push_back( fill ); -} - -// 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 }; -}; - -double lateral( gauge_line const &Line, int const Side, glm::dvec3 const &Point, double &Distance ) { - - glm::dvec3 point, tangent; - for( int step = 0; step < 8; ++step ) { - Line.at( Distance, point, tangent ); - Distance += glm::dot( plan( Point - point ), glm::normalize( plan( tangent ) ) ); - } - Line.at( Distance, point, tangent ); - return -glm::dot( plan( Point - point ), plan( Line.normal( Distance, Side ) ) ); -} - -double nearest_distance( gauge_line const &Line, glm::dvec3 const &Point ) { - - auto result { Line.distances.front() }; - auto nearest { std::numeric_limits::max() }; - for( std::size_t i = 0; i < Line.points.size(); ++i ) { - auto const gap { glm::length( plan( Line.points[ i ] - Point ) ) }; - if( gap < nearest ) { - nearest = gap; - result = Line.distances[ i ]; - } - } - return result; -} - -std::vector tread_edge( TTrack const &Track, int const Path, int const Side, TTrack const &Other, int const Otherpath, int const Otherside, double const From, double const To, bool &Below, std::vector &Gauge ) { - - auto const &ours { rail_line( Track, Path, Side > 0, false ) }; - auto const &theirs { rail_line( Other, Otherpath, Otherside > 0, false ) }; - glm::dvec3 point, tangent; - ours.at( From, point, tangent ); - auto otherat { nearest_distance( theirs, point ) }; - std::vector knots; - auto below { 0 }; - auto const count { std::max( 2, static_cast( std::ceil( ( To - From ) / 0.1 ) ) + 1 ) }; - for( int i = 0; i < count; ++i ) { - auto distance { glm::mix( From, To, static_cast( i ) / ( count - 1 ) ) }; - ours.at( distance, point, tangent ); - lateral( theirs, Otherside, point, otherat ); - glm::dvec3 line; - theirs.at( otherat, line, tangent ); - auto const normal { theirs.normal( otherat, Otherside ) }; - auto at { distance }; - auto const gauge { lateral( ours, Side, line, at ) }; - Gauge.emplace_back( at, gauge ); - at = distance; - auto const edge { lateral( ours, Side, line - normal * rolled_band, at ) }; - below += ( gauge < edge ? 1 : -1 ); - knots.emplace_back( at, edge ); - } - auto const byposition = []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; }; - std::sort( knots.begin(), knots.end(), byposition ); - std::sort( Gauge.begin(), Gauge.end(), byposition ); - Below = ( below > 0 ); - return knots; -} - -std::vector nose_extents( TTrack const &Track, int const Path, int const Side, TTrack const &Other, int const Otherpath, int const Otherside, double const At, double const Toward, double const Slope, track_rails::rail_size const &Size ) { - - auto const half { Slope / ( 1.0 + std::sqrt( 1.0 + Slope * Slope ) ) }; - auto const tip { 0.5 * nose_tip }; - auto const apart { Size.high + 0.01 }; - auto const nose { At - Toward * tip / half }; - auto const blunt { At - Toward * apart / half }; - std::vector straight { { std::min( nose, blunt ), Toward < 0.0 ? tip : apart }, { std::max( nose, blunt ), Toward < 0.0 ? apart : tip } }; - auto const &ours { rail_line( Track, Path, Side > 0, false ) }; - auto const &theirs { rail_line( Other, Otherpath, Otherside > 0, false ) }; - glm::dvec3 point, tangent; - ours.at( nose, point, tangent ); - auto otherat { nearest_distance( theirs, point ) }; - auto extent { tip }; - auto const step { 0.1 }; - auto const limit { static_cast( std::ceil( 3.0 * std::abs( blunt - nose ) / step ) ) }; - std::vector knots; - for( int i = 0; i <= limit; ++i ) { - auto const distance { nose - Toward * step * i }; - ours.at( distance, point, tangent ); - auto const normal { ours.normal( distance, Side ) }; - for( int iteration = 0; iteration < 6; ++iteration ) { - extent += 0.5 * ( lateral( theirs, Otherside, point - normal * extent, otherat ) - extent ); - } - if( extent >= apart && false == knots.empty() ) { - auto const &last { knots.back() }; - knots.emplace_back( glm::mix( last.x, distance, ( apart - last.y ) / ( extent - last.y ) ), apart ); - break; - } - knots.emplace_back( distance, std::max( extent, 0.0 ) ); - } - if( knots.size() < 2 || knots.back().y < apart ) { - return straight; - } - std::sort( knots.begin(), knots.end(), []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; } ); - return knots; -} - -} // namespace - -material_handle TTrack::rail_material( int const Path ) const { - - return ( Path == 1 && eType == tt_Switch ? m_material2 : m_material1 ); -} - -glm::vec2 TTrack::tread_band() const { - - glm::vec2 band { 0.45f, 0.55f }; - if( m_profiles.empty() ) { - return band; - } - auto const &outline { track_rail_profile( m_profile1.second ) }; - auto const count { outline.size() / 2 }; - auto top { -1e9f }; - for( std::size_t i = 0; i < count; ++i ) { - top = std::max( top, outline[ i ].position.y ); - } - glm::vec2 found { 1e9f, -1e9f }; - for( std::size_t i = 0; i < count; ++i ) { - if( outline[ i ].position.y > top - 0.0005f ) { - found = { std::min( found.x, outline[ i ].texture.s ), std::max( found.y, outline[ i ].texture.s ) }; - } - } - if( found.x > found.y ) { - return band; - } - auto const middle { 0.5f * ( found.x + found.y ) }; - auto const half { std::max( 0.5f * ( found.y - found.x ), 0.01f ) }; - return { middle - half, middle + half }; -} - -material_handle TTrack::shared_tread( int const Path, TTrack const &Other, int const Otherpath ) const { - - auto const own { rail_material( Path ) }; - auto const theirs { Other.rail_material( Otherpath ) }; - if( theirs == null_handle || theirs == own ) { - return own; - } - return ( track_rails::tread_brightness( theirs, Other.tread_band() ) > track_rails::tread_brightness( own, tread_band() ) ? theirs : own ); -} - -track_rails::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 = ( Crossings ? rail_checks( *this ) : 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 ) }; - auto const tread = [&]( int const Path, int const Side, glm::dvec2 const &Span, TTrack const &Other, int const Otherpath, int const Anchor = 0, int const Otherside = 0 ) { - auto const material { shared_tread( Path, Other, Otherpath ) }; - if( material == rail_material( Path ) || Span.y <= Span.x ) { - return; - } - if( Otherside == 0 ) { - layout.treads[ Path ][ Side > 0 ].push_back( { Span.x, Span.y, material, Anchor } ); - return; - } - rail_tread part { Span.x, Span.y, material, Anchor }; - part.edge = tread_edge( *this, Path, Side, Other, Otherpath, Otherside, Span.x, Span.y, part.below, part.gauge ); - auto from { 1e9 }; - auto to { -1e9 }; - for( std::size_t i = 0; i < part.edge.size() && i < part.gauge.size(); ++i ) { - auto const low { std::min( part.edge[ i ].y, part.gauge[ i ].y ) }; - auto const high { std::max( part.edge[ i ].y, part.gauge[ i ].y ) }; - if( low - tread_fade < size.head && high + tread_fade > 0.0 ) { - from = std::min( from, part.edge[ i ].x ); - to = std::max( to, part.edge[ i ].x ); - } - } - part.from = std::max( Span.x, from - 0.1 ); - part.to = std::min( Span.y, to + 0.1 ); - if( part.to > part.from ) { - layout.treads[ Path ][ Side > 0 ].push_back( part ); - } - }; - auto const across = [&]( rail_crossing const &Crossing, rail_size const &Other ) { - glm::dvec2 span { 1e9, -1e9 }; - for( auto const theirs : { Other.grooved ? Other.ledge : 0.0, Other.grooved ? Other.tread : Other.head } ) { - for( auto const ours : { size.grooved ? size.ledge : 0.0, size.grooved ? size.tread : size.head } ) { - auto const distance { Crossing.at + ( Crossing.facing * ours - theirs ) / Crossing.into }; - span = { std::min( span.x, distance ), std::max( span.y, distance ) }; - } - } - return span; }; - std::vector corners; - if( Crossings ) { - for( auto const &crossing : crossings ) { - auto &plan { plans[ crossing.path ][ crossing.side > 0 ] }; - if( std::find( layout.crossed.begin(), layout.crossed.end(), crossing.track ) == layout.crossed.end() ) { - layout.crossed.push_back( crossing.track ); - } - auto const theirs { crossing.track->rail_dimensions() }; - if( size.grooved || theirs.grooved ) { - auto const through { size.grooved != theirs.grooved ? size.grooved : goes_through( crossing.owner ? *crossing.owner : *this, *crossing.track ) }; - layout.bands[ crossing.path ][ crossing.side > 0 ].push_back( groove_band( crossing.at, crossing.into, crossing.facing, through, theirs ) ); - if( size.grooved && theirs.grooved && crossing.angle <= middle_angle ) { - std::vector vee; - vee_fill( vee, crossing.at, crossing.into, crossing.facing, size, theirs, true ); - vee_fill( vee, crossing.at, crossing.into, crossing.facing, size, theirs, false ); - glm::dvec2 const block { std::min( vee[ 0 ].from, vee[ 1 ].from ), std::max( vee[ 0 ].to, vee[ 1 ].to ) }; - layout.blocks[ crossing.path ][ crossing.side > 0 ].push_back( block ); - tread( crossing.path, crossing.side, block, *crossing.track, crossing.otherpath ); - if( through ) { - auto &fills { layout.fills[ crossing.path ][ crossing.side > 0 ] }; - fills.insert( fills.end(), vee.begin(), vee.end() ); - } - } - else if( through ) { - tread( crossing.path, crossing.side, across( crossing, theirs ), *crossing.track, crossing.otherpath ); - } - continue; - } - 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, through ? std::vector{} : glue_line( other.field, 0.0 ) ); - if( through ) { - tread( crossing.path, crossing.side, across( crossing, other ), *crossing.track, crossing.otherpath ); - } - auto const reach { ( flangeway + railhead + 0.3 ) / std::abs( crossing.into ) }; - auto &spans { guards[ crossing.path ][ crossing.side > 0 ] }; - track_rails::guard_rail guard { crossing.at - reach, crossing.at + reach }; - guard.through = through; - wide( guard.plan, crossing.at, crossing.into, crossing.facing, -other.head, through ? flangeway : flangeway + other.head, guardoffset, guardoffset + railhead, glue_line( other.field, 0.0 ), through ? std::vector{} : glue_line( other.gauge, flangeway + other.head ) ); - 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() ); - span.through = ( span.through && guard.through ); - 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, - wing_extension( *this, checks, crossing.path, crossing.side, crossing.at, toward, slope, *crossing.track, crossing.otherpath, crossing.otherside, crossing.otherat, crossing.aligned ? -toward : toward ), - nose_extents( *this, crossing.path, crossing.side, *crossing.track, crossing.otherpath, crossing.otherside, crossing.at, toward, slope, size ) ); - auto const nose { toward < 0.0 ? plan.cuts.back().start : plan.cuts.back().end }; - auto const shared { crossing.at - toward * ( rolled_band + size.head + tread_fade ) / slope }; - tread( crossing.path, crossing.side, { std::min( nose, shared ), std::max( nose, shared ) }, *crossing.track, crossing.otherpath, toward > 0.0 ? 1 : -1, crossing.otherside ); - } - else { - obtuse( plan, crossing.at, crossing.into, size ); - auto const reach { ( rolled_band + size.head + tread_fade ) / std::max( std::tan( crossing.angle ), 0.01 ) + 0.2 }; - tread( crossing.path, crossing.side, { crossing.at - reach, crossing.at + reach }, *crossing.track, crossing.otherpath, 0, crossing.otherside ); - } - } - 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 ); - } - } - } - } - } - for( int path = 0; path < 2; ++path ) { - for( int side = 0; side < 2; ++side ) { - for( auto const &guard : guards[ path ][ side ] ) { - layout.fills[ path ][ side ].push_back( guard_fill( guard, size ) ); - } - } - } - 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 ) { - track_rails::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 && cut_rails( *this ) ) { - for( int path = 0; path < 2; ++path ) { - tread( path, -blade_side( path ), { 0.0, blade_length( *this, 1 - path ) }, *this, 1 - path, 0, -blade_side( path ) ); - } - } - if( eType == tt_Switch ) { - auto const frog { this->frog() }; - if( frog.valid ) { - glm::dvec3 point, tangents[ 2 ]; - glm::dvec2 normals[ 2 ]; - for( int path = 0; path < 2; ++path ) { - auto const &line { rail_line( *this, path, frog.side[ path ] > 0, false ) }; - line.at( frog.at[ path ], point, tangents[ path ] ); - normals[ path ] = glm::normalize( plan( line.normal( frog.at[ path ], frog.side[ path ] ) ) ); - } - auto const facing { glm::dot( normals[ 0 ], normals[ 1 ] ) }; - auto const slope { std::tan( frog.angle ) }; - for( int path = 0; path < 2; ++path ) { - auto const into { glm::dot( glm::normalize( plan( tangents[ path ] ) ), normals[ 1 - path ] ) }; - auto &fills { layout.fills[ path ][ frog.side[ path ] > 0 ] }; - if( size.grooved ) { - layout.bands[ path ][ frog.side[ path ] > 0 ].push_back( groove_band( frog.at[ path ], into, facing, path == 0, size ) ); - std::vector vee; - vee_fill( vee, frog.at[ path ], into, facing, size, size, true ); - vee_fill( vee, frog.at[ path ], into, facing, size, size, false ); - glm::dvec2 const block { std::min( vee[ 0 ].from, vee[ 1 ].from ), std::max( vee[ 0 ].to, vee[ 1 ].to ) }; - layout.blocks[ path ][ frog.side[ path ] > 0 ].push_back( block ); - tread( path, frog.side[ path ], block, *this, 1 - path ); - if( path == 0 ) { - fills.insert( fills.end(), vee.begin(), vee.end() ); - } - continue; - } - 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, - 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 ), - nose_extents( *this, path, frog.side[ path ], *this, 1 - path, frog.side[ 1 - path ], frog.at[ path ], -1.0, slope, size ) ); - auto const nose { plans[ path ][ frog.side[ path ] > 0 ].cuts.back().start }; - auto const shared { frog.at[ path ] + ( rolled_band + size.head + tread_fade ) / slope }; - tread( path, frog.side[ path ], { std::min( nose, shared ), std::max( nose, shared ) }, *this, 1 - path, -1, frog.side[ 1 - path ] ); - } - } - } - 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() ); - } - if( false == m_profiles.empty() ) { - auto const &outline { track_rail_profile( m_profile1.second ) }; - auto texture { 0.f }; - auto highest { -1e9f }; - for( std::size_t i = 0; i < outline.size() / 2; ++i ) { - if( outline[ i ].position.y > highest ) { - highest = outline[ i ].position.y; - texture = outline[ i ].texture.s; - } - } - if( drawing.size.grooved ) { - auto sum { 0.f }; - auto points { 0 }; - for( std::size_t i = 0; i < outline.size() / 2; ++i ) { - if( outline[ i ].position.y > highest - 0.0005f && outline[ i ].position.x > -0.02f ) { - sum += outline[ i ].texture.s; - ++points; - } - } - texture = sum / std::max( points, 1 ); - } - auto const top { static_cast( ( drawing.size.grooved ? drawing.size.liptop : highest ) - 0.0005 ) }; - auto const bottom { static_cast( -drawing.size.height ) }; - gfx::vertex_array plate { - {{ 1.f, bottom, 0.f }, { 1.f, 0.f, 0.f }, { texture, 0.f }}, - {{ 1.f, top, 0.f }, { 1.f, 0.f, 0.f }, { texture, 0.f }}, - {{ 1.f, top, 0.f }, { 0.f, 1.f, 0.f }, { texture, 0.f }}, - {{ -1.f, top, 0.f }, { 0.f, 1.f, 0.f }, { texture, 0.f }}, - {{ -1.f, top, 0.f }, { -1.f, 0.f, 0.f }, { texture, 0.f }}, - {{ -1.f, bottom, 0.f }, { -1.f, 0.f, 0.f }, { texture, 0.f }} }; - drawing.platetexture = texture; - create_track_rail_profile( drawing.plates[ 0 ], drawing.plates[ 1 ], &plate ); - for( auto &shape : drawing.plates ) { - shape.resize( plate.size() ); - } - auto const neck { static_cast( drawing.size.neck ) }; - auto webtexture { texture }; - for( std::size_t i = 0; i < outline.size() / 2; ++i ) { - auto const &point { outline[ i ] }; - if( point.position.y < neck && point.normal.y > 0.9f ) { - webtexture = point.texture.s; - break; - } - } - gfx::vertex_array filler { - {{ 1.f, bottom, 0.f }, { 1.f, 0.f, 0.f }, { webtexture, 0.f }}, - {{ 1.f, neck, 0.f }, { 1.f, 0.f, 0.f }, { webtexture, 0.f }}, - {{ 1.f, neck, 0.f }, { 0.f, 1.f, 0.f }, { webtexture, 0.f }}, - {{ -1.f, neck, 0.f }, { 0.f, 1.f, 0.f }, { webtexture, 0.f }}, - {{ -1.f, neck, 0.f }, { -1.f, 0.f, 0.f }, { webtexture, 0.f }}, - {{ -1.f, bottom, 0.f }, { -1.f, 0.f, 0.f }, { webtexture, 0.f }} }; - create_track_rail_profile( drawing.fillers[ 0 ], drawing.fillers[ 1 ], &filler ); - for( auto &shape : drawing.fillers ) { - shape.resize( filler.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() }; - auto const &treads { Layout.treads[ Path ][ Side > 0 ] }; - material_handle current { null_handle }; - std::vector tints; - static auto const marked { std::getenv( "EU07_EDITOR_TREADS" ) != nullptr }; - auto const paint = [&]( material_handle const Material, glm::vec3 const &Colour ) { - auto const tinted { tint_material( Material, Colour ) }; - if( tinted != Material ) { - tints.push_back( tinted ); - } - return tinted; }; - std::vector const unramped; - auto const sink = [&]( bool const Matte ) -> geometryhandle_sequence & { - if( current == null_handle ) { - return Matte ? Cuts : Geometry; - } - if( current == m_material1 ) { - return Matte ? GeometryCut1 : Geometry1; - } - if( eType == tt_Switch && current == m_material2 ) { - return Matte ? GeometryCut2 : Geometry2; - } - auto const tinted { std::find( tints.begin(), tints.end(), current ) != tints.end() }; - auto found { std::find_if( GeometryTreads.begin(), GeometryTreads.end(), [&]( tread_geometry const &Tread ) { return Tread.material == current; } ) }; - if( found == GeometryTreads.end() ) { - found = GeometryTreads.insert( GeometryTreads.end(), { current, tinted ? current : cut_material( current ), {}, {} } ); - } - return Matte && false == tinted ? found->cuts : found->geometry; }; - auto const texturelength = [&]() { - return ( current == null_handle ? Texturelength : texture_length( current ) ); }; - auto const aim = [&]( std::vector const &Spans, double const Middle ) -> rail_tread const * { - current = null_handle; - for( auto const &span : Spans ) { - if( Middle > span.from && Middle < span.to ) { - current = span.material; - return &span; - } - } - return nullptr; - }; - // parts no wheel rolls on go in the matte material of the cut faces - glm::dvec2 bendfrom { 0.0 }; - glm::dvec2 bendto { 0.0 }; - auto const bends = [&]( double const Start, double const End ) { - return glm::dvec2 { - std::abs( Start - bendfrom.x ) < 1e-6 ? bendfrom.y : 0.0, - std::abs( End - bendto.x ) < 1e-6 ? bendto.y : 0.0 }; }; - std::pair> fitfrom, fitto; - std::vector const unfitted; - auto const fitted = [&]( std::pair> const &Fit, double const Distance ) -> std::vector const & { - return ( std::abs( Distance - Fit.first ) < 1e-6 ? Fit.second : unfitted ); }; - auto const span = [&]( double const Start, double const End, knots::line const &Offsets, knots::line const &Extents, knots::line const &Inner ) { - loft_span result; - result.side = Side; - result.halfwidth = halfwidth; - result.texturelength = texturelength(); - result.from = Start; - result.to = End; - result.offsets = Offsets; - result.extents = Extents; - result.inner = Inner; - auto const bent { bends( Start, End ) }; - result.fromend = { bent.x, fitted( fitfrom, Start ) }; - result.toend = { bent.y, fitted( fitto, End ) }; - return result; }; - 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 = {}, bool const Walls = false, float const Lift = 0.f, float const Wallu = -1.f, double const Fade = blend ) { - gfx::vertex_array vertices; - gfx::userdata_array userdata; - auto loft { span( Start, End, Offsets, Extents, Inner ) }; - loft.lift = Lift; - loft.userdata = ( Rolled ? nullptr : &userdata ); - loft.ramp = Ramp; - loft.rampwidth = ( Ramp.empty() ? 0.0 : Fade ); - loft.walls = Walls; - loft.wallu = Wallu; - if( Shape.size() > 1 && segment.RenderLoftSpan( vertices, m_origin, Shape, loft ) ) { - cut_faces( vertices, Shape.size(), Capstart, Capend, Drawing.bank, sink( true ) ); - 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 ] ); - } - } - } - } - sink( false == Rolled ).emplace_back( GfxRenderer->Insert( triangles, Rolled ? empty_userdata : triangleuserdata, Drawing.bank, GL_TRIANGLES ) ); - } - }; - // 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 crossings = []( knots::line const &A, knots::line const &B, double const Start, double const End, double const Margin, std::vector &Splits ) { - for( auto const distance : knots::meetings( A, B ) ) { - if( distance > Start + Margin && distance < End - Margin ) { - Splits.push_back( distance ); - } - } - }; - auto const running = [&]( double const Start, double const End, std::vector const &Offsets, std::vector const &Extents, std::vector const &Inner, std::vector const &Rolled, bool const Capstart, bool const Capend ) { - gfx::vertex_array vertices; - if( segment.RenderLoftSpan( vertices, m_origin, Profile, span( Start, End, Offsets, Extents, Inner ) ) ) { - cut_faces( vertices, Profile.size(), Capstart, Capend, Drawing.bank, sink( true ) ); - } - piece( Start, End, Offsets, Extents, false, false, strip( 0, topfirst ), true, Inner ); - piece( Start, End, Offsets, Extents, false, false, strip( toplast, Profile.size() - 1 ), true, Inner ); - 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 ) ); - } - auto const inner { knots::shifted( Rolled, -blend ) }; - std::vector splits { Start, End }; - std::vector const lowline { { Start, low }, { End, low } }; - std::vector const highline { { Start, high }, { End, high } }; - for( auto const *knots : { &Rolled, &inner } ) { - for( auto const *limit : { &lowline, &highline, &Extents, &Inner } ) { - crossings( *knots, *limit, Start, End, 0.0, splits ); - } - } - std::sort( splits.begin(), splits.end() ); - auto const shiny { knots::bound( Extents, inner, true ) }; - auto const blended { knots::bound( Extents, Rolled, true ) }; - for( std::size_t i = 1; i < splits.size(); ++i ) { - auto const middle { 0.5 * ( splits[ i - 1 ] + splits[ i ] ) }; - auto const reach { knots::sample( Rolled, middle, 0.0 ) }; - auto const least { std::max( low, Inner.empty() ? low : knots::sample( Inner, middle, 0.0 ) ) }; - auto const most { std::min( high, Extents.empty() ? high : knots::sample( Extents, middle, 0.0 ) ) }; - auto const shown = [&]( double const Lowest, double const Highest ) { - return Highest > Lowest + 1e-6 && Highest > least && Lowest < most; }; - if( shown( -1e9, reach - blend ) ) { - piece( splits[ i - 1 ], splits[ i ], Offsets, shiny, false, false, top, true, Inner ); - } - auto const plain { current }; - auto const own { current == null_handle ? rail_material( Path ) : current }; - if( shown( reach - blend, reach ) ) { - current = ( marked ? paint( own, { 1.f, 1.f, 0.f } ) : plain ); - piece( splits[ i - 1 ], splits[ i ], Offsets, blended, false, false, top, false, knots::bound( inner, Inner, false ), marked ? unramped : Rolled ); - } - if( shown( reach, 1e9 ) ) { - current = ( marked ? paint( own, { 1.f, 0.f, 1.f } ) : plain ); - piece( splits[ i - 1 ], splits[ i ], Offsets, Extents, false, false, top, false, knots::bound( Rolled, Inner, false ) ); - } - current = plain; - } - }; - auto const treaded = [&]( double const Start, double const End, std::vector const &Offsets, std::vector const &Extents, std::vector const &Inner, bool const Capstart, bool const Capend, rail_tread const &Span ) { - current = null_handle; - gfx::vertex_array vertices; - if( segment.RenderLoftSpan( vertices, m_origin, Profile, span( Start, End, Offsets, Extents, Inner ) ) ) { - cut_faces( vertices, Profile.size(), Capstart, Capend, Drawing.bank, sink( true ) ); - } - piece( Start, End, Offsets, Extents, false, false, strip( 0, topfirst ), true, Inner ); - piece( Start, End, Offsets, Extents, false, false, strip( toplast, Profile.size() - 1 ), true, Inner ); - 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 ) ); - } - auto const &lower { Span.below ? Span.gauge : Span.edge }; - auto const &upper { Span.below ? Span.edge : Span.gauge }; - auto const own { rail_material( Path ) }; - auto const colour { 0.5f * ( tread_colour( Span.material, tread_band() ) + tread_colour( own, tread_band() ) ) }; - auto const tint = [&]( material_handle const Material, glm::vec3 const &Mark ) { - return paint( Material, marked ? Mark : colour ); }; - auto const owntint { tint( own, { 1.f, 0.f, 0.f } ) }; - auto const sharedtint { tint( Span.material, { 0.f, 1.f, 0.f } ) }; - auto const shared { marked ? tint( Span.material, { 0.f, 0.f, 1.f } ) : Span.material }; - auto const ramp = [&]( std::vector const &Knots ) -> std::vector const & { - return marked ? unramped : Knots; }; - auto const centre { knots::middle( lower, upper ) }; - auto const beneath { knots::shifted( lower, -tread_fade ) }; - auto const beyond { knots::shifted( upper, tread_fade ) }; - auto const lowfade { knots::bound( knots::shifted( lower, tread_fade ), centre, true ) }; - auto const highfade { knots::bound( knots::shifted( upper, -tread_fade ), centre, false ) }; - std::vector splits { Start, End }; - std::vector const lowline { { Start, low }, { End, low } }; - std::vector const highline { { Start, high }, { End, high } }; - for( auto const *knots : { &lower, &upper, &lowfade, &highfade, &beneath, &beyond } ) { - for( auto const *limit : { &lowline, &highline, &Extents, &Inner } ) { - crossings( *knots, *limit, Start, End, 0.001, splits ); - } - } - crossings( knots::shifted( lower, 2.0 * tread_fade ), upper, Start, End, 0.001, splits ); - std::sort( splits.begin(), splits.end() ); - for( std::size_t i = 1; i < splits.size(); ++i ) { - auto const a { splits[ i - 1 ] }; - auto const b { splits[ i ] }; - auto const middle { 0.5 * ( a + b ) }; - auto const from { knots::sample( lower, middle, 0.0 ) }; - auto const to { knots::sample( upper, middle, 0.0 ) }; - auto const inner { knots::sample( lowfade, middle, 0.0 ) }; - auto const outer { knots::sample( highfade, middle, 0.0 ) }; - auto const least { std::max( low, Inner.empty() ? low : knots::sample( Inner, middle, 0.0 ) ) }; - auto const most { std::min( high, Extents.empty() ? high : knots::sample( Extents, middle, 0.0 ) ) }; - auto const shown = [&]( double const Lowest, double const Highest ) { - return Highest > Lowest + 1e-6 && Highest > least && Lowest < most; }; - if( shown( -1e9, from - tread_fade ) ) { - current = null_handle; - piece( a, b, Offsets, knots::bound( Extents, beneath, true ), false, false, top, true, Inner ); - } - if( shown( from - tread_fade, from ) ) { - current = owntint; - piece( a, b, Offsets, knots::bound( Extents, lower, true ), false, false, top, false, knots::bound( Inner, beneath, false ), ramp( lower ), false, 0.f, -1.f, tread_fade ); - } - if( shown( from, inner ) ) { - current = sharedtint; - piece( a, b, Offsets, knots::bound( Extents, lowfade, true ), false, false, top, false, knots::bound( Inner, lower, false ), ramp( lower ), false, 0.f, -1.f, -tread_fade ); - } - if( shown( inner, outer ) ) { - current = shared; - piece( a, b, Offsets, knots::bound( Extents, highfade, true ), false, false, top, false == marked, knots::bound( Inner, lowfade, false ) ); - } - if( shown( outer, to ) ) { - current = sharedtint; - piece( a, b, Offsets, knots::bound( Extents, upper, true ), false, false, top, false, knots::bound( Inner, highfade, false ), ramp( upper ), false, 0.f, -1.f, tread_fade ); - } - if( shown( to, to + tread_fade ) ) { - current = owntint; - piece( a, b, Offsets, knots::bound( Extents, beyond, true ), false, false, top, false, knots::bound( Inner, upper, false ), ramp( upper ), false, 0.f, -1.f, -tread_fade ); - } - if( shown( to + tread_fade, 1e9 ) ) { - current = null_handle; - piece( a, b, Offsets, Extents, false, false, top, true, knots::bound( Inner, beyond, false ) ); - } - } - current = null_handle; - }; - 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 }; - rail_cut startcut; - 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 endcut { i < cuts.size() ? cuts[ i ] : rail_cut {} }; - 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 ); - } - } - auto const glued { a == start && false == startcut.startfit.empty() }; - auto const gluedend { b == end && false == endcut.endfit.empty() }; - bendfrom = { a, glued ? startcut.startbend : 0.0 }; - bendto = { b, gluedend ? endcut.endbend : 0.0 }; - fitfrom = { a, glued ? startcut.startfit : unfitted }; - fitto = { b, gluedend ? endcut.endfit : unfitted }; - piece( a, b, offsets, envelope( uppers, true, a, b ), false == glued, false == gluedend, Profile, false, envelope( lowers, false, a, b ) ); - bendfrom = bendto = glm::dvec2 { 0.0 }; - fitfrom = fitto = {}; - } - if( i < cuts.size() && cuts[ i ].start > start ) { - start = cuts[ i ].start; - startmiter = cuts[ i ].startmiter; - startcut = cuts[ i ]; - } - } - } - 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 } ); - } - bendfrom = { a, a == from ? check.frombend : 0.0 }; - bendto = { b, b == to ? check.tobend : 0.0 }; - piece( a, b, offsets, {}, a > 0.0 || from >= 0.0, to <= length, shape, false, {} ); - bendfrom = bendto = glm::dvec2 { 0.0 }; - } - for( auto const &fill : Layout.fills[ Path ][ Side > 0 ] ) { - auto const a { std::max( fill.from, From ) }; - auto const b { std::min( fill.to, length ) }; - if( b - a > 0.01 ) { - current = null_handle; - if( false == fill.web ) { - aim( treads, 0.5 * ( a + b ) ); - } - auto const &shapes { fill.web ? Drawing.fillers : Drawing.plates }; - piece( a, b, {}, fill.outer, a == fill.from && fill.capfrom, b == fill.to && fill.capto, shapes[ Side > 0 ? 0 : 1 ], true, fill.inner, {}, true, static_cast( fill.lift ) ); - current = null_handle; - } - } - auto const &bands { Layout.bands[ Path ][ Side > 0 ] }; - if( plan.cuts.empty() && bands.empty() && treads.empty() ) { - return false; - } - auto const byposition = []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; }; - auto const reaches = [&]( gfx::vertex_array const &Shape ) { - glm::dvec2 result { 1e9, -1e9 }; - for( auto const &point : Shape ) { - auto const reach { static_cast( Side * point.position.x - halfwidth ) }; - result = { std::min( result.x, reach ), std::max( result.y, reach ) }; - } - return result; }; - auto const band_cut = [&]( rail_band const &Band, glm::dvec2 const &Reach ) { - rail_cut cut; - cut.at = Band.at; - auto const before { Band.into > 0.0 ? Band.from : Band.to }; - auto const after { Band.into > 0.0 ? Band.to : Band.from }; - auto const meets = [&]( double const Edge, double const Reach ) { - return glm::dvec2{ Band.at + ( Edge + Band.facing * Reach ) / Band.into, Reach }; }; - if( std::abs( Band.facing ) < 1e-3 ) { - cut.end = meets( before, 0.0 ).x; - cut.start = meets( after, 0.0 ).x; - return cut; - } - std::vector endknots { meets( before, Reach.x ), meets( before, Reach.y ) }; - std::vector startknots { meets( after, Reach.x ), meets( after, Reach.y ) }; - std::sort( endknots.begin(), endknots.end(), byposition ); - std::sort( startknots.begin(), startknots.end(), byposition ); - cut.end = endknots.back().x; - cut.start = startknots.front().x; - cut.endcap = false; - cut.startcap = false; - auto const outwards { Band.facing / Band.into > 0.0 }; - ( outwards ? cut.endinner : cut.endextents ) = endknots; - ( outwards ? cut.startextents : cut.startinner ) = startknots; - return cut; }; - auto const lay = [&]( gfx::vertex_array const &Shape, std::vector cuts, bool const Wheels, std::vector const &Spans, float const Wallu = -1.f ) { - std::sort( cuts.begin(), cuts.end(), []( rail_cut const &A, rail_cut const &B ) { return A.at < B.at; } ); - auto start { -1e9 }; - auto startcap { true }; - auto startbend { 0.0 }; - std::vector startfit; - std::vector rolledfrom; - std::vector offsets, extents, inner; - 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 }; - auto pieceinner { inner }; - 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() ); - pieceinner.insert( pieceinner.end(), cuts[ i ].endinner.begin(), cuts[ i ].endinner.end() ); - } - std::sort( pieceoffsets.begin(), pieceoffsets.end(), byposition ); - std::sort( pieceextents.begin(), pieceextents.end(), byposition ); - std::sort( pieceinner.begin(), pieceinner.end(), byposition ); - auto const a { std::max( start, From ) }; - auto const b { std::min( end, length ) }; - auto const rolled { i < cuts.size() ? knots::bound( rolledfrom, cuts[ i ].endrolled, true ) : rolledfrom }; - auto const capstart { start > From ? startcap : Openstart }; - auto const capend { end < length ? cuts[ i ].endcap : Openend }; - bendfrom = { a, start > From ? startbend : 0.0 }; - bendto = { b, end < length ? cuts[ i ].endbend : 0.0 }; - fitfrom = { a, start > From ? startfit : unfitted }; - fitto = { b, end < length ? cuts[ i ].endfit : unfitted }; - if( b - a > 0.01 ) { - std::vector active; - for( auto const &span : Spans ) { - if( span.anchor == 0 - || ( span.anchor > 0 && b > span.from + 0.01 && b < span.to + 0.01 ) - || ( span.anchor < 0 && a > span.from - 0.01 && a < span.to - 0.01 ) ) { - active.push_back( span ); - } - } - std::vector splits; - for( auto const &span : active ) { - for( auto const edge : { span.from, span.to } ) { - if( edge > a + 0.01 && edge < b - 0.01 ) { - splits.push_back( edge ); - } - } - } - std::sort( splits.begin(), splits.end() ); - splits.erase( std::unique( splits.begin(), splits.end(), []( double const A, double const B ) { return B - A < 0.002; } ), splits.end() ); - splits.insert( splits.begin(), a ); - splits.push_back( b ); - for( std::size_t k = 1; k < splits.size(); ++k ) { - auto const first { k == 1 }; - auto const last { k + 1 == splits.size() }; - auto const *span { aim( active, 0.5 * ( splits[ k - 1 ] + splits[ k ] ) ) }; - if( Wheels && span != nullptr && false == span->edge.empty() && topfirst < toplast ) { - treaded( splits[ k - 1 ], splits[ k ], pieceoffsets, pieceextents, pieceinner, first && capstart, last && capend, *span ); - } - else if( false == Wheels || rolled.empty() || topfirst >= toplast ) { - piece( splits[ k - 1 ], splits[ k ], pieceoffsets, pieceextents, first && capstart, last && capend, Shape, true, pieceinner, {}, false == Wheels, 0.f, Wallu ); - } - else { - running( splits[ k - 1 ], splits[ k ], pieceoffsets, pieceextents, pieceinner, rolled, first && capstart, last && capend ); - } - } - current = null_handle; - } - bendfrom = bendto = glm::dvec2 { 0.0 }; - fitfrom = fitto = {}; - if( i < cuts.size() ) { - rolledfrom = cuts[ i ].startrolled; - start = cuts[ i ].start; - startcap = cuts[ i ].startcap; - startbend = cuts[ i ].startbend; - startfit = cuts[ i ].startfit; - offsets = cuts[ i ].startoffsets; - extents = cuts[ i ].startextents; - inner = cuts[ i ].startinner; - } - } - }; - auto const &outline { track_rail_profile( m_profile1.second ) }; - auto const count { Profile.size() }; - if( false == size.grooved || outline.size() != 2 * count ) { - auto cuts { plan.cuts }; - auto const reach { reaches( Profile ) }; - for( auto const &band : bands ) { - cuts.push_back( band_cut( band, reach ) ); - } - lay( Profile, cuts, true, treads ); - return true; - } - auto const height = [&]( std::size_t const Index ) { - return static_cast( outline[ Side > 0 ? Index : count - 1 - Index ].position.y ) - size.groove; }; - auto const above = [&]( std::size_t const Index ) { - return height( Index ) > 1e-4; }; - auto const floorpoint = [&]( std::size_t const A, std::size_t const B ) { - auto const t { static_cast( height( A ) / ( height( A ) - height( B ) ) ) }; - auto point { Profile[ above( A ) ? A : B ] }; - point.position = glm::mix( Profile[ A ].position, Profile[ B ].position, t ); - point.texture = glm::mix( Profile[ A ].texture, Profile[ B ].texture, t ); - return point; }; - auto const flattened = [&]( std::size_t const Index ) { - auto point { Profile[ Index ] }; - auto const &file { outline[ Side > 0 ? Index : count - 1 - Index ].position }; - if( file.y > size.crest - 0.0005 || ( file.x < -0.02 && file.y > size.liptop - 0.0005 ) ) { - point.normal = { 0.f, 1.f, 0.f }; - point.texture.x = Drawing.platetexture; - } - return point; }; - std::vector tops, flats; - gfx::vertex_array body; - std::vector floors; - auto floortexture { 0.f }; - auto floortextures { 0 }; - for( std::size_t i = 0; i < count; ++i ) { - if( false == above( i ) ) { - if( height( i ) > -1e-4 ) { - floors.push_back( body.size() ); - floortexture += Profile[ i ].texture.x; - ++floortextures; - } - body.push_back( Profile[ i ] ); - continue; - } - auto const first { i }; - while( i + 1 < count && above( i + 1 ) ) { - ++i; - } - gfx::vertex_array top, flat; - if( first > 0 ) { - if( height( first - 1 ) > -1e-4 ) { - top.push_back( Profile[ first - 1 ] ); - flat.push_back( flattened( first - 1 ) ); - } - else { - top.push_back( floorpoint( first - 1, first ) ); - flat.push_back( top.back() ); - body.push_back( top.back() ); - floors.push_back( body.size() ); - body.push_back( top.back() ); - } - } - for( auto k { first }; k <= i; ++k ) { - top.push_back( Profile[ k ] ); - flat.push_back( flattened( k ) ); - } - if( i + 1 < count ) { - if( height( i + 1 ) > -1e-4 ) { - top.push_back( Profile[ i + 1 ] ); - flat.push_back( flattened( i + 1 ) ); - } - else { - top.push_back( floorpoint( i, i + 1 ) ); - flat.push_back( top.back() ); - floors.push_back( body.size() ); - body.push_back( top.back() ); - body.push_back( top.back() ); - } - } - for( auto *strip : { &top, &flat } ) { - gfx::vertex_array doubled { strip->front(), strip->front() }; - for( std::size_t k = 1; k + 1 < strip->size(); ++k ) { - doubled.insert( doubled.end(), 2, ( *strip )[ k ] ); - } - doubled.insert( doubled.end(), 2, strip->back() ); - *strip = doubled; - } - tops.push_back( top ); - flats.push_back( flat ); - } - if( floors.size() > 1 ) { - auto const across { body[ floors.back() ].position - body[ floors.front() ].position }; - auto up { glm::normalize( glm::vec3{ -across.y, across.x, 0.f } ) }; - if( up.y < 0.f ) { - up = -up; - } - for( auto const index : floors ) { - body[ index ].normal = up; - if( floortextures > 0 ) { - body[ index ].texture.x = floortexture / floortextures; - } - } - } - std::vector wholecuts { plan.cuts }; - auto const bodyreach { reaches( body ) }; - for( auto const &band : bands ) { - if( band.whole ) { - wholecuts.push_back( band_cut( band, bodyreach ) ); - } - } - lay( body, wholecuts, false, {} ); - auto const wallu { floortextures > 0 ? floortexture / floortextures : -1.f }; - for( std::size_t k = 0; k < tops.size(); ++k ) { - auto const reach { reaches( tops[ k ] ) }; - auto const head { reach.x + reach.y > 0.0 }; - auto edge { head ? 1e9 : -1e9 }; - for( auto const &point : tops[ k ] ) { - if( point.position.y > ( head ? size.crest : size.liptop ) - 0.0005 ) { - auto const across { static_cast( Side * point.position.x - halfwidth ) }; - edge = ( head ? std::min( edge, across ) : std::max( edge, across ) ); - } - } - std::vector cuts { plan.cuts }; - for( auto const &band : bands ) { - auto cut { band_cut( band, reach ) }; - if( false == band.whole && std::abs( band.facing ) >= 1e-3 && std::abs( edge ) < 1.0 ) { - auto const blunt { edge + ( head ? nose_tip : -nose_tip ) }; - if( head == ( band.facing / band.into > 0.0 ) ) { - cut.start = std::max( cut.start, band.at + ( ( band.into > 0.0 ? band.to : band.from ) + band.facing * blunt ) / band.into ); - cut.startcap = true; - } - else { - cut.end = std::min( cut.end, band.at + ( ( band.into > 0.0 ? band.from : band.to ) + band.facing * blunt ) / band.into ); - cut.endcap = true; - } - } - cuts.push_back( cut ); - } - std::vector spans { Layout.blocks[ Path ][ Side > 0 ] }; - for( auto const &cut : cuts ) { - for( auto &span : spans ) { - if( cut.at > span.x && cut.at < span.y ) { - span = { std::min( span.x, cut.end ), std::max( span.y, cut.start ) }; - } - } - } - std::sort( spans.begin(), spans.end(), byposition ); - std::vector merged; - for( auto const &span : spans ) { - if( false == merged.empty() && span.x <= merged.back().y ) { - merged.back().y = std::max( merged.back().y, span.y ); - } - else { - merged.push_back( span ); - } - } - if( merged.empty() ) { - lay( tops[ k ], cuts, false, treads, wallu ); - continue; - } - std::vector blocktreads; - for( auto const &span : merged ) { - for( auto const &tread : treads ) { - if( tread.from < span.y && tread.to > span.x ) { - blocktreads.push_back( { span.x, span.y, tread.material } ); - break; - } - } - } - auto const seam = []( double const At, double const End, double const Start ) { - rail_cut cut; - cut.at = At; - cut.end = End; - cut.start = Start; - cut.endcap = false; - cut.startcap = false; - return cut; }; - std::vector outside, inside; - for( auto const &cut : cuts ) { - auto const within { std::any_of( merged.begin(), merged.end(), [&]( glm::dvec2 const &Span ) { return cut.at > Span.x && cut.at < Span.y; } ) }; - ( within ? inside : outside ).push_back( cut ); - } - inside.push_back( seam( merged.front().x - 0.001, -1e9, merged.front().x ) ); - for( std::size_t m = 0; m < merged.size(); ++m ) { - outside.push_back( seam( 0.5 * ( merged[ m ].x + merged[ m ].y ), merged[ m ].x, merged[ m ].y ) ); - inside.push_back( m + 1 < merged.size() ? - seam( 0.5 * ( merged[ m ].y + merged[ m + 1 ].x ), merged[ m ].y, merged[ m + 1 ].x ) : - seam( merged[ m ].y + 0.001, merged[ m ].y, 1e9 ) ); - } - lay( tops[ k ], outside, false, treads, wallu ); - lay( flats[ k ], inside, false, blocktreads, wallu ); - } - return true; -} diff --git a/world/TrackRails.h b/world/TrackRails.h index feb35a45..1ae62522 100644 --- a/world/TrackRails.h +++ b/world/TrackRails.h @@ -10,10 +10,12 @@ http://mozilla.org/MPL/2.0/. #pragma once #include "world/Track.h" +#include "world/GaugeLine.h" namespace track_rails { void forget(); +rail_size measure_rail( gfx::vertex_array const &Profile ); // 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 ); @@ -31,6 +33,11 @@ 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 ); +TSegment const &path_segment( TTrack const &Track, int const Path ); +double knuckle_bend( double const Into ); +bool replaced_check( std::vector const &Checks, double const Wingend, double const Direction, double &Far ); +gauge_line const &rail_line( TTrack const &Track, int const Path, int const Side, bool const Blades ); +check_rail_set const &rail_checks( TTrack &Track ); glm::dvec3 rail_point( TTrack const &Track, int const Path, int const Side, double const Distance, double const Lateral ); void rail_offset( TTrack const &Track, int const Path, int const Side, glm::dvec3 const &Point, double &Distance, double &Lateral ); From 22e378b910b05ff9d45232f4e51f1ede1da859b2 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 09:40:28 +0200 Subject: [PATCH 06/19] refactor(track): a rail is laid by the class TTrack::rail_layer instead of one function of lambdas sharing a current material and the bends and fits of the ends; a piece of rail is a rail_piece value with its material, the ends of a run are passed as piece_ends, the bands across the top of the head of a running and of a shared tread are laid from one table each by lay_top, the guard and check rails take their flared offsets from one helper, and the grooved profile is split by split_grooved and cut by top_cuts --- world/RailLayer.cpp | 1305 ++++++++++++++++++++++++------------------- world/Track.h | 1 + 2 files changed, 733 insertions(+), 573 deletions(-) diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp index 1e19cb39..d858314c 100644 --- a/world/RailLayer.cpp +++ b/world/RailLayer.cpp @@ -233,310 +233,324 @@ TTrack::rail_drawing TTrack::make_rail_drawing( gfx::geometrybank_handle const & 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 ) { +namespace { + +struct piece_ends { + double from { 0.0 }; + double to { 0.0 }; + loft_end fromend; + loft_end toend; +}; + +struct rail_piece { + material_handle material { null_handle }; + double from { 0.0 }; + double to { 0.0 }; + knots::line offsets; + knots::line extents; + knots::line inner; + bool capfrom { false }; + bool capto { false }; + bool rolled { true }; + knots::line ramp; + double fade { blend }; + bool walls { false }; + float lift { 0.f }; + float wallu { -1.f }; +}; + +struct top_band { + material_handle material { null_handle }; + knots::line extents; + knots::line inner; + bool rolled { true }; + knots::line ramp; + double fade { blend }; +}; + +struct grooved_profile { + gfx::vertex_array body; + std::vector tops; + std::vector flats; + float wallu { -1.f }; +}; + +bool treads_marked() { - 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() }; - auto const &treads { Layout.treads[ Path ][ Side > 0 ] }; - material_handle current { null_handle }; - std::vector tints; static auto const marked { std::getenv( "EU07_EDITOR_TREADS" ) != nullptr }; - auto const paint = [&]( material_handle const Material, glm::vec3 const &Colour ) { - auto const tinted { tint_material( Material, Colour ) }; - if( tinted != Material ) { - tints.push_back( tinted ); - } - return tinted; }; - std::vector const unramped; - auto const sink = [&]( bool const Matte ) -> geometryhandle_sequence & { - if( current == null_handle ) { - return Matte ? Cuts : Geometry; - } - if( current == m_material1 ) { - return Matte ? GeometryCut1 : Geometry1; - } - if( eType == tt_Switch && current == m_material2 ) { - return Matte ? GeometryCut2 : Geometry2; - } - auto const tinted { std::find( tints.begin(), tints.end(), current ) != tints.end() }; - auto found { std::find_if( GeometryTreads.begin(), GeometryTreads.end(), [&]( tread_geometry const &Tread ) { return Tread.material == current; } ) }; - if( found == GeometryTreads.end() ) { - found = GeometryTreads.insert( GeometryTreads.end(), { current, tinted ? current : cut_material( current ), {}, {} } ); - } - return Matte && false == tinted ? found->cuts : found->geometry; }; - auto const texturelength = [&]() { - return ( current == null_handle ? Texturelength : texture_length( current ) ); }; - auto const aim = [&]( std::vector const &Spans, double const Middle ) -> rail_tread const * { - current = null_handle; - for( auto const &span : Spans ) { - if( Middle > span.from && Middle < span.to ) { - current = span.material; - return &span; - } - } - return nullptr; - }; - // parts no wheel rolls on go in the matte material of the cut faces - glm::dvec2 bendfrom { 0.0 }; - glm::dvec2 bendto { 0.0 }; - auto const bends = [&]( double const Start, double const End ) { - return glm::dvec2 { - std::abs( Start - bendfrom.x ) < 1e-6 ? bendfrom.y : 0.0, - std::abs( End - bendto.x ) < 1e-6 ? bendto.y : 0.0 }; }; - std::pair> fitfrom, fitto; - std::vector const unfitted; - auto const fitted = [&]( std::pair> const &Fit, double const Distance ) -> std::vector const & { - return ( std::abs( Distance - Fit.first ) < 1e-6 ? Fit.second : unfitted ); }; - auto const span = [&]( double const Start, double const End, knots::line const &Offsets, knots::line const &Extents, knots::line const &Inner ) { - loft_span result; - result.side = Side; - result.halfwidth = halfwidth; - result.texturelength = texturelength(); - result.from = Start; - result.to = End; - result.offsets = Offsets; - result.extents = Extents; - result.inner = Inner; - auto const bent { bends( Start, End ) }; - result.fromend = { bent.x, fitted( fitfrom, Start ) }; - result.toend = { bent.y, fitted( fitto, End ) }; - return result; }; - 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 = {}, bool const Walls = false, float const Lift = 0.f, float const Wallu = -1.f, double const Fade = blend ) { - gfx::vertex_array vertices; - gfx::userdata_array userdata; - auto loft { span( Start, End, Offsets, Extents, Inner ) }; - loft.lift = Lift; - loft.userdata = ( Rolled ? nullptr : &userdata ); - loft.ramp = Ramp; - loft.rampwidth = ( Ramp.empty() ? 0.0 : Fade ); - loft.walls = Walls; - loft.wallu = Wallu; - if( Shape.size() > 1 && segment.RenderLoftSpan( vertices, m_origin, Shape, loft ) ) { - cut_faces( vertices, Shape.size(), Capstart, Capend, Drawing.bank, sink( true ) ); - 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 ] ); - } - } - } - } - sink( false == Rolled ).emplace_back( GfxRenderer->Insert( triangles, Rolled ? empty_userdata : triangleuserdata, Drawing.bank, GL_TRIANGLES ) ); - } - }; - // 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 ); + return marked; +} + +void sort_cuts( std::vector &Cuts ) { + + std::sort( Cuts.begin(), Cuts.end(), []( rail_cut const &A, rail_cut const &B ) { return A.at < B.at; } ); +} + +void add_meetings( knots::line const &A, knots::line const &B, double const Start, double const End, double const Margin, std::vector &Splits ) { + + for( auto const distance : knots::meetings( A, B ) ) { + if( distance > Start + Margin && distance < End - Margin ) { + Splits.push_back( distance ); } } - 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 crossings = []( knots::line const &A, knots::line const &B, double const Start, double const End, double const Margin, std::vector &Splits ) { - for( auto const distance : knots::meetings( A, B ) ) { - if( distance > Start + Margin && distance < End - Margin ) { - Splits.push_back( distance ); - } +} + +std::vector cross_splits( rail_piece const &Whole, glm::dvec2 const &Reach, std::initializer_list const Edges, double const Margin ) { + + std::vector result { Whole.from, Whole.to }; + knots::line const lowline { { Whole.from, Reach.x }, { Whole.to, Reach.x } }; + knots::line const highline { { Whole.from, Reach.y }, { Whole.to, Reach.y } }; + for( auto const *edge : Edges ) { + for( auto const *limit : { &lowline, &highline, &Whole.extents, &Whole.inner } ) { + add_meetings( *edge, *limit, Whole.from, Whole.to, Margin, result ); } - }; - auto const running = [&]( double const Start, double const End, std::vector const &Offsets, std::vector const &Extents, std::vector const &Inner, std::vector const &Rolled, bool const Capstart, bool const Capend ) { - gfx::vertex_array vertices; - if( segment.RenderLoftSpan( vertices, m_origin, Profile, span( Start, End, Offsets, Extents, Inner ) ) ) { - cut_faces( vertices, Profile.size(), Capstart, Capend, Drawing.bank, sink( true ) ); - } - piece( Start, End, Offsets, Extents, false, false, strip( 0, topfirst ), true, Inner ); - piece( Start, End, Offsets, Extents, false, false, strip( toplast, Profile.size() - 1 ), true, Inner ); - 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 ) ); - } - auto const inner { knots::shifted( Rolled, -blend ) }; - std::vector splits { Start, End }; - std::vector const lowline { { Start, low }, { End, low } }; - std::vector const highline { { Start, high }, { End, high } }; - for( auto const *knots : { &Rolled, &inner } ) { - for( auto const *limit : { &lowline, &highline, &Extents, &Inner } ) { - crossings( *knots, *limit, Start, End, 0.0, splits ); - } - } - std::sort( splits.begin(), splits.end() ); - auto const shiny { knots::bound( Extents, inner, true ) }; - auto const blended { knots::bound( Extents, Rolled, true ) }; - for( std::size_t i = 1; i < splits.size(); ++i ) { - auto const middle { 0.5 * ( splits[ i - 1 ] + splits[ i ] ) }; - auto const reach { knots::sample( Rolled, middle, 0.0 ) }; - auto const least { std::max( low, Inner.empty() ? low : knots::sample( Inner, middle, 0.0 ) ) }; - auto const most { std::min( high, Extents.empty() ? high : knots::sample( Extents, middle, 0.0 ) ) }; - auto const shown = [&]( double const Lowest, double const Highest ) { - return Highest > Lowest + 1e-6 && Highest > least && Lowest < most; }; - if( shown( -1e9, reach - blend ) ) { - piece( splits[ i - 1 ], splits[ i ], Offsets, shiny, false, false, top, true, Inner ); - } - auto const plain { current }; - auto const own { current == null_handle ? rail_material( Path ) : current }; - if( shown( reach - blend, reach ) ) { - current = ( marked ? paint( own, { 1.f, 1.f, 0.f } ) : plain ); - piece( splits[ i - 1 ], splits[ i ], Offsets, blended, false, false, top, false, knots::bound( inner, Inner, false ), marked ? unramped : Rolled ); - } - if( shown( reach, 1e9 ) ) { - current = ( marked ? paint( own, { 1.f, 0.f, 1.f } ) : plain ); - piece( splits[ i - 1 ], splits[ i ], Offsets, Extents, false, false, top, false, knots::bound( Rolled, Inner, false ) ); - } - current = plain; - } - }; - auto const treaded = [&]( double const Start, double const End, std::vector const &Offsets, std::vector const &Extents, std::vector const &Inner, bool const Capstart, bool const Capend, rail_tread const &Span ) { - current = null_handle; - gfx::vertex_array vertices; - if( segment.RenderLoftSpan( vertices, m_origin, Profile, span( Start, End, Offsets, Extents, Inner ) ) ) { - cut_faces( vertices, Profile.size(), Capstart, Capend, Drawing.bank, sink( true ) ); - } - piece( Start, End, Offsets, Extents, false, false, strip( 0, topfirst ), true, Inner ); - piece( Start, End, Offsets, Extents, false, false, strip( toplast, Profile.size() - 1 ), true, Inner ); - 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 ) ); - } - auto const &lower { Span.below ? Span.gauge : Span.edge }; - auto const &upper { Span.below ? Span.edge : Span.gauge }; - auto const own { rail_material( Path ) }; - auto const colour { 0.5f * ( tread_colour( Span.material, tread_band() ) + tread_colour( own, tread_band() ) ) }; - auto const tint = [&]( material_handle const Material, glm::vec3 const &Mark ) { - return paint( Material, marked ? Mark : colour ); }; - auto const owntint { tint( own, { 1.f, 0.f, 0.f } ) }; - auto const sharedtint { tint( Span.material, { 0.f, 1.f, 0.f } ) }; - auto const shared { marked ? tint( Span.material, { 0.f, 0.f, 1.f } ) : Span.material }; - auto const ramp = [&]( std::vector const &Knots ) -> std::vector const & { - return marked ? unramped : Knots; }; - auto const centre { knots::middle( lower, upper ) }; - auto const beneath { knots::shifted( lower, -tread_fade ) }; - auto const beyond { knots::shifted( upper, tread_fade ) }; - auto const lowfade { knots::bound( knots::shifted( lower, tread_fade ), centre, true ) }; - auto const highfade { knots::bound( knots::shifted( upper, -tread_fade ), centre, false ) }; - std::vector splits { Start, End }; - std::vector const lowline { { Start, low }, { End, low } }; - std::vector const highline { { Start, high }, { End, high } }; - for( auto const *knots : { &lower, &upper, &lowfade, &highfade, &beneath, &beyond } ) { - for( auto const *limit : { &lowline, &highline, &Extents, &Inner } ) { - crossings( *knots, *limit, Start, End, 0.001, splits ); - } - } - crossings( knots::shifted( lower, 2.0 * tread_fade ), upper, Start, End, 0.001, splits ); - std::sort( splits.begin(), splits.end() ); - for( std::size_t i = 1; i < splits.size(); ++i ) { - auto const a { splits[ i - 1 ] }; - auto const b { splits[ i ] }; - auto const middle { 0.5 * ( a + b ) }; - auto const from { knots::sample( lower, middle, 0.0 ) }; - auto const to { knots::sample( upper, middle, 0.0 ) }; - auto const inner { knots::sample( lowfade, middle, 0.0 ) }; - auto const outer { knots::sample( highfade, middle, 0.0 ) }; - auto const least { std::max( low, Inner.empty() ? low : knots::sample( Inner, middle, 0.0 ) ) }; - auto const most { std::min( high, Extents.empty() ? high : knots::sample( Extents, middle, 0.0 ) ) }; - auto const shown = [&]( double const Lowest, double const Highest ) { - return Highest > Lowest + 1e-6 && Highest > least && Lowest < most; }; - if( shown( -1e9, from - tread_fade ) ) { - current = null_handle; - piece( a, b, Offsets, knots::bound( Extents, beneath, true ), false, false, top, true, Inner ); - } - if( shown( from - tread_fade, from ) ) { - current = owntint; - piece( a, b, Offsets, knots::bound( Extents, lower, true ), false, false, top, false, knots::bound( Inner, beneath, false ), ramp( lower ), false, 0.f, -1.f, tread_fade ); - } - if( shown( from, inner ) ) { - current = sharedtint; - piece( a, b, Offsets, knots::bound( Extents, lowfade, true ), false, false, top, false, knots::bound( Inner, lower, false ), ramp( lower ), false, 0.f, -1.f, -tread_fade ); - } - if( shown( inner, outer ) ) { - current = shared; - piece( a, b, Offsets, knots::bound( Extents, highfade, true ), false, false, top, false == marked, knots::bound( Inner, lowfade, false ) ); - } - if( shown( outer, to ) ) { - current = sharedtint; - piece( a, b, Offsets, knots::bound( Extents, upper, true ), false, false, top, false, knots::bound( Inner, highfade, false ), ramp( upper ), false, 0.f, -1.f, tread_fade ); - } - if( shown( to, to + tread_fade ) ) { - current = owntint; - piece( a, b, Offsets, knots::bound( Extents, beyond, true ), false, false, top, false, knots::bound( Inner, upper, false ), ramp( upper ), false, 0.f, -1.f, -tread_fade ); - } - if( shown( to + tread_fade, 1e9 ) ) { - current = null_handle; - piece( a, b, Offsets, Extents, false, false, top, true, knots::bound( Inner, beyond, false ) ); - } - } - current = null_handle; - }; - 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( result.begin(), result.end() ); + return result; +} + +knots::line flared( double const From, double const To, double const Flare, double const Base, double const Fromdepth, double const Todepth ) { + + knots::line result; + if( Fromdepth > 0.0 ) { + result.insert( result.end(), { { From, Base - Fromdepth }, { From + Flare, Base } } ); + } + else { + result.push_back( { From, Base } ); + } + if( Todepth > 0.0 ) { + result.insert( result.end(), { { To - Flare, Base }, { To, Base - Todepth } } ); + } + else { + result.push_back( { To, Base } ); + } + return result; +} + +knots::line envelope( knots::line const &Lines, bool const Upper, double const A, double const B ) { + + knots::line result; + if( Lines.empty() ) { + return result; + } + 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 ); + } + } + 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 ); + } + result.emplace_back( d, value ); + } + return result; +} + +rail_cut band_cut( rail_band const &Band, glm::dvec2 const &Reach ) { + + rail_cut cut; + cut.at = Band.at; + auto const before { Band.into > 0.0 ? Band.from : Band.to }; + auto const after { Band.into > 0.0 ? Band.to : Band.from }; + auto const meets = [&]( double const Edge, double const Across ) { + return glm::dvec2{ Band.at + ( Edge + Band.facing * Across ) / Band.into, Across }; }; + if( std::abs( Band.facing ) < 1e-3 ) { + cut.end = meets( before, 0.0 ).x; + cut.start = meets( after, 0.0 ).x; + return cut; + } + knots::line endknots { meets( before, Reach.x ), meets( before, Reach.y ) }; + knots::line startknots { meets( after, Reach.x ), meets( after, Reach.y ) }; + knots::sort( endknots ); + knots::sort( startknots ); + cut.end = endknots.back().x; + cut.start = startknots.front().x; + cut.endcap = false; + cut.startcap = false; + auto const outwards { Band.facing / Band.into > 0.0 }; + ( outwards ? cut.endinner : cut.endextents ) = endknots; + ( outwards ? cut.startextents : cut.startinner ) = startknots; + return cut; +} + +rail_cut seam( double const At, double const End, double const Start ) { + + rail_cut cut; + cut.at = At; + cut.end = End; + cut.start = Start; + cut.endcap = false; + cut.startcap = false; + return cut; +} + +knots::line merged_blocks( knots::line Blocks, std::vector const &Cuts ) { + + for( auto const &cut : Cuts ) { + for( auto &block : Blocks ) { + if( cut.at > block.x && cut.at < block.y ) { + block = { std::min( block.x, cut.end ), std::max( block.y, cut.start ) }; } - return knots; }; + } + } + knots::sort( Blocks ); + knots::line result; + for( auto const &block : Blocks ) { + if( false == result.empty() && block.x <= result.back().y ) { + result.back().y = std::max( result.back().y, block.y ); + } + else { + result.push_back( block ); + } + } + return result; +} + +rail_tread const *tread_at( std::vector const &Treads, double const Middle ) { + + for( auto const &tread : Treads ) { + if( Middle > tread.from && Middle < tread.to ) { + return &tread; + } + } + return nullptr; +} + +material_handle tread_material( rail_tread const *Tread ) { + + return ( Tread != nullptr ? Tread->material : null_handle ); +} + +} // namespace + +class TTrack::rail_layer { + +public: +// constructors + rail_layer( TTrack &Track, 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 ); +// methods + bool lay(); + +private: +// methods + template + Type_ const &ours( Type_ const ( &Sides )[ 2 ][ 2 ] ) const { + return Sides[ m_path ][ m_side > 0 ]; } + std::vector const &treads() const { + return ours( m_layout.treads ); } + void lay_guards(); + void lay_checks(); + void lay_fills(); + void lay_cuts( gfx::vertex_array const &Shape, std::vector Cuts, bool const Wheels, std::vector const &Treads, float const Wallu = -1.f ); + void lay_grooved( gfx::vertex_array const &Outline ); + grooved_profile split_grooved( gfx::vertex_array const &Outline ) const; + std::vector top_cuts( gfx::vertex_array const &Top ) const; + void running( rail_piece const &Whole, knots::line const &Rolled, piece_ends const &Ends ); + void treaded( rail_piece Whole, rail_tread const &Tread, piece_ends const &Ends ); + gfx::vertex_array lay_sides( rail_piece const &Whole, piece_ends const &Ends ); + void lay_top( gfx::vertex_array const &Top, rail_piece const &Whole, glm::dvec2 const &Reach, std::vector const &Splits, std::vector const &Bands, std::function( double )> const &Edges, piece_ends const &Ends ); + void piece( gfx::vertex_array const &Shape, rail_piece const &Piece, piece_ends const &Ends = {} ); + loft_span span( rail_piece const &Piece, piece_ends const &Ends ) const; + double texture_length( material_handle const Material ) const; + geometryhandle_sequence &sink( material_handle const Material, bool const Matte ); + material_handle paint( material_handle const Material, glm::vec3 const &Colour ); + glm::dvec2 reaches( gfx::vertex_array const &Shape ) const; + gfx::vertex_array strip( std::size_t const First, std::size_t const Last ) const; +// members + TTrack &m_track; + rail_layout const &m_layout; + rail_drawing const &m_drawing; + int const m_path; + gfx::vertex_array const &m_profile; + int const m_side; + float const m_texturelength; + double const m_from; + bool const m_openstart; + bool const m_openend; + geometryhandle_sequence &m_geometry; + geometryhandle_sequence &m_cuts; + TSegment const &m_segment; + double const m_length; + double const m_guardoffset; + std::size_t m_topfirst; + std::size_t m_toplast { 0 }; + std::vector m_tints; +}; + +TTrack::rail_layer::rail_layer( TTrack &Track, 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 ) : + m_track { Track }, + m_layout { Layout }, + m_drawing { Drawing }, + m_path { Path }, + m_profile { Profile }, + m_side { Side }, + m_texturelength { Texturelength }, + m_from { From }, + m_openstart { Openstart }, + m_openend { Openend }, + m_geometry { Geometry }, + m_cuts { Cuts }, + m_segment { path_segment( Track, Path ) }, + m_length { m_segment.GetLength() }, + m_guardoffset { -( flangeway + Drawing.size.head ) }, + m_topfirst { Profile.size() } { + // 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 + for( std::size_t j = 0; j < Profile.size(); ++j ) { + if( Profile[ j ].position.y > static_cast( -0.087 * Drawing.size.height ) ) { + m_topfirst = std::min( m_topfirst, j ); + m_toplast = std::max( m_toplast, j ); + } + } +} + +bool TTrack::rail_layer::lay() { + + lay_guards(); + lay_checks(); + lay_fills(); + auto const &plan { ours( m_layout.plans ) }; + auto const &bands { ours( m_layout.bands ) }; + if( plan.cuts.empty() && bands.empty() && treads().empty() ) { + return false; + } + auto const &outline { track_rail_profile( m_track.m_profile1.second ) }; + if( false == m_drawing.size.grooved || outline.size() != 2 * m_profile.size() ) { + auto cuts { plan.cuts }; + auto const reach { reaches( m_profile ) }; + for( auto const &band : bands ) { + cuts.push_back( band_cut( band, reach ) ); + } + lay_cuts( m_profile, cuts, true, treads() ); + return true; + } + lay_grooved( outline ); + return true; +} + +void TTrack::rail_layer::lay_guards() { + + auto const &size { m_drawing.size }; + auto const top { flangeway + size.head + 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 + m_guardoffset ) }; + auto const outside { Miter.x + Miter.y * ( top + m_guardoffset ) }; + return Latest ? std::max( inside, outside ) : std::min( inside, outside ); }; + for( auto const &guard : ours( m_layout.guards ) ) { + auto const offsets { flared( guard.from, guard.to, std::min( 0.3, 0.25 * ( guard.to - guard.from ) ), m_guardoffset, guard.flarefrom ? 0.02 : 0.0, guard.flareto ? 0.02 : 0.0 ) }; auto cuts { guard.plan.cuts }; - std::sort( cuts.begin(), cuts.end(), []( rail_cut const &A, rail_cut const &B ) { return A.at < B.at; } ); + sort_cuts( cuts ); auto start { guard.from }; auto startmiter { guard.frommiter }; rail_cut startcut; @@ -544,25 +558,22 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, 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 endcut { i < cuts.size() ? cuts[ i ] : rail_cut {} }; - 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 ) } ) }; + auto const a { std::max( { mitered( startmiter, start, false ), m_from, 0.0 } ) }; + auto const b { std::min( { mitered( endmiter, end, true ), m_length, mitered( guard.tomiter, guard.to, true ) } ) }; if( b - a > 0.01 ) { - std::vector uppers { { 0.0, top } }; - std::vector lowers; + knots::line uppers { { 0.0, top } }; + knots::line 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 ); + ( ending == ( miter.y < 0.0 ) ? uppers : lowers ).emplace_back( 1.0 / miter.y, -miter.x / miter.y - m_guardoffset ); } } auto const glued { a == start && false == startcut.startfit.empty() }; auto const gluedend { b == end && false == endcut.endfit.empty() }; - bendfrom = { a, glued ? startcut.startbend : 0.0 }; - bendto = { b, gluedend ? endcut.endbend : 0.0 }; - fitfrom = { a, glued ? startcut.startfit : unfitted }; - fitto = { b, gluedend ? endcut.endfit : unfitted }; - piece( a, b, offsets, envelope( uppers, true, a, b ), false == glued, false == gluedend, Profile, false, envelope( lowers, false, a, b ) ); - bendfrom = bendto = glm::dvec2 { 0.0 }; - fitfrom = fitto = {}; + piece( + m_profile, + { .from = a, .to = b, .offsets = offsets, .extents = envelope( uppers, true, a, b ), .inner = envelope( lowers, false, a, b ), .capfrom = false == glued, .capto = false == gluedend, .rolled = false }, + { a, b, { glued ? startcut.startbend : 0.0, glued ? startcut.startfit : knots::line {} }, { gluedend ? endcut.endbend : 0.0, gluedend ? endcut.endfit : knots::line {} } } ); } if( i < cuts.size() && cuts[ i ].start > start ) { start = cuts[ i ].start; @@ -571,198 +582,200 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i } } } - 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 ) { +} + +void TTrack::rail_layer::lay_checks() { + + auto const &shape { m_drawing.checkshapes[ m_side > 0 ? 0 : 1 ] }; + auto const &wingends { ours( m_layout.wingends ) }; + for( auto const &check : m_layout.checks[ m_path ][ m_side > 0 ] ) { + auto const replaced { std::any_of( wingends.begin(), wingends.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 ) }; + auto const a { std::max( check.from, m_from ) }; + auto const b { std::min( check.to, m_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 } ); - } - bendfrom = { a, a == from ? check.frombend : 0.0 }; - bendto = { b, b == to ? check.tobend : 0.0 }; - piece( a, b, offsets, {}, a > 0.0 || from >= 0.0, to <= length, shape, false, {} ); - bendfrom = bendto = glm::dvec2 { 0.0 }; + piece( + shape, + { .from = a, .to = b, .offsets = flared( check.from, check.to, std::min( 0.6, 0.4 * ( check.to - check.from ) ), 0.0, check.flarefrom, check.flareto ), .capfrom = a > 0.0 || check.from >= 0.0, .capto = check.to <= m_length, .rolled = false }, + { a, b, { a == check.from ? check.frombend : 0.0 }, { b == check.to ? check.tobend : 0.0 } } ); } - for( auto const &fill : Layout.fills[ Path ][ Side > 0 ] ) { - auto const a { std::max( fill.from, From ) }; - auto const b { std::min( fill.to, length ) }; +} + +void TTrack::rail_layer::lay_fills() { + + for( auto const &fill : ours( m_layout.fills ) ) { + auto const a { std::max( fill.from, m_from ) }; + auto const b { std::min( fill.to, m_length ) }; if( b - a > 0.01 ) { - current = null_handle; - if( false == fill.web ) { - aim( treads, 0.5 * ( a + b ) ); - } - auto const &shapes { fill.web ? Drawing.fillers : Drawing.plates }; - piece( a, b, {}, fill.outer, a == fill.from && fill.capfrom, b == fill.to && fill.capto, shapes[ Side > 0 ? 0 : 1 ], true, fill.inner, {}, true, static_cast( fill.lift ) ); - current = null_handle; + auto const &shapes { fill.web ? m_drawing.fillers : m_drawing.plates }; + piece( + shapes[ m_side > 0 ? 0 : 1 ], + { .material = ( fill.web ? null_handle : tread_material( tread_at( treads(), 0.5 * ( a + b ) ) ) ), .from = a, .to = b, .extents = fill.outer, .inner = fill.inner, .capfrom = a == fill.from && fill.capfrom, .capto = b == fill.to && fill.capto, .walls = true, .lift = static_cast( fill.lift ) } ); } } - auto const &bands { Layout.bands[ Path ][ Side > 0 ] }; - if( plan.cuts.empty() && bands.empty() && treads.empty() ) { - return false; +} + +void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vector Cuts, bool const Wheels, std::vector const &Treads, float const Wallu ) { + + sort_cuts( Cuts ); + auto start { -1e9 }; + auto startcap { true }; + auto startbend { 0.0 }; + knots::line startfit; + knots::line rolledfrom; + knots::line offsets, extents, inner; + 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 }; + auto pieceinner { inner }; + 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() ); + pieceinner.insert( pieceinner.end(), Cuts[ i ].endinner.begin(), Cuts[ i ].endinner.end() ); + } + knots::sort( pieceoffsets ); + knots::sort( pieceextents ); + knots::sort( pieceinner ); + auto const a { std::max( start, m_from ) }; + auto const b { std::min( end, m_length ) }; + auto const rolled { i < Cuts.size() ? knots::bound( rolledfrom, Cuts[ i ].endrolled, true ) : rolledfrom }; + auto const capstart { start > m_from ? startcap : m_openstart }; + auto const capend { end < m_length ? Cuts[ i ].endcap : m_openend }; + piece_ends const ends { + a, b, + { start > m_from ? startbend : 0.0, start > m_from ? startfit : knots::line {} }, + { end < m_length ? Cuts[ i ].endbend : 0.0, end < m_length ? Cuts[ i ].endfit : knots::line {} } }; + if( b - a > 0.01 ) { + std::vector active; + for( auto const &tread : Treads ) { + if( tread.anchor == 0 + || ( tread.anchor > 0 && b > tread.from + 0.01 && b < tread.to + 0.01 ) + || ( tread.anchor < 0 && a > tread.from - 0.01 && a < tread.to - 0.01 ) ) { + active.push_back( tread ); + } + } + std::vector splits; + for( auto const &tread : active ) { + for( auto const edge : { tread.from, tread.to } ) { + if( edge > a + 0.01 && edge < b - 0.01 ) { + splits.push_back( edge ); + } + } + } + std::sort( splits.begin(), splits.end() ); + splits.erase( std::unique( splits.begin(), splits.end(), []( double const A, double const B ) { return B - A < 0.002; } ), splits.end() ); + splits.insert( splits.begin(), a ); + splits.push_back( b ); + for( std::size_t k = 1; k < splits.size(); ++k ) { + auto const *tread { tread_at( active, 0.5 * ( splits[ k - 1 ] + splits[ k ] ) ) }; + rail_piece const whole { + .material = tread_material( tread ), + .from = splits[ k - 1 ], + .to = splits[ k ], + .offsets = pieceoffsets, + .extents = pieceextents, + .inner = pieceinner, + .capfrom = k == 1 && capstart, + .capto = k + 1 == splits.size() && capend, + .walls = false == Wheels, + .wallu = Wallu }; + if( Wheels && tread != nullptr && false == tread->edge.empty() && m_topfirst < m_toplast ) { + treaded( whole, *tread, ends ); + } + else if( false == Wheels || rolled.empty() || m_topfirst >= m_toplast ) { + piece( Shape, whole, ends ); + } + else { + running( whole, rolled, ends ); + } + } + } + if( i < Cuts.size() ) { + rolledfrom = Cuts[ i ].startrolled; + start = Cuts[ i ].start; + startcap = Cuts[ i ].startcap; + startbend = Cuts[ i ].startbend; + startfit = Cuts[ i ].startfit; + offsets = Cuts[ i ].startoffsets; + extents = Cuts[ i ].startextents; + inner = Cuts[ i ].startinner; + } } - auto const byposition = []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; }; - auto const reaches = [&]( gfx::vertex_array const &Shape ) { - glm::dvec2 result { 1e9, -1e9 }; - for( auto const &point : Shape ) { - auto const reach { static_cast( Side * point.position.x - halfwidth ) }; - result = { std::min( result.x, reach ), std::max( result.y, reach ) }; +} + +void TTrack::rail_layer::lay_grooved( gfx::vertex_array const &Outline ) { + + auto const grooved { split_grooved( Outline ) }; + auto const &bands { ours( m_layout.bands ) }; + auto wholecuts { ours( m_layout.plans ).cuts }; + auto const bodyreach { reaches( grooved.body ) }; + for( auto const &band : bands ) { + if( band.whole ) { + wholecuts.push_back( band_cut( band, bodyreach ) ); } - return result; }; - auto const band_cut = [&]( rail_band const &Band, glm::dvec2 const &Reach ) { - rail_cut cut; - cut.at = Band.at; - auto const before { Band.into > 0.0 ? Band.from : Band.to }; - auto const after { Band.into > 0.0 ? Band.to : Band.from }; - auto const meets = [&]( double const Edge, double const Reach ) { - return glm::dvec2{ Band.at + ( Edge + Band.facing * Reach ) / Band.into, Reach }; }; - if( std::abs( Band.facing ) < 1e-3 ) { - cut.end = meets( before, 0.0 ).x; - cut.start = meets( after, 0.0 ).x; - return cut; - } - std::vector endknots { meets( before, Reach.x ), meets( before, Reach.y ) }; - std::vector startknots { meets( after, Reach.x ), meets( after, Reach.y ) }; - std::sort( endknots.begin(), endknots.end(), byposition ); - std::sort( startknots.begin(), startknots.end(), byposition ); - cut.end = endknots.back().x; - cut.start = startknots.front().x; - cut.endcap = false; - cut.startcap = false; - auto const outwards { Band.facing / Band.into > 0.0 }; - ( outwards ? cut.endinner : cut.endextents ) = endknots; - ( outwards ? cut.startextents : cut.startinner ) = startknots; - return cut; }; - auto const lay = [&]( gfx::vertex_array const &Shape, std::vector cuts, bool const Wheels, std::vector const &Spans, float const Wallu = -1.f ) { - std::sort( cuts.begin(), cuts.end(), []( rail_cut const &A, rail_cut const &B ) { return A.at < B.at; } ); - auto start { -1e9 }; - auto startcap { true }; - auto startbend { 0.0 }; - std::vector startfit; - std::vector rolledfrom; - std::vector offsets, extents, inner; - 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 }; - auto pieceinner { inner }; - 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() ); - pieceinner.insert( pieceinner.end(), cuts[ i ].endinner.begin(), cuts[ i ].endinner.end() ); - } - std::sort( pieceoffsets.begin(), pieceoffsets.end(), byposition ); - std::sort( pieceextents.begin(), pieceextents.end(), byposition ); - std::sort( pieceinner.begin(), pieceinner.end(), byposition ); - auto const a { std::max( start, From ) }; - auto const b { std::min( end, length ) }; - auto const rolled { i < cuts.size() ? knots::bound( rolledfrom, cuts[ i ].endrolled, true ) : rolledfrom }; - auto const capstart { start > From ? startcap : Openstart }; - auto const capend { end < length ? cuts[ i ].endcap : Openend }; - bendfrom = { a, start > From ? startbend : 0.0 }; - bendto = { b, end < length ? cuts[ i ].endbend : 0.0 }; - fitfrom = { a, start > From ? startfit : unfitted }; - fitto = { b, end < length ? cuts[ i ].endfit : unfitted }; - if( b - a > 0.01 ) { - std::vector active; - for( auto const &span : Spans ) { - if( span.anchor == 0 - || ( span.anchor > 0 && b > span.from + 0.01 && b < span.to + 0.01 ) - || ( span.anchor < 0 && a > span.from - 0.01 && a < span.to - 0.01 ) ) { - active.push_back( span ); - } - } - std::vector splits; - for( auto const &span : active ) { - for( auto const edge : { span.from, span.to } ) { - if( edge > a + 0.01 && edge < b - 0.01 ) { - splits.push_back( edge ); - } - } - } - std::sort( splits.begin(), splits.end() ); - splits.erase( std::unique( splits.begin(), splits.end(), []( double const A, double const B ) { return B - A < 0.002; } ), splits.end() ); - splits.insert( splits.begin(), a ); - splits.push_back( b ); - for( std::size_t k = 1; k < splits.size(); ++k ) { - auto const first { k == 1 }; - auto const last { k + 1 == splits.size() }; - auto const *span { aim( active, 0.5 * ( splits[ k - 1 ] + splits[ k ] ) ) }; - if( Wheels && span != nullptr && false == span->edge.empty() && topfirst < toplast ) { - treaded( splits[ k - 1 ], splits[ k ], pieceoffsets, pieceextents, pieceinner, first && capstart, last && capend, *span ); - } - else if( false == Wheels || rolled.empty() || topfirst >= toplast ) { - piece( splits[ k - 1 ], splits[ k ], pieceoffsets, pieceextents, first && capstart, last && capend, Shape, true, pieceinner, {}, false == Wheels, 0.f, Wallu ); - } - else { - running( splits[ k - 1 ], splits[ k ], pieceoffsets, pieceextents, pieceinner, rolled, first && capstart, last && capend ); - } - } - current = null_handle; - } - bendfrom = bendto = glm::dvec2 { 0.0 }; - fitfrom = fitto = {}; - if( i < cuts.size() ) { - rolledfrom = cuts[ i ].startrolled; - start = cuts[ i ].start; - startcap = cuts[ i ].startcap; - startbend = cuts[ i ].startbend; - startfit = cuts[ i ].startfit; - offsets = cuts[ i ].startoffsets; - extents = cuts[ i ].startextents; - inner = cuts[ i ].startinner; - } - } - }; - auto const &outline { track_rail_profile( m_profile1.second ) }; - auto const count { Profile.size() }; - if( false == size.grooved || outline.size() != 2 * count ) { - auto cuts { plan.cuts }; - auto const reach { reaches( Profile ) }; - for( auto const &band : bands ) { - cuts.push_back( band_cut( band, reach ) ); - } - lay( Profile, cuts, true, treads ); - return true; } + lay_cuts( grooved.body, wholecuts, false, {} ); + for( std::size_t k = 0; k < grooved.tops.size(); ++k ) { + auto const cuts { top_cuts( grooved.tops[ k ] ) }; + auto const merged { merged_blocks( ours( m_layout.blocks ), cuts ) }; + if( merged.empty() ) { + lay_cuts( grooved.tops[ k ], cuts, false, treads(), grooved.wallu ); + continue; + } + std::vector blocktreads; + for( auto const &span : merged ) { + for( auto const &tread : treads() ) { + if( tread.from < span.y && tread.to > span.x ) { + blocktreads.push_back( { span.x, span.y, tread.material } ); + break; + } + } + } + std::vector outside, inside; + for( auto const &cut : cuts ) { + auto const within { std::any_of( merged.begin(), merged.end(), [&]( glm::dvec2 const &Span ) { return cut.at > Span.x && cut.at < Span.y; } ) }; + ( within ? inside : outside ).push_back( cut ); + } + inside.push_back( seam( merged.front().x - 0.001, -1e9, merged.front().x ) ); + for( std::size_t m = 0; m < merged.size(); ++m ) { + outside.push_back( seam( 0.5 * ( merged[ m ].x + merged[ m ].y ), merged[ m ].x, merged[ m ].y ) ); + inside.push_back( m + 1 < merged.size() ? + seam( 0.5 * ( merged[ m ].y + merged[ m + 1 ].x ), merged[ m ].y, merged[ m + 1 ].x ) : + seam( merged[ m ].y + 0.001, merged[ m ].y, 1e9 ) ); + } + lay_cuts( grooved.tops[ k ], outside, false, treads(), grooved.wallu ); + lay_cuts( grooved.flats[ k ], inside, false, blocktreads, grooved.wallu ); + } +} + +grooved_profile TTrack::rail_layer::split_grooved( gfx::vertex_array const &Outline ) const { + + auto const &size { m_drawing.size }; + auto const count { m_profile.size() }; auto const height = [&]( std::size_t const Index ) { - return static_cast( outline[ Side > 0 ? Index : count - 1 - Index ].position.y ) - size.groove; }; + return static_cast( Outline[ m_side > 0 ? Index : count - 1 - Index ].position.y ) - size.groove; }; auto const above = [&]( std::size_t const Index ) { return height( Index ) > 1e-4; }; auto const floorpoint = [&]( std::size_t const A, std::size_t const B ) { auto const t { static_cast( height( A ) / ( height( A ) - height( B ) ) ) }; - auto point { Profile[ above( A ) ? A : B ] }; - point.position = glm::mix( Profile[ A ].position, Profile[ B ].position, t ); - point.texture = glm::mix( Profile[ A ].texture, Profile[ B ].texture, t ); + auto point { m_profile[ above( A ) ? A : B ] }; + point.position = glm::mix( m_profile[ A ].position, m_profile[ B ].position, t ); + point.texture = glm::mix( m_profile[ A ].texture, m_profile[ B ].texture, t ); return point; }; auto const flattened = [&]( std::size_t const Index ) { - auto point { Profile[ Index ] }; - auto const &file { outline[ Side > 0 ? Index : count - 1 - Index ].position }; + auto point { m_profile[ Index ] }; + auto const &file { Outline[ m_side > 0 ? Index : count - 1 - Index ].position }; if( file.y > size.crest - 0.0005 || ( file.x < -0.02 && file.y > size.liptop - 0.0005 ) ) { point.normal = { 0.f, 1.f, 0.f }; - point.texture.x = Drawing.platetexture; + point.texture.x = m_drawing.platetexture; } return point; }; - std::vector tops, flats; - gfx::vertex_array body; + grooved_profile result; + auto &body { result.body }; std::vector floors; auto floortexture { 0.f }; auto floortextures { 0 }; @@ -770,10 +783,10 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i if( false == above( i ) ) { if( height( i ) > -1e-4 ) { floors.push_back( body.size() ); - floortexture += Profile[ i ].texture.x; + floortexture += m_profile[ i ].texture.x; ++floortextures; } - body.push_back( Profile[ i ] ); + body.push_back( m_profile[ i ] ); continue; } auto const first { i }; @@ -783,7 +796,7 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i gfx::vertex_array top, flat; if( first > 0 ) { if( height( first - 1 ) > -1e-4 ) { - top.push_back( Profile[ first - 1 ] ); + top.push_back( m_profile[ first - 1 ] ); flat.push_back( flattened( first - 1 ) ); } else { @@ -795,12 +808,12 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i } } for( auto k { first }; k <= i; ++k ) { - top.push_back( Profile[ k ] ); + top.push_back( m_profile[ k ] ); flat.push_back( flattened( k ) ); } if( i + 1 < count ) { if( height( i + 1 ) > -1e-4 ) { - top.push_back( Profile[ i + 1 ] ); + top.push_back( m_profile[ i + 1 ] ); flat.push_back( flattened( i + 1 ) ); } else { @@ -819,8 +832,8 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i doubled.insert( doubled.end(), 2, strip->back() ); *strip = doubled; } - tops.push_back( top ); - flats.push_back( flat ); + result.tops.push_back( top ); + result.flats.push_back( flat ); } if( floors.size() > 1 ) { auto const across { body[ floors.back() ].position - body[ floors.front() ].position }; @@ -835,95 +848,241 @@ bool TTrack::lay_rail( rail_layout const &Layout, rail_drawing const &Drawing, i } } } - std::vector wholecuts { plan.cuts }; - auto const bodyreach { reaches( body ) }; - for( auto const &band : bands ) { - if( band.whole ) { - wholecuts.push_back( band_cut( band, bodyreach ) ); - } - } - lay( body, wholecuts, false, {} ); - auto const wallu { floortextures > 0 ? floortexture / floortextures : -1.f }; - for( std::size_t k = 0; k < tops.size(); ++k ) { - auto const reach { reaches( tops[ k ] ) }; - auto const head { reach.x + reach.y > 0.0 }; - auto edge { head ? 1e9 : -1e9 }; - for( auto const &point : tops[ k ] ) { - if( point.position.y > ( head ? size.crest : size.liptop ) - 0.0005 ) { - auto const across { static_cast( Side * point.position.x - halfwidth ) }; - edge = ( head ? std::min( edge, across ) : std::max( edge, across ) ); - } - } - std::vector cuts { plan.cuts }; - for( auto const &band : bands ) { - auto cut { band_cut( band, reach ) }; - if( false == band.whole && std::abs( band.facing ) >= 1e-3 && std::abs( edge ) < 1.0 ) { - auto const blunt { edge + ( head ? nose_tip : -nose_tip ) }; - if( head == ( band.facing / band.into > 0.0 ) ) { - cut.start = std::max( cut.start, band.at + ( ( band.into > 0.0 ? band.to : band.from ) + band.facing * blunt ) / band.into ); - cut.startcap = true; - } - else { - cut.end = std::min( cut.end, band.at + ( ( band.into > 0.0 ? band.from : band.to ) + band.facing * blunt ) / band.into ); - cut.endcap = true; - } - } - cuts.push_back( cut ); - } - std::vector spans { Layout.blocks[ Path ][ Side > 0 ] }; - for( auto const &cut : cuts ) { - for( auto &span : spans ) { - if( cut.at > span.x && cut.at < span.y ) { - span = { std::min( span.x, cut.end ), std::max( span.y, cut.start ) }; - } - } - } - std::sort( spans.begin(), spans.end(), byposition ); - std::vector merged; - for( auto const &span : spans ) { - if( false == merged.empty() && span.x <= merged.back().y ) { - merged.back().y = std::max( merged.back().y, span.y ); - } - else { - merged.push_back( span ); - } - } - if( merged.empty() ) { - lay( tops[ k ], cuts, false, treads, wallu ); - continue; - } - std::vector blocktreads; - for( auto const &span : merged ) { - for( auto const &tread : treads ) { - if( tread.from < span.y && tread.to > span.x ) { - blocktreads.push_back( { span.x, span.y, tread.material } ); - break; - } - } - } - auto const seam = []( double const At, double const End, double const Start ) { - rail_cut cut; - cut.at = At; - cut.end = End; - cut.start = Start; - cut.endcap = false; - cut.startcap = false; - return cut; }; - std::vector outside, inside; - for( auto const &cut : cuts ) { - auto const within { std::any_of( merged.begin(), merged.end(), [&]( glm::dvec2 const &Span ) { return cut.at > Span.x && cut.at < Span.y; } ) }; - ( within ? inside : outside ).push_back( cut ); - } - inside.push_back( seam( merged.front().x - 0.001, -1e9, merged.front().x ) ); - for( std::size_t m = 0; m < merged.size(); ++m ) { - outside.push_back( seam( 0.5 * ( merged[ m ].x + merged[ m ].y ), merged[ m ].x, merged[ m ].y ) ); - inside.push_back( m + 1 < merged.size() ? - seam( 0.5 * ( merged[ m ].y + merged[ m + 1 ].x ), merged[ m ].y, merged[ m + 1 ].x ) : - seam( merged[ m ].y + 0.001, merged[ m ].y, 1e9 ) ); - } - lay( tops[ k ], outside, false, treads, wallu ); - lay( flats[ k ], inside, false, blocktreads, wallu ); - } - return true; + result.wallu = ( floortextures > 0 ? floortexture / floortextures : -1.f ); + return result; } +std::vector TTrack::rail_layer::top_cuts( gfx::vertex_array const &Top ) const { + + auto const &size { m_drawing.size }; + auto const reach { reaches( Top ) }; + auto const head { reach.x + reach.y > 0.0 }; + auto edge { head ? 1e9 : -1e9 }; + for( auto const &point : Top ) { + if( point.position.y > ( head ? size.crest : size.liptop ) - 0.0005 ) { + auto const across { static_cast( m_side * point.position.x - m_drawing.halfwidth ) }; + edge = ( head ? std::min( edge, across ) : std::max( edge, across ) ); + } + } + auto cuts { ours( m_layout.plans ).cuts }; + for( auto const &band : ours( m_layout.bands ) ) { + auto cut { band_cut( band, reach ) }; + if( false == band.whole && std::abs( band.facing ) >= 1e-3 && std::abs( edge ) < 1.0 ) { + auto const blunt { edge + ( head ? nose_tip : -nose_tip ) }; + if( head == ( band.facing / band.into > 0.0 ) ) { + cut.start = std::max( cut.start, band.at + ( ( band.into > 0.0 ? band.to : band.from ) + band.facing * blunt ) / band.into ); + cut.startcap = true; + } + else { + cut.end = std::min( cut.end, band.at + ( ( band.into > 0.0 ? band.from : band.to ) + band.facing * blunt ) / band.into ); + cut.endcap = true; + } + } + cuts.push_back( cut ); + } + return cuts; +} + +void TTrack::rail_layer::running( rail_piece const &Whole, knots::line const &Rolled, piece_ends const &Ends ) { + + auto const top { lay_sides( Whole, Ends ) }; + auto const reach { reaches( top ) }; + auto const inner { knots::shifted( Rolled, -blend ) }; + auto const splits { cross_splits( Whole, reach, { &Rolled, &inner }, 0.0 ) }; + auto const marked { treads_marked() }; + auto const own { Whole.material == null_handle ? m_track.rail_material( m_path ) : Whole.material }; + lay_top( + top, Whole, reach, splits, + { + { Whole.material, knots::bound( Whole.extents, inner, true ), Whole.inner }, + { marked ? paint( own, { 1.f, 1.f, 0.f } ) : Whole.material, knots::bound( Whole.extents, Rolled, true ), knots::bound( inner, Whole.inner, false ), false, marked ? knots::line {} : Rolled }, + { marked ? paint( own, { 1.f, 0.f, 1.f } ) : Whole.material, Whole.extents, knots::bound( Rolled, Whole.inner, false ), false } }, + [&]( double const Middle ) { + auto const rolled { knots::sample( Rolled, Middle, 0.0 ) }; + return std::vector { rolled - blend, rolled }; }, + Ends ); +} + +void TTrack::rail_layer::treaded( rail_piece Whole, rail_tread const &Tread, piece_ends const &Ends ) { + + Whole.material = null_handle; + auto const top { lay_sides( Whole, Ends ) }; + auto const reach { reaches( top ) }; + auto const &lower { Tread.below ? Tread.gauge : Tread.edge }; + auto const &upper { Tread.below ? Tread.edge : Tread.gauge }; + auto const marked { treads_marked() }; + auto const own { m_track.rail_material( m_path ) }; + auto const colour { 0.5f * ( tread_colour( Tread.material, m_track.tread_band() ) + tread_colour( own, m_track.tread_band() ) ) }; + auto const tint = [&]( material_handle const Material, glm::vec3 const &Mark ) { + return paint( Material, marked ? Mark : colour ); }; + auto const owntint { tint( own, { 1.f, 0.f, 0.f } ) }; + auto const sharedtint { tint( Tread.material, { 0.f, 1.f, 0.f } ) }; + auto const shared { marked ? tint( Tread.material, { 0.f, 0.f, 1.f } ) : Tread.material }; + auto const ramp = [&]( knots::line const &Knots ) { + return marked ? knots::line {} : Knots; }; + auto const centre { knots::middle( lower, upper ) }; + auto const beneath { knots::shifted( lower, -tread_fade ) }; + auto const beyond { knots::shifted( upper, tread_fade ) }; + auto const lowfade { knots::bound( knots::shifted( lower, tread_fade ), centre, true ) }; + auto const highfade { knots::bound( knots::shifted( upper, -tread_fade ), centre, false ) }; + auto splits { cross_splits( Whole, reach, { &lower, &upper, &lowfade, &highfade, &beneath, &beyond }, 0.001 ) }; + add_meetings( knots::shifted( lower, 2.0 * tread_fade ), upper, Whole.from, Whole.to, 0.001, splits ); + std::sort( splits.begin(), splits.end() ); + lay_top( + top, Whole, reach, splits, + { + { null_handle, knots::bound( Whole.extents, beneath, true ), Whole.inner }, + { owntint, knots::bound( Whole.extents, lower, true ), knots::bound( Whole.inner, beneath, false ), false, ramp( lower ), tread_fade }, + { sharedtint, knots::bound( Whole.extents, lowfade, true ), knots::bound( Whole.inner, lower, false ), false, ramp( lower ), -tread_fade }, + { shared, knots::bound( Whole.extents, highfade, true ), knots::bound( Whole.inner, lowfade, false ), false == marked }, + { sharedtint, knots::bound( Whole.extents, upper, true ), knots::bound( Whole.inner, highfade, false ), false, ramp( upper ), tread_fade }, + { owntint, knots::bound( Whole.extents, beyond, true ), knots::bound( Whole.inner, upper, false ), false, ramp( upper ), -tread_fade }, + { null_handle, Whole.extents, knots::bound( Whole.inner, beyond, false ) } }, + [&]( double const Middle ) { + auto const from { knots::sample( lower, Middle, 0.0 ) }; + auto const to { knots::sample( upper, Middle, 0.0 ) }; + return std::vector { from - tread_fade, from, knots::sample( lowfade, Middle, 0.0 ), knots::sample( highfade, Middle, 0.0 ), to, to + tread_fade }; }, + Ends ); +} + +gfx::vertex_array TTrack::rail_layer::lay_sides( rail_piece const &Whole, piece_ends const &Ends ) { + + gfx::vertex_array vertices; + if( m_segment.RenderLoftSpan( vertices, m_track.m_origin, m_profile, span( Whole, Ends ) ) ) { + cut_faces( vertices, m_profile.size(), Whole.capfrom, Whole.capto, m_drawing.bank, sink( Whole.material, true ) ); + } + auto side { Whole }; + side.capfrom = false; + side.capto = false; + piece( strip( 0, m_topfirst ), side, Ends ); + piece( strip( m_toplast, m_profile.size() - 1 ), side, Ends ); + return strip( m_topfirst, m_toplast ); +} + +void TTrack::rail_layer::lay_top( gfx::vertex_array const &Top, rail_piece const &Whole, glm::dvec2 const &Reach, std::vector const &Splits, std::vector const &Bands, std::function( double )> const &Edges, piece_ends const &Ends ) { + + for( std::size_t i = 1; i < Splits.size(); ++i ) { + auto const middle { 0.5 * ( Splits[ i - 1 ] + Splits[ i ] ) }; + auto const least { std::max( Reach.x, knots::sample( Whole.inner, middle, Reach.x ) ) }; + auto const most { std::min( Reach.y, knots::sample( Whole.extents, middle, Reach.y ) ) }; + auto edges { Edges( middle ) }; + edges.insert( edges.begin(), -1e9 ); + edges.push_back( 1e9 ); + for( std::size_t k = 0; k < Bands.size(); ++k ) { + auto const &band { Bands[ k ] }; + if( edges[ k + 1 ] > edges[ k ] + 1e-6 && edges[ k + 1 ] > least && edges[ k ] < most ) { + piece( + Top, + { .material = band.material, .from = Splits[ i - 1 ], .to = Splits[ i ], .offsets = Whole.offsets, .extents = band.extents, .inner = band.inner, .rolled = band.rolled, .ramp = band.ramp, .fade = band.fade }, + Ends ); + } + } + } +} + +void TTrack::rail_layer::piece( gfx::vertex_array const &Shape, rail_piece const &Piece, piece_ends const &Ends ) { + + gfx::vertex_array vertices; + gfx::userdata_array userdata; + auto loft { span( Piece, Ends ) }; + loft.userdata = ( Piece.rolled ? nullptr : &userdata ); + if( Shape.size() < 2 || false == m_segment.RenderLoftSpan( vertices, m_track.m_origin, Shape, loft ) ) { + return; + } + cut_faces( vertices, Shape.size(), Piece.capfrom, Piece.capto, m_drawing.bank, sink( Piece.material, true ) ); + 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 == Piece.rolled ) { + triangleuserdata.push_back( userdata[ vertex ] ); + } + } + } + } + sink( Piece.material, false == Piece.rolled ).emplace_back( GfxRenderer->Insert( triangles, triangleuserdata, m_drawing.bank, GL_TRIANGLES ) ); +} + +loft_span TTrack::rail_layer::span( rail_piece const &Piece, piece_ends const &Ends ) const { + + loft_span result; + result.side = m_side; + result.halfwidth = m_drawing.halfwidth; + result.texturelength = texture_length( Piece.material ); + result.from = Piece.from; + result.to = Piece.to; + result.offsets = Piece.offsets; + result.extents = Piece.extents; + result.inner = Piece.inner; + result.lift = Piece.lift; + result.ramp = Piece.ramp; + result.rampwidth = ( Piece.ramp.empty() ? 0.0 : Piece.fade ); + result.walls = Piece.walls; + result.wallu = Piece.wallu; + if( std::abs( Piece.from - Ends.from ) < 1e-6 ) { + result.fromend = Ends.fromend; + } + if( std::abs( Piece.to - Ends.to ) < 1e-6 ) { + result.toend = Ends.toend; + } + return result; +} + +double TTrack::rail_layer::texture_length( material_handle const Material ) const { + + return ( Material == null_handle ? m_texturelength : m_track.texture_length( Material ) ); +} + +// parts no wheel rolls on go in the matte material of the cut faces +TTrack::geometryhandle_sequence &TTrack::rail_layer::sink( material_handle const Material, bool const Matte ) { + + if( Material == null_handle ) { + return Matte ? m_cuts : m_geometry; + } + if( Material == m_track.m_material1 ) { + return Matte ? m_track.GeometryCut1 : m_track.Geometry1; + } + if( m_track.eType == tt_Switch && Material == m_track.m_material2 ) { + return Matte ? m_track.GeometryCut2 : m_track.Geometry2; + } + auto const tinted { std::find( m_tints.begin(), m_tints.end(), Material ) != m_tints.end() }; + auto &treads { m_track.GeometryTreads }; + auto found { std::find_if( treads.begin(), treads.end(), [&]( tread_geometry const &Tread ) { return Tread.material == Material; } ) }; + if( found == treads.end() ) { + found = treads.insert( treads.end(), { Material, tinted ? Material : cut_material( Material ), {}, {} } ); + } + return Matte && false == tinted ? found->cuts : found->geometry; +} + +material_handle TTrack::rail_layer::paint( material_handle const Material, glm::vec3 const &Colour ) { + + auto const tinted { tint_material( Material, Colour ) }; + if( tinted != Material ) { + m_tints.push_back( tinted ); + } + return tinted; +} + +glm::dvec2 TTrack::rail_layer::reaches( gfx::vertex_array const &Shape ) const { + + glm::dvec2 result { 1e9, -1e9 }; + for( auto const &point : Shape ) { + auto const reach { static_cast( m_side * point.position.x - m_drawing.halfwidth ) }; + result = { std::min( result.x, reach ), std::max( result.y, reach ) }; + } + return result; +} + +gfx::vertex_array TTrack::rail_layer::strip( std::size_t const First, std::size_t const Last ) const { + + return gfx::vertex_array( m_profile.begin() + First, m_profile.begin() + Last + 1 ); +} + +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 ) { + + return rail_layer( *this, Layout, Drawing, Path, Profile, Side, Texturelength, From, Openstart, Openend, Geometry, Cuts ).lay(); +} diff --git a/world/Track.h b/world/Track.h index 8d740251..f6ba32b8 100644 --- a/world/Track.h +++ b/world/Track.h @@ -412,6 +412,7 @@ private: 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( track_rails::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 ); + class rail_layer; int blade_side( int const Path ) const; material_handle rail_material( int const Path ) const; glm::vec2 tread_band() const; From 569194ceae747ad08fdc13c1c287946ac893299f Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 09:48:12 +0200 Subject: [PATCH 07/19] refactor(track): the rails are planned by the class TTrack::rail_planner instead of one function: each crossing is added by its kind (grooved, wide, acute, obtuse), the acute frog of a crossing and of a switch is one acute_frog and the vee of grooved rails one add_vee, the switch frog is described as a rail_crossing, the corners of crossings at a middle angle are kept as rail_crossing instead of a copy of its fields and framed by frame_guard, wide() takes the crossing and a wide_cut value instead of ten arguments, and the knots are sorted by knots::sort --- world/RailPlan.cpp | 661 +++++++++++++++++++++++++-------------------- world/Track.h | 1 + 2 files changed, 376 insertions(+), 286 deletions(-) diff --git a/world/RailPlan.cpp b/world/RailPlan.cpp index cb5a4fbf..2bae47c6 100644 --- a/world/RailPlan.cpp +++ b/world/RailPlan.cpp @@ -55,7 +55,7 @@ void acute( track_rails::rail_plan &Plan, double const At, double const Toward, 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; } ); + knots::sort( rolled ); track_rails::rail_cut cut; cut.at = At; if( Toward < 0.0 ) { @@ -100,29 +100,41 @@ void obtuse( track_rails::rail_plan &Plan, double const At, double const Into, t } // 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( track_rails::rail_plan &Plan, double const At, double const Into, double const Facing, double const From, double const To, double const Inner, double const Outer, std::vector const &Fromline = {}, std::vector const &Toline = {} ) { +struct wide_cut { + double from { 0.0 }; + double to { 0.0 }; + double inner { 0.0 }; + double outer { 0.0 }; + knots::line fromline; + knots::line toline; +}; - 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 }; +void wide( track_rails::rail_plan &Plan, track_rails::rail_crossing const &Crossing, wide_cut const &Cut ) { + + auto const at { Crossing.at }; + auto const into { Crossing.into }; + auto const facing { Crossing.facing }; + auto const low { ( Cut.from + std::min( Cut.inner * facing, Cut.outer * facing ) - 0.005 ) / into }; + auto const high { ( Cut.to + std::max( Cut.inner * facing, Cut.outer * facing ) + 0.005 ) / into }; track_rails::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 }; - auto const glue = [&]( std::vector const &Line, double const Edge, double &Position, glm::dvec2 &Miter, double &Bend, std::vector &Fit, bool &Cap ) { + cut.at = at; + cut.end = at + std::min( low, high ); + cut.start = at + std::max( low, high ); + cut.endmiter = { at + ( into > 0.0 ? Cut.from - 0.005 : Cut.to + 0.005 ) / into, facing / into }; + cut.startmiter = { at + ( into > 0.0 ? Cut.to + 0.005 : Cut.from - 0.005 ) / into, facing / into }; + auto const glue = [&]( knots::line const &Line, double const Edge, double &Position, glm::dvec2 &Miter, double &Bend, knots::line &Fit, bool &Cap ) { if( Line.empty() ) { return; } - Position = At + Edge / Into; + Position = at + Edge / into; Miter = {}; - Bend = Facing / Into; + Bend = facing / into; for( auto const &knot : Line ) { - Fit.emplace_back( knot.x, ( knot.y - Edge ) / Into ); + Fit.emplace_back( knot.x, ( knot.y - Edge ) / into ); } Cap = false; }; - glue( Into > 0.0 ? Fromline : Toline, Into > 0.0 ? From : To, cut.end, cut.endmiter, cut.endbend, cut.endfit, cut.endcap ); - glue( Into > 0.0 ? Toline : Fromline, Into > 0.0 ? To : From, cut.start, cut.startmiter, cut.startbend, cut.startfit, cut.startcap ); + glue( into > 0.0 ? Cut.fromline : Cut.toline, into > 0.0 ? Cut.from : Cut.to, cut.end, cut.endmiter, cut.endbend, cut.endfit, cut.endcap ); + glue( into > 0.0 ? Cut.toline : Cut.fromline, into > 0.0 ? Cut.to : Cut.from, cut.start, cut.startmiter, cut.startbend, cut.startfit, cut.startcap ); Plan.cuts.push_back( cut ); } @@ -198,24 +210,13 @@ void vee_fill( std::vector &Fills, double const At, doub fill.lift = std::min( Outwards ? Own.crest : Own.liptop, inside ? Other.liptop : Other.crest ) - Own.liptop; } std::vector line { { apex, edge + sign * reach }, { end, edge + sign * ( gap + reach ) } }; - std::sort( line.begin(), line.end(), []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; } ); + knots::sort( line ); std::vector fixed { { fill.from, flat - sign * seam }, { fill.to, flat - sign * seam } }; fill.outer = ( Outwards ? line : fixed ); fill.inner = ( Outwards ? fixed : line ); Fills.push_back( fill ); } -// 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 }; -}; - std::vector tread_edge( TTrack const &Track, int const Path, int const Side, TTrack const &Other, int const Otherpath, int const Otherside, double const From, double const To, bool &Below, std::vector &Gauge ) { auto const &ours { rail_line( Track, Path, Side > 0, false ) }; @@ -241,9 +242,8 @@ std::vector tread_edge( TTrack const &Track, int const Path, int con below += ( gauge < edge ? 1 : -1 ); knots.emplace_back( at, edge ); } - auto const byposition = []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; }; - std::sort( knots.begin(), knots.end(), byposition ); - std::sort( Gauge.begin(), Gauge.end(), byposition ); + knots::sort( knots ); + knots::sort( Gauge ); Below = ( below > 0 ); return knots; } @@ -282,267 +282,356 @@ std::vector nose_extents( TTrack const &Track, int const Path, int c if( knots.size() < 2 || knots.back().y < apart ) { return straight; } - std::sort( knots.begin(), knots.end(), []( glm::dvec2 const &A, glm::dvec2 const &B ) { return A.x < B.x; } ); + knots::sort( knots ); return knots; } } // namespace +namespace { + +void merge_guard( std::vector &Guards, track_rails::guard_rail const &Guard ) { + + for( auto &span : Guards ) { + 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() ); + span.through = ( span.through && Guard.through ); + return; + } + } + Guards.push_back( Guard ); +} + +void join_opposite_guards( std::vector ( &Guards )[ 2 ] ) { + + knots::line spans[ 2 ]; + for( int side = 0; side < 2; ++side ) { + for( auto const &guard : Guards[ side ] ) { + spans[ side ].emplace_back( guard.from, guard.to ); + } + } + for( int side = 0; side < 2; ++side ) { + for( auto &guard : Guards[ 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 ); + } + } + } + } +} + +bool framed( track_rails::rail_crossing const &Corner, track_rails::rail_crossing const &Other, int const Side ) { + + return Other.track == Corner.track && Other.path == Corner.path && Other.side == Side && Other.otherpath == Corner.otherpath; +} + +} // namespace + +class TTrack::rail_planner { + +public: +// constructors + rail_planner( TTrack &Track, bool const Crossings ); +// methods + rail_layout lay_out(); + +private: +// methods + void add_crossing( rail_crossing const &Crossing ); + void cross_grooved( rail_crossing const &Crossing, rail_size const &Other ); + void cross_wide( rail_crossing const &Crossing, rail_size const &Other ); + void acute_frog( rail_crossing const &Crossing, double const Toward ); + void add_vee( rail_crossing const &Crossing, rail_size const &Other, bool const Filled ); + void frame_middle_crossings(); + track_rails::guard_rail frame_guard( rail_crossing const &Start, rail_crossing const &End ) const; + void switch_frog(); + void add_tread( int const Path, int const Side, glm::dvec2 const &Span, TTrack const &Other, int const Otherpath, int const Anchor = 0, int const Otherside = 0 ); + glm::dvec2 shared_span( rail_crossing const &Crossing, rail_size const &Other ) const; + rail_plan &plan_of( rail_crossing const &Crossing ) { + return m_layout.plans[ Crossing.path ][ Crossing.side > 0 ]; } +// members + TTrack &m_track; + bool const m_crossings; + rail_size const m_size; + double const m_guardoffset; + rail_layout m_layout; + std::vector m_corners; +}; + +TTrack::rail_planner::rail_planner( TTrack &Track, bool const Crossings ) : + m_track { Track }, + m_crossings { Crossings }, + m_size { Track.rail_dimensions() }, + m_guardoffset { -( flangeway + m_size.head ) } +{} + +rail_layout TTrack::rail_planner::lay_out() { + + std::vector crossings; + if( m_crossings ) { + crossings = m_track.rail_crossings(); + } + m_layout.checks = ( m_crossings ? rail_checks( m_track ) : m_track.check_rails( crossings ) ); + if( m_crossings ) { + for( auto const &crossing : crossings ) { + add_crossing( crossing ); + } + for( auto &pathguards : m_layout.guards ) { + join_opposite_guards( pathguards ); + } + for( int path = 0; path < 2; ++path ) { + for( int side = 0; side < 2; ++side ) { + for( auto const &guard : m_layout.guards[ path ][ side ] ) { + m_layout.fills[ path ][ side ].push_back( guard_fill( guard, m_size ) ); + } + } + } + frame_middle_crossings(); + } + if( m_track.eType == tt_Switch && cut_rails( m_track ) ) { + for( int path = 0; path < 2; ++path ) { + add_tread( path, -m_track.blade_side( path ), { 0.0, blade_length( m_track, 1 - path ) }, m_track, 1 - path, 0, -m_track.blade_side( path ) ); + } + } + if( m_track.eType == tt_Switch ) { + switch_frog(); + } + return m_layout; +} + +void TTrack::rail_planner::add_crossing( rail_crossing const &Crossing ) { + + if( std::find( m_layout.crossed.begin(), m_layout.crossed.end(), Crossing.track ) == m_layout.crossed.end() ) { + m_layout.crossed.push_back( Crossing.track ); + } + auto const theirs { Crossing.track->rail_dimensions() }; + if( m_size.grooved || theirs.grooved ) { + cross_grooved( Crossing, theirs ); + return; + } + if( Crossing.angle > middle_angle && Crossing.angle <= wide_angle ) { + m_corners.push_back( Crossing ); + } + if( Crossing.angle > wide_angle ) { + cross_wide( Crossing, theirs ); + } + else if( Crossing.facing < 0.0 ) { + acute_frog( Crossing, Crossing.into > 0.0 ? 1.0 : -1.0 ); + } + else { + obtuse( plan_of( Crossing ), Crossing.at, Crossing.into, m_size ); + auto const reach { ( rolled_band + m_size.head + tread_fade ) / std::max( std::tan( Crossing.angle ), 0.01 ) + 0.2 }; + add_tread( Crossing.path, Crossing.side, { Crossing.at - reach, Crossing.at + reach }, *Crossing.track, Crossing.otherpath, 0, Crossing.otherside ); + } +} + +void TTrack::rail_planner::cross_grooved( rail_crossing const &Crossing, rail_size const &Other ) { + + auto const through { m_size.grooved != Other.grooved ? m_size.grooved : goes_through( Crossing.owner ? *Crossing.owner : m_track, *Crossing.track ) }; + m_layout.bands[ Crossing.path ][ Crossing.side > 0 ].push_back( groove_band( Crossing.at, Crossing.into, Crossing.facing, through, Other ) ); + if( m_size.grooved && Other.grooved && Crossing.angle <= middle_angle ) { + add_vee( Crossing, Other, through ); + } + else if( through ) { + add_tread( Crossing.path, Crossing.side, shared_span( Crossing, Other ), *Crossing.track, Crossing.otherpath ); + } +} + +void TTrack::rail_planner::cross_wide( rail_crossing const &Crossing, rail_size const &Other ) { + + auto const through { goes_through( m_track, *Crossing.track ) }; + wide( plan_of( Crossing ), Crossing, { through ? 0.0 : -Other.head, flangeway, 0.0, m_size.head, through ? knots::line {} : glue_line( Other.field, 0.0 ) } ); + if( through ) { + add_tread( Crossing.path, Crossing.side, shared_span( Crossing, Other ), *Crossing.track, Crossing.otherpath ); + } + auto const reach { ( flangeway + m_size.head + 0.3 ) / std::abs( Crossing.into ) }; + track_rails::guard_rail guard { Crossing.at - reach, Crossing.at + reach }; + guard.through = through; + wide( guard.plan, Crossing, { -Other.head, through ? flangeway : flangeway + Other.head, m_guardoffset, m_guardoffset + m_size.head, glue_line( Other.field, 0.0 ), through ? knots::line {} : glue_line( Other.gauge, flangeway + Other.head ) } ); + merge_guard( m_layout.guards[ Crossing.path ][ Crossing.side > 0 ], guard ); +} + +void TTrack::rail_planner::acute_frog( rail_crossing const &Crossing, double const Toward ) { + + auto &plan { plan_of( Crossing ) }; + auto const slope { std::tan( Crossing.angle ) }; + m_layout.wingends[ Crossing.path ][ Crossing.side > 0 ].emplace_back( Crossing.at - Toward * wing_reach( slope ), -Toward ); + acute( plan, Crossing.at, Toward, slope, + wing_extension( m_track, m_layout.checks, Crossing.path, Crossing.side, Crossing.at, Toward, slope, *Crossing.track, Crossing.otherpath, Crossing.otherside, Crossing.otherat, Crossing.aligned ? -Toward : Toward ), + nose_extents( m_track, Crossing.path, Crossing.side, *Crossing.track, Crossing.otherpath, Crossing.otherside, Crossing.at, Toward, slope, m_size ) ); + auto const nose { Toward < 0.0 ? plan.cuts.back().start : plan.cuts.back().end }; + auto const shared { Crossing.at - Toward * ( rolled_band + m_size.head + tread_fade ) / slope }; + add_tread( Crossing.path, Crossing.side, { std::min( nose, shared ), std::max( nose, shared ) }, *Crossing.track, Crossing.otherpath, Toward > 0.0 ? 1 : -1, Crossing.otherside ); +} + +void TTrack::rail_planner::add_vee( rail_crossing const &Crossing, rail_size const &Other, bool const Filled ) { + + std::vector vee; + vee_fill( vee, Crossing.at, Crossing.into, Crossing.facing, m_size, Other, true ); + vee_fill( vee, Crossing.at, Crossing.into, Crossing.facing, m_size, Other, false ); + glm::dvec2 const block { std::min( vee[ 0 ].from, vee[ 1 ].from ), std::max( vee[ 0 ].to, vee[ 1 ].to ) }; + m_layout.blocks[ Crossing.path ][ Crossing.side > 0 ].push_back( block ); + add_tread( Crossing.path, Crossing.side, block, *Crossing.track, Crossing.otherpath ); + if( Filled ) { + auto &fills { m_layout.fills[ Crossing.path ][ Crossing.side > 0 ] }; + fills.insert( fills.end(), vee.begin(), vee.end() ); + } +} + +// 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 +void TTrack::rail_planner::frame_middle_crossings() { + + std::sort( m_corners.begin(), m_corners.end(), []( rail_crossing const &A, rail_crossing const &B ) { return A.at < B.at; } ); + for( std::size_t i = 0; i < m_corners.size(); ++i ) { + auto const &start { m_corners[ i ] }; + if( start.into <= 0.0 ) { + continue; + } + for( std::size_t j = i + 1; j < m_corners.size(); ++j ) { + auto const &end { m_corners[ j ] }; + if( false == framed( start, end, start.side ) ) { + continue; + } + if( end.into < 0.0 ) { + auto const guard { frame_guard( start, end ) }; + if( guard.to - guard.from > 0.1 ) { + m_layout.guards[ start.path ][ start.side > 0 ].push_back( guard ); + } + } + break; + } + } +} + +track_rails::guard_rail TTrack::rail_planner::frame_guard( rail_crossing const &Start, rail_crossing const &End ) const { + + auto const glued = [&]( rail_crossing const &Corner ) { + return Corner.at + ( flangeway + std::max( m_guardoffset * Corner.facing, ( m_guardoffset + m_size.head ) * Corner.facing ) ) / Corner.into; }; + auto const gluedmiter = [&]( rail_crossing const &Corner ) { + return glm::dvec2 { Corner.at + flangeway / Corner.into, Corner.facing / Corner.into }; }; + track_rails::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 : m_corners ) { + if( false == framed( Start, opposite, -Start.side ) ) { + continue; + } + auto const margin { ( flangeway + m_size.head + 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, { -corner->track->rail_dimensions().head, flangeway, m_guardoffset, m_guardoffset + m_size.head } ); + 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; + } + } + return guard; +} + +void TTrack::rail_planner::switch_frog() { + + auto const frog { m_track.frog() }; + if( false == frog.valid ) { + return; + } + glm::dvec3 point, tangents[ 2 ]; + glm::dvec2 normals[ 2 ]; + for( int path = 0; path < 2; ++path ) { + auto const &line { rail_line( m_track, path, frog.side[ path ] > 0, false ) }; + line.at( frog.at[ path ], point, tangents[ path ] ); + normals[ path ] = glm::normalize( plan( line.normal( frog.at[ path ], frog.side[ path ] ) ) ); + } + auto const facing { glm::dot( normals[ 0 ], normals[ 1 ] ) }; + for( int path = 0; path < 2; ++path ) { + rail_crossing crossing; + crossing.track = &m_track; + crossing.path = path; + crossing.side = frog.side[ path ]; + crossing.at = frog.at[ path ]; + crossing.angle = frog.angle; + crossing.into = glm::dot( glm::normalize( plan( tangents[ path ] ) ), normals[ 1 - path ] ); + crossing.facing = facing; + crossing.otherpath = 1 - path; + crossing.otherside = frog.side[ 1 - path ]; + crossing.otherat = frog.at[ 1 - path ]; + if( m_size.grooved ) { + m_layout.bands[ path ][ crossing.side > 0 ].push_back( groove_band( crossing.at, crossing.into, facing, path == 0, m_size ) ); + add_vee( crossing, m_size, path == 0 ); + continue; + } + acute_frog( crossing, -1.0 ); + } +} + +void TTrack::rail_planner::add_tread( int const Path, int const Side, glm::dvec2 const &Span, TTrack const &Other, int const Otherpath, int const Anchor, int const Otherside ) { + + auto const material { m_track.shared_tread( Path, Other, Otherpath ) }; + if( material == m_track.rail_material( Path ) || Span.y <= Span.x ) { + return; + } + auto &treads { m_layout.treads[ Path ][ Side > 0 ] }; + if( Otherside == 0 ) { + treads.push_back( { Span.x, Span.y, material, Anchor } ); + return; + } + rail_tread part { Span.x, Span.y, material, Anchor }; + part.edge = tread_edge( m_track, Path, Side, Other, Otherpath, Otherside, Span.x, Span.y, part.below, part.gauge ); + auto from { 1e9 }; + auto to { -1e9 }; + for( std::size_t i = 0; i < part.edge.size() && i < part.gauge.size(); ++i ) { + auto const low { std::min( part.edge[ i ].y, part.gauge[ i ].y ) }; + auto const high { std::max( part.edge[ i ].y, part.gauge[ i ].y ) }; + if( low - tread_fade < m_size.head && high + tread_fade > 0.0 ) { + from = std::min( from, part.edge[ i ].x ); + to = std::max( to, part.edge[ i ].x ); + } + } + part.from = std::max( Span.x, from - 0.1 ); + part.to = std::min( Span.y, to + 0.1 ); + if( part.to > part.from ) { + treads.push_back( part ); + } +} + +glm::dvec2 TTrack::rail_planner::shared_span( rail_crossing const &Crossing, rail_size const &Other ) const { + + glm::dvec2 span { 1e9, -1e9 }; + for( auto const theirs : { Other.grooved ? Other.ledge : 0.0, Other.grooved ? Other.tread : Other.head } ) { + for( auto const ours : { m_size.grooved ? m_size.ledge : 0.0, m_size.grooved ? m_size.tread : m_size.head } ) { + auto const distance { Crossing.at + ( Crossing.facing * ours - theirs ) / Crossing.into }; + span = { std::min( span.x, distance ), std::max( span.y, distance ) }; + } + } + return span; +} + track_rails::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 = ( Crossings ? rail_checks( *this ) : 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 ) }; - auto const tread = [&]( int const Path, int const Side, glm::dvec2 const &Span, TTrack const &Other, int const Otherpath, int const Anchor = 0, int const Otherside = 0 ) { - auto const material { shared_tread( Path, Other, Otherpath ) }; - if( material == rail_material( Path ) || Span.y <= Span.x ) { - return; - } - if( Otherside == 0 ) { - layout.treads[ Path ][ Side > 0 ].push_back( { Span.x, Span.y, material, Anchor } ); - return; - } - rail_tread part { Span.x, Span.y, material, Anchor }; - part.edge = tread_edge( *this, Path, Side, Other, Otherpath, Otherside, Span.x, Span.y, part.below, part.gauge ); - auto from { 1e9 }; - auto to { -1e9 }; - for( std::size_t i = 0; i < part.edge.size() && i < part.gauge.size(); ++i ) { - auto const low { std::min( part.edge[ i ].y, part.gauge[ i ].y ) }; - auto const high { std::max( part.edge[ i ].y, part.gauge[ i ].y ) }; - if( low - tread_fade < size.head && high + tread_fade > 0.0 ) { - from = std::min( from, part.edge[ i ].x ); - to = std::max( to, part.edge[ i ].x ); - } - } - part.from = std::max( Span.x, from - 0.1 ); - part.to = std::min( Span.y, to + 0.1 ); - if( part.to > part.from ) { - layout.treads[ Path ][ Side > 0 ].push_back( part ); - } - }; - auto const across = [&]( rail_crossing const &Crossing, rail_size const &Other ) { - glm::dvec2 span { 1e9, -1e9 }; - for( auto const theirs : { Other.grooved ? Other.ledge : 0.0, Other.grooved ? Other.tread : Other.head } ) { - for( auto const ours : { size.grooved ? size.ledge : 0.0, size.grooved ? size.tread : size.head } ) { - auto const distance { Crossing.at + ( Crossing.facing * ours - theirs ) / Crossing.into }; - span = { std::min( span.x, distance ), std::max( span.y, distance ) }; - } - } - return span; }; - std::vector corners; - if( Crossings ) { - for( auto const &crossing : crossings ) { - auto &plan { plans[ crossing.path ][ crossing.side > 0 ] }; - if( std::find( layout.crossed.begin(), layout.crossed.end(), crossing.track ) == layout.crossed.end() ) { - layout.crossed.push_back( crossing.track ); - } - auto const theirs { crossing.track->rail_dimensions() }; - if( size.grooved || theirs.grooved ) { - auto const through { size.grooved != theirs.grooved ? size.grooved : goes_through( crossing.owner ? *crossing.owner : *this, *crossing.track ) }; - layout.bands[ crossing.path ][ crossing.side > 0 ].push_back( groove_band( crossing.at, crossing.into, crossing.facing, through, theirs ) ); - if( size.grooved && theirs.grooved && crossing.angle <= middle_angle ) { - std::vector vee; - vee_fill( vee, crossing.at, crossing.into, crossing.facing, size, theirs, true ); - vee_fill( vee, crossing.at, crossing.into, crossing.facing, size, theirs, false ); - glm::dvec2 const block { std::min( vee[ 0 ].from, vee[ 1 ].from ), std::max( vee[ 0 ].to, vee[ 1 ].to ) }; - layout.blocks[ crossing.path ][ crossing.side > 0 ].push_back( block ); - tread( crossing.path, crossing.side, block, *crossing.track, crossing.otherpath ); - if( through ) { - auto &fills { layout.fills[ crossing.path ][ crossing.side > 0 ] }; - fills.insert( fills.end(), vee.begin(), vee.end() ); - } - } - else if( through ) { - tread( crossing.path, crossing.side, across( crossing, theirs ), *crossing.track, crossing.otherpath ); - } - continue; - } - 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, through ? std::vector{} : glue_line( other.field, 0.0 ) ); - if( through ) { - tread( crossing.path, crossing.side, across( crossing, other ), *crossing.track, crossing.otherpath ); - } - auto const reach { ( flangeway + railhead + 0.3 ) / std::abs( crossing.into ) }; - auto &spans { guards[ crossing.path ][ crossing.side > 0 ] }; - track_rails::guard_rail guard { crossing.at - reach, crossing.at + reach }; - guard.through = through; - wide( guard.plan, crossing.at, crossing.into, crossing.facing, -other.head, through ? flangeway : flangeway + other.head, guardoffset, guardoffset + railhead, glue_line( other.field, 0.0 ), through ? std::vector{} : glue_line( other.gauge, flangeway + other.head ) ); - 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() ); - span.through = ( span.through && guard.through ); - 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, - wing_extension( *this, checks, crossing.path, crossing.side, crossing.at, toward, slope, *crossing.track, crossing.otherpath, crossing.otherside, crossing.otherat, crossing.aligned ? -toward : toward ), - nose_extents( *this, crossing.path, crossing.side, *crossing.track, crossing.otherpath, crossing.otherside, crossing.at, toward, slope, size ) ); - auto const nose { toward < 0.0 ? plan.cuts.back().start : plan.cuts.back().end }; - auto const shared { crossing.at - toward * ( rolled_band + size.head + tread_fade ) / slope }; - tread( crossing.path, crossing.side, { std::min( nose, shared ), std::max( nose, shared ) }, *crossing.track, crossing.otherpath, toward > 0.0 ? 1 : -1, crossing.otherside ); - } - else { - obtuse( plan, crossing.at, crossing.into, size ); - auto const reach { ( rolled_band + size.head + tread_fade ) / std::max( std::tan( crossing.angle ), 0.01 ) + 0.2 }; - tread( crossing.path, crossing.side, { crossing.at - reach, crossing.at + reach }, *crossing.track, crossing.otherpath, 0, crossing.otherside ); - } - } - 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 ); - } - } - } - } - } - for( int path = 0; path < 2; ++path ) { - for( int side = 0; side < 2; ++side ) { - for( auto const &guard : guards[ path ][ side ] ) { - layout.fills[ path ][ side ].push_back( guard_fill( guard, size ) ); - } - } - } - 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 ) { - track_rails::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 && cut_rails( *this ) ) { - for( int path = 0; path < 2; ++path ) { - tread( path, -blade_side( path ), { 0.0, blade_length( *this, 1 - path ) }, *this, 1 - path, 0, -blade_side( path ) ); - } - } - if( eType == tt_Switch ) { - auto const frog { this->frog() }; - if( frog.valid ) { - glm::dvec3 point, tangents[ 2 ]; - glm::dvec2 normals[ 2 ]; - for( int path = 0; path < 2; ++path ) { - auto const &line { rail_line( *this, path, frog.side[ path ] > 0, false ) }; - line.at( frog.at[ path ], point, tangents[ path ] ); - normals[ path ] = glm::normalize( plan( line.normal( frog.at[ path ], frog.side[ path ] ) ) ); - } - auto const facing { glm::dot( normals[ 0 ], normals[ 1 ] ) }; - auto const slope { std::tan( frog.angle ) }; - for( int path = 0; path < 2; ++path ) { - auto const into { glm::dot( glm::normalize( plan( tangents[ path ] ) ), normals[ 1 - path ] ) }; - auto &fills { layout.fills[ path ][ frog.side[ path ] > 0 ] }; - if( size.grooved ) { - layout.bands[ path ][ frog.side[ path ] > 0 ].push_back( groove_band( frog.at[ path ], into, facing, path == 0, size ) ); - std::vector vee; - vee_fill( vee, frog.at[ path ], into, facing, size, size, true ); - vee_fill( vee, frog.at[ path ], into, facing, size, size, false ); - glm::dvec2 const block { std::min( vee[ 0 ].from, vee[ 1 ].from ), std::max( vee[ 0 ].to, vee[ 1 ].to ) }; - layout.blocks[ path ][ frog.side[ path ] > 0 ].push_back( block ); - tread( path, frog.side[ path ], block, *this, 1 - path ); - if( path == 0 ) { - fills.insert( fills.end(), vee.begin(), vee.end() ); - } - continue; - } - 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, - 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 ), - nose_extents( *this, path, frog.side[ path ], *this, 1 - path, frog.side[ 1 - path ], frog.at[ path ], -1.0, slope, size ) ); - auto const nose { plans[ path ][ frog.side[ path ] > 0 ].cuts.back().start }; - auto const shared { frog.at[ path ] + ( rolled_band + size.head + tread_fade ) / slope }; - tread( path, frog.side[ path ], { std::min( nose, shared ), std::max( nose, shared ) }, *this, 1 - path, -1, frog.side[ 1 - path ] ); - } - } - } - return layout; + return rail_planner( *this, Crossings ).lay_out(); } diff --git a/world/Track.h b/world/Track.h index f6ba32b8..bab10a79 100644 --- a/world/Track.h +++ b/world/Track.h @@ -413,6 +413,7 @@ private: // 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( track_rails::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 ); class rail_layer; + class rail_planner; int blade_side( int const Path ) const; material_handle rail_material( int const Path ) const; glm::vec2 tread_band() const; From 697592c4aa867473309c26ec52112a7bd71e24af Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 09:52:19 +0200 Subject: [PATCH 08/19] refactor(track): the crossings of the rails are found in steps, the tracks nearby, the meeting of two rails with its refined place and the crossing made of it, and the crossings of the neighbours mapped on this track, and the check rails are planned, merged, kept clear of blocks and cut in separate functions instead of two long functions; path_count and path_neighbour replace the repeated choice of the paths and the ends of a switch or a plain track --- world/RailCrossings.cpp | 656 +++++++++++++++++++++++----------------- world/Track.cpp | 6 +- world/TrackRails.h | 2 + 3 files changed, 380 insertions(+), 284 deletions(-) diff --git a/world/RailCrossings.cpp b/world/RailCrossings.cpp index 4fce7bde..5f749fd1 100644 --- a/world/RailCrossings.cpp +++ b/world/RailCrossings.cpp @@ -161,173 +161,219 @@ void rail_offset( TTrack const &Track, int const Path, int const Side, glm::dvec Lateral = lateral( line, Side, Point, Distance ); } +int path_count( TTrack const &Track ) { + + return ( Track.eType == tt_Switch ? 2 : 1 ); +} + +TTrack *path_neighbour( TTrack const &Track, int const Path, int const End ) { + + if( Track.eType == tt_Switch ) { + return ( End ? Track.SwitchExtension->pNexts[ Path ] : Track.SwitchExtension->pPrevs[ Path ] ); + } + return ( End ? Track.trNext : Track.trPrev ); +} + } // namespace track_rails -std::vector TTrack::rail_crossings( bool const Joined ) { +namespace { - std::vector result; - if( false == cut_rails( *this ) || simulation::Region == nullptr ) { - return result; - } - auto &memo { memo_of( *this ) }; - if( Joined && memo.joined.has_value() ) { - return *memo.joined; - } - auto const extension { 10.0 }; - auto const paths { eType == tt_Switch ? 2 : 1 }; - if( memo.own.has_value() ) { - result = *memo.own; - } - else { - 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 ) { +struct rail_run { + TTrack *track { nullptr }; + int path { 0 }; + int side { 0 }; + gauge_line const *line { nullptr }; + double from { 0.0 }; + double length { 0.0 }; +}; + +std::vector nearby_rails( TTrack &Track ) { + + std::vector others; + auto const sections { simulation::Region->sections( Track.location(), static_cast( Track.radius() + 50.0 ) ) }; + for( auto *section : sections ) { + for( auto const &cell : section->m_cells ) { + if( glm::length( cell.area().center - Track.location() ) > cell.area().radius + Track.radius() + 1.0 ) { + continue; + } + for( auto *other : cell.m_directories.paths ) { + if( other == nullptr || other == &Track || false == cut_rails( *other ) ) { 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 ); + if( glm::length( other->location() - Track.location() ) > other->radius() + Track.radius() + 1.0 ) { + continue; + } + if( std::find( others.begin(), others.end(), other ) == others.end() ) { + others.push_back( other ); + } + } + } + } + return others; +} + +double line_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 ) ) ); +} + +double heading( glm::dvec2 const &Tangent ) { + + auto const angle { std::atan2( Tangent.y, Tangent.x ) }; + return ( angle < 0.0 ? angle + M_PI : angle ); +} + +bool 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; +} + +bool same_place( rail_crossing const &Crossing, int const Path, int const Side, double const At ) { + + return Crossing.path == Path && Crossing.side == Side && std::abs( Crossing.at - At ) < 0.05; +} + +void refine_meeting( gauge_line const &Ours, gauge_line const &Theirs, double &S, double &T ) { + + glm::dvec3 p, tp, q, tq; + for( int step = 0; step < 6; ++step ) { + Ours.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; + } +} + +rail_crossing make_crossing( rail_run const &Ours, rail_run const &Theirs, double const S, double const T, glm::dvec2 const &Tangent, glm::dvec2 const &Othertangent, double const Angle ) { + + auto const normal { glm::normalize( plan( Ours.line->normal( S, Ours.side ) ) ) }; + auto const othernormal { glm::normalize( plan( Theirs.line->normal( T, Theirs.side ) ) ) }; + rail_crossing crossing; + crossing.track = Theirs.track; + crossing.path = Ours.path; + crossing.side = Ours.side; + crossing.at = S; + crossing.angle = Angle; + crossing.into = glm::dot( Tangent, othernormal ); + crossing.facing = glm::dot( normal, othernormal ); + auto const ourbend { line_bend( *Ours.line, S ) }; + auto const theirbend { line_bend( *Theirs.line, T ) }; + crossing.nose = ( std::abs( ourbend - theirbend ) > 1e-4 ? ourbend < theirbend : heading( Tangent ) < heading( Othertangent ) ); + crossing.otherpath = Theirs.path; + crossing.otherside = Theirs.side; + crossing.otherat = T; + crossing.aligned = glm::dot( Tangent, Othertangent ) > 0.0; + return crossing; +} + +void meet_rails( rail_run const &Ours, rail_run const &Theirs, std::vector &Result ) { + + auto const &line { *Ours.line }; + auto const &theirs { *Theirs.line }; + if( false == overlap( line, theirs ) ) { + return; + } + 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 }; + auto const alow { glm::min( a0, a0 + a ) - 0.02 * glm::length( a ) - 1e-6 }; + auto const ahigh { glm::max( a0, a0 + a ) + 0.02 * glm::length( a ) + 1e-6 }; + if( alow.x > theirs.high.x + 1.0 || theirs.low.x - 1.0 > ahigh.x || alow.y > theirs.high.y + 1.0 || theirs.low.y - 1.0 > ahigh.y ) { + continue; + } + 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 reach { 0.02 * glm::length( b ) }; + if( alow.x > std::max( b0.x, b0.x + b.x ) + reach || std::min( b0.x, b0.x + b.x ) - reach > ahigh.x + || alow.y > std::max( b0.y, b0.y + b.y ) + reach || std::min( b0.y, b0.y + b.y ) - reach > ahigh.y ) { + continue; + } + double u, v; + if( false == meet( a0, a, b0, b, 1e-12, u, v ) ) { + continue; + } + 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 ) }; + refine_meeting( line, theirs, s, t ); + glm::dvec3 p, tp, q, tq; + line.at( s, p, tp ); + theirs.at( t, q, tq ); + if( glm::length( plan( p ) - plan( q ) ) > 0.01 + || s < 0.0 || s > Ours.length + || t < Theirs.from || t > Theirs.length ) { + continue; + } + if( std::abs( p.y - q.y ) > 0.5 ) { + continue; + } + if( ( s < 0.5 || s > Ours.length - 0.5 ) && ( t < 0.5 || t > Theirs.length - 0.5 ) && joined( *Ours.track, *Theirs.track ) ) { + 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; + } + if( std::any_of( Result.begin(), Result.end(), [&]( rail_crossing const &Crossing ) { return same_place( Crossing, Ours.path, Ours.side, s ); } ) ) { + continue; + } + Result.push_back( make_crossing( Ours, Theirs, s, t, tangent, othertangent, angle ) ); + } + } +} + +std::vector own_crossings( TTrack &Track ) { + + std::vector result; + for( auto *other : nearby_rails( Track ) ) { + for( int otherpath = 0; otherpath < path_count( *other ); ++otherpath ) { + rail_run theirs { other, otherpath }; + theirs.from = ( other->eType == tt_Switch ? blade_length( *other, otherpath ) : 0.0 ); + theirs.length = path_segment( *other, otherpath ).GetLength(); + for( int otherside = 0; otherside < 2; ++otherside ) { + theirs.side = ( otherside ? 1 : -1 ); + theirs.line = &rail_line( *other, otherpath, otherside, true ); + for( int path = 0; path < path_count( Track ); ++path ) { + auto const length { path_segment( Track, path ).GetLength() }; + for( int side = 0; side < 2; ++side ) { + meet_rails( { &Track, path, side ? 1 : -1, &rail_line( Track, path, side, false ), 0.0, length }, theirs, result ); } } } } - 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 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 { rail_line( *other, otherpath, otherside, true ) }; - 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 { rail_line( *this, path, side, false ) }; - 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 }; - auto const alow { glm::min( a0, a0 + a ) - 0.02 * glm::length( a ) - 1e-6 }; - auto const ahigh { glm::max( a0, a0 + a ) + 0.02 * glm::length( a ) + 1e-6 }; - if( alow.x > theirs.high.x + 1.0 || theirs.low.x - 1.0 > ahigh.x || alow.y > theirs.high.y + 1.0 || theirs.low.y - 1.0 > ahigh.y ) { - continue; - } - 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 reach { 0.02 * glm::length( b ) }; - if( alow.x > std::max( b0.x, b0.x + b.x ) + reach || std::min( b0.x, b0.x + b.x ) - reach > ahigh.x - || alow.y > std::max( b0.y, b0.y + b.y ) + reach || std::min( b0.y, b0.y + b.y ) - reach > ahigh.y ) { - continue; - } - double u, v; - if( false == meet( a0, a, b0, b, 1e-12, u, v ) ) { - continue; - } - 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 ); - } - } - } - } - } - } - } - memo.own = result; } - 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 ) }; + return result; +} + +// crossings of the tracks going on from the ends, near enough for their check rails and wings to reach this one +void add_neighbour_crossings( TTrack &Track, std::vector &Result ) { + + auto const extension { 10.0 }; + auto const owncount { Result.size() }; + for( int path = 0; path < path_count( Track ); ++path ) { + auto const &segment { path_segment( Track, 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 ) ) { + auto *neighbour { path_neighbour( Track, path, end ) }; + if( neighbour == nullptr || neighbour == &Track || 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 ) { + for( int neighbourpath = 0; neighbourpath < path_count( *neighbour ); ++neighbourpath ) { auto const &neighboursegment { path_segment( *neighbour, neighbourpath ) }; auto const neighbourlength { neighboursegment.GetLength() }; auto const tostart { glm::length( neighboursegment.FastGetPoint_0() - joint ) }; @@ -338,7 +384,7 @@ std::vector TTrack::rail_crossings( bool const Joine auto const atstart { tostart <= toend }; auto const same { atstart == ( end == 1 ) }; for( auto const &crossing : theirs ) { - if( crossing.path != neighbourpath || crossing.track == this ) { + if( crossing.path != neighbourpath || crossing.track == &Track ) { continue; } auto const along { atstart ? crossing.at : neighbourlength - crossing.at }; @@ -354,46 +400,42 @@ std::vector TTrack::rail_crossings( bool const Joine 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 ); + if( std::none_of( Result.begin(), Result.begin() + owncount, [&]( rail_crossing const &Crossing ) { return same_place( Crossing, mapped.path, mapped.side, mapped.at ); } ) ) { + Result.push_back( mapped ); } } } } } - memo.joined = result; - return result; } -track_rails::check_rail_set TTrack::check_rails( std::vector const &Crossings ) { +struct planned_check { + double from { 0.0 }; + double to { 0.0 }; + bool flarefrom { true }; + bool flareto { true }; + double shortest { 1.5 }; + double flare { 0.02 }; + double frombend { 0.0 }; + double tobend { 0.0 }; +}; +using planned_check_set = std::array, 2>, 2>; // path, right side - struct planned { - double from { 0.0 }; - double to { 0.0 }; - bool flarefrom { true }; - bool flareto { true }; - double shortest { 1.5 }; - double flare { 0.02 }; - double frombend { 0.0 }; - double tobend { 0.0 }; - }; - std::vector plans[ 2 ][ 2 ]; - check_rail_set result; - auto const size { rail_dimensions() }; - if( size.grooved ) { - return result; - } - 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 } ); - }; +void add_acute_check( std::vector &Checks, double const At, double const Toward, double const Slope, double const Flare ) { + + auto const half { 0.5 * check_length( Slope ) }; + Checks.push_back( { At + Toward * 0.25 - half, At + Toward * 0.25 + half, true, true, 1.5, Flare } ); +} + +planned_check_set planned_checks( TTrack const &Track, std::vector const &Crossings ) { + + planned_check_set plans; for( auto const &crossing : Crossings ) { if( crossing.angle > middle_angle || crossing.track->rail_dimensions().grooved ) { 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 ); + add_acute_check( 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, crossing.at + 1.2, false, true, 0.5, 0.02, -knuckle_bend( crossing.into ), 0.0 } ); @@ -402,120 +444,172 @@ track_rails::check_rail_set TTrack::check_rails( std::vector cons plans[ crossing.path ][ crossing.side > 0 ].push_back( { crossing.at - 1.2, crossing.at, true, false, 0.5, 0.02, 0.0, knuckle_bend( crossing.into ) } ); } } - if( eType == tt_Switch ) { - auto const frog { this->frog() }; + if( Track.eType == tt_Switch ) { + auto const frog { Track.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 ); + add_acute_check( 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 ) }; + return plans; +} + +void merge_checks( std::vector &Checks ) { + + std::sort( Checks.begin(), Checks.end(), []( planned_check const &A, planned_check 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 ].tobend = Checks[ i ].tobend; + } + 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-- ); + } + } +} + +bool blocked_by( std::vector const &Others, glm::dvec2 const &Gauge, glm::dvec2 const &Inward, double const Clearance ) { + + auto blocked { false }; + for( auto const &other : Others ) { + for( std::size_t i = 1; i < other.points.size() && false == blocked; ++i ) { + auto const a { plan( other.points[ i - 1 ] ) }; + auto const edge { plan( other.points[ i ] ) - a }; + double t, u; + if( false == meet( Gauge, Inward, a, edge, 1e-9, t, u ) ) { + continue; + } + blocked = ( u > -1e-6 && u < 1.0 + 1e-6 && t > -0.05 && t < Clearance ); + } + } + return blocked; +} + +// 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 +knots::line check_blocks( TTrack const &Track, int const Path, int const Side, std::vector const &Checks, double const Clearance ) { + + auto const halfwidth { 0.5 * std::abs( Track.fTrackWidth ) }; + auto const &segment { path_segment( Track, Path ) }; + auto const length { segment.GetLength() }; + knots::line blocks; + auto const start { plan( segment.FastGetPoint_0() ) }; + auto const end { plan( 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( Track.eType == tt_Switch ) { + blocks.emplace_back( -1e9, 0.3 ); + add_path( path_segment( Track, 1 - Path ) ); + } + for( auto const *neighbour : { Track.trPrev, Track.trNext } ) { + if( neighbour == nullptr || neighbour == &Track || neighbour->eType != tt_Switch || neighbour->SwitchExtension == nullptr ) { + continue; + } + auto const tip { plan( 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, plan( other.FastGetPoint_0() ) ) < 0.1 || glm::distance( Point, plan( 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 ); + if( blocked_by( others, plan( point ), plan( ours.normal( s, Side ) ), Clearance ) ) { + blocks.emplace_back( s - 0.3, s + 0.3 ); + } + } + } + } + knots::sort( blocks ); + return blocks; +} + +void cut_check( planned_check const &Check, knots::line const &Blocks, std::vector &Result ) { + + auto const add = [&]( double const From, double const To ) { + Result.push_back( { + From, To, + ( From > Check.from || Check.flarefrom ) ? Check.flare : 0.0, + ( To < Check.to || Check.flareto ) ? Check.flare : 0.0, + From > Check.from ? 0.0 : Check.frombend, + To < Check.to ? 0.0 : Check.tobend } ); }; + 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 ); + } +} + +} // namespace + +std::vector TTrack::rail_crossings( bool const Joined ) { + + if( false == cut_rails( *this ) || simulation::Region == nullptr ) { + return {}; + } + auto &memo { memo_of( *this ) }; + if( Joined && memo.joined.has_value() ) { + return *memo.joined; + } + if( false == memo.own.has_value() ) { + memo.own = own_crossings( *this ); + } + auto result { *memo.own }; + if( false == Joined ) { + return result; + } + add_neighbour_crossings( *this, result ); + memo.joined = result; + return result; +} + +track_rails::check_rail_set TTrack::check_rails( std::vector const &Crossings ) { + + check_rail_set result; + auto const size { rail_dimensions() }; + if( size.grooved ) { + return result; + } + auto plans { planned_checks( *this, Crossings ) }; auto const clearance { 0.046 + 0.04 * size.head / 0.0735 + size.head }; - 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 path = 0; path < path_count( *this ); ++path ) { 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 ].tobend = checks[ i ].tobend; - } - 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 { plan( segment.FastGetPoint_0() ) }; - auto const end { plan( 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 { plan( 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, plan( other.FastGetPoint_0() ) ) < 0.1 || glm::distance( Point, plan( 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 { plan( point ) }; - auto const inward { plan( 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 { plan( other.points[ i - 1 ] ) }; - auto const edge { plan( other.points[ i ] ) - a }; - double t, u; - if( false == meet( gauge, inward, a, edge, 1e-9, t, u ) ) { - continue; - } - 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; } ); + merge_checks( checks ); + auto const blocks { check_blocks( *this, path, right ? 1 : -1, checks, clearance ) }; 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, - From > check.from ? 0.0 : check.frombend, - To < check.to ? 0.0 : check.tobend } ); }; - 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 ); - } + cut_check( check, blocks, result[ path ][ right ] ); } } } diff --git a/world/Track.cpp b/world/Track.cpp index 20ee978f..a5bec236 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -1381,20 +1381,20 @@ void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { void TTrack::share_joints() { - for( int path = 0; path < ( eType == tt_Switch ? 2 : 1 ); ++path ) { + for( int path = 0; path < track_rails::path_count( *this ); ++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 ) }; + auto *neighbour { track_rails::path_neighbour( *this, path, end ) }; 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 ) { + for( int theirpath = 0; theirpath < track_rails::path_count( *neighbour ); ++theirpath ) { auto const *theirs { neighbour->eType == tt_Switch ? neighbour->SwitchExtension->Segments[ theirpath ].get() : neighbour->Segment.get() }; if( theirs == nullptr ) { continue; diff --git a/world/TrackRails.h b/world/TrackRails.h index 1ae62522..b7404661 100644 --- a/world/TrackRails.h +++ b/world/TrackRails.h @@ -34,6 +34,8 @@ gfx::userdata_array worn_userdata( std::size_t const Count ); 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 ); TSegment const &path_segment( TTrack const &Track, int const Path ); +int path_count( TTrack const &Track ); +TTrack *path_neighbour( TTrack const &Track, int const Path, int const End ); double knuckle_bend( double const Into ); bool replaced_check( std::vector const &Checks, double const Wingend, double const Direction, double &Far ); gauge_line const &rail_line( TTrack const &Track, int const Path, int const Side, bool const Blades ); From 3d1c97442cb213dfb50d83c767268511e99272f1 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 09:55:24 +0200 Subject: [PATCH 09/19] refactor(track): the plates, fillers and check rails of a rail drawing are made from one box_profile and one helper for both sides, and the colour of a tread is sampled by sample_dds and sample_image into one colour_mean, a block of a DXT texture decoded by add_dxt_block, instead of one function --- world/RailLayer.cpp | 49 ++++++------ world/RailMaterials.cpp | 166 ++++++++++++++++++++++++---------------- 2 files changed, 123 insertions(+), 92 deletions(-) diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp index d858314c..5e121032 100644 --- a/world/RailLayer.cpp +++ b/world/RailLayer.cpp @@ -149,8 +149,28 @@ void cut_faces( gfx::vertex_array const &Strip, std::size_t const Ring, bool con using namespace track_rails; +namespace { + +gfx::vertex_array box_profile( float const Bottom, float const Top, float const Texture ) { + + return { + {{ 1.f, Bottom, 0.f }, { 1.f, 0.f, 0.f }, { Texture, 0.f }}, + {{ 1.f, Top, 0.f }, { 1.f, 0.f, 0.f }, { Texture, 0.f }}, + {{ 1.f, Top, 0.f }, { 0.f, 1.f, 0.f }, { Texture, 0.f }}, + {{ -1.f, Top, 0.f }, { 0.f, 1.f, 0.f }, { Texture, 0.f }}, + {{ -1.f, Top, 0.f }, { -1.f, 0.f, 0.f }, { Texture, 0.f }}, + {{ -1.f, Bottom, 0.f }, { -1.f, 0.f, 0.f }, { Texture, 0.f }} }; +} + +} // namespace + TTrack::rail_drawing TTrack::make_rail_drawing( gfx::geometrybank_handle const &Bank ) { + auto const sides = [&]( gfx::vertex_array const &Shape, gfx::vertex_array ( &Sides )[ 2 ] ) { + create_track_rail_profile( Sides[ 0 ], Sides[ 1 ], &Shape ); + for( auto &side : Sides ) { + side.resize( Shape.size() ); + } }; rail_drawing drawing; drawing.bank = Bank; drawing.size = rail_dimensions(); @@ -170,10 +190,7 @@ TTrack::rail_drawing TTrack::make_rail_drawing( gfx::geometrybank_handle const & 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() ); - } + sides( checkshape, drawing.checkshapes ); if( false == m_profiles.empty() ) { auto const &outline { track_rail_profile( m_profile1.second ) }; auto texture { 0.f }; @@ -197,18 +214,8 @@ TTrack::rail_drawing TTrack::make_rail_drawing( gfx::geometrybank_handle const & } auto const top { static_cast( ( drawing.size.grooved ? drawing.size.liptop : highest ) - 0.0005 ) }; auto const bottom { static_cast( -drawing.size.height ) }; - gfx::vertex_array plate { - {{ 1.f, bottom, 0.f }, { 1.f, 0.f, 0.f }, { texture, 0.f }}, - {{ 1.f, top, 0.f }, { 1.f, 0.f, 0.f }, { texture, 0.f }}, - {{ 1.f, top, 0.f }, { 0.f, 1.f, 0.f }, { texture, 0.f }}, - {{ -1.f, top, 0.f }, { 0.f, 1.f, 0.f }, { texture, 0.f }}, - {{ -1.f, top, 0.f }, { -1.f, 0.f, 0.f }, { texture, 0.f }}, - {{ -1.f, bottom, 0.f }, { -1.f, 0.f, 0.f }, { texture, 0.f }} }; drawing.platetexture = texture; - create_track_rail_profile( drawing.plates[ 0 ], drawing.plates[ 1 ], &plate ); - for( auto &shape : drawing.plates ) { - shape.resize( plate.size() ); - } + sides( box_profile( bottom, top, texture ), drawing.plates ); auto const neck { static_cast( drawing.size.neck ) }; auto webtexture { texture }; for( std::size_t i = 0; i < outline.size() / 2; ++i ) { @@ -218,17 +225,7 @@ TTrack::rail_drawing TTrack::make_rail_drawing( gfx::geometrybank_handle const & break; } } - gfx::vertex_array filler { - {{ 1.f, bottom, 0.f }, { 1.f, 0.f, 0.f }, { webtexture, 0.f }}, - {{ 1.f, neck, 0.f }, { 1.f, 0.f, 0.f }, { webtexture, 0.f }}, - {{ 1.f, neck, 0.f }, { 0.f, 1.f, 0.f }, { webtexture, 0.f }}, - {{ -1.f, neck, 0.f }, { 0.f, 1.f, 0.f }, { webtexture, 0.f }}, - {{ -1.f, neck, 0.f }, { -1.f, 0.f, 0.f }, { webtexture, 0.f }}, - {{ -1.f, bottom, 0.f }, { -1.f, 0.f, 0.f }, { webtexture, 0.f }} }; - create_track_rail_profile( drawing.fillers[ 0 ], drawing.fillers[ 1 ], &filler ); - for( auto &shape : drawing.fillers ) { - shape.resize( filler.size() ); - } + sides( box_profile( bottom, neck, webtexture ), drawing.fillers ); } return drawing; } diff --git a/world/RailMaterials.cpp b/world/RailMaterials.cpp index 50e402e5..b3a94b09 100644 --- a/world/RailMaterials.cpp +++ b/world/RailMaterials.cpp @@ -26,6 +26,101 @@ material_handle tint_material( material_handle const Material, glm::vec3 const & return GfxRenderer->Tinted_Material( Material, Colour ); } +namespace { + +struct colour_mean { + double sum { 0.0 }; + glm::dvec3 colour { 0.0 }; + std::size_t count { 0 }; + + void add( float const Red, float const Green, float const Blue ) { + sum += 0.2126 * Red + 0.7152 * Green + 0.0722 * Blue; + colour += glm::pow( glm::dvec3{ Red, Green, Blue } / 255.0, glm::dvec3{ 2.2 } ); + ++count; } +}; + +std::pair band_columns( glm::vec2 const &Band, int const Width ) { + + auto const first { std::clamp( static_cast( std::floor( std::min( Band.x, Band.y ) * Width ) ), 0, Width - 1 ) }; + auto const last { std::clamp( static_cast( std::ceil( std::max( Band.x, Band.y ) * Width ) ) - 1, first, Width - 1 ) }; + return { first, last }; +} + +glm::vec3 unpack_565( unsigned const Color ) { + + return glm::vec3{ ( ( Color >> 11 ) & 31 ) * 255.f / 31.f, ( ( Color >> 5 ) & 63 ) * 255.f / 63.f, ( Color & 31 ) * 255.f / 31.f }; +} + +void add_dxt_block( unsigned char const *Block, bool const Alpha, int const Column, int const First, int const Last, colour_mean &Mean ) { + + auto const low { static_cast( Block[ 0 ] | Block[ 1 ] << 8 ) }; + auto const top { static_cast( Block[ 2 ] | Block[ 3 ] << 8 ) }; + auto const opaque { Alpha || low > top }; + glm::vec3 const colors[ 4 ] { + unpack_565( low ), + unpack_565( top ), + opaque ? ( 2.f * unpack_565( low ) + unpack_565( top ) ) / 3.f : 0.5f * ( unpack_565( low ) + unpack_565( top ) ), + opaque ? ( unpack_565( low ) + 2.f * unpack_565( top ) ) / 3.f : glm::vec3{ 0.f } }; + auto const bits { static_cast( Block[ 4 ] ) | static_cast( Block[ 5 ] ) << 8 | static_cast( Block[ 6 ] ) << 16 | static_cast( Block[ 7 ] ) << 24 }; + for( int texel = 0; texel < 16; ++texel ) { + auto const x { Column * 4 + texel % 4 }; + auto const index { ( bits >> ( 2 * texel ) ) & 3 }; + if( x < First || x > Last || ( false == opaque && index == 3 ) ) { + continue; + } + Mean.add( colors[ index ].r, colors[ index ].g, colors[ index ].b ); + } +} + +void sample_dds( std::string const &File, glm::vec2 const &Band, colour_mean &Mean ) { + + std::ifstream input( File, std::ios::binary ); + std::array header {}; + if( !input.read( reinterpret_cast( header.data() ), header.size() ) || false == std::equal( header.begin(), header.begin() + 4, "DDS " ) ) { + return; + } + auto const word = [&]( std::size_t const Offset ) { + return static_cast( header[ Offset ] ) | static_cast( header[ Offset + 1 ] ) << 8 | static_cast( header[ Offset + 2 ] ) << 16 | static_cast( header[ Offset + 3 ] ) << 24; }; + auto const height { static_cast( word( 12 ) ) }; + auto const width { static_cast( word( 16 ) ) }; + auto const fourcc { word( 84 ) }; + auto const blocksize { fourcc == 0x31545844u ? 8 : ( fourcc == 0x33545844u || fourcc == 0x35545844u ) ? 16 : 0 }; + if( blocksize == 0 || width <= 0 || height <= 0 ) { + return; + } + auto const wide { ( width + 3 ) / 4 }; + auto const high { ( height + 3 ) / 4 }; + std::vector data( static_cast( wide ) * high * blocksize ); + if( !input.read( reinterpret_cast( data.data() ), data.size() ) ) { + return; + } + auto const [ first, last ] { band_columns( Band, width ) }; + for( int row = 0; row < high; ++row ) { + for( int column = first / 4; column <= last / 4; ++column ) { + add_dxt_block( &data[ ( static_cast( row ) * wide + column ) * blocksize + ( blocksize - 8 ) ], blocksize == 16, column, first, last, Mean ); + } + } +} + +void sample_image( std::string const &File, glm::vec2 const &Band, colour_mean &Mean ) { + + int width, height, components; + auto *pixels { stbi_load( File.c_str(), &width, &height, &components, 4 ) }; + if( pixels == nullptr ) { + return; + } + auto const [ first, last ] { band_columns( Band, width ) }; + for( int y = 0; y < height; ++y ) { + for( int x = first; x <= last; ++x ) { + auto const *pixel { &pixels[ ( static_cast( y ) * width + x ) * 4 ] }; + Mean.add( pixel[ 0 ], pixel[ 1 ], pixel[ 2 ] ); + } + } + stbi_image_free( pixels ); +} + +} // namespace + std::pair const &tread_sample( material_handle const Material, glm::vec2 const &Band ) { static std::map, std::pair> samples; @@ -45,76 +140,15 @@ std::pair const &tread_sample( material_handle const Material, auto const &image { GfxRenderer->Texture( texture ) }; auto const type { std::string( image.get_type() ) }; auto const file { std::string( image.get_name() ) + type }; - auto const columns = [&]( int const Width ) { - auto const first { std::clamp( static_cast( std::floor( std::min( Band.x, Band.y ) * Width ) ), 0, Width - 1 ) }; - auto const last { std::clamp( static_cast( std::ceil( std::max( Band.x, Band.y ) * Width ) ) - 1, first, Width - 1 ) }; - return std::pair{ first, last }; }; - auto sum { 0.0 }; - glm::dvec3 colour { 0.0 }; - std::size_t count { 0 }; - auto const add = [&]( float const Red, float const Green, float const Blue ) { - sum += 0.2126 * Red + 0.7152 * Green + 0.0722 * Blue; - colour += glm::pow( glm::dvec3{ Red, Green, Blue } / 255.0, glm::dvec3{ 2.2 } ); - ++count; }; + colour_mean mean; if( type == ".dds" ) { - std::ifstream input( file, std::ios::binary ); - std::array header {}; - if( input.read( reinterpret_cast( header.data() ), header.size() ) && std::equal( header.begin(), header.begin() + 4, "DDS " ) ) { - auto const word = [&]( std::size_t const Offset ) { - return static_cast( header[ Offset ] ) | static_cast( header[ Offset + 1 ] ) << 8 | static_cast( header[ Offset + 2 ] ) << 16 | static_cast( header[ Offset + 3 ] ) << 24; }; - auto const height { static_cast( word( 12 ) ) }; - auto const width { static_cast( word( 16 ) ) }; - auto const fourcc { word( 84 ) }; - auto const blocksize { fourcc == 0x31545844u ? 8 : ( fourcc == 0x33545844u || fourcc == 0x35545844u ) ? 16 : 0 }; - if( blocksize > 0 && width > 0 && height > 0 ) { - auto const wide { ( width + 3 ) / 4 }; - auto const high { ( height + 3 ) / 4 }; - std::vector data( static_cast( wide ) * high * blocksize ); - if( input.read( reinterpret_cast( data.data() ), data.size() ) ) { - auto const [ first, last ] { columns( width ) }; - auto const unpack = []( unsigned const Color ) { - return glm::vec3{ ( ( Color >> 11 ) & 31 ) * 255.f / 31.f, ( ( Color >> 5 ) & 63 ) * 255.f / 63.f, ( Color & 31 ) * 255.f / 31.f }; }; - for( int row = 0; row < high; ++row ) { - for( int column = first / 4; column <= last / 4; ++column ) { - auto const *block { &data[ ( static_cast( row ) * wide + column ) * blocksize + ( blocksize - 8 ) ] }; - auto const low { static_cast( block[ 0 ] | block[ 1 ] << 8 ) }; - auto const top { static_cast( block[ 2 ] | block[ 3 ] << 8 ) }; - auto const opaque { blocksize == 16 || low > top }; - glm::vec3 const colors[ 4 ] { - unpack( low ), - unpack( top ), - opaque ? ( 2.f * unpack( low ) + unpack( top ) ) / 3.f : 0.5f * ( unpack( low ) + unpack( top ) ), - opaque ? ( unpack( low ) + 2.f * unpack( top ) ) / 3.f : glm::vec3{ 0.f } }; - auto const bits { static_cast( block[ 4 ] ) | static_cast( block[ 5 ] ) << 8 | static_cast( block[ 6 ] ) << 16 | static_cast( block[ 7 ] ) << 24 }; - for( int texel = 0; texel < 16; ++texel ) { - auto const x { column * 4 + texel % 4 }; - auto const index { ( bits >> ( 2 * texel ) ) & 3 }; - if( x < first || x > last || ( false == opaque && index == 3 ) ) { - continue; - } - add( colors[ index ].r, colors[ index ].g, colors[ index ].b ); - } - } - } - } - } - } + sample_dds( file, Band, mean ); } else { - int width, height, components; - if( auto *pixels { stbi_load( file.c_str(), &width, &height, &components, 4 ) } ) { - auto const [ first, last ] { columns( width ) }; - for( int y = 0; y < height; ++y ) { - for( int x = first; x <= last; ++x ) { - auto const *pixel { &pixels[ ( static_cast( y ) * width + x ) * 4 ] }; - add( pixel[ 0 ], pixel[ 1 ], pixel[ 2 ] ); - } - } - stbi_image_free( pixels ); - } + sample_image( file, Band, mean ); } - if( count > 0 ) { - result = { static_cast( sum / count ), glm::vec3{ colour / static_cast( count ) } }; + if( mean.count > 0 ) { + result = { static_cast( mean.sum / mean.count ), glm::vec3{ mean.colour / static_cast( mean.count ) } }; } return result; } From 08d439a2f6243379a313093db2e6bcc15d34a91b Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 09:57:04 +0200 Subject: [PATCH 10/19] refactor(track): the cap of a lofted rail end takes its texture from cap_texture and its triangles from ear_clip instead of computing both inside cap_loft --- world/RailLayer.cpp | 152 +++++++++++++++++++++++++------------------- 1 file changed, 85 insertions(+), 67 deletions(-) diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp index 5e121032..6aec8a62 100644 --- a/world/RailLayer.cpp +++ b/world/RailLayer.cpp @@ -14,7 +14,90 @@ http://mozilla.org/MPL/2.0/. namespace track_rails { +namespace { + // in the colour of the top of the rail foot +glm::vec2 cap_texture( std::vector const &Ring, std::vector const &Plan ) { + + auto bottom { 0.0 }, top { 0.0 }; + for( auto const &point : Plan ) { + bottom = std::min( bottom, point.y ); + top = std::max( top, point.y ); + } + auto web { Ring[ 0 ].texture }; + auto nearest { std::numeric_limits::max() }; + for( std::size_t j = 0; j < Ring.size() / 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.size() / 2; ++j ) { + if( Ring[ j ].normal.y > 0.5f && Plan[ j ].y < lowest ) { + lowest = Plan[ j ].y; + web = Ring[ j ].texture; + } + } + return web; +} + +std::vector> ear_clip( std::vector const &Polygon ) { + + 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 < Polygon.size(); ++j ) { + auto const &a { Polygon[ j ] }; + auto const &b { Polygon[ ( j + 1 ) % Polygon.size() ] }; + area += a.x * b.y - b.x * a.y; + } + auto const orientation { area < 0.0 ? -1.0 : 1.0 }; + std::vector left( Polygon.size() ); + for( std::size_t j = 0; j < Polygon.size(); ++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 { Polygon[ left[ ( j + left.size() - 1 ) % left.size() ] ] }; + auto const &b { Polygon[ left[ j ] ] }; + auto const &c { Polygon[ 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 { Polygon[ 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 ] } ); + return triangles; +} + +} // namespace + 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 ) { @@ -48,77 +131,12 @@ void cap_loft( gfx::vertex_array const &Strip, std::size_t const Ring, bool cons } 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 ] } ); + auto const web { cap_texture( ring, plan ) }; + auto const triangles { ear_clip( plan ) }; for( auto const &triangle : triangles ) { for( auto const &order : { std::array{ 0, 1, 2 }, std::array{ 0, 2, 1 } } ) { for( auto const k : order ) { From 4016c4ce04ade4fee2afde24141e0184676e8c28 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 10:53:28 +0200 Subject: [PATCH 11/19] feat(track): the guard rails of a crossing at a wide angle reach a metre past the rails of the other track instead of 0.3 m and open their flangeway over 0.6 m at the ends, the way the guard arms of the corners of a diamond run out on real crossings, the reach and the flare are the constants guard_reach and guard_flare shared by the plan and the layer --- world/RailLayer.cpp | 2 +- world/RailLayout.h | 2 ++ world/RailPlan.cpp | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp index 6aec8a62..9c3f2bbb 100644 --- a/world/RailLayer.cpp +++ b/world/RailLayer.cpp @@ -563,7 +563,7 @@ void TTrack::rail_layer::lay_guards() { auto const outside { Miter.x + Miter.y * ( top + m_guardoffset ) }; return Latest ? std::max( inside, outside ) : std::min( inside, outside ); }; for( auto const &guard : ours( m_layout.guards ) ) { - auto const offsets { flared( guard.from, guard.to, std::min( 0.3, 0.25 * ( guard.to - guard.from ) ), m_guardoffset, guard.flarefrom ? 0.02 : 0.0, guard.flareto ? 0.02 : 0.0 ) }; + auto const offsets { flared( guard.from, guard.to, std::min( guard_flare, 0.25 * ( guard.to - guard.from ) ), m_guardoffset, guard.flarefrom ? 0.02 : 0.0, guard.flareto ? 0.02 : 0.0 ) }; auto cuts { guard.plan.cuts }; sort_cuts( cuts ); auto start { guard.from }; diff --git a/world/RailLayout.h b/world/RailLayout.h index b56859c7..25fc56d5 100644 --- a/world/RailLayout.h +++ b/world/RailLayout.h @@ -28,6 +28,8 @@ 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 guard_reach { 1.0 }; +double constexpr guard_flare { 0.6 }; // where the inner rails of the two paths of a switch cross, found from their gauge lines behind the blades struct switch_frog { diff --git a/world/RailPlan.cpp b/world/RailPlan.cpp index 2bae47c6..313491f4 100644 --- a/world/RailPlan.cpp +++ b/world/RailPlan.cpp @@ -150,7 +150,7 @@ std::vector glue_line( std::vector const &Outline, doubl track_rails::rail_fill guard_fill( track_rails::guard_rail const &Guard, track_rails::rail_size const &Size ) { auto const offset { Size.web - ( flangeway + Size.head ) }; - auto const flare { std::min( 0.3, 0.25 * ( Guard.to - Guard.from ) ) }; + auto const flare { std::min( guard_flare, 0.25 * ( Guard.to - Guard.from ) ) }; track_rails::rail_fill fill; fill.web = true; fill.from = Guard.from; @@ -448,7 +448,7 @@ void TTrack::rail_planner::cross_wide( rail_crossing const &Crossing, rail_size if( through ) { add_tread( Crossing.path, Crossing.side, shared_span( Crossing, Other ), *Crossing.track, Crossing.otherpath ); } - auto const reach { ( flangeway + m_size.head + 0.3 ) / std::abs( Crossing.into ) }; + auto const reach { ( flangeway + m_size.head + guard_reach ) / std::abs( Crossing.into ) }; track_rails::guard_rail guard { Crossing.at - reach, Crossing.at + reach }; guard.through = through; wide( guard.plan, Crossing, { -Other.head, through ? flangeway : flangeway + Other.head, m_guardoffset, m_guardoffset + m_size.head, glue_line( Other.field, 0.0 ), through ? knots::line {} : glue_line( Other.gauge, flangeway + Other.head ) } ); From f03c6b8ebc0a1fa7a8b6cd5110d98a238b8ce6c9 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 11:08:02 +0200 Subject: [PATCH 12/19] feat(track): at a crossing at a wide angle every head of one track meets every head of the other in a mitre from the corner of their flangeways to the far corner of the heads, the way the corners of a cast diamond look, so the running rails and guard rails of both tracks form closed corners and only the flangeways stay open, instead of the rails of one track going through and the other ones glued to their sides; corner_cut gives the mitres, as bends of the running rails and as miters of the guard rails; the fit of a loft end to the profile of another rail and the field and gauge outlines of the rail profile are gone with the gluing --- world/RailLayer.cpp | 15 ++++--------- world/RailLayout.h | 2 -- world/RailPlan.cpp | 50 ++++++++++++++++++------------------------- world/RailProfile.cpp | 30 -------------------------- world/Segment.cpp | 25 ++++++---------------- world/Segment.h | 1 - 6 files changed, 31 insertions(+), 92 deletions(-) diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp index 9c3f2bbb..4d6b07ed 100644 --- a/world/RailLayer.cpp +++ b/world/RailLayer.cpp @@ -568,11 +568,9 @@ void TTrack::rail_layer::lay_guards() { sort_cuts( cuts ); auto start { guard.from }; auto startmiter { guard.frommiter }; - rail_cut startcut; 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 endcut { i < cuts.size() ? cuts[ i ] : rail_cut {} }; auto const a { std::max( { mitered( startmiter, start, false ), m_from, 0.0 } ) }; auto const b { std::min( { mitered( endmiter, end, true ), m_length, mitered( guard.tomiter, guard.to, true ) } ) }; if( b - a > 0.01 ) { @@ -583,17 +581,14 @@ void TTrack::rail_layer::lay_guards() { ( ending == ( miter.y < 0.0 ) ? uppers : lowers ).emplace_back( 1.0 / miter.y, -miter.x / miter.y - m_guardoffset ); } } - auto const glued { a == start && false == startcut.startfit.empty() }; - auto const gluedend { b == end && false == endcut.endfit.empty() }; piece( m_profile, - { .from = a, .to = b, .offsets = offsets, .extents = envelope( uppers, true, a, b ), .inner = envelope( lowers, false, a, b ), .capfrom = false == glued, .capto = false == gluedend, .rolled = false }, - { a, b, { glued ? startcut.startbend : 0.0, glued ? startcut.startfit : knots::line {} }, { gluedend ? endcut.endbend : 0.0, gluedend ? endcut.endfit : knots::line {} } } ); + { .from = a, .to = b, .offsets = offsets, .extents = envelope( uppers, true, a, b ), .inner = envelope( lowers, false, a, b ), .capfrom = true, .capto = true, .rolled = false }, + { a, b, {}, {} } ); } if( i < cuts.size() && cuts[ i ].start > start ) { start = cuts[ i ].start; startmiter = cuts[ i ].startmiter; - startcut = cuts[ i ]; } } } @@ -639,7 +634,6 @@ void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vector m_from ? startbend : 0.0, start > m_from ? startfit : knots::line {} }, - { end < m_length ? Cuts[ i ].endbend : 0.0, end < m_length ? Cuts[ i ].endfit : knots::line {} } }; + { start > m_from ? startbend : 0.0 }, + { end < m_length ? Cuts[ i ].endbend : 0.0 } }; if( b - a > 0.01 ) { std::vector active; for( auto const &tread : Treads ) { @@ -714,7 +708,6 @@ void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vector 0.0 ? Cut.from - 0.005 : Cut.to + 0.005 ) / into, facing / into }; cut.startmiter = { at + ( into > 0.0 ? Cut.to + 0.005 : Cut.from - 0.005 ) / into, facing / into }; - auto const glue = [&]( knots::line const &Line, double const Edge, double &Position, glm::dvec2 &Miter, double &Bend, knots::line &Fit, bool &Cap ) { - if( Line.empty() ) { - return; - } - Position = at + Edge / into; - Miter = {}; - Bend = facing / into; - for( auto const &knot : Line ) { - Fit.emplace_back( knot.x, ( knot.y - Edge ) / into ); - } - Cap = false; }; - glue( into > 0.0 ? Cut.fromline : Cut.toline, into > 0.0 ? Cut.from : Cut.to, cut.end, cut.endmiter, cut.endbend, cut.endfit, cut.endcap ); - glue( into > 0.0 ? Cut.toline : Cut.fromline, into > 0.0 ? Cut.to : Cut.from, cut.start, cut.startmiter, cut.startbend, cut.startfit, cut.startcap ); Plan.cuts.push_back( cut ); } -std::vector glue_line( std::vector const &Outline, double const Base ) { +track_rails::rail_cut corner_cut( track_rails::rail_crossing const &Crossing, double const Face, double const Toward, double const Head, double const Other ) { - std::vector line; - for( auto const &knot : Outline ) { - line.emplace_back( knot.x, Base - knot.y ); - } - return line; + auto const mitre = [&]( double const Edge, double const Flange ) { + auto const slope { Flange * Other / ( Toward * Head ) }; + return glm::dvec2 { Crossing.at + ( Edge - Face * slope ) / Crossing.into, ( slope + Crossing.facing ) / Crossing.into }; }; + auto const rail { mitre( 0.0, 1.0 ) }; + auto const guard { mitre( flangeway, -1.0 ) }; + auto const rising { Crossing.into > 0.0 }; + track_rails::rail_cut cut; + cut.at = Crossing.at; + cut.endmiter = ( rising ? rail : guard ); + cut.startmiter = ( rising ? guard : rail ); + cut.end = cut.endmiter.x + cut.endmiter.y * Face; + cut.start = cut.startmiter.x + cut.startmiter.y * Face; + cut.endbend = cut.endmiter.y; + cut.startbend = cut.startmiter.y; + return cut; } track_rails::rail_fill guard_fill( track_rails::guard_rail const &Guard, track_rails::rail_size const &Size ) { @@ -443,15 +438,12 @@ void TTrack::rail_planner::cross_grooved( rail_crossing const &Crossing, rail_si void TTrack::rail_planner::cross_wide( rail_crossing const &Crossing, rail_size const &Other ) { - auto const through { goes_through( m_track, *Crossing.track ) }; - wide( plan_of( Crossing ), Crossing, { through ? 0.0 : -Other.head, flangeway, 0.0, m_size.head, through ? knots::line {} : glue_line( Other.field, 0.0 ) } ); - if( through ) { - add_tread( Crossing.path, Crossing.side, shared_span( Crossing, Other ), *Crossing.track, Crossing.otherpath ); - } + plan_of( Crossing ).cuts.push_back( corner_cut( Crossing, 0.0, -1.0, m_size.head, Other.head ) ); + add_tread( Crossing.path, Crossing.side, shared_span( Crossing, Other ), *Crossing.track, Crossing.otherpath ); auto const reach { ( flangeway + m_size.head + guard_reach ) / std::abs( Crossing.into ) }; track_rails::guard_rail guard { Crossing.at - reach, Crossing.at + reach }; - guard.through = through; - wide( guard.plan, Crossing, { -Other.head, through ? flangeway : flangeway + Other.head, m_guardoffset, m_guardoffset + m_size.head, glue_line( Other.field, 0.0 ), through ? knots::line {} : glue_line( Other.gauge, flangeway + Other.head ) } ); + guard.through = goes_through( m_track, *Crossing.track ); + guard.plan.cuts.push_back( corner_cut( Crossing, -flangeway, 1.0, m_size.head, Other.head ) ); merge_guard( m_layout.guards[ Crossing.path ][ Crossing.side > 0 ], guard ); } diff --git a/world/RailProfile.cpp b/world/RailProfile.cpp index c3a01cfd..e8d5b244 100644 --- a/world/RailProfile.cpp +++ b/world/RailProfile.cpp @@ -33,36 +33,6 @@ rail_size measure_rail( gfx::vertex_array const &Profile ) { } result.height = top - bottom; result.crest = top; - std::size_t firsttop { count }; - std::size_t lasttop { 0 }; - for( std::size_t i = 0; i < count; ++i ) { - if( Profile[ i ].position.y > top - 1e-4 ) { - firsttop = std::min( firsttop, i ); - lasttop = std::max( lasttop, i ); - } - } - knots::line sides[ 2 ]; - for( std::size_t i = 0; i < count; ++i ) { - auto const &point { Profile[ i ].position }; - if( i <= firsttop ) { - sides[ 0 ].emplace_back( point.y, point.x ); - } - if( i >= lasttop ) { - sides[ 1 ].emplace_back( point.y, point.x ); - } - } - for( auto &side : sides ) { - knots::sort( side ); - } - auto const mean = []( knots::line const &Side ) { - auto sum { 0.0 }; - for( auto const &knot : Side ) { - sum += knot.y; - } - return sum / std::max( Side.size(), 1 ); }; - auto const fieldfirst { mean( sides[ 0 ] ) > mean( sides[ 1 ] ) }; - result.field = sides[ fieldfirst ? 0 : 1 ]; - result.gauge = sides[ fieldfirst ? 1 : 0 ]; auto const middle { top - 0.5 * result.height }; auto webfrom { 1e9 }; auto webto { -1e9 }; diff --git a/world/Segment.cpp b/world/Segment.cpp index d52113c7..c5389f13 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -616,23 +616,13 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig } 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() ); - if( Span.fromend.bend != 0.0 || Span.toend.bend != 0.0 || false == Span.fromend.fit.empty() || false == Span.toend.fit.empty() ) { + if( Span.fromend.bend != 0.0 || Span.toend.bend != 0.0 ) { auto widest { 0.0 }; for( auto const &point : ShapePoints ) { widest = std::max( widest, std::abs( static_cast( Span.side * point.position.x - Span.halfwidth ) ) ); } - auto offsetmost { 0.0 }; - for( auto const &knot : Span.offsets ) { - offsetmost = std::max( offsetmost, std::abs( knot.y ) ); - } - auto const margin = [&]( loft_end const &End ) { - auto fitmost { 0.0 }; - for( auto const &knot : End.fit ) { - fitmost = std::max( fitmost, std::abs( knot.y ) ); - } - return std::abs( End.bend ) * ( End.fit.empty() ? widest : widest + offsetmost ) + fitmost + 0.001; }; - auto const frommargin { margin( Span.fromend ) }; - auto const tomargin { margin( Span.toend ) }; + auto const frommargin { std::abs( Span.fromend.bend ) * widest + 0.001 }; + auto const tomargin { std::abs( Span.toend.bend ) * widest + 0.001 }; stations.erase( std::remove_if( stations.begin(), stations.end(), [&]( double const Distance ) { return ( Distance > Span.from && Distance < Span.to ) && ( Distance < Span.from + frommargin || Distance > Span.to - tomargin ); } ), stations.end() ); @@ -671,17 +661,14 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig static loft_end const unshaped; auto const &shaped { Distance <= Span.from ? Span.fromend : Distance >= Span.to ? Span.toend : unshaped }; auto const bend { shaped.bend }; - auto const &fit { shaped.fit }; auto along { glm::vec3 { 0.f } }; - auto forward { glm::vec3 { 0.f } }; - if( bend != 0.0 || false == fit.empty() ) { + if( bend != 0.0 ) { glm::dvec3 ahead, behind, unused; plan_frame( std::min( Distance + 0.001, static_cast( fLength ) ), ahead, unused ); plan_frame( std::max( Distance - 0.001, 0.0 ), behind, unused ); auto const tangent { glm::dvec3 { ahead.x - behind.x, 0.0, ahead.z - behind.z } }; if( glm::length2( tangent ) > 0.0 ) { - forward = glm::vec3 { glm::normalize( tangent ) }; - along = forward * static_cast( bend ); + along = glm::vec3 { glm::normalize( tangent ) } * static_cast( bend ); } } Ring.clear(); @@ -763,7 +750,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig wall = ( side > 0 ? outerwall : innerwall ); } } - auto pt { parallel * static_cast( Span.side * ( Span.halfwidth + outside + offset ) ) + position + along * static_cast( outside + offset ) + forward * static_cast( knots::sample( fit, shape.position.y, 0.0 ) ) }; + auto pt { parallel * static_cast( Span.side * ( Span.halfwidth + outside + offset ) ) + position + along * static_cast( outside + offset ) }; pt.y += shape.position.y + Span.lift; auto norm { shape.normal.x * parallel }; norm.y += shape.normal.y; diff --git a/world/Segment.h b/world/Segment.h index 971794c3..afe0953c 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -24,7 +24,6 @@ struct map_colored_paths { struct loft_end { double bend { 0.0 }; - knots::line fit; }; struct loft_span { From ae4e828ed2a4002ed60cae957d9d19768bfa9a2c Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 11:25:34 +0200 Subject: [PATCH 13/19] feat(track): at a crossing at a wide angle the tread of the other track is laid where its wheels roll over the mitred corners: the part of a guard rail under the running rail of the other track becomes a running surface in the shared tread material instead of matte, and running rails of different materials share the tread over the rolled band of the other track instead of over a square span; wide_tread draws the edges of the band from the crossing itself, since the gauge lines meet in one point at right angles, and a tread gives its own fade, widened for bands lying across the rail; treaded keeps the matte parts of a guard rail matte, lay_treads splits guard rails at their treads, and a piece of no triangles is not inserted --- world/RailLayer.cpp | 64 +++++++++++++++++++++++++++++++++------------ world/RailLayout.h | 20 +++++++------- world/RailPlan.cpp | 30 ++++++++++++++++++++- 3 files changed, 87 insertions(+), 27 deletions(-) diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp index 4d6b07ed..26479d94 100644 --- a/world/RailLayer.cpp +++ b/world/RailLayer.cpp @@ -465,6 +465,7 @@ private: void lay_guards(); void lay_checks(); void lay_fills(); + void lay_treads( rail_piece const &Whole, std::vector const &Treads ); void lay_cuts( gfx::vertex_array const &Shape, std::vector Cuts, bool const Wheels, std::vector const &Treads, float const Wallu = -1.f ); void lay_grooved( gfx::vertex_array const &Outline ); grooved_profile split_grooved( gfx::vertex_array const &Outline ) const; @@ -581,10 +582,7 @@ void TTrack::rail_layer::lay_guards() { ( ending == ( miter.y < 0.0 ) ? uppers : lowers ).emplace_back( 1.0 / miter.y, -miter.x / miter.y - m_guardoffset ); } } - piece( - m_profile, - { .from = a, .to = b, .offsets = offsets, .extents = envelope( uppers, true, a, b ), .inner = envelope( lowers, false, a, b ), .capfrom = true, .capto = true, .rolled = false }, - { a, b, {}, {} } ); + lay_treads( { .from = a, .to = b, .offsets = offsets, .extents = envelope( uppers, true, a, b ), .inner = envelope( lowers, false, a, b ), .capfrom = true, .capto = true, .rolled = false }, guard.treads ); } if( i < cuts.size() && cuts[ i ].start > start ) { start = cuts[ i ].start; @@ -614,6 +612,34 @@ void TTrack::rail_layer::lay_checks() { } } +void TTrack::rail_layer::lay_treads( rail_piece const &Whole, std::vector const &Treads ) { + + std::vector splits { Whole.from, Whole.to }; + for( auto const &tread : Treads ) { + for( auto const edge : { tread.from, tread.to } ) { + if( edge > Whole.from + 0.01 && edge < Whole.to - 0.01 ) { + splits.push_back( edge ); + } + } + } + std::sort( splits.begin(), splits.end() ); + piece_ends const ends { Whole.from, Whole.to }; + for( std::size_t k = 1; k < splits.size(); ++k ) { + auto part { Whole }; + part.from = splits[ k - 1 ]; + part.to = splits[ k ]; + part.capfrom = ( k == 1 && Whole.capfrom ); + part.capto = ( k + 1 == splits.size() && Whole.capto ); + auto const *tread { tread_at( Treads, 0.5 * ( part.from + part.to ) ) }; + if( tread != nullptr && m_topfirst < m_toplast ) { + treaded( part, *tread, ends ); + } + else { + piece( m_profile, part, ends ); + } + } +} + void TTrack::rail_layer::lay_fills() { for( auto const &fill : ours( m_layout.fills ) ) { @@ -923,33 +949,34 @@ void TTrack::rail_layer::treaded( rail_piece Whole, rail_tread const &Tread, pie auto const colour { 0.5f * ( tread_colour( Tread.material, m_track.tread_band() ) + tread_colour( own, m_track.tread_band() ) ) }; auto const tint = [&]( material_handle const Material, glm::vec3 const &Mark ) { return paint( Material, marked ? Mark : colour ); }; - auto const owntint { tint( own, { 1.f, 0.f, 0.f } ) }; + auto const owntint { Whole.rolled ? tint( own, { 1.f, 0.f, 0.f } ) : null_handle }; auto const sharedtint { tint( Tread.material, { 0.f, 1.f, 0.f } ) }; auto const shared { marked ? tint( Tread.material, { 0.f, 0.f, 1.f } ) : Tread.material }; auto const ramp = [&]( knots::line const &Knots ) { return marked ? knots::line {} : Knots; }; + auto const fade { Tread.fade }; auto const centre { knots::middle( lower, upper ) }; - auto const beneath { knots::shifted( lower, -tread_fade ) }; - auto const beyond { knots::shifted( upper, tread_fade ) }; - auto const lowfade { knots::bound( knots::shifted( lower, tread_fade ), centre, true ) }; - auto const highfade { knots::bound( knots::shifted( upper, -tread_fade ), centre, false ) }; + auto const beneath { knots::shifted( lower, -fade ) }; + auto const beyond { knots::shifted( upper, fade ) }; + auto const lowfade { knots::bound( knots::shifted( lower, fade ), centre, true ) }; + auto const highfade { knots::bound( knots::shifted( upper, -fade ), centre, false ) }; auto splits { cross_splits( Whole, reach, { &lower, &upper, &lowfade, &highfade, &beneath, &beyond }, 0.001 ) }; - add_meetings( knots::shifted( lower, 2.0 * tread_fade ), upper, Whole.from, Whole.to, 0.001, splits ); + add_meetings( knots::shifted( lower, 2.0 * fade ), upper, Whole.from, Whole.to, 0.001, splits ); std::sort( splits.begin(), splits.end() ); lay_top( top, Whole, reach, splits, { - { null_handle, knots::bound( Whole.extents, beneath, true ), Whole.inner }, - { owntint, knots::bound( Whole.extents, lower, true ), knots::bound( Whole.inner, beneath, false ), false, ramp( lower ), tread_fade }, - { sharedtint, knots::bound( Whole.extents, lowfade, true ), knots::bound( Whole.inner, lower, false ), false, ramp( lower ), -tread_fade }, + { null_handle, knots::bound( Whole.extents, beneath, true ), Whole.inner, Whole.rolled }, + { owntint, knots::bound( Whole.extents, lower, true ), knots::bound( Whole.inner, beneath, false ), false, ramp( lower ), fade }, + { sharedtint, knots::bound( Whole.extents, lowfade, true ), knots::bound( Whole.inner, lower, false ), false, ramp( lower ), -fade }, { shared, knots::bound( Whole.extents, highfade, true ), knots::bound( Whole.inner, lowfade, false ), false == marked }, - { sharedtint, knots::bound( Whole.extents, upper, true ), knots::bound( Whole.inner, highfade, false ), false, ramp( upper ), tread_fade }, - { owntint, knots::bound( Whole.extents, beyond, true ), knots::bound( Whole.inner, upper, false ), false, ramp( upper ), -tread_fade }, - { null_handle, Whole.extents, knots::bound( Whole.inner, beyond, false ) } }, + { sharedtint, knots::bound( Whole.extents, upper, true ), knots::bound( Whole.inner, highfade, false ), false, ramp( upper ), fade }, + { owntint, knots::bound( Whole.extents, beyond, true ), knots::bound( Whole.inner, upper, false ), false, ramp( upper ), -fade }, + { null_handle, Whole.extents, knots::bound( Whole.inner, beyond, false ), Whole.rolled } }, [&]( double const Middle ) { auto const from { knots::sample( lower, Middle, 0.0 ) }; auto const to { knots::sample( upper, Middle, 0.0 ) }; - return std::vector { from - tread_fade, from, knots::sample( lowfade, Middle, 0.0 ), knots::sample( highfade, Middle, 0.0 ), to, to + tread_fade }; }, + return std::vector { from - fade, from, knots::sample( lowfade, Middle, 0.0 ), knots::sample( highfade, Middle, 0.0 ), to, to + fade }; }, Ends ); } @@ -1012,6 +1039,9 @@ void TTrack::rail_layer::piece( gfx::vertex_array const &Shape, rail_piece const } } } + if( triangles.empty() ) { + return; + } sink( Piece.material, false == Piece.rolled ).emplace_back( GfxRenderer->Insert( triangles, triangleuserdata, m_drawing.bank, GL_TRIANGLES ) ); } diff --git a/world/RailLayout.h b/world/RailLayout.h index 062bc557..efbd0b2a 100644 --- a/world/RailLayout.h +++ b/world/RailLayout.h @@ -101,6 +101,16 @@ struct rail_cut { struct rail_plan { std::vector cuts; }; +struct rail_tread { + double from { 0.0 }; + double to { 0.0 }; + material_handle material { 0 }; + int anchor { 0 }; + knots::line edge; + knots::line gauge; + bool below { false }; + double fade { tread_fade }; +}; // 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 }; @@ -111,6 +121,7 @@ struct guard_rail { glm::dvec2 frommiter { 0.0 }; glm::dvec2 tomiter { 0.0 }; bool through { true }; + std::vector treads; }; struct rail_band { double at { 0.0 }; @@ -129,15 +140,6 @@ struct rail_fill { double lift { 0.0 }; bool web { false }; }; -struct rail_tread { - double from { 0.0 }; - double to { 0.0 }; - material_handle material { 0 }; - int anchor { 0 }; - knots::line edge; - knots::line gauge; - bool below { false }; -}; struct rail_layout { rail_plan plans[ 2 ][ 2 ]; // path, right side std::vector bands[ 2 ][ 2 ]; diff --git a/world/RailPlan.cpp b/world/RailPlan.cpp index 59fa3abc..2c17a0c3 100644 --- a/world/RailPlan.cpp +++ b/world/RailPlan.cpp @@ -212,6 +212,29 @@ void vee_fill( std::vector &Fills, double const At, doub Fills.push_back( fill ); } +track_rails::rail_tread wide_tread( track_rails::rail_crossing const &Crossing, material_handle const Material, double const Base, double const Head, double const Reach ) { + + auto const facing { std::abs( Crossing.facing ) > 0.02 ? Crossing.facing : std::copysign( 0.02, Crossing.facing ) }; + auto const slope { Crossing.into / facing }; + track_rails::rail_tread tread { 1e9, -1e9, Material }; + for( auto const across : { Base, Base + Head } ) { + for( auto const edge : { tread_fade, -Reach - tread_fade } ) { + auto const distance { Crossing.at + ( edge + Crossing.facing * across ) / Crossing.into }; + tread.from = std::min( tread.from, distance - 0.01 ); + tread.to = std::max( tread.to, distance + 0.01 ); + } + } + auto const line = [&]( double const Edge ) { + auto const across = [&]( double const Distance ) { + return ( ( Distance - Crossing.at ) * Crossing.into - Edge ) / facing - Base; }; + return knots::line { { tread.from, across( tread.from ) }, { tread.to, across( tread.to ) } }; }; + tread.gauge = line( 0.0 ); + tread.edge = line( -Reach ); + tread.below = ( facing > 0.0 ); + tread.fade = tread_fade * std::sqrt( 1.0 + slope * slope ); + return tread; +} + std::vector tread_edge( TTrack const &Track, int const Path, int const Side, TTrack const &Other, int const Otherpath, int const Otherside, double const From, double const To, bool &Below, std::vector &Gauge ) { auto const &ours { rail_line( Track, Path, Side > 0, false ) }; @@ -293,6 +316,7 @@ void merge_guard( std::vector &Guards, track_rails::gua span.to = std::max( span.to, Guard.to ); span.plan.cuts.insert( span.plan.cuts.end(), Guard.plan.cuts.begin(), Guard.plan.cuts.end() ); span.through = ( span.through && Guard.through ); + span.treads.insert( span.treads.end(), Guard.treads.begin(), Guard.treads.end() ); return; } } @@ -439,11 +463,15 @@ void TTrack::rail_planner::cross_grooved( rail_crossing const &Crossing, rail_si void TTrack::rail_planner::cross_wide( rail_crossing const &Crossing, rail_size const &Other ) { plan_of( Crossing ).cuts.push_back( corner_cut( Crossing, 0.0, -1.0, m_size.head, Other.head ) ); - add_tread( Crossing.path, Crossing.side, shared_span( Crossing, Other ), *Crossing.track, Crossing.otherpath ); + auto const material { m_track.shared_tread( Crossing.path, *Crossing.track, Crossing.otherpath ) }; + if( material != m_track.rail_material( Crossing.path ) ) { + m_layout.treads[ Crossing.path ][ Crossing.side > 0 ].push_back( wide_tread( Crossing, material, 0.0, m_size.head, rolled_band ) ); + } auto const reach { ( flangeway + m_size.head + guard_reach ) / std::abs( Crossing.into ) }; track_rails::guard_rail guard { Crossing.at - reach, Crossing.at + reach }; guard.through = goes_through( m_track, *Crossing.track ); guard.plan.cuts.push_back( corner_cut( Crossing, -flangeway, 1.0, m_size.head, Other.head ) ); + guard.treads.push_back( wide_tread( Crossing, material, m_guardoffset, m_size.head, Other.head ) ); merge_guard( m_layout.guards[ Crossing.path ][ Crossing.side > 0 ], guard ); } From c5df55802fed4a69b348b98239e795ce8b2d4862 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 11:41:35 +0200 Subject: [PATCH 14/19] fix(track): the part of a guard rail under the running rail of the other track at a wide crossing takes the rail material of the other track, whose wheels alone roll there, instead of the brighter of the two, so a guard of a used track under an unused rail stays rusty, and it ends at the head of that rail without a tinted fade, since the guard itself is never rolled on --- world/RailPlan.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/world/RailPlan.cpp b/world/RailPlan.cpp index 2c17a0c3..f3e9b0eb 100644 --- a/world/RailPlan.cpp +++ b/world/RailPlan.cpp @@ -471,7 +471,9 @@ void TTrack::rail_planner::cross_wide( rail_crossing const &Crossing, rail_size track_rails::guard_rail guard { Crossing.at - reach, Crossing.at + reach }; guard.through = goes_through( m_track, *Crossing.track ); guard.plan.cuts.push_back( corner_cut( Crossing, -flangeway, 1.0, m_size.head, Other.head ) ); - guard.treads.push_back( wide_tread( Crossing, material, m_guardoffset, m_size.head, Other.head ) ); + auto const theirs { Crossing.track->rail_material( Crossing.otherpath ) }; + guard.treads.push_back( wide_tread( Crossing, theirs != null_handle ? theirs : m_track.rail_material( Crossing.path ), m_guardoffset, m_size.head, Other.head ) ); + guard.treads.back().fade = 0.0; merge_guard( m_layout.guards[ Crossing.path ][ Crossing.side > 0 ], guard ); } From df192fc5e0b7f75b151e626aece6a9259e3d2a87 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 12:25:57 +0200 Subject: [PATCH 15/19] fix(track): a point of a rail profile clamped to a cut edge of a lofted span is never moved past its own place nor raised past its own height, and the edges at a station are read from the side of the span being laid, so the rings at a step of an edge or at a bent mitred end no longer stretch points out of the profile and stand them up to 18 mm above the head as thin fins at the corners of wide crossings, and points of other cut rails sit exactly on the profile instead of up to a millimetre off it --- world/Segment.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/world/Segment.cpp b/world/Segment.cpp index c5389f13..0bdcc6cc 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -639,8 +639,9 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig auto const position { glm::vec3{ point - Origin } }; auto const parallel { glm::vec3{ across } }; auto const offset { knots::sample( Span.offsets, Distance, 0.0 ) }; - auto const extent { knots::sample( Span.extents, Distance, 1e6 ) }; - auto const innerextent { knots::sample( Span.inner, Distance, -1e6 ) }; + auto const inward { Distance + std::copysign( 1e-6, Middle - Distance ) }; + auto const extent { knots::sample( Span.extents, inward, 1e6 ) }; + auto const innerextent { knots::sample( Span.inner, inward, -1e6 ) }; auto const middleextent { knots::sample( Span.extents, Middle, 1e6 ) }; auto const middleinner { knots::sample( Span.inner, Middle, -1e6 ) }; glm::vec3 outerwall { 0.f }, innerwall { 0.f }; @@ -710,7 +711,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig continue; } auto const &other { ShapePoints[ neighbour ] }; - auto const t { static_cast( ( limit - reach( neighbour ) ) / ( side > 0 ? std::max( outside - reach( neighbour ), 1e-6 ) : std::min( outside - reach( neighbour ), -1e-6 ) ) ) }; + auto const t { std::clamp( static_cast( ( limit - reach( neighbour ) ) / ( side > 0 ? std::max( outside - reach( neighbour ), 1e-6 ) : std::min( outside - reach( neighbour ), -1e-6 ) ) ), 0.f, 1.f ) }; shape.position.y = glm::mix( other.position.y, shape.position.y, t ); shape.texture.x = glm::mix( other.texture.x, shape.texture.x, t ); auto const blend { glm::mix( other.normal, shape.normal, t ) }; @@ -744,7 +745,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig shape.position.y = std::max( shape.position.y, bottom ); } } - outside = limit; + outside = ( side > 0 ? std::min( outside, limit ) : std::max( outside, limit ) ); auto const onwall { paired ? ( false == open( partner ) && clamped( partner ) == side ) : moved == count }; if( onwall ) { wall = ( side > 0 ? outerwall : innerwall ); From 7d130be3de49d7941ed3211699f9080c22309df8 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 12:43:36 +0200 Subject: [PATCH 16/19] fix(track): at a crossing at a wide angle the floor of the flangeway between a running rail and its guard rail is laid only where real crossings have it, as the cast floor of each corner up to 0.3 m past the rail of the other track and as separate spacer blocks every 0.5 m along the rest of the guard, and the floor takes its texture across its width at the scale of the head instead of one stretched column; the wheels of the other track roll over a guard rail across their whole tread instead of only over the head of their rail; and when the tracks cross at nearly a right angle the treads of the other track are laid as plain spans along the rail instead of lines running almost straight across it, which left fins standing up at every edge of a tread --- world/RailLayer.cpp | 25 ++++++++++----- world/RailLayout.h | 5 +++ world/RailPlan.cpp | 77 +++++++++++++++++++++++++++++++++++++-------- 3 files changed, 86 insertions(+), 21 deletions(-) diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp index 26479d94..a8a18312 100644 --- a/world/RailLayer.cpp +++ b/world/RailLayer.cpp @@ -169,15 +169,15 @@ using namespace track_rails; namespace { -gfx::vertex_array box_profile( float const Bottom, float const Top, float const Texture ) { +gfx::vertex_array box_profile( float const Bottom, float const Top, float const Texture, float const Spread = 0.f ) { return { - {{ 1.f, Bottom, 0.f }, { 1.f, 0.f, 0.f }, { Texture, 0.f }}, - {{ 1.f, Top, 0.f }, { 1.f, 0.f, 0.f }, { Texture, 0.f }}, - {{ 1.f, Top, 0.f }, { 0.f, 1.f, 0.f }, { Texture, 0.f }}, - {{ -1.f, Top, 0.f }, { 0.f, 1.f, 0.f }, { Texture, 0.f }}, - {{ -1.f, Top, 0.f }, { -1.f, 0.f, 0.f }, { Texture, 0.f }}, - {{ -1.f, Bottom, 0.f }, { -1.f, 0.f, 0.f }, { Texture, 0.f }} }; + {{ 1.f, Bottom, 0.f }, { 1.f, 0.f, 0.f }, { Texture + Spread, 0.f }}, + {{ 1.f, Top, 0.f }, { 1.f, 0.f, 0.f }, { Texture + Spread, 0.f }}, + {{ 1.f, Top, 0.f }, { 0.f, 1.f, 0.f }, { Texture + Spread, 0.f }}, + {{ -1.f, Top, 0.f }, { 0.f, 1.f, 0.f }, { Texture - Spread, 0.f }}, + {{ -1.f, Top, 0.f }, { -1.f, 0.f, 0.f }, { Texture - Spread, 0.f }}, + {{ -1.f, Bottom, 0.f }, { -1.f, 0.f, 0.f }, { Texture - Spread, 0.f }} }; } } // namespace @@ -243,7 +243,16 @@ TTrack::rail_drawing TTrack::make_rail_drawing( gfx::geometrybank_handle const & break; } } - sides( box_profile( bottom, neck, webtexture ), drawing.fillers ); + glm::vec2 across { 1e9f, -1e9f }; + glm::vec2 band { 1e9f, -1e9f }; + for( std::size_t i = 0; i < outline.size() / 2; ++i ) { + if( outline[ i ].position.y > highest - 0.0005f ) { + across = { std::min( across.x, outline[ i ].position.x ), std::max( across.y, outline[ i ].position.x ) }; + band = { std::min( band.x, outline[ i ].texture.s ), std::max( band.y, outline[ i ].texture.s ) }; + } + } + auto const spread { across.y > across.x ? ( band.y - band.x ) / ( across.y - across.x ) : 0.f }; + sides( box_profile( bottom, neck, webtexture, spread ), drawing.fillers ); } return drawing; } diff --git a/world/RailLayout.h b/world/RailLayout.h index efbd0b2a..fae6d5ad 100644 --- a/world/RailLayout.h +++ b/world/RailLayout.h @@ -30,6 +30,10 @@ double constexpr middle_angle { 0.35 }; double constexpr wide_angle { 0.785 }; double constexpr guard_reach { 1.0 }; double constexpr guard_flare { 0.6 }; +double constexpr casting_reach { 0.3 }; +double constexpr square_slope { 3.0 }; +double constexpr spacer_length { 0.12 }; +double constexpr spacer_pitch { 0.5 }; // where the inner rails of the two paths of a switch cross, found from their gauge lines behind the blades struct switch_frog { @@ -122,6 +126,7 @@ struct guard_rail { glm::dvec2 tomiter { 0.0 }; bool through { true }; std::vector treads; + knots::line castings; }; struct rail_band { double at { 0.0 }; diff --git a/world/RailPlan.cpp b/world/RailPlan.cpp index f3e9b0eb..f9ebac10 100644 --- a/world/RailPlan.cpp +++ b/world/RailPlan.cpp @@ -142,20 +142,58 @@ track_rails::rail_cut corner_cut( track_rails::rail_crossing const &Crossing, do return cut; } -track_rails::rail_fill guard_fill( track_rails::guard_rail const &Guard, track_rails::rail_size const &Size ) { +std::vector guard_fills( track_rails::guard_rail const &Guard, track_rails::rail_size const &Size ) { auto const offset { Size.web - ( flangeway + Size.head ) }; auto const flare { std::min( guard_flare, 0.25 * ( Guard.to - Guard.from ) ) }; - track_rails::rail_fill fill; - fill.web = true; - fill.from = Guard.from; - fill.to = Guard.to; - fill.capfrom = true; - fill.capto = true; - fill.lift = ( Guard.through ? 0.0 : -0.001 ); - fill.inner = { { Guard.from, offset - ( Guard.flarefrom ? 0.02 : 0.0 ) }, { Guard.from + flare, offset }, { Guard.to - flare, offset }, { Guard.to, offset - ( Guard.flareto ? 0.02 : 0.0 ) } }; - fill.outer = { { Guard.from, Size.web }, { Guard.to, Size.web } }; - return fill; + track_rails::rail_fill whole; + whole.web = true; + whole.lift = ( Guard.through ? 0.0 : -0.001 ); + whole.inner = { { Guard.from, offset - ( Guard.flarefrom ? 0.02 : 0.0 ) }, { Guard.from + flare, offset }, { Guard.to - flare, offset }, { Guard.to, offset - ( Guard.flareto ? 0.02 : 0.0 ) } }; + whole.outer = { { Guard.from, Size.web }, { Guard.to, Size.web } }; + knots::line pieces; + for( auto const &casting : Guard.castings ) { + if( false == pieces.empty() && casting.x <= pieces.back().y ) { + pieces.back().y = std::max( pieces.back().y, casting.y ); + } + else { + pieces.push_back( casting ); + } + } + knots::line spacers; + auto const place = [&]( double const From, double const To, double const Step ) { + auto const count { static_cast( std::floor( ( To - From - 0.5 * spacer_length ) / spacer_pitch ) ) }; + for( int i = 1; i <= count; ++i ) { + auto const at { ( Step > 0.0 ? From : To ) + Step * i * spacer_pitch }; + spacers.emplace_back( at - 0.5 * spacer_length, at + 0.5 * spacer_length ); + } }; + auto const first { Guard.from + spacer_length }; + auto const last { Guard.to - spacer_length }; + if( pieces.empty() ) { + place( first - spacer_pitch, last, 1.0 ); + } + else { + place( first, pieces.front().x, -1.0 ); + for( std::size_t i = 1; i < pieces.size(); ++i ) { + auto const middle { 0.5 * ( pieces[ i - 1 ].y + pieces[ i ].x ) }; + place( pieces[ i - 1 ].y, middle, 1.0 ); + place( middle, pieces[ i ].x, -1.0 ); + } + place( pieces.back().y, last, 1.0 ); + } + pieces.insert( pieces.end(), spacers.begin(), spacers.end() ); + std::vector fills; + for( auto const &piece : pieces ) { + auto fill { whole }; + fill.from = std::max( piece.x, Guard.from ); + fill.to = std::min( piece.y, Guard.to ); + fill.capfrom = true; + fill.capto = true; + if( fill.to - fill.from > 0.01 ) { + fills.push_back( fill ); + } + } + return fills; } bool goes_through( TTrack const &Track, TTrack const &Other ) { @@ -217,6 +255,14 @@ track_rails::rail_tread wide_tread( track_rails::rail_crossing const &Crossing, auto const facing { std::abs( Crossing.facing ) > 0.02 ? Crossing.facing : std::copysign( 0.02, Crossing.facing ) }; auto const slope { Crossing.into / facing }; track_rails::rail_tread tread { 1e9, -1e9, Material }; + if( std::abs( slope ) > square_slope ) { + for( auto const edge : { 0.0, -Reach } ) { + auto const distance { Crossing.at + ( edge + Crossing.facing * ( Base + 0.5 * Head ) ) / Crossing.into }; + tread.from = std::min( tread.from, distance ); + tread.to = std::max( tread.to, distance ); + } + return tread; + } for( auto const across : { Base, Base + Head } ) { for( auto const edge : { tread_fade, -Reach - tread_fade } ) { auto const distance { Crossing.at + ( edge + Crossing.facing * across ) / Crossing.into }; @@ -317,6 +363,8 @@ void merge_guard( std::vector &Guards, track_rails::gua span.plan.cuts.insert( span.plan.cuts.end(), Guard.plan.cuts.begin(), Guard.plan.cuts.end() ); span.through = ( span.through && Guard.through ); span.treads.insert( span.treads.end(), Guard.treads.begin(), Guard.treads.end() ); + span.castings.insert( span.castings.end(), Guard.castings.begin(), Guard.castings.end() ); + knots::sort( span.castings ); return; } } @@ -405,7 +453,8 @@ rail_layout TTrack::rail_planner::lay_out() { for( int path = 0; path < 2; ++path ) { for( int side = 0; side < 2; ++side ) { for( auto const &guard : m_layout.guards[ path ][ side ] ) { - m_layout.fills[ path ][ side ].push_back( guard_fill( guard, m_size ) ); + auto const fills { guard_fills( guard, m_size ) }; + m_layout.fills[ path ][ side ].insert( m_layout.fills[ path ][ side ].end(), fills.begin(), fills.end() ); } } } @@ -470,9 +519,11 @@ void TTrack::rail_planner::cross_wide( rail_crossing const &Crossing, rail_size auto const reach { ( flangeway + m_size.head + guard_reach ) / std::abs( Crossing.into ) }; track_rails::guard_rail guard { Crossing.at - reach, Crossing.at + reach }; guard.through = goes_through( m_track, *Crossing.track ); + auto const casting { ( flangeway + Other.head + casting_reach ) / std::abs( Crossing.into ) }; + guard.castings.emplace_back( Crossing.at - casting, Crossing.at + casting ); guard.plan.cuts.push_back( corner_cut( Crossing, -flangeway, 1.0, m_size.head, Other.head ) ); auto const theirs { Crossing.track->rail_material( Crossing.otherpath ) }; - guard.treads.push_back( wide_tread( Crossing, theirs != null_handle ? theirs : m_track.rail_material( Crossing.path ), m_guardoffset, m_size.head, Other.head ) ); + guard.treads.push_back( wide_tread( Crossing, theirs != null_handle ? theirs : m_track.rail_material( Crossing.path ), m_guardoffset, m_size.head, rolled_band ) ); guard.treads.back().fade = 0.0; merge_guard( m_layout.guards[ Crossing.path ][ Crossing.side > 0 ], guard ); } From 16f3a63bb29a8147e42a5f3c0c08ac6e313d1576 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 13:09:44 +0200 Subject: [PATCH 17/19] fix(track): at a crossing at nearly a right angle the tip of a guard rail under the wheels of the other track takes their rolling surface on its top, bright under a used track and rusty under an unused one, with the sides staying matte, instead of the whole tip going matte because the plain span of the tread was laid as a band between two lines it does not have; plain spans of treads are painted in the debug colour of EU07_EDITOR_TREADS too --- world/RailLayer.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp index a8a18312..cb9dd0fe 100644 --- a/world/RailLayer.cpp +++ b/world/RailLayer.cpp @@ -299,6 +299,8 @@ struct grooved_profile { float wallu { -1.f }; }; +glm::vec3 const plain_mark { 0.f, 1.f, 1.f }; + bool treads_marked() { static auto const marked { std::getenv( "EU07_EDITOR_TREADS" ) != nullptr }; @@ -480,6 +482,7 @@ private: grooved_profile split_grooved( gfx::vertex_array const &Outline ) const; std::vector top_cuts( gfx::vertex_array const &Top ) const; void running( rail_piece const &Whole, knots::line const &Rolled, piece_ends const &Ends ); + void plain( rail_piece Whole, rail_tread const &Tread, piece_ends const &Ends ); void treaded( rail_piece Whole, rail_tread const &Tread, piece_ends const &Ends ); gfx::vertex_array lay_sides( rail_piece const &Whole, piece_ends const &Ends ); void lay_top( gfx::vertex_array const &Top, rail_piece const &Whole, glm::dvec2 const &Reach, std::vector const &Splits, std::vector const &Bands, std::function( double )> const &Edges, piece_ends const &Ends ); @@ -640,7 +643,10 @@ void TTrack::rail_layer::lay_treads( rail_piece const &Whole, std::vectoredge.empty() ) { + plain( part, *tread, ends ); + } + else if( tread != nullptr && m_topfirst < m_toplast ) { treaded( part, *tread, ends ); } else { @@ -717,7 +723,7 @@ void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vectormaterial, plain_mark ) : tread_material( tread ) ), .from = splits[ k - 1 ], .to = splits[ k ], .offsets = pieceoffsets, @@ -946,6 +952,17 @@ void TTrack::rail_layer::running( rail_piece const &Whole, knots::line const &Ro Ends ); } +void TTrack::rail_layer::plain( rail_piece Whole, rail_tread const &Tread, piece_ends const &Ends ) { + + Whole.material = null_handle; + auto const top { lay_sides( Whole, Ends ) }; + Whole.material = ( treads_marked() ? paint( Tread.material, plain_mark ) : Tread.material ); + Whole.capfrom = false; + Whole.capto = false; + Whole.rolled = true; + piece( top, Whole, Ends ); +} + void TTrack::rail_layer::treaded( rail_piece Whole, rail_tread const &Tread, piece_ends const &Ends ) { Whole.material = null_handle; From 306555a6546cc4ccc1877f7056c2ae6c0fb35a48 Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 14:01:08 +0200 Subject: [PATCH 18/19] feat(track): a check rail is held to its running rail by spacer blocks every 0.5 m along its straight part, laid in the same way as the spacers of a guard rail at a wide crossing, below the head from the web of the running rail to the face of the check rail, so the flangeway stays open above them; a check rail replaced by a wing rail gets none; the spacing of spacers and castings is shared by guard rails and check rails --- world/RailCrossings.cpp | 6 +++++ world/RailLayer.cpp | 8 ++---- world/RailLayout.h | 5 ++++ world/RailPlan.cpp | 60 +++++++++++++++++++++++++++++------------ world/TrackRails.h | 1 + 5 files changed, 57 insertions(+), 23 deletions(-) diff --git a/world/RailCrossings.cpp b/world/RailCrossings.cpp index 5f749fd1..8ffbde5b 100644 --- a/world/RailCrossings.cpp +++ b/world/RailCrossings.cpp @@ -126,6 +126,12 @@ bool replaced_check( std::vector const &Checks, double const Wingend return false; } +bool replaced_by_wing( check_rail const &Check, knots::line const &Wingends ) { + + auto farend { 0.0 }; + return std::any_of( Wingends.begin(), Wingends.end(), [&]( glm::dvec2 const &Wing ) { return replaced_check( { Check }, Wing.x, Wing.y, farend ); } ); +} + // gauge line of a rail of a track, along a path of a switch from past its blades when Blades gauge_line const &rail_line( TTrack const &Track, int const Path, int const Side, bool const Blades ) { diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp index cb9dd0fe..0df4538f 100644 --- a/world/RailLayer.cpp +++ b/world/RailLayer.cpp @@ -607,19 +607,15 @@ void TTrack::rail_layer::lay_guards() { void TTrack::rail_layer::lay_checks() { auto const &shape { m_drawing.checkshapes[ m_side > 0 ? 0 : 1 ] }; - auto const &wingends { ours( m_layout.wingends ) }; for( auto const &check : m_layout.checks[ m_path ][ m_side > 0 ] ) { - auto const replaced { std::any_of( wingends.begin(), wingends.end(), [&]( glm::dvec2 const &Wing ) { - auto farend { 0.0 }; - return replaced_check( { check }, Wing.x, Wing.y, farend ); } ) }; auto const a { std::max( check.from, m_from ) }; auto const b { std::min( check.to, m_length ) }; - if( replaced || b - a <= 0.01 ) { + if( replaced_by_wing( check, ours( m_layout.wingends ) ) || b - a <= 0.01 ) { continue; } piece( shape, - { .from = a, .to = b, .offsets = flared( check.from, check.to, std::min( 0.6, 0.4 * ( check.to - check.from ) ), 0.0, check.flarefrom, check.flareto ), .capfrom = a > 0.0 || check.from >= 0.0, .capto = check.to <= m_length, .rolled = false }, + { .from = a, .to = b, .offsets = flared( check.from, check.to, check_flare( check ), 0.0, check.flarefrom, check.flareto ), .capfrom = a > 0.0 || check.from >= 0.0, .capto = check.to <= m_length, .rolled = false }, { a, b, { a == check.from ? check.frombend : 0.0 }, { b == check.to ? check.tobend : 0.0 } } ); } } diff --git a/world/RailLayout.h b/world/RailLayout.h index fae6d5ad..f596844a 100644 --- a/world/RailLayout.h +++ b/world/RailLayout.h @@ -34,6 +34,7 @@ double constexpr casting_reach { 0.3 }; double constexpr square_slope { 3.0 }; double constexpr spacer_length { 0.12 }; double constexpr spacer_pitch { 0.5 }; +double constexpr check_flangeway { 0.041 }; // where the inner rails of the two paths of a switch cross, found from their gauge lines behind the blades struct switch_frog { @@ -87,6 +88,10 @@ struct check_rail { double tobend { 0.0 }; }; using check_rail_set = std::array, 2>, 2>; // path, right side +inline double check_flare( check_rail const &Check ) { + + return std::min( guard_flare, 0.4 * ( Check.to - Check.from ) ); +} // 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 }; diff --git a/world/RailPlan.cpp b/world/RailPlan.cpp index f9ebac10..d3869015 100644 --- a/world/RailPlan.cpp +++ b/world/RailPlan.cpp @@ -142,17 +142,10 @@ track_rails::rail_cut corner_cut( track_rails::rail_crossing const &Crossing, do return cut; } -std::vector guard_fills( track_rails::guard_rail const &Guard, track_rails::rail_size const &Size ) { +std::vector spacer_fills( track_rails::rail_fill const &Whole, double const From, double const To, knots::line const &Castings ) { - auto const offset { Size.web - ( flangeway + Size.head ) }; - auto const flare { std::min( guard_flare, 0.25 * ( Guard.to - Guard.from ) ) }; - track_rails::rail_fill whole; - whole.web = true; - whole.lift = ( Guard.through ? 0.0 : -0.001 ); - whole.inner = { { Guard.from, offset - ( Guard.flarefrom ? 0.02 : 0.0 ) }, { Guard.from + flare, offset }, { Guard.to - flare, offset }, { Guard.to, offset - ( Guard.flareto ? 0.02 : 0.0 ) } }; - whole.outer = { { Guard.from, Size.web }, { Guard.to, Size.web } }; knots::line pieces; - for( auto const &casting : Guard.castings ) { + for( auto const &casting : Castings ) { if( false == pieces.empty() && casting.x <= pieces.back().y ) { pieces.back().y = std::max( pieces.back().y, casting.y ); } @@ -161,14 +154,14 @@ std::vector guard_fills( track_rails::guard_rail const & } } knots::line spacers; - auto const place = [&]( double const From, double const To, double const Step ) { - auto const count { static_cast( std::floor( ( To - From - 0.5 * spacer_length ) / spacer_pitch ) ) }; + auto const place = [&]( double const Start, double const End, double const Step ) { + auto const count { static_cast( std::floor( ( End - Start - 0.5 * spacer_length ) / spacer_pitch ) ) }; for( int i = 1; i <= count; ++i ) { - auto const at { ( Step > 0.0 ? From : To ) + Step * i * spacer_pitch }; + auto const at { ( Step > 0.0 ? Start : End ) + Step * i * spacer_pitch }; spacers.emplace_back( at - 0.5 * spacer_length, at + 0.5 * spacer_length ); } }; - auto const first { Guard.from + spacer_length }; - auto const last { Guard.to - spacer_length }; + auto const first { From + spacer_length }; + auto const last { To - spacer_length }; if( pieces.empty() ) { place( first - spacer_pitch, last, 1.0 ); } @@ -184,9 +177,9 @@ std::vector guard_fills( track_rails::guard_rail const & pieces.insert( pieces.end(), spacers.begin(), spacers.end() ); std::vector fills; for( auto const &piece : pieces ) { - auto fill { whole }; - fill.from = std::max( piece.x, Guard.from ); - fill.to = std::min( piece.y, Guard.to ); + auto fill { Whole }; + fill.from = std::max( piece.x, From ); + fill.to = std::min( piece.y, To ); fill.capfrom = true; fill.capto = true; if( fill.to - fill.from > 0.01 ) { @@ -196,6 +189,29 @@ std::vector guard_fills( track_rails::guard_rail const & return fills; } +std::vector guard_fills( track_rails::guard_rail const &Guard, track_rails::rail_size const &Size ) { + + auto const offset { Size.web - ( flangeway + Size.head ) }; + auto const flare { std::min( guard_flare, 0.25 * ( Guard.to - Guard.from ) ) }; + track_rails::rail_fill whole; + whole.web = true; + whole.lift = ( Guard.through ? 0.0 : -0.001 ); + whole.inner = { { Guard.from, offset - ( Guard.flarefrom ? 0.02 : 0.0 ) }, { Guard.from + flare, offset }, { Guard.to - flare, offset }, { Guard.to, offset - ( Guard.flareto ? 0.02 : 0.0 ) } }; + whole.outer = { { Guard.from, Size.web }, { Guard.to, Size.web } }; + return spacer_fills( whole, Guard.from, Guard.to, Guard.castings ); +} + +std::vector check_fills( track_rails::check_rail const &Check, track_rails::rail_size const &Size ) { + + auto const from { Check.from + check_flare( Check ) }; + auto const to { Check.to - check_flare( Check ) }; + track_rails::rail_fill whole; + whole.web = true; + whole.inner = { { from, -check_flangeway }, { to, -check_flangeway } }; + whole.outer = { { from, Size.web }, { to, Size.web } }; + return spacer_fills( whole, from, to, {} ); +} + bool goes_through( TTrack const &Track, TTrack const &Other ) { if( Track.name() != Other.name() ) { @@ -468,6 +484,16 @@ rail_layout TTrack::rail_planner::lay_out() { if( m_track.eType == tt_Switch ) { switch_frog(); } + for( int path = 0; path < 2; ++path ) { + for( int side = 0; side < 2; ++side ) { + for( auto const &check : m_layout.checks[ path ][ side ] ) { + if( false == replaced_by_wing( check, m_layout.wingends[ path ][ side ] ) ) { + auto const fills { check_fills( check, m_size ) }; + m_layout.fills[ path ][ side ].insert( m_layout.fills[ path ][ side ].end(), fills.begin(), fills.end() ); + } + } + } + } return m_layout; } diff --git a/world/TrackRails.h b/world/TrackRails.h index b7404661..8a7e7edf 100644 --- a/world/TrackRails.h +++ b/world/TrackRails.h @@ -38,6 +38,7 @@ int path_count( TTrack const &Track ); TTrack *path_neighbour( TTrack const &Track, int const Path, int const End ); double knuckle_bend( double const Into ); bool replaced_check( std::vector const &Checks, double const Wingend, double const Direction, double &Far ); +bool replaced_by_wing( check_rail const &Check, knots::line const &Wingends ); gauge_line const &rail_line( TTrack const &Track, int const Path, int const Side, bool const Blades ); check_rail_set const &rail_checks( TTrack &Track ); glm::dvec3 rail_point( TTrack const &Track, int const Path, int const Side, double const Distance, double const Lateral ); From 3307cb94e8e6dc93d32a3fa3bd93316fced469cc Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 14 Oct 2026 14:01:15 +0200 Subject: [PATCH 19/19] feat(track): frogs, crossings, guard rails with their castings and spacers, check rails and shared running surfaces for every rail profile again, not only for tracks without a profile, default, s60, uic60 and s39; grooved profiles keep their shared grooves without check rails --- world/RailCrossings.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/world/RailCrossings.cpp b/world/RailCrossings.cpp index 8ffbde5b..078a882f 100644 --- a/world/RailCrossings.cpp +++ b/world/RailCrossings.cpp @@ -17,12 +17,6 @@ using namespace track_rails; namespace { -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 ) { @@ -85,7 +79,6 @@ 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 ] ); }