17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-31 21:39:03 +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

@@ -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();
}