17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-02 16:19:13 +01:00

feat(track): 3D sleepers and switch timbers, blade heel and throw from templates, one rail carving pass

- sleepers3d / sleepers3dplain / sleepers3dbed in eu07.ini: timbers put together from the begin, main and end parts of a model
  and stretched to their length, plain sleepers of their own model; tracks whose plain sleepers would touch are laid as one
  cluster, lines of timbers led by priority and turned to the bisectors of frogs and crossings at a limited rate, whole timbers
  under crossings, split ones between tracks with room to part them, gaps filled per rail and overlaps shortened or dropped;
  clusters worked out in a background thread while driving, on demand in the editor
- the ballast can be left out under the beams (sleepers3dbed no)
- blades get their heel and throw from the switch template or blades <heel> <throw> in the scenery; blades of double switches
  run on into the track linking them to the next switch
- rail cutting at frogs, wing rails, movable noses and switch crossings done by one carving pass (RailCarve)
- tint of the rail treads applied to the colour only, without the matte band at the frog
- a switch trailed through with a movable nose set the other way derails the vehicle
- self-tests: sleepercheck, sharedzones, beamsnear, throw, sleepersbg
This commit is contained in:
maj00r
2026-10-17 23:28:36 +02:00
parent 21eca4d9a6
commit d25f20a56c
49 changed files with 9802 additions and 848 deletions

View File

@@ -35,12 +35,13 @@ inline double sample( line const &Line, double const Position, double const Defa
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 ) );
}
// the first knot at or past the position, after the first one
auto const next { std::lower_bound( Line.begin() + 1, Line.end(), Position, []( glm::dvec2 const &Knot, double const At ) { return Knot.x < At; } ) };
if( next == Line.end() ) {
return Line.back().y;
}
return Line.back().y;
auto const &before { *( next - 1 ) };
return glm::mix( before.y, next->y, ( Position - before.x ) / std::max( next->x - before.x, 1e-6 ) );
}
inline line shifted( line Line, double const Value ) {
@@ -107,4 +108,44 @@ inline line middle( line const &A, line const &B ) {
return result;
}
// both lines added up, each going on flat past its ends
inline line sum( line const &A, line const &B ) {
if( A.empty() || B.empty() ) {
return ( A.empty() ? B : A );
}
line result;
for( auto const position : stations( A, B ) ) {
if( false == result.empty() && position - result.back().x < 1e-9 ) {
continue;
}
result.emplace_back( position, sample( A, position, 0.0 ) + sample( B, position, 0.0 ) );
}
return result;
}
// the line without the knots lying on the straight between the knots kept around them, within Tolerance
inline line simplified( line const &Line, double const Tolerance ) {
if( Line.size() < 3 ) {
return Line;
}
line result { Line.front() };
std::size_t kept { 0 };
for( std::size_t i = 2; i < Line.size(); ++i ) {
auto const &a { Line[ kept ] };
auto const &b { Line[ i ] };
for( std::size_t j = kept + 1; j < i; ++j ) {
auto const t { ( Line[ j ].x - a.x ) / std::max( b.x - a.x, 1e-9 ) };
if( std::abs( glm::mix( a.y, b.y, t ) - Line[ j ].y ) > Tolerance ) {
kept = i - 1;
result.push_back( Line[ kept ] );
break;
}
}
}
result.push_back( Line.back() );
return result;
}
} // namespace knots

1167
world/RailCarve.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -63,12 +63,22 @@ void forget() {
++stamp;
}
unsigned int memo_stamp() {
return stamp;
}
double wing_reach( double const Slope ) {
return 0.15 / Slope + 0.3;
}
double movable_nose_length( double const Slope ) {
return 0.4 / Slope;
}
double check_length( double const Slope ) {
return std::clamp( 0.5 / Slope, 3.6, 9.0 );
@@ -92,12 +102,49 @@ int blade_steps( TTrack const &Track, int const Path ) {
double blade_length( TTrack const &Track, int const Path ) {
if( auto const &shape { Track.blade( Path ) }; shape.valid ) {
return shape.heel;
return std::min( shape.heel, Track.SwitchExtension->Segments[ Path ]->GetLength() );
}
auto const &segment { *Track.SwitchExtension->Segments[ Path ] };
return segment.GetLength() * blade_steps( Track, Path ) / segment.RaSegCount();
}
TTrack const *runon_track( TTrack const &Switch, int const Path ) {
auto const *next { path_neighbour( Switch, Path, 1 ) };
if( next == nullptr || next->eType != tt_Normal || next->Segment == nullptr || next == &Switch ) {
return nullptr;
}
if( glm::distance( Switch.SwitchExtension->Segments[ Path ]->FastGetPoint_1(), next->Segment->FastGetPoint_0() ) > 0.01 ) {
return nullptr;
}
// only as in a double switch: the plain track leads on to the start of the next switch, not to the end of the other half of a slip
auto const *after { next->trNext };
if( after == nullptr || after->eType != tt_Switch || after->SwitchExtension == nullptr
|| glm::distance( after->SwitchExtension->Segments[ 0 ]->FastGetPoint_0(), next->Segment->FastGetPoint_1() ) > 0.01 ) {
return nullptr;
}
return next;
}
double blade_runon( TTrack const &Track, int const Side ) {
auto const *owner { Track.trPrev };
if( Track.eType != tt_Normal || Track.Segment == nullptr || owner == nullptr || owner->eType != tt_Switch || false == cut_rails( *owner ) ) {
return 0.0;
}
for( int path = 0; path < 2; ++path ) {
if( runon_track( *owner, path ) != &Track || ( owner->SwitchExtension->RightSwitch == ( path == 0 ) ? 1 : -1 ) != Side ) {
continue;
}
auto const &shape { owner->blade( path ) };
auto const length { owner->SwitchExtension->Segments[ path ]->GetLength() };
if( shape.valid && shape.heel > length ) {
return std::min( shape.heel - length, Track.Segment->GetLength() );
}
}
return 0.0;
}
TSegment const &path_segment( TTrack const &Track, int const Path ) {
return ( Track.eType == tt_Switch ? *Track.SwitchExtension->Segments[ Path ] : *Track.Segment );
@@ -361,6 +408,59 @@ std::vector<rail_crossing> own_crossings( TTrack &Track ) {
return result;
}
// how far the blades of a part of a double slip may reach: up to the check rails of the crossings of the rails of the diamond, found
// with the blades of the other parts left out of the account, as they're yet to be made as short; at the latest short of their throats,
// the check rails then cut to begin past the heels
// crossings of the rails of the track with the rails of the tracks around, with the blades of switches left out of the account
std::vector<rail_crossing> unbladed_crossings( TTrack const &Track ) {
auto &track { const_cast<TTrack &>( Track ) };
std::vector<rail_crossing> found;
for( auto *other : nearby_rails( track ) ) {
for( int otherpath = 0; otherpath < path_count( *other ); ++otherpath ) {
rail_run theirs { other, otherpath };
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, false );
for( int path = 0; path < path_count( track ); ++path ) {
for( int side = 0; side < 2; ++side ) {
meet_rails( { &track, path, side ? 1 : -1, &rail_line( track, path, side, false ), 0.0, path_segment( track, path ).GetLength() }, theirs, found );
}
}
}
}
}
return found;
}
std::pair<double, double> slip_blade_reach( TTrack const &Track ) {
auto reach { 1e9 };
auto latest { 1e9 };
for( auto const &crossing : unbladed_crossings( Track ) ) {
if( crossing.angle <= middle_angle ) {
auto const slope { std::tan( crossing.angle ) };
reach = std::min( reach, crossing.at - 0.25 - 0.5 * check_length( slope ) - 0.3 );
latest = std::min( latest, crossing.at - flangeway / slope - 0.6 );
}
}
return { reach, latest };
}
// how far the blade of a switch may reach: short of the throat of a frog of another track on its rail, as in a double switch with the middle frog right past the heels,
// where the wing rail has to bend out of the line of the rail coming from the blade
double blade_reach( TTrack const &Track, int const Path, int const Side ) {
auto reach { 1e9 };
for( auto const &crossing : unbladed_crossings( Track ) ) {
if( crossing.path == Path && crossing.side == Side && crossing.angle <= middle_angle ) {
reach = std::min( reach, crossing.at - flangeway / std::tan( crossing.angle ) - 0.05 );
}
}
return reach;
}
// 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<rail_crossing> &Result ) {
@@ -421,13 +521,14 @@ struct planned_check {
double flare { 0.02 };
double frombend { 0.0 };
double tobend { 0.0 };
bool acute { false }; // opposite an acute crossing, it keeps clear of the rails of the tracks crossing this one
};
using planned_check_set = std::array<std::array<std::vector<planned_check>, 2>, 2>; // path, right side
void add_acute_check( std::vector<planned_check> &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 } );
Checks.push_back( { At + Toward * 0.25 - half, At + Toward * 0.25 + half, true, true, 1.5, Flare, 0.0, 0.0, true } );
}
planned_check_set planned_checks( TTrack const &Track, std::vector<rail_crossing> const &Crossings ) {
@@ -449,7 +550,8 @@ planned_check_set planned_checks( TTrack const &Track, std::vector<rail_crossing
}
if( Track.eType == tt_Switch ) {
auto const frog { Track.frog() };
if( frog.valid ) {
// the movable nose leaves no gap for the wheels, the frog has no check rails
if( frog.valid && false == Track.SwitchExtension->m_movablenose ) {
for( int path = 0; path < 2; ++path ) {
add_acute_check( plans[ path ][ frog.side[ path ] < 0 ], frog.at[ path ], -1.0, std::tan( frog.angle ), 0.04 );
}
@@ -470,6 +572,7 @@ void merge_checks( std::vector<planned_check> &Checks ) {
}
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[ i - 1 ].acute = Checks[ i - 1 ].acute || Checks[ i ].acute;
Checks.erase( Checks.begin() + i-- );
}
}
@@ -492,8 +595,9 @@ bool blocked_by( std::vector<gauge_line> const &Others, glm::dvec2 const &Gauge,
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<planned_check> const &Checks, double const Clearance ) {
// 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;
// those opposite acute crossings also of the rails of the Crossing tracks, as where the slip curves of a double slip cross the diamond
knots::line check_blocks( TTrack const &Track, int const Path, int const Side, std::vector<planned_check> const &Checks, double const Clearance, std::vector<gauge_line> const &Crossing ) {
auto const halfwidth { 0.5 * std::abs( Track.fTrackWidth ) };
auto const &segment { path_segment( Track, Path ) };
@@ -506,7 +610,15 @@ knots::line check_blocks( TTrack const &Track, int const Path, int const Side, s
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 );
// past the heels of the blades
auto heel { 0.0 };
for( int path = 0; path < 2; ++path ) {
auto const &blade { Track.blade( path ) };
if( blade.valid ) {
heel = std::max( heel, blade.heel );
}
}
blocks.emplace_back( -1e9, heel + 0.3 );
add_path( path_segment( Track, 1 - Path ) );
}
for( auto const *neighbour : { Track.trPrev, Track.trNext } ) {
@@ -531,13 +643,15 @@ knots::line check_blocks( TTrack const &Track, int const Path, int const Side, s
}
}
}
if( false == others.empty() ) {
if( false == others.empty() || false == Crossing.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 ) ) {
auto const inward { plan( ours.normal( s, Side ) ) };
if( blocked_by( others, plan( point ), inward, Clearance )
|| ( check.acute && blocked_by( Crossing, plan( point ), inward, Clearance ) ) ) {
blocks.emplace_back( s - 0.3, s + 0.3 );
}
}
@@ -603,6 +717,19 @@ track_rails::check_rail_set TTrack::check_rails( std::vector<rail_crossing> cons
}
auto plans { planned_checks( *this, Crossings ) };
auto const clearance { 0.046 + 0.04 * size.head / 0.0735 + size.head };
std::vector<gauge_line> crossing;
std::vector<TTrack const *> crossers;
for( auto const &entry : Crossings ) {
if( entry.track == nullptr || entry.track == this || std::find( crossers.begin(), crossers.end(), entry.track ) != crossers.end() ) {
continue;
}
crossers.push_back( entry.track );
for( int otherpath = 0; otherpath < path_count( *entry.track ); ++otherpath ) {
for( auto const side : { -1.0, 1.0 } ) {
crossing.push_back( make_gauge_line( path_segment( *entry.track, otherpath ), side * 0.5 * std::abs( entry.track->fTrackWidth ), 0.0 ) );
}
}
}
for( int path = 0; path < path_count( *this ); ++path ) {
for( int right = 0; right < 2; ++right ) {
auto &checks { plans[ path ][ right ] };
@@ -610,7 +737,7 @@ track_rails::check_rail_set TTrack::check_rails( std::vector<rail_crossing> cons
continue;
}
merge_checks( checks );
auto const blocks { check_blocks( *this, path, right ? 1 : -1, checks, clearance ) };
auto const blocks { check_blocks( *this, path, right ? 1 : -1, checks, clearance, crossing ) };
for( auto const &check : checks ) {
cut_check( check, blocks, result[ path ][ right ] );
}
@@ -689,6 +816,11 @@ track_rails::switch_frog TTrack::frog() const {
return result;
}
double TTrack::blades_throw() const {
return ( eType == tt_Switch && SwitchExtension && SwitchExtension->m_bladethrow > 0.0 ? SwitchExtension->m_bladethrow : blade_throw );
}
track_rails::switch_blade const &TTrack::blade( int const Path ) const {
auto &cached { memo_of( *this ).blades[ Path ] };
@@ -704,7 +836,16 @@ track_rails::switch_blade const &TTrack::blade( int const Path ) const {
auto const &ours { rail_line( *this, Path, side > 0, false ) };
auto const &theirs { rail_line( *this, 1 - Path, side > 0, false ) };
auto const size { rail_dimensions() };
auto const length { std::min( SwitchExtension->Segments[ Path ]->GetLength(), SwitchExtension->Segments[ 1 - Path ]->GetLength() ) };
// a path ending short of the other one with a plain track going on from it, as the first switch of a double one has, lets the blades run on
// into that track, taken straight on from the end, short of the track past it
auto const runs = [&]( int const P ) {
auto const *next { track_rails::runon_track( *this, P ) };
return SwitchExtension->Segments[ P ]->GetLength() + ( next != nullptr ? next->Segment->GetLength() - blade_runon_margin : 0.0 ); };
auto const shortest { std::min( SwitchExtension->Segments[ Path ]->GetLength(), SwitchExtension->Segments[ 1 - Path ]->GetLength() ) };
auto const length { std::max( shortest, std::min( {
std::max( SwitchExtension->Segments[ Path ]->GetLength(), SwitchExtension->Segments[ 1 - Path ]->GetLength() ),
runs( Path ),
runs( 1 - Path ) } ) ) };
auto const count { std::max( 2, static_cast<int>( std::ceil( length / 0.1 ) ) ) };
auto &gauge { result.gauge };
glm::dvec3 point, tangent;
@@ -777,9 +918,32 @@ track_rails::switch_blade const &TTrack::blade( int const Path ) const {
}
}
result.full = reach( size.head + blade_gap );
auto const clear { reach( size.head + blade_clearance ) };
auto clear { reach( size.head + blade_clearance ) };
auto const foot { reach( size.high + blade_gap ) };
auto const limit { 0.9 * length };
auto limit { length > shortest + 1e-3 ? length : 0.9 * length };
// the blades of a double slip with them outside of the diamond end before the check rails of its crossings, shorter than they'd bend;
// with the throat of an acute frog of the diamond nearer than the blade gets clear of the stock rail, it ends short of the throat with the head clear of it,
// the frog over the blade wouldn't be made at all
auto capped { false };
if( DoubleSlip() && clear >= 0.0 ) {
auto const [ reach, latest ] { slip_blade_reach( *this ) };
auto const cap { reach >= clear ? reach : latest };
if( cap < limit && ( cap >= clear || ( result.full >= 0.0 && cap > result.full ) ) ) {
limit = cap;
capped = true;
clear = std::min( clear, cap );
}
}
// the blade of a switch with a frog of another track right past the heel keeps as long as it may, it doesn't bend as far as it would
auto reached { false };
if( false == DoubleSlip() && clear >= 0.0 ) {
auto const cap { blade_reach( *this, Path, side ) };
if( cap >= clear && cap < limit ) {
limit = cap;
capped = true;
reached = true;
}
}
if( result.tip < 0.0 || result.full < 0.0 || clear < 0.0 || clear > limit ) {
return result;
}
@@ -792,7 +956,7 @@ track_rails::switch_blade const &TTrack::blade( int const Path ) const {
if( sample.x < result.tip || sample.x > Heel ) {
continue;
}
auto const gap { std::max( 0.0, sample.y - size.head ) + blade_throw * bend( sample.x, Heel ) };
auto const gap { std::max( 0.0, sample.y - size.head ) + blades_throw() * bend( sample.x, Heel ) };
if( gap < least ) {
least = gap;
At = sample.x;
@@ -800,19 +964,39 @@ track_rails::switch_blade const &TTrack::blade( int const Path ) const {
}
return least; };
auto at { 0.0 };
if( opening( limit, at ) < blade_clearance ) {
result.rigid = true;
auto short_blade { false };
auto const given { SwitchExtension->m_bladeheel };
if( given > 0.0 ) {
// the template of the switch says where its blades end; they bend stiff about the heel where the flangeway is too narrow for them to flex
result.heel = std::clamp( given, result.tip + blade_spacing, limit );
result.rigid = ( opening( result.heel, at ) < blade_clearance );
}
else {
if( opening( limit, at ) < blade_clearance ) {
return result;
result.rigid = true;
if( opening( limit, at ) < blade_clearance || reached ) {
if( false == capped ) {
return result;
}
short_blade = true;
}
}
auto low { clear };
auto high { limit };
for( int step = 0; step < 40 && false == short_blade; ++step ) {
auto const middle { 0.5 * ( low + high ) };
( opening( middle, at ) >= blade_clearance ? high : low ) = middle;
}
result.heel = high;
// the blade goes on until the rail behind it stands clear of the stock rail with its whole foot, so it moves out of the place of the rail
// of the other path all along; only the frog of another track or the crossings of a slip end it sooner
auto const apart { reach( size.high - size.low + blade_gap ) };
if( apart > result.heel && result.heel < limit ) {
result.heel = std::min( apart, limit );
result.rigid = false;
result.rigid = ( opening( result.heel, at ) < blade_clearance );
}
}
auto low { clear };
auto high { limit };
for( int step = 0; step < 40; ++step ) {
auto const middle { 0.5 * ( low + high ) };
( opening( middle, at ) >= blade_clearance ? high : low ) = middle;
}
result.heel = high;
result.opening = opening( result.heel, result.openingat );
for( std::size_t i = 1; i < gauge.size() && gauge[ i ].x <= result.heel; ++i ) {
if( gauge[ i ].y < gauge[ i - 1 ].y - 1e-4 ) {

View File

@@ -58,7 +58,28 @@ std::vector<std::array<std::size_t, 3>> ear_clip( std::vector<glm::dvec2> const
for( std::size_t j = 0; j < Polygon.size(); ++j ) {
left[ j ] = j;
}
// the parts of the outline of no width, as the web and the head of a rail planed away to nothing at the tip of a nose with its foot whole,
// go out first: the ears cut across them would cover what lies beside them
auto const thin = [&]( std::size_t const 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 longest { std::max( { glm::length( b - a ), glm::length( c - b ), glm::length( a - c ) } ) };
return ( longest <= 0.0 || std::abs( cross( a, b, c ) ) / longest < 1e-4 ); };
for( auto pruned { true }; pruned && left.size() > 3; ) {
pruned = false;
for( std::size_t j = 0; j < left.size() && left.size() > 3; ++j ) {
if( thin( j ) ) {
left.erase( left.begin() + j );
pruned = true;
break;
}
}
}
std::vector<std::array<std::size_t, 3>> triangles;
if( left.size() < 3 || ( left.size() == 3 && thin( 0 ) ) ) {
return triangles;
}
while( left.size() > 3 ) {
auto ear { left.size() };
for( std::size_t j = 0; j < left.size() && ear == left.size(); ++j ) {
@@ -81,6 +102,23 @@ std::vector<std::array<std::size_t, 3>> ear_clip( std::vector<glm::dvec2> const
break;
}
}
// an outline narrower than a millimetre in places lets an ear past the test above across a notch of it, with no corner inside;
// the middle of the ear tells: out of the outline, the ear isn't one; the outline of a planed rail may go over itself, so it's what it winds around
if( false == inside ) {
auto const middle { ( a + b + c ) / 3.0 };
auto winding { 0 };
for( std::size_t k = 0; k < left.size(); ++k ) {
auto const &p { Polygon[ left[ k ] ] };
auto const &q { Polygon[ left[ ( k + 1 ) % left.size() ] ] };
if( p.y <= middle.y && q.y > middle.y && cross( p, q, middle ) > 0.0 ) {
++winding;
}
else if( q.y <= middle.y && p.y > middle.y && cross( p, q, middle ) < 0.0 ) {
--winding;
}
}
inside = ( winding == 0 );
}
}
if( false == inside ) {
ear = j;
@@ -153,13 +191,59 @@ 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 } } );
}
namespace {
chunk_pool *reused { nullptr };
} // namespace
reused_chunks::reused_chunks( chunk_pool *Pool ) :
m_previous { reused } {
if( Pool != nullptr ) {
for( auto &chunk : *Pool ) {
chunk.used = false;
}
}
reused = Pool;
}
reused_chunks::~reused_chunks() {
reused = m_previous;
}
gfx::geometry_handle insert_chunk( gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Bank, int const Type ) {
if( reused == nullptr ) {
return GfxRenderer->Insert( Vertices, Userdata, Bank, Type );
}
for( auto &chunk : *reused ) {
if( chunk.used || chunk.type != Type || chunk.handle.bank != Bank.bank ) {
continue;
}
// the data are swapped into the chunk when of the same size, the caller keeps its own
auto vertices { Vertices };
auto userdata { Userdata };
if( GfxRenderer->Replace( vertices, userdata, chunk.handle, Type ) ) {
chunk.used = true;
return chunk.handle;
}
}
auto const handle { GfxRenderer->Insert( Vertices, Userdata, Bank, Type ) };
if( handle != null_handle ) {
reused->push_back( { Type, handle, true } );
}
return handle;
}
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 ) );
Geometry.emplace_back( insert_chunk( triangles, userdata, Bank, GL_TRIANGLES ) );
}
}
@@ -335,22 +419,17 @@ std::vector<double> cross_splits( rail_piece const &Whole, glm::dvec2 const &Rea
return result;
}
knots::line flared( double const From, double const To, double const Flare, double const Base, double const Fromdepth, double const Todepth ) {
// the face of a check or a guard rail toward the running rail, planed off at the ends by Fromdepth and Todepth down to nothing over Length; lines for the upper envelope of the reach
void planed( knots::line &Lines, double const From, double const To, double const Length, double const Face, 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 } );
auto const slope { Fromdepth / Length };
Lines.emplace_back( slope, Face - Fromdepth - slope * From );
}
if( Todepth > 0.0 ) {
result.insert( result.end(), { { To - Flare, Base }, { To, Base - Todepth } } );
auto const slope { Todepth / Length };
Lines.emplace_back( -slope, Face - Todepth + slope * To );
}
else {
result.push_back( { To, Base } );
}
return result;
}
knots::line envelope( knots::line const &Lines, bool const Upper, double const A, double const B ) {
@@ -476,17 +555,18 @@ private:
void lay_guards();
void lay_checks();
void lay_fills();
void lay_treads( rail_piece const &Whole, std::vector<rail_tread> const &Treads );
void lay_treads( rail_piece const &Whole, std::vector<rail_tread> const &Treads, piece_ends const &Ends );
void lay_cuts( gfx::vertex_array const &Shape, std::vector<rail_cut> Cuts, bool const Wheels, std::vector<rail_tread> 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<rail_cut> top_cuts( gfx::vertex_array const &Top ) const;
void running( rail_piece const &Whole, knots::line const &Rolled, piece_ends const &Ends );
void running( rail_piece const &Whole, knots::line const &Rolled, knots::line const &Worn, material_handle const Wornby, 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<double> const &Splits, std::vector<top_band> const &Bands, std::function<std::vector<double>( double )> const &Edges, piece_ends const &Ends );
void piece( gfx::vertex_array const &Shape, rail_piece const &Piece, piece_ends const &Ends = {} );
// the faces between the points Leftfrom and Leftto of the shape are left out, laid apart
void piece( gfx::vertex_array const &Shape, rail_piece const &Piece, piece_ends const &Ends = {}, std::size_t const Leftfrom = 0, std::size_t const Leftto = 0 );
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 );
@@ -575,8 +655,10 @@ void TTrack::rail_layer::lay_guards() {
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 ); };
// a flared end is cut slanted from the face of the guard rail toward the running rail: its back reaches out to the end, its face stops short of it
auto const back { -( reaches( m_profile ).x + m_guardoffset ) };
for( auto const &guard : ours( m_layout.guards ) ) {
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 ) };
knots::line const offsets { { guard.from, m_guardoffset }, { guard.to, m_guardoffset } };
auto cuts { guard.plan.cuts };
sort_cuts( cuts );
auto start { guard.from };
@@ -584,17 +666,23 @@ void TTrack::rail_layer::lay_guards() {
for( std::size_t i = 0; i <= cuts.size(); ++i ) {
auto const end { i < cuts.size() ? cuts[ i ].end : guard.to };
auto const endmiter { i < cuts.size() ? cuts[ i ].endmiter : guard.tomiter };
auto const a { std::max( { mitered( startmiter, start, false ), m_from, 0.0 } ) };
auto const b { std::min( { mitered( endmiter, end, true ), m_length, mitered( guard.tomiter, guard.to, true ) } ) };
auto const bevelfrom { start == guard.from && startmiter.y == 0.0 && guard.flarefrom && guard.from >= std::max( m_from, 0.0 ) };
auto const bevelto { i == cuts.size() && endmiter.y == 0.0 && guard.flareto && guard.to <= m_length };
auto const a { bevelfrom ? guard.from + guard_bevel * back : std::max( { mitered( startmiter, start, false ), m_from, 0.0 } ) };
auto const b { bevelto ? guard.to - guard_bevel * back : std::min( { mitered( endmiter, end, true ), m_length, mitered( guard.tomiter, guard.to, true ) } ) };
if( b - a > 0.01 ) {
knots::line uppers { { 0.0, top } };
knots::line lowers;
planed( uppers, guard.from, guard.to, std::min( guard_flare, 0.25 * ( guard.to - guard.from ) ), size.head, guard.flarefrom ? guard_planing : 0.0, guard.flareto ? guard_planing : 0.0 );
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 - m_guardoffset );
}
}
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 );
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,
{ a, b, { bevelfrom ? guard_bevel : 0.0 }, { bevelto ? -guard_bevel : 0.0 } } );
}
if( i < cuts.size() && cuts[ i ].start > start ) {
start = cuts[ i ].start;
@@ -613,14 +701,17 @@ void TTrack::rail_layer::lay_checks() {
if( replaced_by_wing( check, ours( m_layout.wingends ) ) || b - a <= 0.01 ) {
continue;
}
// the flared ends are bent away from the running rail, the whole of the check rail
auto const flare { check_flare( check ) };
knots::line const offsets { { check.from, -check.flarefrom }, { check.from + flare, 0.0 }, { check.to - flare, 0.0 }, { check.to, -check.flareto } };
piece(
shape,
{ .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 },
{ .from = a, .to = b, .offsets = offsets, .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 } } );
}
}
void TTrack::rail_layer::lay_treads( rail_piece const &Whole, std::vector<rail_tread> const &Treads ) {
void TTrack::rail_layer::lay_treads( rail_piece const &Whole, std::vector<rail_tread> const &Treads, piece_ends const &Ends ) {
std::vector<double> splits { Whole.from, Whole.to };
for( auto const &tread : Treads ) {
@@ -631,7 +722,7 @@ void TTrack::rail_layer::lay_treads( rail_piece const &Whole, std::vector<rail_t
}
}
std::sort( splits.begin(), splits.end() );
piece_ends const ends { Whole.from, Whole.to };
auto const &ends { Ends };
for( std::size_t k = 1; k < splits.size(); ++k ) {
auto part { Whole };
part.from = splits[ k - 1 ];
@@ -671,24 +762,31 @@ void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vector<r
auto start { -1e9 };
auto startcap { true };
auto startbend { 0.0 };
knots::line rolledfrom;
knots::line rolledfrom, wornfrom;
material_handle wornbyfrom { null_handle };
auto const brighter = [&]( material_handle const A, material_handle const B ) {
if( A == null_handle || B == null_handle ) {
return ( A == null_handle ? B : A );
}
return ( tread_brightness( B, m_track.tread_band() ) > tread_brightness( A, m_track.tread_band() ) ? B : A ); };
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 };
// a piece carved between two cuts has the same lines at both
auto const own { i < Cuts.size() && false == ( i > 0 && Cuts[ i - 1 ].carved && Cuts[ i ].carved ) };
auto pieceoffsets { offsets };
auto pieceextents { extents };
auto pieceinner { inner };
if( i < Cuts.size() ) {
if( own ) {
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 );
// the limits of the head from both ends can overlap along a short piece, as the planed wing rail of one frog and the cut of the next: the tighter of them holds
auto const pieceextents { own ? knots::bound( extents, Cuts[ i ].endextents, true ) : extents };
auto const pieceinner { own ? knots::bound( inner, Cuts[ i ].endinner, false ) : inner };
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 rolled { own ? knots::bound( rolledfrom, Cuts[ i ].endrolled, true ) : rolledfrom };
auto const worn { own ? knots::bound( wornfrom, Cuts[ i ].endworn, true ) : wornfrom };
auto const wornby { own ? brighter( wornbyfrom, Cuts[ i ].endwornby ) : wornbyfrom };
auto const capstart { start > m_from ? startcap : m_openstart };
auto const capend { end < m_length ? Cuts[ i ].endcap : m_openend };
piece_ends const ends {
@@ -736,12 +834,14 @@ void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vector<r
piece( Shape, whole, ends );
}
else {
running( whole, rolled, ends );
running( whole, rolled, worn, wornby, ends );
}
}
}
if( i < Cuts.size() ) {
rolledfrom = Cuts[ i ].startrolled;
wornfrom = Cuts[ i ].startworn;
wornbyfrom = Cuts[ i ].startwornby;
start = Cuts[ i ].start;
startcap = Cuts[ i ].startcap;
startbend = Cuts[ i ].startbend;
@@ -928,23 +1028,67 @@ std::vector<rail_cut> TTrack::rail_layer::top_cuts( gfx::vertex_array const &Top
return cuts;
}
void TTrack::rail_layer::running( rail_piece const &Whole, knots::line const &Rolled, piece_ends const &Ends ) {
void TTrack::rail_layer::running( rail_piece const &Whole, knots::line const &Rolled, knots::line const &Worn, material_handle const Wornby, 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 };
// the wheels of the rail beside polish the head as they do their own; where the wheels of both roll on it, the brighter polish shows
auto const theirs { ( Wornby == null_handle || Worn.empty() ) ? own : Wornby };
auto const theirsbrighter { theirs != own && tread_brightness( theirs, m_track.tread_band() ) > tread_brightness( own, m_track.tread_band() ) };
auto const theirmaterial { theirs == own ? Whole.material : theirs };
// the wheels wander across, rolling a little further than their reach in the middle of the gauge
auto const ownreach { knots::shifted( Rolled, wheel_play ) };
auto const theirreach { Worn.empty() ? knots::line {} : knots::shifted( Worn, -wheel_play ) };
// the polish fades across the edge of the reach of the wheels, as it does at the treads
auto reached { knots::shifted( ownreach, 0.5 * blend ) };
// where the head is planed flush against a rail beside it, the wheels of that rail roll on it again from the outside; where the reach of both
// overlaps, the head is rolled across, each fade cut off halfway between them, or the duller polish where the brighter one reaches. the wheels
// wander across the gap between the reaches narrower than a fade, so both fades close in on it, down to none where the reaches touch
knots::line outside, meet;
if( false == theirreach.empty() ) {
auto const widest { knots::shifted( ownreach, blend ) };
auto all { knots::stations( ownreach, theirreach ) };
for( auto const *edge : { &ownreach, &widest } ) {
auto const crossed { knots::meetings( theirreach, *edge ) };
all.insert( all.end(), crossed.begin(), crossed.end() );
}
std::sort( all.begin(), all.end() );
reached.clear();
for( auto const position : all ) {
if( false == outside.empty() && position - outside.back().x < 1e-9 ) {
continue;
}
auto const rolled { knots::sample( ownreach, position, 0.0 ) };
auto const worn { knots::sample( theirreach, position, 0.0 ) };
auto const half { 0.5 * std::clamp( worn - rolled, 0.0, blend ) };
reached.emplace_back( position, rolled - half + blend );
outside.emplace_back( position, worn + half - blend );
meet.emplace_back( position, ( worn >= rolled || theirs == own ) ? 0.5 * ( rolled + worn ) : theirsbrighter ? worn : rolled );
}
}
auto const inner { outside.empty() ? knots::shifted( reached, -blend ) : knots::bound( knots::shifted( reached, -blend ), meet, true ) };
auto const rolled { outside.empty() ? reached : knots::bound( reached, meet, true ) };
auto const worn { outside.empty() ? knots::line {} : knots::bound( outside, meet, false ) };
auto const outer { outside.empty() ? knots::line {} : knots::bound( knots::shifted( outside, blend ), meet, false ) };
auto const splits { cross_splits( Whole, reach, { &rolled, &inner, &worn, &outer }, 0.0 ) };
auto const marked { treads_marked() };
std::vector<top_band> bands {
{ 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 {} : reached },
{ marked ? paint( own, { 1.f, 0.f, 1.f } ) : Whole.material, worn.empty() ? Whole.extents : knots::bound( Whole.extents, worn, true ), knots::bound( rolled, Whole.inner, false ), false } };
if( false == worn.empty() ) {
bands.push_back( { marked ? paint( own, { 0.f, 1.f, 1.f } ) : theirmaterial, knots::bound( Whole.extents, outer, true ), knots::bound( worn, Whole.inner, false ), false, marked ? knots::line {} : outside, -blend } );
bands.push_back( { theirmaterial, Whole.extents, knots::bound( outer, Whole.inner, false ) } );
}
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 } },
top, Whole, reach, splits, bands,
[&]( double const Middle ) {
auto const rolled { knots::sample( Rolled, Middle, 0.0 ) };
return std::vector<double> { rolled - blend, rolled }; },
if( worn.empty() ) {
auto const edge { knots::sample( reached, Middle, 0.0 ) };
return std::vector<double> { edge - blend, edge };
}
return std::vector<double> { knots::sample( inner, Middle, 0.0 ), knots::sample( rolled, Middle, 0.0 ), knots::sample( worn, Middle, 0.0 ), knots::sample( outer, Middle, 0.0 ) }; },
Ends );
}
@@ -961,21 +1105,39 @@ void TTrack::rail_layer::plain( rail_piece Whole, rail_tread const &Tread, piece
void TTrack::rail_layer::treaded( rail_piece Whole, rail_tread const &Tread, piece_ends const &Ends ) {
auto const marked { treads_marked() };
auto const own { m_track.rail_material( m_path ) };
// where the wheels from beside polish the head as its own do, there's no tint between two polishes, only the polish of the rail itself
auto const same { Tread.material == own && false == marked };
if( same && Whole.rolled ) {
plain( Whole, Tread, Ends );
return;
}
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() ) ) };
// the lines of the tread run out from the gauge line of the rail, the piece is laid shifted off it
knots::line back;
for( auto const &knot : Whole.offsets ) {
back.emplace_back( knot.x, -knot.y );
}
auto const edge { knots::sum( Tread.edge, back ) };
auto const gauge { knots::sum( Tread.gauge, back ) };
auto const &lower { Tread.below ? gauge : edge };
auto const &upper { Tread.below ? edge : gauge };
// the brighter polish of the tread fades out towards the edge of the reach of the wheels into the colour of the polish of the rail itself,
// keeping the gloss of its steel
auto const colour { glm::clamp( tread_colour( own, m_track.tread_band() ) / glm::max( tread_colour( Tread.material, m_track.tread_band() ), glm::vec3 { 1e-3f } ), 0.f, 4.f ) };
auto const tint = [&]( material_handle const Material, glm::vec3 const &Mark ) {
return paint( Material, marked ? Mark : colour ); };
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 owned = [&]( knots::line const &Extents, knots::line const &Inner, double const Fade ) {
return marked ?
top_band { Whole.rolled ? tint( own, { 1.f, 0.f, 0.f } ) : null_handle, Extents, Inner, false, {}, Fade } :
top_band { null_handle, Extents, Inner, Whole.rolled }; };
auto const fade { Tread.fade };
auto const centre { knots::middle( lower, upper ) };
auto const beneath { knots::shifted( lower, -fade ) };
@@ -985,15 +1147,32 @@ void TTrack::rail_layer::treaded( rail_piece Whole, rail_tread const &Tread, pie
auto splits { cross_splits( Whole, reach, { &lower, &upper, &lowfade, &highfade, &beneath, &beyond }, 0.001 ) };
add_meetings( knots::shifted( lower, 2.0 * fade ), upper, Whole.from, Whole.to, 0.001, splits );
std::sort( splits.begin(), splits.end() );
if( same ) {
// the head no wheel of its own rolls on is polished across the tread alone, fading into the matte outside it
lay_top(
top, Whole, reach, splits,
{
{ null_handle, knots::bound( Whole.extents, beneath, true ), Whole.inner, false },
{ null_handle, knots::bound( Whole.extents, lower, true ), knots::bound( Whole.inner, beneath, false ), false, beneath, -fade },
{ shared, knots::bound( Whole.extents, upper, true ), knots::bound( Whole.inner, lower, false ) },
{ null_handle, knots::bound( Whole.extents, beyond, true ), knots::bound( Whole.inner, upper, false ), false, beyond, fade },
{ null_handle, Whole.extents, knots::bound( Whole.inner, beyond, false ), 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<double> { from - fade, from, to, to + fade }; },
Ends );
return;
}
lay_top(
top, Whole, reach, splits,
{
{ 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 },
owned( knots::bound( Whole.extents, lower, true ), knots::bound( Whole.inner, beneath, false ), 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 ), fade },
{ owntint, knots::bound( Whole.extents, beyond, true ), knots::bound( Whole.inner, upper, false ), false, ramp( upper ), -fade },
owned( knots::bound( Whole.extents, beyond, true ), knots::bound( Whole.inner, upper, false ), -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 ) };
@@ -1004,15 +1183,9 @@ void TTrack::rail_layer::treaded( rail_piece Whole, rail_tread const &Tread, pie
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 );
// the sides are lofted in the whole profile, the top left out: lofted apart, the points of a side of the head planed along the rail had none
// of the top beside them to stop at, and went down past the wall of the cut, which went missing between the top and the underside of the head
piece( m_profile, Whole, Ends, m_topfirst, m_toplast );
return strip( m_topfirst, m_toplast );
}
@@ -1037,7 +1210,7 @@ void TTrack::rail_layer::lay_top( gfx::vertex_array const &Top, rail_piece const
}
}
void TTrack::rail_layer::piece( gfx::vertex_array const &Shape, rail_piece const &Piece, piece_ends const &Ends ) {
void TTrack::rail_layer::piece( gfx::vertex_array const &Shape, rail_piece const &Piece, piece_ends const &Ends, std::size_t const Leftfrom, std::size_t const Leftto ) {
gfx::vertex_array vertices;
gfx::userdata_array userdata;
@@ -1052,6 +1225,9 @@ void TTrack::rail_layer::piece( gfx::vertex_array const &Shape, rail_piece const
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 ) {
if( j >= Leftfrom && j < Leftto ) {
continue;
}
for( auto const index : { 0, 1, 2, 1, 3, 2 } ) {
auto const vertex { block + 2 * j + index };
triangles.push_back( vertices[ vertex ] );
@@ -1064,7 +1240,7 @@ 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 ) );
sink( Piece.material, false == Piece.rolled ).emplace_back( insert_chunk( triangles, triangleuserdata, m_drawing.bank, GL_TRIANGLES ) );
}
loft_span TTrack::rail_layer::span( rail_piece const &Piece, piece_ends const &Ends ) const {

View File

@@ -25,6 +25,7 @@ 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 wheel_play { 0.005 }; // how far the wheels wander across each way, by the play of their flanges in the gauge
double constexpr nose_tip { 0.012 };
double constexpr middle_angle { 0.35 };
double constexpr wide_angle { 0.785 };
@@ -35,6 +36,8 @@ double constexpr square_slope { 3.0 };
double constexpr spacer_length { 0.12 };
double constexpr spacer_pitch { 0.5 };
double constexpr check_flangeway { 0.041 };
double constexpr guard_planing { 0.015 }; // how far the ends of guard rails are planed off their face toward the running rail
double constexpr guard_bevel { 1.7320508 }; // how far along the flared ends of guard rails are cut back across them, from their face toward the running rail: 30 degrees to the rail
double constexpr blade_throw { 0.160 };
double constexpr blade_clearance { 0.058 };
double constexpr blade_point { 0.003 };
@@ -45,6 +48,7 @@ double constexpr blade_mark { 0.010 };
double constexpr blade_gap { 0.0005 };
double constexpr blade_lowered { 0.050 };
double constexpr blade_spacing { 0.5 };
double constexpr blade_runon_margin { 0.2 }; // how far short of the end of the plain track a blade running on into it ends
// 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 +91,7 @@ struct rail_crossing {
// 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 shoulder { 0.005 }; // how far in from the outer edge of the head its top is rounded off
double height { 0.172 };
double low { -0.0385 }; // the reach of the profile inwards
double high { 0.1115 }; // the reach of the profile outwards
@@ -102,7 +107,7 @@ struct rail_size {
double web { 0.0365 };
double neck { -0.0467 };
};
// a check rail beside a rail, how far its ends turn away from the running rail
// a check rail beside a rail, how far its ends are bent away from the running rail
struct check_rail {
double from { 0.0 };
double to { 0.0 };
@@ -116,6 +121,16 @@ inline double check_flare( check_rail const &Check ) {
return std::min( guard_flare, 0.4 * ( Check.to - Check.from ) );
}
// how the end of a part of a rail planed by the rails around it is made: square where it's put, a point where the head is planed down to nothing
// (a wing rail), blunt where it's wide enough for the wheels running at it (a leg of the nose, a point rail), sharp where it's put as it gets wider
// (a leg of a movable nose), mitred against the end of the other rail (an obtuse crossing)
enum class rail_end {
square,
point,
blunt,
sharp,
mitre
};
// 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 };
@@ -130,6 +145,21 @@ struct rail_cut {
knots::line endinner, startinner;
bool endcap { true };
bool startcap { true };
knots::line endworn, startworn; // from how far out from the gauge line the wheels of a rail beside roll on the head, where it's planed flush against that rail
material_handle endwornby { 0 }; // what the wheels rolling on it from beside polish the head to, the brightest of them
material_handle startwornby { 0 };
// what carving the parts against the rails around takes from the design of the crossing
bool carved { false }; // the lines of the parts are left to carving
rail_end endkind { rail_end::square };
rail_end startkind { rail_end::square };
double endanchor { 0.0 }; // from where on a part planed down to its point can end
double startanchor { 0.0 };
double endrunon { 0.0 }; // how far past its end a wing rail can go on to be planed down to its point
double startrunon { 0.0 };
knots::line endcarve, startcarve; // offsets the parts are carved with where they aren't those they're laid with: the legs of a movable nose, as they lie with the first path set
bool throwable { false }; // the part going on moves with the switch: a leg of a movable nose
glm::dvec2 mark { 0.0 }; // where the rails cross, in plan: the parts at an obtuse crossing aren't carved against the other rail's there, they're designed to meet
bool obtuse { false };
};
struct rail_plan {
std::vector<rail_cut> cuts;

View File

@@ -42,59 +42,118 @@ std::vector<glm::dvec2> wing_extension( TTrack &Track, track_rails::check_rail_s
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<glm::dvec2> const &Extension, std::vector<glm::dvec2> const &Legs ) {
// a wing rail bent out along the flangeway of the other rail of its frog, from where that closes in on ours on to Wingend
knots::line flangeway_wing( TTrack const &Track, track_rails::rail_crossing const &Crossing, double const Throat, double const Wingend ) {
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<glm::dvec2> 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<glm::dvec2> rolled;
for( auto const &knot : wing ) {
rolled.emplace_back( knot.x, rolled_band - knot.y );
auto const &ours { rail_line( Track, Crossing.path, Crossing.side > 0, false ) };
auto const &theirs { rail_line( *Crossing.track, Crossing.otherpath, Crossing.otherside > 0, false ) };
auto const direction { Wingend > Throat ? 1.0 : -1.0 };
auto const start { Throat - direction * 2.0 };
auto const count { std::max( 2, static_cast<int>( std::ceil( std::abs( Wingend - start ) / 0.05 ) ) ) };
glm::dvec3 point, tangent;
ours.at( start, point, tangent );
auto distance { nearest_distance( theirs, point ) };
knots::line wing;
for( int i = 0; i <= count; ++i ) {
auto const at { glm::mix( start, Wingend, static_cast<double>( i ) / count ) };
ours.at( at, point, tangent );
auto const outwards { -glm::normalize( plan( ours.normal( at, Crossing.side ) ) ) };
auto offset { 0.0 };
for( int iteration = 0; iteration < 3; ++iteration ) {
glm::dvec3 const shifted { point.x + outwards.x * offset, point.y, point.z + outwards.y * offset };
auto const across { lateral( theirs, Crossing.otherside, shifted, distance ) };
auto const facing { glm::dot( outwards, -glm::normalize( plan( theirs.normal( distance, Crossing.otherside ) ) ) ) };
if( std::abs( facing ) < 0.1 ) {
break;
}
offset += ( -flangeway - across ) / facing;
}
wing.emplace_back( at, offset );
}
knots::sort( rolled );
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;
// from where the flangeway of the other rail reaches the gauge line of ours
knots::line result;
std::size_t first { 0 };
while( first + 1 < wing.size() && wing[ first + 1 ].y <= 0.0 ) {
++first;
}
if( wing[ first ].y <= 0.0 && first + 1 < wing.size() ) {
auto const &a { wing[ first ] };
auto const &b { wing[ first + 1 ] };
result.emplace_back( glm::mix( a.x, b.x, a.y / ( a.y - b.y ) ), 0.0 );
++first;
}
else {
cut.end = nose;
cut.endextents = Legs;
cut.start = wingend;
result.emplace_back( start, 0.0 );
}
for( auto i { first }; i < wing.size(); ++i ) {
result.emplace_back( wing[ i ].x, std::max( wing[ i ].y, 0.0 ) );
}
knots::sort( result );
return knots::simplified( result, 0.0001 );
}
knots::line rolled_wing( knots::line const &Wing ) {
knots::line rolled;
for( auto const &knot : Wing ) {
rolled.emplace_back( knot.x, rolled_band - knot.y );
}
return rolled;
}
// acute frog as designed for carving: the wing rail bent out along the flangeway of the other rail ends in a point, or run on a bit past its end into
// what planes it to one; the leg of the nose begins blunt past where the rails cross
void designed_acute( track_rails::rail_plan &Plan, TTrack const &Track, track_rails::rail_crossing const &Crossing, double const Toward, double const Slope, double const Wingend ) {
auto const throat { Crossing.at + Toward * flangeway / Slope };
auto const wing { flangeway_wing( Track, Crossing, throat, Wingend ) };
track_rails::rail_cut cut;
cut.at = Crossing.at;
cut.carved = true;
if( Toward < 0.0 ) {
cut.end = Wingend;
cut.endoffsets = wing;
cut.endrolled = rolled_wing( wing );
cut.endkind = track_rails::rail_end::point;
cut.endanchor = throat;
cut.endrunon = 1.0;
cut.start = Crossing.at;
cut.startkind = track_rails::rail_end::blunt;
}
else {
cut.end = Crossing.at;
cut.endkind = track_rails::rail_end::blunt;
cut.start = Wingend;
cut.startoffsets = wing;
cut.startrolled = rolled;
cut.startrolled = rolled_wing( wing );
cut.startkind = track_rails::rail_end::point;
cut.startanchor = throat;
cut.startrunon = 1.0;
}
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 ) {
// obtuse crossing as designed for carving: the rail ends mitred where it meets the other one, the knuckle, and starts again where its point keeps
// out of the flangeway of the other rail
void designed_obtuse( track_rails::rail_plan &Plan, double const At, double const Into, glm::dvec2 const &Mark ) {
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;
cut.end = At;
cut.start = At;
cut.carved = true;
cut.obtuse = true;
cut.mark = Mark;
if( Into > 0.0 ) {
cut.end = At;
cut.endbend = -bend;
cut.start = At + gap;
cut.startextents = { { At + gap, 0.006 }, { At + taper, full } };
cut.endkind = track_rails::rail_end::mitre;
cut.startkind = track_rails::rail_end::blunt;
}
else {
cut.end = At - gap;
cut.endextents = { { At - taper, full }, { At - gap, 0.006 } };
cut.start = At;
cut.endkind = track_rails::rail_end::blunt;
cut.startbend = bend;
cut.startkind = track_rails::rail_end::mitre;
}
Plan.cuts.push_back( cut );
}
@@ -192,11 +251,10 @@ std::vector<track_rails::rail_fill> spacer_fills( track_rails::rail_fill const &
std::vector<track_rails::rail_fill> 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.inner = { { Guard.from, offset }, { Guard.to, offset } };
whole.outer = { { Guard.from, Size.web }, { Guard.to, Size.web } };
return spacer_fills( whole, Guard.from, Guard.to, Guard.castings );
}
@@ -328,44 +386,6 @@ std::vector<glm::dvec2> tread_edge( TTrack const &Track, int const Path, int con
return knots;
}
std::vector<glm::dvec2> 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<glm::dvec2> 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<int>( std::ceil( 3.0 * std::abs( blunt - nose ) / step ) ) };
std::vector<glm::dvec2> 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;
}
knots::sort( knots );
return knots;
}
} // namespace
namespace {
@@ -418,7 +438,7 @@ class TTrack::rail_planner {
public:
// constructors
rail_planner( TTrack &Track, bool const Crossings );
rail_planner( TTrack &Track, bool const Crossings, bool const Carve );
// methods
rail_layout lay_out();
@@ -428,6 +448,7 @@ private:
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 movable_frog( rail_crossing const &Crossing, double const Push );
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;
@@ -439,21 +460,30 @@ private:
// members
TTrack &m_track;
bool const m_crossings;
bool const m_carve; // the parts of the rails at crossings carved out of what lies around them, or only designed
rail_size const m_size;
double const m_guardoffset;
rail_layout m_layout;
std::vector<rail_crossing> m_corners;
};
TTrack::rail_planner::rail_planner( TTrack &Track, bool const Crossings ) :
TTrack::rail_planner::rail_planner( TTrack &Track, bool const Crossings, bool const Carve ) :
m_track { Track },
m_crossings { Crossings },
m_carve { Carve },
m_size { Track.rail_dimensions() },
m_guardoffset { -( flangeway + m_size.head ) }
{}
rail_layout TTrack::rail_planner::lay_out() {
if( m_carve && m_crossings ) {
if( auto const *designed { designed_layout( m_track ) } ) {
m_layout = *designed;
carve_plans( m_track, m_layout );
return m_layout;
}
}
std::vector<rail_crossing> crossings;
if( m_crossings ) {
crossings = m_track.rail_crossings();
@@ -494,6 +524,9 @@ rail_layout TTrack::rail_planner::lay_out() {
}
}
}
if( m_carve && m_crossings ) {
carve_plans( m_track, m_layout );
}
return m_layout;
}
@@ -517,7 +550,9 @@ void TTrack::rail_planner::add_crossing( rail_crossing const &Crossing ) {
acute_frog( Crossing, Crossing.into > 0.0 ? 1.0 : -1.0 );
}
else {
obtuse( plan_of( Crossing ), Crossing.at, Crossing.into, m_size );
glm::dvec3 point, tangent;
rail_line( m_track, Crossing.path, Crossing.side > 0, false ).at( Crossing.at, point, tangent );
designed_obtuse( plan_of( Crossing ), Crossing.at, Crossing.into, plan( point ) );
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 );
}
@@ -559,10 +594,11 @@ void TTrack::rail_planner::acute_frog( rail_crossing const &Crossing, double con
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 extension { 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 ) };
designed_acute( plan, m_track, Crossing, Toward, slope, extension.empty() ? Crossing.at - Toward * wing_reach( slope ) : extension.back().x );
// the nose is carved yet, it ends near where its legs get as wide as its tip
auto const half { slope / ( 1.0 + std::sqrt( 1.0 + slope * slope ) ) };
auto const nose { Crossing.at - Toward * ( 0.5 * nose_tip / half - 0.1 ) };
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 );
}
@@ -684,10 +720,62 @@ void TTrack::rail_planner::switch_frog() {
add_vee( crossing, m_size, path == 0 );
continue;
}
if( m_track.SwitchExtension->m_movablenose ) {
auto const nosethrow { std::max( 0.0, m_track.SwitchExtension->m_nosethrow ) };
movable_frog( crossing, path == 0 ? nosethrow : 1.0 - nosethrow );
continue;
}
acute_frog( crossing, -1.0 );
}
}
// the frog of a switch with a movable nose: the nose reaches back to the throat and lies against the wing rail of the track set,
// its leg of the other track pushed over the flangeway and bending back to its place towards the heel; no gap on the way of the wheels.
// Push: how much of the flangeway the leg of this path is pushed over, 0 when its track is set, 1 when the other one is
void TTrack::rail_planner::movable_frog( rail_crossing const &Crossing, double const Push ) {
auto &plan { plan_of( Crossing ) };
auto const slope { std::tan( Crossing.angle ) };
auto const throat { Crossing.at - flangeway / slope };
auto const length { movable_nose_length( slope ) };
// how far the leg of the track not set is pushed over: the whole flangeway at the tip, nothing at the heel, smooth at both
auto const pushed = [&]( double const At ) {
auto const t { std::clamp( ( At - throat ) / length, 0.0, 1.0 ) };
return flangeway * ( 1.0 - t * t * ( 3.0 - 2.0 * t ) ); };
// the legs are carved as they lie with the first path set, the leg of the second one pushed over: the same whichever is set
auto const flare { std::max( Crossing.at + wing_reach( slope ), throat + length + 0.5 ) };
auto const wing { flangeway_wing( m_track, Crossing, throat, flare ) };
knots::line carved, offsets;
for( int i = 0; i <= static_cast<int>( std::ceil( length / 0.1 ) ); ++i ) {
auto const at { std::min( throat + 0.1 * i, throat + length ) };
carved.emplace_back( at, -pushed( at ) );
offsets.emplace_back( at, -Push * pushed( at ) );
}
m_layout.wingends[ Crossing.path ][ Crossing.side > 0 ].emplace_back( flare, 1.0 );
track_rails::rail_cut cut;
cut.at = Crossing.at;
cut.carved = true;
cut.end = flare;
cut.endoffsets = wing;
cut.endrolled = rolled_wing( wing );
cut.endkind = rail_end::point;
cut.endanchor = throat;
cut.start = throat;
cut.startkind = rail_end::sharp;
cut.throwable = true;
if( Push > 0.0 ) {
cut.startoffsets = offsets;
}
if( Crossing.path == 1 ) {
cut.startcarve = carved;
}
plan.cuts.push_back( cut );
if( Push <= 0.0 ) {
auto const shared { Crossing.at + ( rolled_band + m_size.head + tread_fade ) / slope };
add_tread( Crossing.path, Crossing.side, { throat, shared }, *Crossing.track, Crossing.otherpath, -1, Crossing.otherside );
}
}
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 ) };
@@ -730,7 +818,7 @@ glm::dvec2 TTrack::rail_planner::shared_span( rail_crossing const &Crossing, rai
return span;
}
track_rails::rail_layout TTrack::plan_rails( bool const Crossings ) {
track_rails::rail_layout TTrack::plan_rails( bool const Crossings, bool const Carve ) {
return rail_planner( *this, Crossings ).lay_out();
return rail_planner( *this, Crossings, Carve ).lay_out();
}

View File

@@ -63,6 +63,14 @@ rail_size measure_rail( gfx::vertex_array const &Profile ) {
}
}
result.tread = result.head;
auto flat { -1e9 };
for( std::size_t i = 0; i < count; ++i ) {
auto const &point { Profile[ i ].position };
if( point.y > top - 0.0005 ) {
flat = std::max( flat, static_cast<double>( point.x ) );
}
}
result.shoulder = std::clamp( result.head - flat, 0.0, 0.5 * result.head );
std::vector<std::size_t> heads, lips;
for( std::size_t i = 0; i < count; ++i ) {
auto const &point { Profile[ i ].position };

View File

@@ -633,6 +633,65 @@ bool TSegment::RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin,
return true;
};
bool TSegment::RenderLoftSteps( gfx::vertex_array &Output, glm::dvec3 const &Origin, gfx::vertex_array const &ShapePoints, double fTextureLength, int const Steps ) const
{
if( fTsBuffer.empty() || Steps < 1 || fLength <= 0.0 )
return false;
auto const shapepoints { ShapePoints.size() / 2 };
auto const texturelength { static_cast<float>( fTextureLength ) };
auto const step { static_cast<float>( fLength / Steps ) };
auto const offset { 0.1 / fLength };
// point and the unit vector across the path at the k-th of the Steps stations
auto const station = [&]( int const k, glm::vec3 &Position, glm::vec3 &Parallel ) {
if( k == 0 && m_joints[ 0 ] ) {
Position = glm::vec3{ m_joints[ 0 ]->first - Origin };
Parallel = glm::vec3{ m_joints[ 0 ]->second };
return; }
if( k == Steps && m_joints[ 1 ] ) {
Position = glm::vec3{ m_joints[ 1 ]->first - Origin };
Parallel = glm::vec3{ m_joints[ 1 ]->second };
return; }
auto const t { k == 0 ? 0.0 : k == Steps ? 1.0 : FastGetTFromS( fLength * k / Steps ) };
Position = glm::vec3{ FastGetPoint( t ) - Origin };
auto const dir { FastGetDirection( t, offset ) };
Parallel = glm::vec3{ -dir.z, 0.0, dir.x };
if( glm::length2( Parallel ) == 0.f ) {
// the same workaround as in RenderLoft for control points placed above endpoints
Parallel = glm::vec3{ FastGetPoint_1() - FastGetPoint_0() };
}
Parallel = glm::normalize( Parallel ); };
// a vertex of the profile blended Share of the way from its start to its end shape
auto const vertex = [&]( std::size_t const j, float const Share, glm::vec3 const &Position, glm::vec3 const &Parallel, float const V ) {
auto const &from { ShapePoints[ j ] };
auto const &to { ShapePoints[ j + shapepoints ] };
auto const rest { 1.f - Share };
auto point { Parallel * ( rest * from.position.x + Share * to.position.x ) + Position };
point.y += rest * from.position.y + Share * to.position.y;
auto normal { ( rest * from.normal.x + Share * to.normal.x ) * Parallel };
normal.y += rest * from.normal.y + Share * to.normal.y;
Output.emplace_back( point, glm::normalize( normal ), glm::vec2 { rest * from.texture.x + Share * to.texture.x, V } ); };
glm::vec3 pos1, pos2, parallel1, parallel2;
station( 0, pos1, parallel1 );
auto tv1 { 1.f };
for( int k = 1; k <= Steps; ++k ) {
station( k, pos2, parallel2 );
tv1 = clamp_circular( tv1, 1.0f );
auto const tv2 { tv1 - step / texturelength };
auto const m1 { static_cast<float>( k - 1 ) / Steps };
auto const m2 { static_cast<float>( k ) / Steps };
for( std::size_t j = 0; j < shapepoints; ++j ) {
vertex( j, m1, pos1, parallel1, tv1 );
vertex( j, m2, pos2, parallel2, tv2 );
}
pos1 = pos2;
parallel1 = parallel2;
tv1 = tv2;
}
return true;
}
void TSegment::plan_frame( double const Distance, glm::dvec3 &Point, glm::dvec3 &Across ) const {
auto const t { FastGetTFromS( std::clamp( Distance, 0.0, fLength ) ) };
Point = FastGetPoint( t );
@@ -751,6 +810,23 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig
auto const clamped = [&]( std::size_t const Index ) {
auto const outside { reach( Index ) };
return ( outside > middleextent ? 1 : outside < middleinner ? -1 : 0 ); };
auto nearest { 1e6 };
auto farthest { -1e6 };
for( std::size_t j = 0; j < count; ++j ) {
nearest = std::min( nearest, reach( j ) );
farthest = std::max( farthest, reach( j ) );
}
if( middleextent <= middleinner || middleextent < nearest || middleinner > farthest ) {
// nothing of the profile between the limits there: the ring shrinks to one point, the step draws nothing
auto const outside { std::clamp( 0.5 * ( std::min( extent, farthest ) + std::max( innerextent, nearest ) ), nearest, farthest ) };
auto pt { parallel * static_cast<float>( Span.side * ( Span.halfwidth + outside + offset ) ) + position };
pt.y += ShapePoints.front().position.y + Span.lift;
for( std::size_t j = 0; j < count; ++j ) {
Ring.emplace_back( pt, glm::vec3{ 0.f, 1.f, 0.f }, glm::vec2{ ShapePoints[ j ].texture.x, 0.f } );
Wear.push_back( 1.f );
}
return;
}
for( std::size_t j = 0; j < count; ++j ) {
auto shape { ShapePoints[ j ] };
auto outside { reach( j ) };

View File

@@ -185,6 +185,9 @@ public:
bool
RenderLoft( gfx::vertex_array &Output, glm::dvec3 const &Origin, gfx::vertex_array const &ShapePoints, bool const Transition, double fTextureLength, double Texturescale = 1.0, int iSkip = 0, int iEnd = 0, std::pair<float, float> fOffsetX = {0.f, 0.f}, glm::vec3 **p = nullptr, bool bRender = true );
// a transition profile lofted in Steps pieces of equal length, so two paths of different make-up line up piece by piece
bool
RenderLoftSteps( gfx::vertex_array &Output, glm::dvec3 const &Origin, gfx::vertex_array const &ShapePoints, double fTextureLength, int const Steps ) const;
// 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, loft_span const &Span ) const;

View File

@@ -677,6 +677,30 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin)
SwitchExtension->m_frogs = true;
}
}
else if( str == "movablenose" ) {
// the frog of the switch with a movable nose
if( eType == tt_Switch ) {
SwitchExtension->m_movablenose = true;
}
}
else if( str == "blades" ) {
// blades <heel> <throw>: where the moving blades of the switch end and how far their tips move over, 0: from the shape of the switch
auto const heel { parser->getToken<double>() };
auto const bladethrow { parser->getToken<double>() };
if( eType == tt_Switch ) {
SwitchExtension->m_bladeheel = std::max( 0.0, heel );
SwitchExtension->m_bladethrow = std::max( 0.0, bladethrow );
}
}
else if( str == "timbermodel" ) {
// timbermodel <model> <skin>: the model of the timbers under the switch or the crossing the track is part of, put together
// from its submodels begin, main and end, or drawn whole; skin none: the textures of the model
auto modelpath { parser->getToken<std::string>( false ) };
auto skinpath { parser->getToken<std::string>( false ) };
replace_slashes( modelpath );
m_timber_model_name = modelpath;
m_timber_skin_name = ( skinpath == "none" ? std::string{} : skinpath );
}
else if( str == "friction" ) {
// memory cell holding friction value modifiers
m_friction.first = parser->getToken<std::string>();
@@ -1452,6 +1476,9 @@ void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) {
SwitchExtension->Geometry3 = gfx::geometry_handle{};
}
m_sleeper_local_transforms.clear();
// the sleepers of this track and of the tracks around depend on where it runs
m_sleepers3d.dirty = true;
track_sleepers::unsettle_around( *this );
if( Bank == null_handle ) { return; }
@@ -1501,7 +1528,14 @@ void TTrack::share_joints() {
void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
gfx::userdata_array empty_userdata;
// the rails of a frog with a movable nose are laid again as it moves over, in the same chunks
auto const movable { eType == tt_Switch && SwitchExtension && SwitchExtension->m_movablenose };
track_rails::reused_chunks const reuse { movable ? &SwitchExtension->m_nosechunks : nullptr };
if( eType == tt_Switch && SwitchExtension ) {
SwitchExtension->m_nosethrow = ( Bank != null_handle ? nose_throw() : -1.0 );
}
share_joints();
m_sleepers3d.active = track_sleepers::lays( *this );
// bake per-instance sleeper transforms now that the owning cell has assigned m_origin.
// safe to call here even if the track has no sleepermodel (early-outs internally).
if( m_sleeper_enabled && m_sleeper_local_transforms.empty() ) {
@@ -1528,7 +1562,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
{
case tt_Table: // obrotnica jak zwykły tor, tylko animacja dochodzi
case tt_Normal:
if (m_material2)
if (m_material2 && ( Global.Sleepers3DBed || false == m_sleepers3d.active ) )
{ // podsypka z podkładami jest tylko dla zwykłego toru
gfx::vertex_array bpts1;
create_track_bed_profile( bpts1, trPrev, trNext );
@@ -1544,7 +1578,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
gfx::vertex_array vertices;
Segment->RenderLoft(vertices, m_origin, bpts1, iTrapezoid > 0, texturelength);
if( Bank != 0 && true == Geometry2.empty() ) {
Geometry2.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP));
Geometry2.emplace_back(track_rails::insert_chunk(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP));
}
if( Bank == 0 && false == Geometry2.empty() ) {
// special variant, replace existing data for a turntable track
@@ -1560,7 +1594,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
if( Bank != 0 && true == Geometry1.empty() ) {
for( auto const side : { 1, -1 } ) {
auto const &profile { side > 0 ? rpts1 : rpts2 };
if( eType == tt_Normal && iTrapezoid == 0 && lay_rail( 0, profile, side, texturelength, 0.0, trPrev == nullptr, trNext == nullptr, Geometry1, GeometryCut1 ) ) {
if( eType == tt_Normal && iTrapezoid == 0 && lay_rail( 0, profile, side, texturelength, track_rails::blade_runon( *this, side ), trPrev == nullptr, trNext == nullptr, Geometry1, GeometryCut1 ) ) {
continue;
}
vertices.clear(); // reuse the scratchpad
@@ -1568,7 +1602,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
if( open ) {
cut_faces( vertices, nnumPts, trPrev == nullptr, trNext == nullptr, GeometryCut1 );
}
Geometry1.emplace_back(GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP));
Geometry1.emplace_back(track_rails::insert_chunk(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP));
}
}
if( Bank == 0 && false == Geometry1.empty() ) {
@@ -1606,7 +1640,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
else {
SwitchExtension->Segments[ Path ]->RenderLoft( vertices, m_origin, Profile, false, Texturelength, 1.0, track_rails::blade_steps( *this, Path ) );
}
Geometry.emplace_back( GfxRenderer->Insert( vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP ) );
Geometry.emplace_back( track_rails::insert_chunk( vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP ) );
}
};
// rail along the whole path
@@ -1614,7 +1648,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
if( false == lay_rail( Path, Profile, Side, Texturelength, 0.0, false, false, Geometry, Cuts ) ) {
gfx::vertex_array vertices;
SwitchExtension->Segments[ Path ]->RenderLoft( vertices, m_origin, Profile, false, Texturelength, 1.0, jointlength );
Geometry.emplace_back( GfxRenderer->Insert( vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP ) );
Geometry.emplace_back( track_rails::insert_chunk( vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP ) );
}
};
gfx::vertex_array vertices;
@@ -1626,7 +1660,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
auto const side { blade_side( path ) };
loft_blade( path, side > 0 ? rpts1 : rpts2, side > 0 ? rpts3 : rpts4, texture_length( material ), vertices );
cut_faces( vertices, nnumPts, true, false, path ? GeometryCut2 : GeometryCut1 );
( path ? Geometry2 : Geometry1 ).emplace_back( GfxRenderer->Insert( vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP ) );
( path ? Geometry2 : Geometry1 ).emplace_back( track_rails::insert_chunk( vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP ) );
vertices.clear();
}
for( int path = 0; path < 2; ++path ) {
@@ -1645,7 +1679,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
if( path == 0 && jointlength > 0 ) {
// part of the diverging rail touched by wheels of vehicle going straight
SwitchExtension->Segments[ 1 ]->RenderLoft( vertices, m_origin, rail, false, texturelength, 1.0, 0, jointlength );
geometry.emplace_back( GfxRenderer->Insert( vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP ) );
geometry.emplace_back( track_rails::insert_chunk( vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP ) );
vertices.clear();
}
// other rail, full length; on the diverging path potentially minus part touched by wheels of vehicle going straight
@@ -1656,7 +1690,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) {
if( true == Global.CreateSwitchTrackbeds ) {
gfx::vertex_array vertices;
create_switch_trackbed( vertices );
SwitchExtension->Geometry3 = GfxRenderer->Insert(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP);
SwitchExtension->Geometry3 = track_rails::insert_chunk(vertices, empty_userdata, Bank, GL_TRIANGLE_STRIP);
vertices.clear();
}
@@ -2037,6 +2071,27 @@ bool TTrack::Switch(int i, float const t, float const d)
return false;
};
int TTrack::nose_set() const {
return ( SwitchExtension->fOffset > 0.5f * fMaxOffset ? 1 : 0 );
}
double TTrack::nose_throw() const {
return ( fMaxOffset > 0.f ? std::clamp( static_cast<double>( SwitchExtension->fOffset / fMaxOffset ), 0.0, 1.0 ) : nose_set() );
}
double TTrack::trailing_point( int const Path ) const {
if( SwitchExtension->m_movablenose ) {
// the nose set the other way stands in the way of the wheels before they get to the blades
if( auto const frog { this->frog() }; frog.valid ) {
return frog.at[ Path ];
}
}
return track_rails::blade_length( *this, Path );
}
bool TTrack::SwitchForced(int i, TDynamicObject *o)
{ // rozprucie rozjazdu
if (SwitchExtension)
@@ -2056,6 +2111,10 @@ bool TTrack::SwitchForced(int i, TDynamicObject *o)
break;
}
Switch(i); // jeśli się tu nie przełączy, to każdy pojazd powtórzy event rozrprucia
if( SwitchExtension->m_movablenose && o != nullptr ) {
// the wheels can't push the nose of the frog over, the vehicle runs off the rails at it
o->MoverParameters->Derail( TRAILED_SWITCH );
}
}
}
else if (eType == tt_Cross)
@@ -2151,13 +2210,24 @@ void TTrack::loft_blade( int const Path, gfx::vertex_array const &Rail, gfx::ver
auto const opening { Path == 0 ? SwitchExtension->fOffset2 : fMaxOffset - SwitchExtension->fOffset1 };
auto const offset { blade_side( Path ) * opening };
if( auto const &shape { blade( Path ) }; shape.valid ) {
auto const thrown { track_rails::blade_throw * std::clamp( static_cast<double>( opening ) / ( fMaxOffset + SwitchExtension->fOffsetDelay ), 0.0, 1.0 ) };
auto const thrown { blades_throw() * std::clamp( static_cast<double>( opening ) / ( fMaxOffset + SwitchExtension->fOffsetDelay ), 0.0, 1.0 ) };
std::vector<glm::dvec2> offsets;
for( auto const &bend : shape.bends ) {
offsets.emplace_back( bend.x, -thrown * bend.y );
}
auto const size { rail_dimensions() };
SwitchExtension->Segments[ Path ]->RenderLoftSpan( Vertices, m_origin, Rail, { .side = blade_side( Path ), .halfwidth = 0.5f * std::abs( fTrackWidth ), .texturelength = Texturelength, .from = shape.tip, .to = shape.heel, .offsets = offsets, .extents = shape.extents, .walls = true, .drops = shape.drops, .head = { size.neck, size.crest } } );
auto const length { SwitchExtension->Segments[ Path ]->GetLength() };
SwitchExtension->Segments[ Path ]->RenderLoftSpan( Vertices, m_origin, Rail, { .side = blade_side( Path ), .halfwidth = 0.5f * std::abs( fTrackWidth ), .texturelength = Texturelength, .from = shape.tip, .to = std::min( shape.heel, length ), .offsets = offsets, .extents = shape.extents, .walls = true, .drops = shape.drops, .head = { size.neck, size.crest } } );
// the blade running on past the end of the path goes on along the plain track joined there, whose rail starts behind the heel
if( auto const *next { track_rails::runon_track( *this, Path ) }; next != nullptr && shape.heel > length + 1e-3 ) {
auto const along = [&]( std::vector<glm::dvec2> const &Knots ) {
std::vector<glm::dvec2> result;
for( auto const &knot : Knots ) {
result.emplace_back( knot.x - length, knot.y );
}
return result; };
next->Segment->RenderLoftSpan( Vertices, m_origin, Rail, { .side = blade_side( Path ), .halfwidth = 0.5f * std::abs( fTrackWidth ), .texturelength = Texturelength, .from = 0.0, .to = shape.heel - length, .offsets = along( offsets ), .extents = along( shape.extents ), .walls = true, .drops = along( shape.drops ), .head = { size.neck, size.crest } } );
}
return;
}
auto const bladelength { track_rails::blade_steps( *this, Path ) };
@@ -2200,6 +2270,22 @@ TTrack * TTrack::RaAnimate()
m = false; // koniec animacji
}
}
auto const nosethrow { SwitchExtension->m_movablenose ? nose_throw() : 0.0 };
if( SwitchExtension->m_movablenose
&& SwitchExtension->m_nosethrow >= 0.0
&& ( std::abs( nosethrow - SwitchExtension->m_nosethrow ) >= 0.05
|| ( false == m && nosethrow != SwitchExtension->m_nosethrow ) )
&& false == Geometry1.empty() ) {
// the nose of the frog goes over to the wing rail of the other track along with the blades, laid again in steps
gfx::geometrybank_handle const bank { Geometry1.front().bank, 0 };
Geometry1.clear();
Geometry2.clear();
GeometryCut1.clear();
GeometryCut2.clear();
GeometryTreads.clear();
SwitchExtension->Geometry3 = gfx::geometry_handle{};
create_geometry( bank );
}
// skip the geometry update if no geometry for this track was generated yet
if( ( m_material1 != 0
|| m_material2 != 0 )
@@ -2592,6 +2678,14 @@ TTrack::export_as_text_( std::ostream &Output ) const {
&& SwitchExtension->m_frogs ) {
Output << "frogs ";
}
if( eType == tt_Switch
&& SwitchExtension->m_movablenose ) {
Output << "movablenose ";
}
if( eType == tt_Switch
&& ( SwitchExtension->m_bladeheel > 0.0 || SwitchExtension->m_bladethrow > 0.0 ) ) {
Output << "blades " << SwitchExtension->m_bladeheel << ' ' << SwitchExtension->m_bladethrow << ' ';
}
if( iCategoryFlag == 1
&& false == m_profile1.first.empty() ) {
Output << "railprofile " << m_profile1.first << ' ';
@@ -2610,6 +2704,12 @@ TTrack::export_as_text_( std::ostream &Output ) const {
<< m_sleeper_offset.z << ' '
<< m_sleeper_ballast_z << ' ';
}
if( false == m_timber_model_name.empty() ) {
Output
<< "timbermodel "
<< m_timber_model_name << ' '
<< ( m_timber_skin_name.empty() ? std::string{ "none" } : m_timber_skin_name ) << ' ';
}
// footer
Output
<< "endtrack"
@@ -2749,6 +2849,13 @@ TTrack::texture_length( material_handle const Material ) {
return texturelength < 0.f ? fTexLength : texturelength;
}
float
TTrack::rail_height() const {
// the same height the ballast is laid at, under the rail profile
return std::abs( track_rail_profile( m_profile1.second ).front().position.y );
}
void TTrack::MovedUp1(float const dh)
{ // poprawienie przechyłki wymaga wydłużenia podsypki
fTexHeight1 += dh;
@@ -2818,10 +2925,18 @@ TTrack::DoubleSlip() const {
}
TTrack * TTrack::Connected(int s, double &d) const
TTrack * TTrack::Connected(int s, double &d, int const Path) const
{ // zwraca wskaźnik na sąsiedni tor, w kierunku określonym znakiem (s), odwraca (d) w razie
// niezgodności kierunku torów
TTrack *t; // nie zmieniamy kierunku (d), jeśli nie ma toru dalej
if( eType == tt_Switch && Path >= 0 && Path < 2 ) {
// the neighbour of the path given, not of the one the switch is set to
t = ( s > 0 ? SwitchExtension->pNexts[ Path ] : SwitchExtension->pPrevs[ Path ] );
if( t != nullptr && t->eType != tt_Cross && ( s > 0 ? SwitchExtension->iNextDirection[ Path ] : !SwitchExtension->iPrevDirection[ Path ] ) ) {
d = -d;
}
return t;
}
if (eType != tt_Cross)
{ // jeszcze trzeba sprawdzić zgodność
t = s > 0 ? trNext : trPrev;
@@ -3262,6 +3377,12 @@ TTrack::create_track_bed_profile( gfx::vertex_array &Output, TTrack const *Previ
{0.5f + map12, 0.f} }; // prawy skos
}
}
if( m_sleepers3d.active ) {
// the ballast lowered under the sleepers laid on it, so they stick out of it
for( auto &point : Output ) {
point.position.y -= static_cast<float>( track_sleepers::bed_drop );
}
}
}
// creates road profile data for current path
@@ -3498,8 +3619,8 @@ TTrack::create_switch_trackbed( gfx::vertex_array &Output ) {
&& trNext->eType == tt_Normal ) {
SwitchExtension->m_material3 = trNext->m_material2;
}
// without material don't bother
if( SwitchExtension->m_material3 == null_handle ) { return; }
// without material don't bother, nor when the ballast isn't laid under the 3D sleepers
if( SwitchExtension->m_material3 == null_handle || ( m_sleepers3d.active && false == Global.Sleepers3DBed ) ) { return; }
// generate trackbed for each path of the switch...
auto const texturelength { texture_length( SwitchExtension->m_material3 ) };
gfx::vertex_array trackbedprofile;
@@ -3510,9 +3631,20 @@ TTrack::create_switch_trackbed( gfx::vertex_array &Output ) {
// side trackbed
create_track_bed_profile( trackbedprofile, SwitchExtension->pPrevs[ 1 ], SwitchExtension->pNexts[ 1 ] );
SwitchExtension->Segments[ 1 ]->RenderLoft( trackbedvertices2, m_origin, trackbedprofile, true, texturelength );
if( trackbedvertices1.size() != trackbedvertices2.size() ) {
// a path made of several curves has more pieces than the other one, and the two are combined piece by piece,
// so both are lofted again in the same number of equal pieces, else the trackbed would end where the shorter count does
auto const steps { std::max( SwitchExtension->Segments[ 0 ]->RaSegCount(), SwitchExtension->Segments[ 1 ]->RaSegCount() ) };
trackbedvertices1.clear();
trackbedvertices2.clear();
create_track_bed_profile( trackbedprofile, SwitchExtension->pPrevs[ 0 ], SwitchExtension->pNexts[ 0 ] );
SwitchExtension->Segments[ 0 ]->RenderLoftSteps( trackbedvertices1, m_origin, trackbedprofile, texturelength, steps );
create_track_bed_profile( trackbedprofile, SwitchExtension->pPrevs[ 1 ], SwitchExtension->pNexts[ 1 ] );
SwitchExtension->Segments[ 1 ]->RenderLoftSteps( trackbedvertices2, m_origin, trackbedprofile, texturelength, steps );
}
// ...then combine them into a single geometry sequence
auto const segmentsize { 10 };
auto const segmentcount { trackbedvertices1.size() / segmentsize };
auto const segmentcount { std::min( trackbedvertices1.size(), trackbedvertices2.size() ) / segmentsize };
auto *sampler1 { trackbedvertices1.data() };
auto *sampler2 { trackbedvertices2.data() };
auto const isright { SwitchExtension->RightSwitch };

View File

@@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/.
#include "utilities/Classes.h"
#include "world/Segment.h"
#include "world/RailLayout.h"
#include "world/TrackSleepers.h"
#include "model/material.h"
#include "scene/scenenode.h"
#include "utilities/Names.h"
@@ -55,6 +56,14 @@ class TGroundNode;
class TSubRect;
class TTraction;
// a chunk of the geometry of a track kept to be laid again in, for the rails changing with the state of a switch
struct pooled_chunk {
int type { 0 };
gfx::geometry_handle handle;
bool used { false };
};
using chunk_pool = std::vector<pooled_chunk>;
class TSwitchExtension
{ // dodatkowe dane do toru, który jest zwrotnicą
public:
@@ -101,6 +110,11 @@ class TSwitchExtension
material_handle m_material3 = 0; // texture of auto generated switch trackbed
gfx::geometry_handle Geometry3; // geometry of auto generated switch trackbed
bool m_frogs { false }; // the frog and check rails are made from the rails, for switches with no model of them in their includes
bool m_movablenose { false }; // the nose of the frog moves over to the rail of the track set, closing its flangeway; no check rails
double m_bladeheel { 0.0 }; // where the moving blades end along the paths, as the template of the switch gives it; 0: as long as the flangeway needs
double m_bladethrow { 0.0 }; // how far the tips of the blades move over; 0: the usual throw
double m_nosethrow { -1.0 }; // how far over to the second path the movable nose was laid, -1: no geometry yet
chunk_pool m_nosechunks; // the geometry of the switch, laid again in the same chunks as the nose moves over
gfx::geometrybank_handle map_geometry[6]; // geometry for map visualisation
};
@@ -217,12 +231,19 @@ public:
std::string m_sleeper_skin_name; // replacable skin path, or "none" for default
glm::vec3 m_sleeper_offset { 0.f, 0.f, 0.f }; // local offset from track centerline (x: left/right, y: forward/back, z: up/down)
float m_sleeper_ballast_z { 0.f }; // vertical offset applied to the trackbed (ballast) profile
// timbermodel optional parameter: the model of the timbers laid under the track where it's part of a switch or a crossing,
// in place of the one set with sleepers3d in eu07.ini. Defined in scenery file as:
// timbermodel <model> <skin>
std::string m_timber_model_name;
std::string m_timber_skin_name; // replacable skin, empty for the textures of the model
TModel3d *m_sleeper_model { nullptr }; // resolved on init; nullptr means no model / failed to load
material_handle m_sleeper_skin { 0 }; // resolved replacable skin handle, 0 = use model defaults
// precomputed local-space transforms (relative to m_origin) for every sleeper instance along the path.
// Each matrix is translate(world_pos - m_origin) * rotate(direction, roll) * translate(local_offset).
// The renderer composes this with (view * translate(m_origin - camera_pos)) per draw.
std::vector<glm::mat4> m_sleeper_local_transforms;
// sleepers and switch timbers made of the model set with sleepers3d in eu07.ini, laid where the simulator makes the ballast
track_sleepers::laid m_sleepers3d;
public:
using dynamics_sequence = std::deque<TDynamicObject *>;
@@ -277,13 +298,20 @@ public:
material_handle copy_adjacent_trackbed_material( TTrack const *Exclude = nullptr );
inline double Length() const {
return Segment->GetLength(); };
// how far below the top of the rails their feet are, by the rail profile of the track
float rail_height() const;
inline std::shared_ptr<TSegment> CurrentSegment() const {
return Segment; };
inline TTrack *CurrentNext() const {
return trNext; };
inline TTrack *CurrentPrev() const {
return trPrev; };
TTrack *Connected(int s, double &d) const;
TTrack *Connected(int s, double &d, int const Path = -1) const; // Path: of a switch, the one to leave by whichever way it is set
double trailing_point( int const Path ) const; // where along the path the wheels coming from the frog end push the blades over
int nose_set() const; // the path the movable nose of the frog lies for, following the blades
double nose_throw() const; // how far the movable nose of the frog went over to the second path, following the blades
material_handle rail_material( int const Path ) const;
glm::vec2 tread_band() const;
bool SetConnections(int i);
bool Switch(int i, float const t = -1.f, float const d = -1.f);
bool SwitchForced(int i, TDynamicObject *o);
@@ -302,11 +330,13 @@ public:
void init_segments( bool const Initial );
track_rails::switch_frog frog() const;
track_rails::switch_blade const &blade( int const Path ) const;
double blades_throw() const; // how far the tips of the blades move over between the two paths
std::vector<track_rails::rail_crossing> 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
track_rails::check_rail_set check_rails( std::vector<track_rails::rail_crossing> const &Crossings );
track_rails::rail_layout plan_rails( bool const Crossings );
// Carve: the parts of the rails at crossings carved out of what lies around them, false: only their design
track_rails::rail_layout plan_rails( bool const Crossings, bool const Carve = true );
void update_location();
bool AssignEvents();
bool AssignForcedEvents(basic_event *NewEventPlus, basic_event *NewEventMinus);
@@ -416,8 +446,6 @@ private:
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;
material_handle shared_tread( int const Path, TTrack const &Other, int const Otherpath ) const;
void loft_blade( int const Path, gfx::vertex_array const &Rail, gfx::vertex_array const &Blade, float const Texturelength, gfx::vertex_array &Vertices ) const;
// members

View File

@@ -15,14 +15,30 @@ http://mozilla.org/MPL/2.0/.
namespace track_rails {
void forget();
// changes with every forget(): what's kept of the rails around holds as long as it doesn't
unsigned int memo_stamp();
rail_size measure_rail( gfx::vertex_array const &Profile );
// the noses, wing rails and point rails at crossings at small angles are carved out of what lies around them, all by the same rules: no rail
// reaches into the flangeway of another one where its wheels run, and two heads lying over each other share the room between them by what
// the wheels of each roll on
void carve_plans( TTrack &Track, rail_layout &Layout );
// the layout of the track designed already to carve the tracks around against, before carving
rail_layout const *designed_layout( TTrack &Track );
// how many parts carving left with nothing of the head somewhere along them, since the start
int carve_faults();
// 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 );
// how far from the tip the nose of a movable frog bends over to the wing rail
double movable_nose_length( double const Slope );
// the rails of the track are cut where they cross other rails: plain tracks, and switches whose frog is made from the rails
bool cut_rails( TTrack const &Track );
int blade_steps( TTrack const &Track, int const Path );
double blade_length( TTrack const &Track, int const Path );
// the plain track a path of a switch goes on into at its end, laid the same way, which the blade of the path may run on into
TTrack const *runon_track( TTrack const &Switch, int const Path );
// how far the blade of the switch the track goes on from runs on into its rail of Side
double blade_runon( TTrack const &Track, int const Side );
material_handle cut_material( material_handle const Material );
float tread_brightness( material_handle const Material, glm::vec2 const &Band );
glm::vec3 tread_colour( material_handle const Material, glm::vec2 const &Band );
@@ -33,6 +49,16 @@ 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 );
// the geometry of a track laid while one is alive goes into the chunks of the pool given, of the same type, before new ones
class reused_chunks {
public:
reused_chunks( chunk_pool *Pool );
~reused_chunks();
private:
chunk_pool *m_previous { nullptr };
};
gfx::geometry_handle insert_chunk( gfx::vertex_array &Vertices, gfx::userdata_array &Userdata, gfx::geometrybank_handle const &Bank, int const Type );
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 );

2564
world/TrackSleepers.cpp Normal file

File diff suppressed because it is too large Load Diff

160
world/TrackSleepers.h Normal file
View File

@@ -0,0 +1,160 @@
/*
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 <array>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
#include <glm/glm.hpp>
class TTrack;
class TModel3d;
class TSubModel;
// sleepers and switch timbers laid under the rails of the tracks whose ballast the simulator makes, when a model of them
// is set with sleepers3d in eu07.ini. A timber is put together from the parts of the model: its two ends and the piece between
// them, repeated and stretched to the length of the beam, so one model makes the timbers of any length; a model without such
// parts is drawn whole, stretched. The sleepers of plain tracks are a whole model of their own, set with sleepers3dplain, drawn
// at their own length. A track can have models of its own: of its sleepers with sleepermodel, of its timbers with timbermodel.
namespace track_sleepers {
// the parts of the model a beam is put together from, by the names of the submodels
enum part : int {
part_begin, // the end of the beam on its +x side
part_main, // the piece between the ends
part_end, // the end on the -x side
part_count
};
// a sleeper or a timber, by the middle of its top face at both ends
struct beam {
glm::dvec3 from;
glm::dvec3 to;
glm::dvec3 along; // unit vector across the beam, along the tracks it carries
bool plain { false }; // a sleeper of a track running alone, square to it, not a timber of a switch or a crossing
double corefrom { 0.0 }; // the stretch the feet of the rails it carries take, along it from its from end
double coreto { 0.0 };
};
// a model sleepers or timbers are made of, as found in it
struct beam_model {
std::string name;
std::string skinname;
TModel3d *model { nullptr };
int skin { 0 }; // the replacable skin, 0 for the textures of the model
// put together from its submodels begin, main and end, the one between stretched and repeated to the length of a timber; a
// model without these is drawn whole, a plain sleeper at its own length and a timber stretched to its length
bool stretchable { false };
std::array<TSubModel *, part_count> parts {};
std::array<glm::vec2, part_count> spans {}; // from and to along x, of the parts
glm::vec2 span { -1.3f, 1.3f }; // from and to along x, of the most detailed submodels of the whole model
float seat { 0.f }; // the height in the model the feet of the rails go to
std::vector<float> bands; // the squared distances the levels of detail of the model are seen from, the nearest first
float length() const { return span.y - span.x; }
};
// a run of beams of a track drawn or left out together, by the sphere around them
struct chunk {
glm::vec3 centre; // relative to the origin of the track
float radius { 0.f };
std::array<std::uint32_t, part_count> first {};
std::array<std::uint32_t, part_count> last {};
std::uint32_t wholefirst { 0 }; // the timbers of the run made of a whole model
std::uint32_t wholelast { 0 };
std::uint32_t plainfirst { 0 }; // the sleepers of the run made of the model of the plain sleepers
std::uint32_t plainlast { 0 };
};
// the beams laid under a track, as the instances of the parts of the model
struct laid {
bool active { false }; // the track gets beams and its ballast is lowered under them; set as its geometry is made
bool dirty { true }; // the beams are to be laid again before they're drawn
bool laying { false }; // the beams are being placed, so the tracks about it work out its beams themselves meanwhile
beam_model const *timbers { nullptr }; // the model the timbers are made of
beam_model const *plains { nullptr }; // the model of the plain sleepers, null where they're made of that of the timbers
std::array<std::vector<glm::mat4>, part_count> instances; // transforms of the parts relative to the origin of the track
std::vector<glm::mat4> whole; // transforms of the timbers of a model without parts, drawn whole
std::vector<glm::mat4> plain; // transforms of the plain sleepers, whole models, relative to the origin of the track
std::vector<chunk> chunks;
std::vector<beam> beams; // as they're laid
};
// how far the ballast of a track with beams is lowered, so they stick out of it
double constexpr bed_drop { 0.06 };
// the model the timbers are made of where a track has none of its own, set with sleepers3d in eu07.ini and loaded on the first
// use; null when there's none, and no beams are laid
beam_model const *timber_model();
// the model of the sleepers of the tracks running alone, set with sleepers3dplain; null when there's none
beam_model const *plain_model();
// the models the track lays its beams of: those set for it with timbermodel and sleepermodel, else the ones of eu07.ini
beam_model const *timber_model( TTrack const &Track );
beam_model const *plain_model( TTrack const &Track );
// how long the plain sleepers of the track are, of the model it lays them of; 2.6 m at the standard gauge where there's none
double plain_length( TTrack const &Track );
// whether the track has the ballast the simulator makes, which the beams are laid on when there's a model of them
bool bedded( TTrack const &Track );
// whether the track gets beams: a rail track with the ballast made by the simulator, and the model is there
bool lays( TTrack const &Track );
// where along the path of the track its rails lie on timbers shared with other tracks: where another track runs so near that
// the plain sleepers of the two would touch, of the lengths of the models they'd be made of. this goes by the shape of the
// tracks alone, so it holds whether the beams are drawn or not. the stretches in the order along the path, none where there
// are no shared timbers
std::vector<std::pair<double, double>> shared_zones( TTrack const &Track, int const Path );
// from the start of the first of these stretches to the end of the last, both zero where there's none
std::pair<double, double> shared_zone( TTrack const &Track, int const Path );
// a model of sleepers or timbers in the scenery data, by the name it's loaded by
struct pack_model {
std::string name;
bool timber { false }; // has the submodels begin, main and end a timber is put together from
};
// the models of sleepers and timbers in the scenery data, found once in models/ by the names of their files telling sleepers
// or timbers: podklad, podrozjazd, sleeper, timber. the timber ones first, then by the name
std::vector<pack_model> const &pack_models();
// the beams of the track placed anew, as instances of the parts of the model
void lay( TTrack &Track );
// the beams of the tracks around the track to be placed again, as they depend on where it runs
void unsettle_around( TTrack &Track );
// the beams worked out in a thread of their own as the tracks come near, while the simulation runs; off in the editor, where the
// tracks change and the beams are worked out as they're needed. turning it off waits for the beams being worked out
void lay_in_background( bool const Background );
// the beams of the track worked out ahead, before it comes in range
void prefetch( TTrack &Track );
// what's wrong with the layout of the beams of all the tracks: beams over each other, rails with too few beams under them,
// timbers shared by rails out of one plane
struct layout_check {
std::size_t beams { 0 };
std::size_t overlaps { 0 };
std::size_t gaps { 0 }; // stretches of rail with the beams under it further apart than the pitch allows
std::size_t bare { 0 }; // rails with no beams under them at all
std::size_t toolong { 0 };
std::size_t offplane { 0 }; // timbers shared by rails out of one plane, where the tracks run too close to part them
std::size_t clipped { 0 }; // beam ends cut shorter than the least overhang to keep clear of the beams of other tracks
std::size_t dropped { 0 }; // beams left out where they'd lie over one laid before
double slowest { 0.0 }; // the longest a cluster of tracks took to lay, in ms
std::size_t biggest { 0 }; // the most tracks in a cluster
std::size_t panels { 0 }; // switches with their paths out of one plane where they share the timbers of the panel
double longest { 0.0 };
double widest { 0.0 }; // the widest spacing of the beams under a rail
std::string widestat;
std::vector<std::string> examples;
};
layout_check check();
// the beams laid with the track: the tracks whose plain sleepers would meet make a cluster laid together, lines of timbers
// across all of their rails turned to the bisectors of frogs and crossings, and the plain sleepers square to each track where no
// timber carries it, in step with the timbers. every beam is laid with one track of the cluster
std::vector<beam> place( TTrack &Track );
} // namespace track_sleepers

View File

@@ -44,8 +44,19 @@ void TTrackFollower::Reset()
fDirection = 1.0;
}
// the end of the next track joined to the end of the current one, of the path of a switch the axle runs on
int TTrackFollower::JoinedEnd(int const End) const
{
if( iPath >= 0 && pCurrentTrack->eType == tt_Switch ) {
return End ? pCurrentTrack->SwitchExtension->iNextDirection[ iPath ] : pCurrentTrack->SwitchExtension->iPrevDirection[ iPath ];
}
return End ? pCurrentTrack->iNextDirection : pCurrentTrack->iPrevDirection;
}
TTrack * TTrackFollower::SetCurrentTrack(TTrack *pTrack, int end)
{ // przejechanie na inny odcinkek toru, z ewentualnym rozpruciem
auto path { -1 };
auto trailat { -1.0 };
if (pTrack)
switch (pTrack->eType)
{
@@ -54,8 +65,14 @@ TTrack * TTrackFollower::SetCurrentTrack(TTrack *pTrack, int end)
if( pCurrentTrack == nullptr ) {
break;
}
int i = end ? pCurrentTrack->iNextDirection : pCurrentTrack->iPrevDirection;
if (i > 0) // jeżeli wjazd z ostrza
int i = JoinedEnd( end );
if( i & 1 ) {
// coming from the frog end the axle runs on its path whichever way the switch is set; it gets trailed through
// only where the wheels get to the blades, or to the nose of a movable frog
path = i >> 1;
trailat = pTrack->trailing_point( path );
}
else if (i > 0) // jeżeli wjazd z ostrza
pTrack->SwitchForced(i >> 1, Owner); // to przełożenie zwrotnicy - rozprucie!
}
break;
@@ -94,7 +111,13 @@ TTrack * TTrackFollower::SetCurrentTrack(TTrack *pTrack, int end)
pCurrentTrack->AxleCounter(-1, Owner); // opuszczenie tamtego toru
}
pCurrentTrack = pTrack;
pCurrentSegment = pCurrentTrack != nullptr ? pCurrentTrack->CurrentSegment() : nullptr;
// on a switch the axle keeps to the path it came on, the switch thrown under it doesn't move it over to the other one
iPath = ( pCurrentTrack != nullptr && pCurrentTrack->eType == tt_Switch && pCurrentTrack->SwitchExtension ) ? ( path >= 0 ? path : pCurrentTrack->SwitchExtension->CurrentIndex ) : -1;
fTrailAt = ( iPath >= 0 ? trailat : -1.0 );
pCurrentSegment =
iPath >= 0 ? pCurrentTrack->SwitchExtension->Segments[ iPath ] :
pCurrentTrack != nullptr ? pCurrentTrack->CurrentSegment() :
nullptr;
if (!pCurrentTrack)
Error(Owner->MoverParameters->Name + " at NULL track");
return pCurrentTrack;
@@ -167,6 +190,11 @@ bool TTrackFollower::Move(double fDistance, bool bPrimary)
return false;
// if (fDistance==0.0) return true; //Ra: jak stoi, to chyba dalej nie ma co kombinować?
s = fCurrentDistance + fDistance; // doliczenie przesunięcia
if( fTrailAt >= 0.0 && s <= fTrailAt ) {
// the wheels got to the blades of the switch coming from its frog end: pushed over if set the other way - rozprucie
fTrailAt = -1.0;
pCurrentTrack->SwitchForced( iPath, Owner );
}
// Ra: W Point2 toru może znajdować się "dziura", która zamieni energię kinetyczną
// ruchu wzdłużnego na energię potencjalną, zamieniającą się potem na energię
// sprężystości na amortyzatorach. Należałoby we wpisie toru umieścić współczynnik
@@ -196,7 +224,7 @@ bool TTrackFollower::Move(double fDistance, bool bPrimary)
if (!SetCurrentTrack(pCurrentTrack->Connected(iSegment, fDirection), 0))
return false; // wyjście z błędem
}
else if (!SetCurrentTrack(pCurrentTrack->Connected(-1, fDirection), 0)) // ustawia fDirection
else if (!SetCurrentTrack(pCurrentTrack->Connected(-1, fDirection, iPath), 0)) // ustawia fDirection
return false; // wyjście z błędem
if (dir == fDirection) //(pCurrentTrack->iPrevDirection)
{ // gdy kierunek bez zmiany (Point1->Point2)
@@ -230,7 +258,7 @@ bool TTrackFollower::Move(double fDistance, bool bPrimary)
if (!SetCurrentTrack(pCurrentTrack->Connected(iSegment, fDirection), 1))
return false; // wyjście z błędem
}
else if (!SetCurrentTrack(pCurrentTrack->Connected(1, fDirection), 1)) // ustawia fDirection
else if (!SetCurrentTrack(pCurrentTrack->Connected(1, fDirection, iPath), 1)) // ustawia fDirection
return false; // wyjście z błędem
if (dir != fDirection) //(pCurrentTrack->iNextDirection)
{ // gdy zmiana kierunku toru (Point2->Point2)

View File

@@ -49,6 +49,7 @@ public:
private:
// methods
bool ComputatePosition(); // przeliczenie pozycji na torze
int JoinedEnd( int const End ) const;
// members
TTrack *pCurrentTrack = nullptr; // na którym torze siê znajduje
std::shared_ptr<TSegment> pCurrentSegment; // zwrotnice mog¹ mieæ dwa segmenty
@@ -58,6 +59,9 @@ public:
int iEventFlag = 0; // McZapkie-020602: informacja o tym czy wyzwalac zdarzenie: 0,1,2,3
int iEventallFlag = 0;
int iSegment = 0; // który segment toru jest używany (żeby nie przeskakiwało po przestawieniu zwrotnicy pod taborem)
int iPath = -1; // the path of the switch the axle runs on
double fTrailAt = -1.0; // coming from the frog end of a switch: where along the path the wheels push the blades over
};
//---------------------------------------------------------------------------
#endif