From 5aedbcdc1b0b21144880054b6a2d648da7c415ad Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 16:46:58 +0200 Subject: [PATCH 01/19] feat(sleepers): the timbers of a switch get the fastenings of the rails from the submodels plate, chair and check of their model: a plate under each rail where it lies on a timber, slide chairs under the stock rails along the blades, a fastening of both where a check rail or another rail of the frog lies close beside, the legs of the nose in one plate where they're joined; the rails sit on the plate, the blades ride on the slide plates of the chairs as high as the model has them, their webs shortened; the fastenings drawn within 120 m; selftest fastenings --- CMakeLists.txt | 1 + application/editormodeselftestsleepers.cpp | 32 ++++ rendering/opengl46/opengl46renderer.cpp | 12 +- world/Segment.cpp | 13 +- world/Segment.h | 1 + world/SleeperFastenings.cpp | 207 +++++++++++++++++++++ world/SleeperFastenings.h | 73 ++++++++ world/SleeperModels.cpp | 55 +++++- world/SleeperRules.h | 14 ++ world/Track.cpp | 5 +- world/TrackSleepers.cpp | 23 +++ world/TrackSleepers.h | 14 ++ 12 files changed, 440 insertions(+), 10 deletions(-) create mode 100644 world/SleeperFastenings.cpp create mode 100644 world/SleeperFastenings.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6bd71ff7..e53d4aa5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ set(SOURCES "world/SleeperPaths.cpp" "world/SleeperFeatures.cpp" "world/SleeperLines.cpp" +"world/SleeperFastenings.cpp" "world/SleeperLayout.cpp" "world/SleeperModels.cpp" "world/SleeperWorker.cpp" diff --git a/application/editormodeselftestsleepers.cpp b/application/editormodeselftestsleepers.cpp index 15cf6d92..97969ae9 100644 --- a/application/editormodeselftestsleepers.cpp +++ b/application/editormodeselftestsleepers.cpp @@ -49,6 +49,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormodeselftest.h" #include "world/TrackSleepers.h" +#include "world/SleeperFastenings.h" editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &command, std::istringstream &words) { @@ -149,6 +150,37 @@ editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &c } return selftest_result::handled; } + if (command == "fastenings") + { + // the fastenings on the timbers of the track: along the track, the part and where across it, which way its +x goes + std::string name; + words >> name; + auto *track{simulation::Paths.find(name)}; + auto const *model{track != nullptr ? track_sleepers::timber_model(*track) : nullptr}; + if (model == nullptr || false == model->fastened()) + { + WriteLog("SELFTEST fastenings: no track " + name + " with the fastenings in the model of its timbers"); + return selftest_result::handled; + } + std::array const names{"begin", "main", "end", "plate", "chair", "check"}; + track_sleepers::fastened_rails const rails{*track}; + std::array counts{}; + for (auto const &beam : track_sleepers::place(*track)) + { + if (beam.plain) + continue; + auto const middle{0.5 * (beam.from + beam.to)}; + std::string text{format("SELFTEST fastenings %s at %.2f %.2f:", track->name().c_str(), middle.x, middle.z)}; + for (auto const &fastening : rails.on(beam, *model)) + { + text += format(" %s %.3f %.3f %+.0f", names[fastening.part], fastening.point.x, fastening.point.z, glm::degrees(std::atan2(fastening.across.y, fastening.across.x))); + ++counts[fastening.part]; + } + WriteLog(text); + } + WriteLog(format("SELFTEST fastenings %s: %d plates, %d chairs, %d checks", track->name().c_str(), counts[track_sleepers::part_plate], counts[track_sleepers::part_chair], counts[track_sleepers::part_check])); + return selftest_result::handled; + } if (command == "sleepercheck") { auto const start{std::chrono::steady_clock::now()}; diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 1cd93fcd..992b43c7 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/opengl46/opengl46geometrybank.h" #include "rendering/screenshot.h" #include "rendering/editoroverlay.h" +#include "world/SleeperRules.h" #include //#define EU07_DEBUG_OPENGL @@ -6263,6 +6264,9 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ centre, chunk.radius } ) ) { continue; } if( laid.timbers->stretchable ) { for( int part = 0; part < track_sleepers::part_count; ++part ) { + if( chunk.last[ part ] <= chunk.first[ part ] ) { continue; } + // the fastenings only near, they're small + if( part >= track_sleepers::timber_parts && distance > std::min( range, track_sleepers::fastening_range ) + chunk.radius ) { continue; } auto &instances = bucket_for( laid.timbers, part, 0.f ); for( auto index = chunk.first[ part ]; index < chunk.last[ part ]; ++index ) { instances.emplace_back( trackmodelview * laid.instances[ part ][ index ] ); @@ -6289,7 +6293,9 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ if( bucket.part >= 0 ) { // only the part of the model is visible, its siblings are skipped as the model is walked for( int part = 0; part < track_sleepers::part_count; ++part ) { - beammodel.parts[ part ]->iVisible = ( part == bucket.part ? 1 : 0 ); + if( beammodel.parts[ part ] != nullptr ) { + beammodel.parts[ part ]->iVisible = ( part == bucket.part ? 1 : 0 ); + } } } material_data skins {}; @@ -6314,7 +6320,9 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ ++m_renderpass.draw_stats.instanced_drawcalls; if( bucket.part >= 0 ) { for( int part = 0; part < track_sleepers::part_count; ++part ) { - beammodel.parts[ part ]->iVisible = 1; + if( beammodel.parts[ part ] != nullptr ) { + beammodel.parts[ part ]->iVisible = 1; + } } } } diff --git a/world/Segment.cpp b/world/Segment.cpp index 448d421f..afb2f2bc 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -755,6 +755,16 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig && ( Distance < Span.from + frommargin || Distance > Span.to - tomargin ); } ), stations.end() ); } + auto bottom { ShapePoints.front().position.y }; + for( auto const &point : ShapePoints ) { + bottom = std::min( bottom, point.position.y ); + } + auto const raised = [&]( float const Height ) { + if( Span.rise <= 0.0 || Height >= Span.head.x || Span.head.x <= bottom ) { + return Height; + } + return static_cast( Height + Span.rise * ( Span.head.x - Height ) / ( Span.head.x - bottom ) ); }; + auto const ring = [&]( double const Distance, double const Middle, std::vector &Ring, std::vector &Wear ) { glm::dvec3 point; glm::dvec3 across; @@ -820,7 +830,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig // 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( Span.side * ( Span.halfwidth + outside + offset ) ) + position }; - pt.y += ShapePoints.front().position.y + Span.lift; + pt.y += raised( 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 ); @@ -899,6 +909,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig if( drop > 0.0 && shape.position.y > Span.head.x && Span.head.y > Span.head.x ) { shape.position.y -= static_cast( drop * ( shape.position.y - Span.head.x ) / ( Span.head.y - Span.head.x ) ); } + shape.position.y = raised( shape.position.y ); auto pt { parallel * static_cast( Span.side * ( Span.halfwidth + outside + offset ) ) + position + along * static_cast( outside + offset ) }; pt.y += shape.position.y + Span.lift; auto norm { shape.normal.x * parallel }; diff --git a/world/Segment.h b/world/Segment.h index a2e5d9c3..69e47e7f 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -47,6 +47,7 @@ struct loft_span { loft_end toend; knots::line drops; glm::dvec2 head { 0.0 }; + double rise { 0.0 }; // how far the foot of the profile is raised, the web below the head (head.x) shortened by as much }; struct segment_data { diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp new file mode 100644 index 00000000..641a187f --- /dev/null +++ b/world/SleeperFastenings.cpp @@ -0,0 +1,207 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" + +#include "world/SleeperFastenings.h" +#include "world/SleeperGeometry.h" +#include "world/SleeperPaths.h" +#include "world/SleeperRules.h" +#include "world/TrackRails.h" +#include "world/Track.h" + +namespace track_sleepers { + +fastened_rails::role fastened_rails::rail::at( double const Distance ) const { + + auto const within = [&]( glm::dvec2 const &Stretch ) { + return Stretch.y > Stretch.x && Distance >= Stretch.x && Distance <= Stretch.y; }; + if( within( blade ) ) { + return role::blade; + } + if( within( stock ) ) { + return role::stock; + } + if( std::any_of( checks.begin(), checks.end(), within ) ) { + return role::checked; + } + return role::plain; +} + +fastened_rails::fastened_rails( TTrack &Track ) { + + std::vector tracks; + tracks_near( Track, [&]( TTrack *Other ) { + if( bedded( *Other ) && std::find( tracks.begin(), tracks.end(), Other ) == tracks.end() ) { + tracks.push_back( Other ); + } } ); + for( auto *track : tracks ) { + auto const middle { 0.5 * std::abs( track->fTrackWidth ) + track->rail_dimensions().web }; + auto const cut { track_rails::cut_rails( *track ) }; + auto const switched { track->eType == tt_Switch && track->SwitchExtension != nullptr }; + // the side of the blade of the path, and the stretch along it from the tip to the heel + auto const bladeside = [&]( int const Path ) { + return ( track->SwitchExtension->RightSwitch == ( Path == 0 ) ? 1 : -1 ); }; + auto const bladestretch = [&]( int const Path ) { + auto const &shape { track->blade( Path ) }; + return glm::dvec2 { shape.valid ? shape.tip : 0.0, track_rails::blade_length( *track, Path ) }; }; + for( int path = 0; path < path_count( *track ); ++path ) { + auto const *segment { path_segment( *track, path ) }; + if( segment == nullptr ) { + continue; + } + auto const length { segment->GetLength() }; + auto const count { std::max( 1, static_cast( std::ceil( length / fastening_step ) ) ) }; + for( auto const side : { -1, 1 } ) { + rail line; + line.track = track; + line.path = path; + line.side = side; + line.low = glm::dvec2 { std::numeric_limits::max() }; + line.high = glm::dvec2 { std::numeric_limits::lowest() }; + for( int step = 0; step <= count; ++step ) { + auto const distance { length * step / count }; + glm::dvec3 point, across; + segment->plan_frame( distance, point, across ); + auto const flat { plan( across ) }; + auto const inward { glm::length( flat ) > 1e-9 ? glm::normalize( flat ) * static_cast( -side ) : glm::dvec2 { 1.0, 0.0 } }; + auto const position { plan( point ) - inward * middle }; + line.points.push_back( position ); + line.heights.push_back( point.y ); + line.inward.push_back( inward ); + line.distances.push_back( distance ); + line.low = glm::min( line.low, position ); + line.high = glm::max( line.high, position ); + } + if( switched ) { + if( side == bladeside( path ) ) { + line.blade = { 0.0, bladestretch( path ).y }; + } + if( side == bladeside( 1 - path ) ) { + auto const stretch { bladestretch( 1 - path ) }; + line.stock = { std::max( 0.0, stretch.x - chair_lead ), stretch.y }; + } + } + if( cut ) { + for( auto const &check : track_rails::rail_checks( *track )[ path ][ side > 0 ? 1 : 0 ] ) { + line.checks.emplace_back( check.from, check.to ); + } + } + m_rails.emplace_back( std::move( line ) ); + } + } + } +} + +std::vector fastened_rails::on( beam const &Beam, beam_model const &Model ) const { + + std::vector result; + auto const span { Beam.to - Beam.from }; + auto const length { glm::length( plan( span ) ) }; + if( length < 0.1 ) { + return result; + } + auto const start { plan( Beam.from ) }; + auto const axis { plan( span ) / length }; + auto const low { glm::min( start, plan( Beam.to ) ) }; + auto const high { glm::max( start, plan( Beam.to ) ) }; + std::vector hits; + for( auto const &line : m_rails ) { + if( line.high.x < low.x || line.low.x > high.x || line.high.y < low.y || line.low.y > high.y ) { + continue; + } + auto const last { line.points.size() - 1 }; + for( std::size_t index = 1; index <= last; ++index ) { + auto const from { line.points[ index - 1 ] }; + auto const chord { line.points[ index ] - from }; + double u, v; + if( false == track_rails::meet( start, plan( span ), from, chord, 1e-12, u, v ) ) { + continue; + } + if( u < 0.0 || u > 1.0 || v < 0.0 || v > 1.0 || ( v == 1.0 && index < last ) ) { + continue; + } + auto const direction { glm::normalize( chord ) }; + if( std::abs( cross( axis, direction ) ) < crossing_sine_min ) { + continue; + } + auto const height { glm::mix( line.heights[ index - 1 ], line.heights[ index ], v ) }; + if( std::abs( height - glm::mix( Beam.from.y, Beam.to.y, u ) ) > level_tolerance ) { + continue; + } + hits.push_back( { + u * length, + line.at( glm::mix( line.distances[ index - 1 ], line.distances[ index ], v ) ), + glm::normalize( glm::mix( line.inward[ index - 1 ], line.inward[ index ], v ) ) } ); + } + } + std::sort( hits.begin(), hits.end(), []( hit const &Left, hit const &Right ) { return Left.at < Right.at; } ); + // one rail where several lie on one another, the stock rail before the rail checked, before a plain one; a blade with no + // other rail there slides on the chair of its stock rail + auto const rank = []( role const Kind ) { + switch( Kind ) { + case role::stock: { return 3; } + case role::checked: { return 2; } + case role::plain: { return 1; } + default: { return 0; } + } }; + std::vector rails; + for( std::size_t index = 0; index < hits.size(); ) { + auto best { hits[ index ] }; + auto next { index + 1 }; + for( ; next < hits.size() && hits[ next ].at - hits[ next - 1 ].at < fastening_merge; ++next ) { + if( rank( hits[ next ].kind ) > rank( best.kind ) ) { + best = hits[ next ]; + } + } + if( best.kind != role::blade ) { + rails.push_back( best ); + } + index = next; + } + auto const point = [&]( double const At ) { + return Beam.from + span * ( At / length ); }; + auto const has = [&]( int const Part ) { + return Model.parts[ Part ] != nullptr; }; + for( std::size_t index = 0; index < rails.size(); ++index ) { + auto const &rail { rails[ index ] }; + if( index + 1 < rails.size() && rails[ index + 1 ].at - rail.at < fastening_pair && rail.kind != role::stock && rails[ index + 1 ].kind != role::stock ) { + auto const &other { rails[ index + 1 ] }; + if( other.at - rail.at < fastening_nose ) { + result.push_back( { part_plate, point( 0.5 * ( rail.at + other.at ) ), rail.inward } ); + } + else if( has( part_check ) ) { + result.push_back( { part_check, point( rail.at ), axis } ); + } + else { + result.push_back( { part_plate, point( rail.at ), rail.inward } ); + result.push_back( { part_plate, point( other.at ), other.inward } ); + } + ++index; + continue; + } + switch( rail.kind ) { + case role::stock: { + result.push_back( { has( part_chair ) ? part_chair : part_plate, point( rail.at ), -rail.inward } ); + break; + } + case role::checked: { + result.push_back( { has( part_check ) ? part_check : part_plate, point( rail.at ), rail.inward } ); + break; + } + default: { + result.push_back( { part_plate, point( rail.at ), rail.inward } ); + break; + } + } + } + return result; +} + +} // namespace track_sleepers diff --git a/world/SleeperFastenings.h b/world/SleeperFastenings.h new file mode 100644 index 00000000..1d2f876a --- /dev/null +++ b/world/SleeperFastenings.h @@ -0,0 +1,73 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include + +#include + +#include "world/TrackSleepers.h" + +// the fastenings of the rails on the timbers: where a rail lies on a timber, the part of the model of the timbers which holds it +// there. a stock rail with the blade beside it lies in a slide chair, a rail with a check rail or another rail of the frog close +// beside it in a fastening of both, the blade on the chair of its stock rail, and the other rails each in a plate of its own +namespace track_sleepers { + +// a fastening on a timber +struct fastening { + int part { part_plate }; + glm::dvec3 point { 0.0 }; // under the middle of the rail, on the top of the timber + glm::dvec2 across { 1.0, 0.0 }; // unit vector in plan square to the rail, the way the +x of the part goes +}; + +// the rails of the tracks around a track, which lie on its timbers +class fastened_rails { + +public: + explicit fastened_rails( TTrack &Track ); + // the fastenings of the rails on the timber, of the parts the model has + std::vector on( beam const &Beam, beam_model const &Model ) const; + +private: + // what a rail is where it lies on a timber + enum class role { + plain, + blade, // a blade of a switch, which slides on the chair of its stock rail + stock, // the stock rail of a switch with the blade beside it + checked // a rail with a check rail beside it + }; + // a rail by points along its middle + struct rail { + TTrack *track { nullptr }; + int path { 0 }; + int side { 1 }; + std::vector points; + std::vector heights; + std::vector inward; // unit vectors in plan from the rail toward the middle of its path + std::vector distances; + glm::dvec2 low { 0.0 }; + glm::dvec2 high { 0.0 }; + // the stretches along the path the rail is a blade, a stock rail with a chair, checked along + glm::dvec2 blade { -1.0 }; + glm::dvec2 stock { -1.0 }; + std::vector checks; + role at( double const Distance ) const; + }; + // where a rail crosses a timber + struct hit { + double at { 0.0 }; // along the timber, from its from end + role kind { role::plain }; + glm::dvec2 inward { 1.0, 0.0 }; + }; + + std::vector m_rails; +}; + +} // namespace track_sleepers diff --git a/world/SleeperModels.cpp b/world/SleeperModels.cpp index 648fe434..90e0a5d8 100644 --- a/world/SleeperModels.cpp +++ b/world/SleeperModels.cpp @@ -25,6 +25,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include namespace track_sleepers { @@ -50,6 +51,40 @@ void vertices_of( TSubModel *Submodel, Visit Visitor ) { } } +// the highest the faces of the submodel reach over the point of the plan of the model, in the space of the model; none where +// no face lies over it +std::optional surface_at( TSubModel *Submodel, float const X, float const Z ) { + + glm::mat4 transform { 1.f }; + if( ( Submodel->iFlags & 0xC000 ) && ( Submodel->GetMatrix() != nullptr ) ) { + transform = glm::make_mat4( Submodel->GetMatrix()->readArray() ); + } + auto const indexed { Submodel->m_geometry.handle != null_handle }; + auto const &vertices { indexed ? GfxRenderer->Vertices( Submodel->m_geometry.handle ) : Submodel->Vertices }; + auto const &indices { indexed ? GfxRenderer->Indices( Submodel->m_geometry.handle ) : Submodel->Indices }; + auto const count { indices.empty() ? vertices.size() : indices.size() }; + auto const corner = [&]( std::size_t const Index ) { + return glm::vec3 { transform * glm::vec4 { vertices[ indices.empty() ? Index : indices[ Index ] ].position, 1.f } }; }; + std::optional result; + for( std::size_t index = 0; index + 2 < count; index += 3 ) { + auto const a { corner( index ) }; + auto const b { corner( index + 1 ) }; + auto const c { corner( index + 2 ) }; + auto const area { ( b.x - a.x ) * ( c.z - a.z ) - ( c.x - a.x ) * ( b.z - a.z ) }; + if( std::abs( area ) < 1e-9f ) { + continue; + } + auto const u { ( ( X - a.x ) * ( c.z - a.z ) - ( c.x - a.x ) * ( Z - a.z ) ) / area }; + auto const v { ( ( b.x - a.x ) * ( Z - a.z ) - ( X - a.x ) * ( b.z - a.z ) ) / area }; + if( u < -1e-5f || v < -1e-5f || u + v > 1.f + 1e-5f ) { + continue; + } + auto const height { a.y + u * ( b.y - a.y ) + v * ( c.y - a.y ) }; + result = ( result ? std::max( *result, height ) : height ); + } + return result; +} + // the model of the beams of the name and skin, loaded and measured on the first use; null when it can't be loaded. the feet of // the rails go to the given seat in it, or to its top where the seat is below zero beam_model const *find_model( std::string const &Name, std::string const &Skinname, float const Seat ) { @@ -74,16 +109,16 @@ beam_model const *find_model( std::string const &Name, std::string const &Skinna model.skin = GfxRenderer->Fetch_Material( Skin ); } // a timber put together from its parts, with a part between the ends stretched and repeated to the length - std::array const names { "begin", "main", "end" }; + std::array const names { "begin", "main", "end", "plate", "chair", "check" }; model.stretchable = true; for( int part = 0; part < part_count; ++part ) { model.parts[ part ] = model.model->GetFromName( names[ part ] ); - model.stretchable = model.stretchable && model.parts[ part ] != nullptr; + model.stretchable = model.stretchable && ( part >= timber_parts || model.parts[ part ] != nullptr ); } auto top { std::numeric_limits::lowest() }; model.span = { std::numeric_limits::max(), std::numeric_limits::lowest() }; if( model.stretchable ) { - for( int part = 0; part < part_count; ++part ) { + for( int part = 0; part < timber_parts; ++part ) { glm::vec2 span { std::numeric_limits::max(), std::numeric_limits::lowest() }; vertices_of( model.parts[ part ], [&]( glm::vec3 const &Position ) { span = { std::min( span.x, Position.x ), std::max( span.y, Position.x ) }; @@ -112,8 +147,14 @@ beam_model const *find_model( std::string const &Name, std::string const &Skinna } } std::sort( model.bands.begin(), model.bands.end() ); - // the rails sit on the seat of the sleeper, below the fastenings sticking up over it + // the rails sit on the seat of the sleeper, below the fastenings sticking up over it, or on the plate of the fastening model.seat = ( Seat >= 0.f ? Seat : top ); + if( Seat < 0.f && model.fastened() ) { + model.seat = std::max( top, surface_at( model.parts[ part_plate ], 0.f, 0.f ).value_or( top ) ); + } + if( model.fastened() && model.parts[ part_chair ] != nullptr ) { + model.slide = std::max( 0.f, surface_at( model.parts[ part_chair ], static_cast( -slide_probe ), 0.f ).value_or( model.seat ) - model.seat ); + } if( model.length() < 0.5f ) { ErrorLog( "Bad sleepers3d: model \"" + Name + "\" has no beam along x" ); model.model = nullptr; @@ -125,7 +166,11 @@ beam_model const *find_model( std::string const &Name, std::string const &Skinna + ( model.stretchable ? ", main " + to_string( model.spans[ part_main ].x, 3 ) + ".." + to_string( model.spans[ part_main ].y, 3 ) : std::string {} ) - + ", seat " + to_string( model.seat, 3 ) ); + + ( model.fastened() ? + std::string { ", fastenings plate" } + ( model.parts[ part_chair ] != nullptr ? " chair" : "" ) + ( model.parts[ part_check ] != nullptr ? " check" : "" ) : + std::string {} ) + + ", seat " + to_string( model.seat, 3 ) + + ( model.slide > 0.f ? ", slide " + to_string( model.slide, 3 ) : std::string {} ) ); return &model; } return ( found->second.model != nullptr ? &found->second : nullptr ); diff --git a/world/SleeperRules.h b/world/SleeperRules.h index c41dc67c..3fb63044 100644 --- a/world/SleeperRules.h +++ b/world/SleeperRules.h @@ -49,6 +49,20 @@ double constexpr level_tolerance { 1.0 }; double constexpr around_margin { 20.0 }; // how many beams make a run drawn or left out together std::size_t constexpr chunk_beams { 16 }; +// how far apart the points of the rails are, as the places they lie on the timbers are found +double constexpr fastening_step { 0.25 }; +// rails closer than this where they cross a timber are one rail there, held by one fastening +double constexpr fastening_merge { 0.03 }; +// two rails closer than this lie in one fastening of both, as a rail of the frog with the one beside it; closer than the second +// one, as the legs of the nose where they're joined, in one plate between them +double constexpr fastening_pair { 0.24 }; +double constexpr fastening_nose { 0.05 }; +// how far before the tip of the blade the slide chairs of its stock rail begin +double constexpr chair_lead { 0.3 }; +// how far from the stock rail toward its blade the slide plate of a chair is measured +double constexpr slide_probe { 0.12 }; +// how far from the camera the fastenings are drawn at most +double constexpr fastening_range { 120.0 }; // how many tracks a cluster takes at most std::size_t constexpr cluster_cap { 256 }; diff --git a/world/Track.cpp b/world/Track.cpp index b9b6b01d..e0d0ac12 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -2259,8 +2259,9 @@ void TTrack::loft_blade( int const Path, gfx::vertex_array const &Rail, gfx::ver offsets.emplace_back( bend.x, -thrown * bend.y ); } auto const size { rail_dimensions() }; + auto const rise { track_sleepers::blade_rise( *this ) }; 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 } } ); + 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 }, .rise = rise } ); // 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 const &Knots ) { @@ -2269,7 +2270,7 @@ void TTrack::loft_blade( int const Path, gfx::vertex_array const &Rail, gfx::ver 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 } } ); + 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 }, .rise = rise } ); } return; } diff --git a/world/TrackSleepers.cpp b/world/TrackSleepers.cpp index bcb5cf09..e23fa7db 100644 --- a/world/TrackSleepers.cpp +++ b/world/TrackSleepers.cpp @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "world/TrackSleepers.h" +#include "world/SleeperFastenings.h" #include "world/SleeperGeometry.h" #include "world/SleeperLayout.h" #include "world/SleeperPaths.h" @@ -24,6 +25,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include namespace track_sleepers { @@ -63,6 +65,11 @@ bool lays( TTrack const &Track ) { return bedded( Track ) && timber_model( Track ) != nullptr; } +double blade_rise( TTrack const &Track ) { + + return ( Track.eType == tt_Switch && lays( Track ) ? timber_model( Track )->slide : 0.0 ); +} + std::vector> shared_zones( TTrack const &Track, int const Path ) { std::vector> result; @@ -134,6 +141,9 @@ void lay( TTrack &Track ) { // the layout of the cluster isn't needed by this track any more, the instances below are what's drawn forget_layout( Track ); + // the rails around, where they lie on the timbers, for the fastenings of the model + std::optional const rails { parts.fastened() ? std::make_optional( Track ) : std::nullopt }; + auto const partlength = [&]( int const Part ) { return static_cast( parts.spans[ Part ].y - parts.spans[ Part ].x ); }; auto const beginlength { partlength( part_begin ) }; @@ -232,6 +242,19 @@ void lay( TTrack &Track ) { put( part_end, scale, parts.spans[ part_end ].x, -half ); put( part_begin, scale, parts.spans[ part_begin ].y, half ); } + if( rails && false == ( beam.plain && laid.plains != nullptr ) ) { + // each square to its rail on the top of the timber, the plate under the foot of the rail + for( auto const &fastening : rails->on( beam, parts ) ) { + auto across { glm::dvec3 { fastening.across.x, 0.0, fastening.across.y } }; + across = glm::normalize( across - y * glm::dot( across, y ) ); + glm::mat4 place { 1.f }; + place[ 0 ] = glm::vec4 { glm::vec3 { across }, 0.f }; + place[ 1 ] = glm::vec4 { glm::vec3 { y }, 0.f }; + place[ 2 ] = glm::vec4 { glm::vec3 { glm::cross( across, y ) }, 0.f }; + place[ 3 ] = glm::vec4 { glm::vec3 { fastening.point - Track.m_origin }, 1.f }; + laid.instances[ fastening.part ].emplace_back( glm::translate( place, glm::vec3 { 0.f, -parts.seat, 0.f } ) ); + } + } runbeams.emplace_back( middle, half ); if( runbeams.size() >= chunk_beams ) { close_run(); diff --git a/world/TrackSleepers.h b/world/TrackSleepers.h index 06fb1c62..fd994446 100644 --- a/world/TrackSleepers.h +++ b/world/TrackSleepers.h @@ -26,6 +26,8 @@ class TSubModel; // 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. +// A model of timbers put together from parts can have the fastenings of the rails among its submodels too: plate, chair and +// check, put on the timbers where the rails lie on them, and the rails sit on the plate. namespace track_sleepers { // the parts of the model a beam is put together from, by the names of the submodels @@ -33,8 +35,14 @@ 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_plate, // the fastening of a rail, its origin under the middle of the foot of the rail + part_chair, // the slide chair of a stock rail with the blade beside it, its origin under the stock rail, the slide plate on its -x side + part_check, // the fastening of a rail with another one close beside it, as a check rail or a rail of the frog, its origin under the + // rail, the other one on its +x side part_count }; +// the parts a timber is put together from, the fastenings after them +int constexpr timber_parts { part_end + 1 }; // a sleeper or a timber, by the middle of its top face at both ends struct beam { @@ -59,8 +67,11 @@ struct beam_model { std::array 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 + float slide { 0.f }; // how far over the seat the blades of a switch slide, on the slide plates of the chairs; 0 without chairs std::vector 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; } + // the model has the fastenings of the rails, put where the rails lie on its timbers + bool fastened() const { return stretchable && parts[ part_plate ] != nullptr; } }; // a run of beams of a track drawn or left out together, by the sphere around them @@ -105,6 +116,9 @@ double plain_length( TTrack const &Track ); 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, its own or of eu07.ini bool lays( TTrack const &Track ); +// how far the feet of the blades of the switch ride over those of its other rails, on the slide plates of the chairs of its +// timbers; 0 where it lays none with chairs +double blade_rise( 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 From b512460fdfa345d36aff62b0a057e2af3029726c Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 17:25:02 +0200 Subject: [PATCH 02/19] feat(sleepers): the fastenings fitted on the switches crossing and the double ones: the chair of the model split into its brace and its slide plate, the slide plate from the edge of the foot of the stock rail on, not under it; the plates and the slide plates narrowed where the rails lie close, the fastening of two rails centred between them, a rail lying on the fastening of the rail beside it left without one of its own, the brace dropped where the rail beside comes too close; selftest fastencheck --- application/editormodeselftestsleepers.cpp | 32 ++- world/SleeperFastenings.cpp | 231 ++++++++++++++++++++- world/SleeperFastenings.h | 34 +++ world/SleeperModels.cpp | 27 ++- world/SleeperRules.h | 12 +- world/TrackSleepers.cpp | 13 +- world/TrackSleepers.h | 6 +- 7 files changed, 335 insertions(+), 20 deletions(-) diff --git a/application/editormodeselftestsleepers.cpp b/application/editormodeselftestsleepers.cpp index 97969ae9..930bc134 100644 --- a/application/editormodeselftestsleepers.cpp +++ b/application/editormodeselftestsleepers.cpp @@ -162,7 +162,6 @@ editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &c WriteLog("SELFTEST fastenings: no track " + name + " with the fastenings in the model of its timbers"); return selftest_result::handled; } - std::array const names{"begin", "main", "end", "plate", "chair", "check"}; track_sleepers::fastened_rails const rails{*track}; std::array counts{}; for (auto const &beam : track_sleepers::place(*track)) @@ -173,7 +172,7 @@ editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &c std::string text{format("SELFTEST fastenings %s at %.2f %.2f:", track->name().c_str(), middle.x, middle.z)}; for (auto const &fastening : rails.on(beam, *model)) { - text += format(" %s %.3f %.3f %+.0f", names[fastening.part], fastening.point.x, fastening.point.z, glm::degrees(std::atan2(fastening.across.y, fastening.across.x))); + text += format(" %s %.3f %.3f %+.0f", track_sleepers::part_names[fastening.part], fastening.point.x, fastening.point.z, glm::degrees(std::atan2(fastening.across.y, fastening.across.x))); ++counts[fastening.part]; } WriteLog(text); @@ -181,6 +180,35 @@ editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &c WriteLog(format("SELFTEST fastenings %s: %d plates, %d chairs, %d checks", track->name().c_str(), counts[track_sleepers::part_plate], counts[track_sleepers::part_chair], counts[track_sleepers::part_check])); return selftest_result::handled; } + if (command == "fastencheck") + { + // the fastenings of all the tracks laying timbers with them: of each kind, and those over one another on a timber + std::string mode; + words >> mode; + auto const verbose{mode == "all"}; + auto const start{std::chrono::steady_clock::now()}; + auto const rows{track_sleepers::check_fastenings()}; + std::size_t beams{0}, overlaps{0}, tracks{0}; + std::array parts{}; + for (auto const &row : rows) + { + beams += row.beams; + overlaps += row.overlaps; + tracks += (row.overlaps > 0 ? 1 : 0); + for (int part = 0; part < track_sleepers::part_count; ++part) + parts[part] += row.parts[part]; + WriteLog(format("SELFTEST fastencheck %s: %zu timbers, %zu plates, %zu chairs, %zu checks, %zu overlaps%s", + row.track.c_str(), row.beams, row.parts[track_sleepers::part_plate], row.parts[track_sleepers::part_chair], row.parts[track_sleepers::part_check], row.overlaps, + row.overlaps > 0 ? format(", worst %.3f m %s at %.2f %.2f", row.worst, row.worstparts.c_str(), row.worstat.x, row.worstat.z).c_str() : "")); + if (verbose) + for (auto const &overlap : row.cases) + WriteLog(format("SELFTEST fastencheck overlap %s %d %d apart %.3f over %.3f at %.2f %.2f", row.track.c_str(), overlap.parts[0], overlap.parts[1], overlap.apart, overlap.over, overlap.at.x, overlap.at.z)); + } + WriteLog(format("SELFTEST fastencheck: %zu tracks, %zu timbers, %zu plates, %zu chairs, %zu checks, %zu overlaps on %zu tracks; in %.1f ms", + rows.size(), beams, parts[track_sleepers::part_plate], parts[track_sleepers::part_chair], parts[track_sleepers::part_check], overlaps, tracks, + std::chrono::duration(std::chrono::steady_clock::now() - start).count())); + return selftest_result::handled; + } if (command == "sleepercheck") { auto const start{std::chrono::steady_clock::now()}; diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp index 641a187f..72b084c7 100644 --- a/world/SleeperFastenings.cpp +++ b/world/SleeperFastenings.cpp @@ -15,6 +15,11 @@ http://mozilla.org/MPL/2.0/. #include "world/SleeperRules.h" #include "world/TrackRails.h" #include "world/Track.h" +#include "simulation/simulation.h" + +#include +#include +#include namespace track_sleepers { @@ -42,7 +47,8 @@ fastened_rails::fastened_rails( TTrack &Track ) { tracks.push_back( Other ); } } ); for( auto *track : tracks ) { - auto const middle { 0.5 * std::abs( track->fTrackWidth ) + track->rail_dimensions().web }; + auto const size { track->rail_dimensions() }; + auto const middle { 0.5 * std::abs( track->fTrackWidth ) + size.web }; auto const cut { track_rails::cut_rails( *track ) }; auto const switched { track->eType == tt_Switch && track->SwitchExtension != nullptr }; // the side of the blade of the path, and the stretch along it from the tip to the heel @@ -63,6 +69,7 @@ fastened_rails::fastened_rails( TTrack &Track ) { line.track = track; line.path = path; line.side = side; + line.foot = size.web - size.low; line.low = glm::dvec2 { std::numeric_limits::max() }; line.high = glm::dvec2 { std::numeric_limits::lowest() }; for( int step = 0; step <= count; ++step ) { @@ -138,7 +145,8 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M hits.push_back( { u * length, line.at( glm::mix( line.distances[ index - 1 ], line.distances[ index ], v ) ), - glm::normalize( glm::mix( line.inward[ index - 1 ], line.inward[ index ], v ) ) } ); + glm::normalize( glm::mix( line.inward[ index - 1 ], line.inward[ index ], v ) ), + line.foot } ); } } std::sort( hits.begin(), hits.end(), []( hit const &Left, hit const &Right ) { return Left.at < Right.at; } ); @@ -177,7 +185,7 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M result.push_back( { part_plate, point( 0.5 * ( rail.at + other.at ) ), rail.inward } ); } else if( has( part_check ) ) { - result.push_back( { part_check, point( rail.at ), axis } ); + result.push_back( { part_check, point( 0.5 * ( rail.at + other.at - check_seats ) ), axis, 1.f, static_cast( 0.5 * check_seats ) } ); } else { result.push_back( { part_plate, point( rail.at ), rail.inward } ); @@ -189,10 +197,16 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M switch( rail.kind ) { case role::stock: { result.push_back( { has( part_chair ) ? part_chair : part_plate, point( rail.at ), -rail.inward } ); + if( has( part_slide ) ) { + // the slide plate from the edge of the foot of the stock rail on, not under it + auto const &slide { Model.spans[ part_slide ] }; + auto const edge { std::min( slide.y, -rail.foot ) }; + result.push_back( { part_slide, point( rail.at ), -rail.inward, static_cast( ( edge - slide.x ) / ( slide.y - slide.x ) ), slide.y, static_cast( edge - slide.y ) } ); + } break; } case role::checked: { - result.push_back( { has( part_check ) ? part_check : part_plate, point( rail.at ), rail.inward } ); + result.push_back( { has( part_check ) ? part_check : part_plate, point( rail.at ), rail.inward, 1.f, has( part_check ) ? static_cast( 0.5 * check_seats ) : 0.f } ); break; } default: { @@ -201,6 +215,215 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M } } } + fit( result, Beam, Model ); + return result; +} + +void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, beam_model const &Model ) { + + auto const start { plan( Beam.from ) }; + auto const axis { glm::normalize( plan( Beam.to - Beam.from ) ) }; + auto const at = [&]( fastening const &Fastening ) { + return glm::dot( plan( Fastening.point ) - start, axis ); }; + // how far it reaches along the timber before and after where it is, at the scale + auto const reach = [&]( fastening const &Fastening, double const Scale ) { + auto const &span { Model.spans[ Fastening.part ] }; + auto const turn { glm::dot( Fastening.across, axis ) }; + auto const a { turn * Fastening.placed( span.x, Scale ) }; + auto const b { turn * Fastening.placed( span.y, Scale ) }; + return glm::dvec2 { -std::min( a, b ), std::max( a, b ) }; }; + auto const least = [&]( fastening const &Fastening ) { + switch( Fastening.part ) { + case part_plate: { return plate_least; } + case part_slide: { return slide_least; } + case part_check: { return check_least; } + default: { return 1.0; } + } }; + // those of one rail together, the rails in the order along the timber + std::vector order( Fastenings.size() ); + std::iota( order.begin(), order.end(), std::size_t { 0 } ); + std::stable_sort( order.begin(), order.end(), [&]( std::size_t const Left, std::size_t const Right ) { + return at( Fastenings[ Left ] ) < at( Fastenings[ Right ] ) - 1e-6; } ); + std::vector> rails; + for( std::size_t index = 0; index < order.size(); ) { + auto next { index + 1 }; + while( next < order.size() && at( Fastenings[ order[ next ] ] ) - at( Fastenings[ order[ index ] ] ) < 1e-6 ) { + ++next; + } + rails.emplace_back( index, next ); + index = next; + } + for( std::size_t index = 1; index < rails.size(); ++index ) { + auto const [ firstfrom, firstto ] { rails[ index - 1 ] }; + auto const [ secondfrom, secondto ] { rails[ index ] }; + auto const room { at( Fastenings[ order[ secondfrom ] ] ) - at( Fastenings[ order[ firstfrom ] ] ) - fastening_gap }; + // the ones of both reaching less toward the other as they're narrowed; with them narrowed by the share, how far the two + // lie over each other + std::vector narrowed; + for( auto member { firstfrom }; member < secondto; ++member ) { + auto const &fastening { Fastenings[ order[ member ] ] }; + auto const side { member < firstto ? 1 : 0 }; + if( reach( fastening, std::min( fastening.scale, least( fastening ) ) )[ side ] < reach( fastening, fastening.scale )[ side ] - 1e-6 ) { + narrowed.push_back( order[ member ] ); + } + } + auto const over = [&]( double const Share ) { + auto after { 0.0 }; + auto before { 0.0 }; + for( auto member { firstfrom }; member < secondto; ++member ) { + auto const &fastening { Fastenings[ order[ member ] ] }; + auto const shrinks { std::find( narrowed.begin(), narrowed.end(), order[ member ] ) != narrowed.end() }; + auto const scale { shrinks ? glm::mix( fastening.scale, least( fastening ), Share ) : fastening.scale }; + if( member < firstto ) { + after = std::max( after, reach( fastening, scale ).y ); + } + else { + before = std::max( before, reach( fastening, scale ).x ); + } + } + return after + before - room; }; + if( narrowed.empty() || over( 0.0 ) <= 0.0 ) { + continue; + } + auto share { 1.0 }; + if( over( 1.0 ) < 0.0 ) { + auto low { 0.0 }; + for( int step = 0; step < 20; ++step ) { + auto const middle { 0.5 * ( low + share ) }; + ( over( middle ) > 0.0 ? low : share ) = middle; + } + } + for( auto const index : narrowed ) { + auto &fastening { Fastenings[ index ] }; + fastening.scale = static_cast( glm::mix( fastening.scale, least( fastening ), share ) ); + } + } + // a rail still lying on the fastening of the rail beside it goes without one of its own, unless it's a chair + std::vector dropped( Fastenings.size(), false ); + auto const chair = [&]( std::pair const &Rail ) { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + if( Fastenings[ order[ member ] ].part == part_chair || Fastenings[ order[ member ] ].part == part_slide ) { + return true; + } + } + return false; }; + auto const extent = [&]( std::pair const &Rail ) { + glm::dvec2 result { 0.0 }; + for( auto member { Rail.first }; member < Rail.second; ++member ) { + if( false == dropped[ order[ member ] ] ) { + result = glm::max( result, reach( Fastenings[ order[ member ] ], Fastenings[ order[ member ] ].scale ) ); + } + } + return result; }; + auto kept { std::size_t { 0 } }; + for( std::size_t index = 1; index < rails.size(); ++index ) { + auto const &first { rails[ kept ] }; + auto const &second { rails[ index ] }; + auto const apart { at( Fastenings[ order[ second.first ] ] ) - at( Fastenings[ order[ first.first ] ] ) }; + auto const overlaps = [&]() { + return extent( first ).y + extent( second ).x - apart > fastening_gap; }; + if( false == overlaps() ) { + kept = index; + continue; + } + auto const firstreach { extent( first ) }; + auto const secondreach { extent( second ) }; + auto const drop = [&]( std::pair const &Rail ) { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + dropped[ order[ member ] ] = true; + } }; + // the brace of a chair where the rail beside comes too close for it, its slide plate kept + auto const brace = [&]( std::pair const &Rail, int const Side ) { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + auto const &fastening { Fastenings[ order[ member ] ] }; + if( fastening.part == part_chair && reach( fastening, fastening.scale )[ Side ] > fastening_gap ) { + dropped[ order[ member ] ] = true; + return true; + } + } + return false; }; + if( false == chair( second ) && apart <= firstreach.y ) { + drop( second ); + continue; + } + if( false == chair( first ) && apart <= secondreach.x ) { + drop( first ); + } + else { + brace( first, 1 ); + if( overlaps() ) { + brace( second, 0 ); + } + } + kept = index; + } + std::vector fitted; + for( std::size_t index = 0; index < Fastenings.size(); ++index ) { + if( false == dropped[ index ] ) { + fitted.push_back( Fastenings[ index ] ); + } + } + Fastenings = std::move( fitted ); +} + +std::vector check_fastenings() { + + std::vector result; + for( auto *track : simulation::Paths.sequence() ) { + if( track == nullptr || track->m_editorremoved || false == track->m_sleepers3d.active ) { + continue; + } + auto const *model { timber_model( *track ) }; + if( model == nullptr || false == model->fastened() ) { + continue; + } + fastening_row row; + row.track = track->name(); + std::optional rails; + for( auto const &beam : place( *track ) ) { + if( beam.plain ) { + continue; + } + if( false == rails.has_value() ) { + rails.emplace( *track ); + } + ++row.beams; + auto const start { plan( beam.from ) }; + auto const axis { glm::normalize( plan( beam.to - beam.from ) ) }; + struct footprint { + double from; + double to; + int part; + glm::dvec3 point; + }; + std::vector footprints; + for( auto const &fastening : rails->on( beam, *model ) ) { + ++row.parts[ fastening.part ]; + auto const at { glm::dot( plan( fastening.point ) - start, axis ) }; + auto const turn { glm::dot( fastening.across, axis ) }; + auto const a { at + turn * fastening.placed( model->spans[ fastening.part ].x, fastening.scale ) }; + auto const b { at + turn * fastening.placed( model->spans[ fastening.part ].y, fastening.scale ) }; + footprints.push_back( { std::min( a, b ), std::max( a, b ), fastening.part, fastening.point } ); + } + std::sort( footprints.begin(), footprints.end(), []( footprint const &Left, footprint const &Right ) { return Left.from < Right.from; } ); + for( std::size_t index = 1; index < footprints.size(); ++index ) { + auto const over { footprints[ index - 1 ].to - footprints[ index ].from }; + if( over <= fastening_overlap ) { + continue; + } + ++row.overlaps; + row.cases.push_back( { { footprints[ index - 1 ].part, footprints[ index ].part }, glm::length( plan( footprints[ index ].point - footprints[ index - 1 ].point ) ), over, footprints[ index ].point } ); + if( over > row.worst ) { + row.worst = over; + row.worstat = footprints[ index ].point; + row.worstparts = std::string { part_names[ footprints[ index - 1 ].part ] } + "/" + part_names[ footprints[ index ].part ]; + } + } + } + if( row.beams > 0 ) { + result.push_back( std::move( row ) ); + } + } return result; } diff --git a/world/SleeperFastenings.h b/world/SleeperFastenings.h index 1d2f876a..c4f36a82 100644 --- a/world/SleeperFastenings.h +++ b/world/SleeperFastenings.h @@ -9,6 +9,8 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include +#include #include #include @@ -25,6 +27,12 @@ struct fastening { int part { part_plate }; glm::dvec3 point { 0.0 }; // under the middle of the rail, on the top of the timber glm::dvec2 across { 1.0, 0.0 }; // unit vector in plan square to the rail, the way the +x of the part goes + float scale { 1.f }; // along its x, a plate or a slide plate narrowed where it'd lie over the fastenings of rails close beside + float pivot { 0.f }; // the x of the part kept in place as it's narrowed + float shift { 0.f }; // along its x, how far the pivot is moved + // where an x of the part lands along its across, at the scale + double placed( double const X, double const Scale ) const { + return pivot + shift + Scale * ( X - pivot ); } }; // the rails of the tracks around a track, which lie on its timbers @@ -54,6 +62,7 @@ private: std::vector distances; glm::dvec2 low { 0.0 }; glm::dvec2 high { 0.0 }; + double foot { 0.0 }; // from the middle of the rail to the edge of its foot toward the middle of the path // the stretches along the path the rail is a blade, a stock rail with a chair, checked along glm::dvec2 blade { -1.0 }; glm::dvec2 stock { -1.0 }; @@ -65,9 +74,34 @@ private: double at { 0.0 }; // along the timber, from its from end role kind { role::plain }; glm::dvec2 inward { 1.0, 0.0 }; + double foot { 0.0 }; }; + // the fastenings of rails close by fitted to the room between them: narrowed across, and a rail lying on the fastening of the + // rail beside it left without one of its own + static void fit( std::vector &Fastenings, beam const &Beam, beam_model const &Model ); + std::vector m_rails; }; +// the fastenings of a track with them in the model of its timbers, as laid on the timbers laid with it +struct fastening_row { + std::string track; + std::size_t beams { 0 }; + std::array parts {}; + std::size_t overlaps { 0 }; // fastenings lying over one another on a timber, along it + double worst { 0.0 }; + glm::dvec3 worstat { 0.0 }; + std::string worstparts; + struct overlap { + int parts[ 2 ]; + double apart; // of the rails the two lie under + double over; + glm::dvec3 at; + }; + std::vector cases; +}; +// those of all the tracks laying timbers with fastenings +std::vector check_fastenings(); + } // namespace track_sleepers diff --git a/world/SleeperModels.cpp b/world/SleeperModels.cpp index 90e0a5d8..1dc13567 100644 --- a/world/SleeperModels.cpp +++ b/world/SleeperModels.cpp @@ -109,22 +109,29 @@ beam_model const *find_model( std::string const &Name, std::string const &Skinna model.skin = GfxRenderer->Fetch_Material( Skin ); } // a timber put together from its parts, with a part between the ends stretched and repeated to the length - std::array const names { "begin", "main", "end", "plate", "chair", "check" }; model.stretchable = true; for( int part = 0; part < part_count; ++part ) { - model.parts[ part ] = model.model->GetFromName( names[ part ] ); + model.parts[ part ] = model.model->GetFromName( part_names[ part ] ); model.stretchable = model.stretchable && ( part >= timber_parts || model.parts[ part ] != nullptr ); } auto top { std::numeric_limits::lowest() }; model.span = { std::numeric_limits::max(), std::numeric_limits::lowest() }; if( model.stretchable ) { - for( int part = 0; part < timber_parts; ++part ) { + for( int part = 0; part < part_count; ++part ) { + if( model.parts[ part ] == nullptr ) { + continue; + } glm::vec2 span { std::numeric_limits::max(), std::numeric_limits::lowest() }; + auto const timber { part < timber_parts }; vertices_of( model.parts[ part ], [&]( glm::vec3 const &Position ) { span = { std::min( span.x, Position.x ), std::max( span.y, Position.x ) }; - top = std::max( top, Position.y ); } ); + if( timber ) { + top = std::max( top, Position.y ); + } } ); model.spans[ part ] = span; - model.span = { std::min( model.span.x, span.x ), std::max( model.span.y, span.y ) }; + if( timber ) { + model.span = { std::min( model.span.x, span.x ), std::max( model.span.y, span.y ) }; + } } model.stretchable = model.spans[ part_main ].y > model.spans[ part_main ].x; } @@ -152,8 +159,12 @@ beam_model const *find_model( std::string const &Name, std::string const &Skinna if( Seat < 0.f && model.fastened() ) { model.seat = std::max( top, surface_at( model.parts[ part_plate ], 0.f, 0.f ).value_or( top ) ); } - if( model.fastened() && model.parts[ part_chair ] != nullptr ) { - model.slide = std::max( 0.f, surface_at( model.parts[ part_chair ], static_cast( -slide_probe ), 0.f ).value_or( model.seat ) - model.seat ); + if( model.fastened() ) { + for( auto const part : { part_chair, part_slide } ) { + if( model.parts[ part ] != nullptr ) { + model.slide = std::max( model.slide, surface_at( model.parts[ part ], static_cast( -slide_probe ), 0.f ).value_or( model.seat ) - model.seat ); + } + } } if( model.length() < 0.5f ) { ErrorLog( "Bad sleepers3d: model \"" + Name + "\" has no beam along x" ); @@ -167,7 +178,7 @@ beam_model const *find_model( std::string const &Name, std::string const &Skinna ", main " + to_string( model.spans[ part_main ].x, 3 ) + ".." + to_string( model.spans[ part_main ].y, 3 ) : std::string {} ) + ( model.fastened() ? - std::string { ", fastenings plate" } + ( model.parts[ part_chair ] != nullptr ? " chair" : "" ) + ( model.parts[ part_check ] != nullptr ? " check" : "" ) : + std::string { ", fastenings plate" } + ( model.parts[ part_chair ] != nullptr ? " chair" : "" ) + ( model.parts[ part_check ] != nullptr ? " check" : "" ) + ( model.parts[ part_slide ] != nullptr ? " slide" : "" ) : std::string {} ) + ", seat " + to_string( model.seat, 3 ) + ( model.slide > 0.f ? ", slide " + to_string( model.slide, 3 ) : std::string {} ) ); diff --git a/world/SleeperRules.h b/world/SleeperRules.h index 3fb63044..d4573e9d 100644 --- a/world/SleeperRules.h +++ b/world/SleeperRules.h @@ -55,12 +55,22 @@ double constexpr fastening_step { 0.25 }; double constexpr fastening_merge { 0.03 }; // two rails closer than this lie in one fastening of both, as a rail of the frog with the one beside it; closer than the second // one, as the legs of the nose where they're joined, in one plate between them -double constexpr fastening_pair { 0.24 }; +double constexpr fastening_pair { 0.2 }; double constexpr fastening_nose { 0.05 }; +// how far apart the rails lie in the fastening of both, the check part of the model +double constexpr check_seats { 0.113 }; +// the room left between the fastenings of rails close by, and how narrow their plates get to keep it +double constexpr fastening_gap { 0.01 }; +double constexpr plate_least { 0.6 }; +double constexpr slide_least { 0.25 }; +// a fastening of two rails narrowed no more than this, the rails lying a little off its seats then +double constexpr check_least { 0.8 }; // how far before the tip of the blade the slide chairs of its stock rail begin double constexpr chair_lead { 0.3 }; // how far from the stock rail toward its blade the slide plate of a chair is measured double constexpr slide_probe { 0.12 }; +// how far two fastenings on a timber may reach over one another before the check counts them +double constexpr fastening_overlap { 0.01 }; // how far from the camera the fastenings are drawn at most double constexpr fastening_range { 120.0 }; diff --git a/world/TrackSleepers.cpp b/world/TrackSleepers.cpp index e23fa7db..94f239d5 100644 --- a/world/TrackSleepers.cpp +++ b/world/TrackSleepers.cpp @@ -141,8 +141,8 @@ void lay( TTrack &Track ) { // the layout of the cluster isn't needed by this track any more, the instances below are what's drawn forget_layout( Track ); - // the rails around, where they lie on the timbers, for the fastenings of the model - std::optional const rails { parts.fastened() ? std::make_optional( Track ) : std::nullopt }; + // the rails around, where they lie on the timbers, for the fastenings of the model; found at the first timber, none for plain sleepers + std::optional rails; auto const partlength = [&]( int const Part ) { return static_cast( parts.spans[ Part ].y - parts.spans[ Part ].x ); }; @@ -242,7 +242,10 @@ void lay( TTrack &Track ) { put( part_end, scale, parts.spans[ part_end ].x, -half ); put( part_begin, scale, parts.spans[ part_begin ].y, half ); } - if( rails && false == ( beam.plain && laid.plains != nullptr ) ) { + if( parts.fastened() && false == ( beam.plain && laid.plains != nullptr ) ) { + if( false == rails.has_value() ) { + rails.emplace( Track ); + } // each square to its rail on the top of the timber, the plate under the foot of the rail for( auto const &fastening : rails->on( beam, parts ) ) { auto across { glm::dvec3 { fastening.across.x, 0.0, fastening.across.y } }; @@ -252,7 +255,9 @@ void lay( TTrack &Track ) { place[ 1 ] = glm::vec4 { glm::vec3 { y }, 0.f }; place[ 2 ] = glm::vec4 { glm::vec3 { glm::cross( across, y ) }, 0.f }; place[ 3 ] = glm::vec4 { glm::vec3 { fastening.point - Track.m_origin }, 1.f }; - laid.instances[ fastening.part ].emplace_back( glm::translate( place, glm::vec3 { 0.f, -parts.seat, 0.f } ) ); + auto const pivot { glm::vec3 { fastening.pivot, 0.f, 0.f } }; + laid.instances[ fastening.part ].emplace_back( + glm::translate( glm::scale( glm::translate( place, glm::vec3 { fastening.shift, -parts.seat, 0.f } + pivot ), glm::vec3 { fastening.scale, 1.f, 1.f } ), -pivot ) ); } } runbeams.emplace_back( middle, half ); diff --git a/world/TrackSleepers.h b/world/TrackSleepers.h index fd994446..683a451b 100644 --- a/world/TrackSleepers.h +++ b/world/TrackSleepers.h @@ -36,11 +36,15 @@ enum part : int { part_main, // the piece between the ends part_end, // the end on the -x side part_plate, // the fastening of a rail, its origin under the middle of the foot of the rail - part_chair, // the slide chair of a stock rail with the blade beside it, its origin under the stock rail, the slide plate on its -x side + part_chair, // the brace of the slide chair of a stock rail with the blade beside it, its origin under the stock rail, on its +x side part_check, // the fastening of a rail with another one close beside it, as a check rail or a rail of the frog, its origin under the // rail, the other one on its +x side + part_slide, // the slide plate of the chair, apart from it on its -x side, from the edge of the foot of the stock rail on, shortened + // toward it where another rail lies close part_count }; +// the names of the submodels of the parts +inline constexpr std::array part_names { "begin", "main", "end", "plate", "chair", "check", "slide" }; // the parts a timber is put together from, the fastenings after them int constexpr timber_parts { part_end + 1 }; From 62f8932f158ba82874110db5f1cd13a535111dd2 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 17:44:35 +0200 Subject: [PATCH 03/19] fix(sleepers): a rail with its check rail on the side away from a rail close beside, as the outer rails at the acute crossings of the slips with the blades outside of the diamond, in the fastening with its check rail, the rail beside in a plate of its own; a plate still lying over a fastening of more than a plate after the narrowing left out --- world/SleeperFastenings.cpp | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp index 72b084c7..c9cd5945 100644 --- a/world/SleeperFastenings.cpp +++ b/world/SleeperFastenings.cpp @@ -181,7 +181,24 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M auto const &rail { rails[ index ] }; if( index + 1 < rails.size() && rails[ index + 1 ].at - rail.at < fastening_pair && rail.kind != role::stock && rails[ index + 1 ].kind != role::stock ) { auto const &other { rails[ index + 1 ] }; - if( other.at - rail.at < fastening_nose ) { + // a rail of the two with its check rail on the side away from the other one in the fastening with the check rail, + // the other one in a plate of its own + auto const away = [&]( hit const &Rail, double const Toward ) { + return Rail.kind == role::checked && glm::dot( Rail.inward, axis ) * Toward < 0.0; }; + auto const awayfirst { away( rail, 1.0 ) }; + auto const awaysecond { away( other, -1.0 ) }; + if( has( part_check ) && ( awayfirst || awaysecond ) ) { + auto const joined { other.at - rail.at < fastening_nose }; + for( auto const &[ one, checked ] : { std::make_pair( &rail, awayfirst ), std::make_pair( &other, awaysecond ) } ) { + if( checked ) { + result.push_back( { part_check, point( one->at ), one->inward, 1.f, static_cast( 0.5 * check_seats ) } ); + } + else if( false == joined ) { + result.push_back( { part_plate, point( one->at ), one->inward } ); + } + } + } + else if( other.at - rail.at < fastening_nose ) { result.push_back( { part_plate, point( 0.5 * ( rail.at + other.at ) ), rail.inward } ); } else if( has( part_check ) ) { @@ -307,6 +324,13 @@ void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, } } return false; }; + auto const plates = [&]( std::pair const &Rail ) { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + if( Fastenings[ order[ member ] ].part != part_plate ) { + return false; + } + } + return true; }; auto const extent = [&]( std::pair const &Rail ) { glm::dvec2 result { 0.0 }; for( auto member { Rail.first }; member < Rail.second; ++member ) { @@ -355,6 +379,16 @@ void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, brace( second, 0 ); } } + // a rail in a plate still lying over the fastening of a rail beside held with more than a plate goes without its own + if( overlaps() ) { + if( plates( second ) && false == plates( first ) ) { + drop( second ); + continue; + } + if( plates( first ) && false == plates( second ) ) { + drop( first ); + } + } kept = index; } std::vector fitted; From fa62a6138957ae4d928992a405f8f6f8193b1261 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 18:25:00 +0200 Subject: [PATCH 04/19] fix(sleepers): no rail left lying on a timber without a fastening: the stock rails on a plate as wide as their foot between the brace and the slide plate, the stock rails back to back, as the wing rails of the slips with the blades outside of the diamond, on one plate under both; three rails or more close beside one another under fastenings of two rails in turn; a plate lying over the fastening of the rail beside cut short at its edge, as on one plate with it, a rail with its check rail on the fastening of the rail beside in such a plate, not left out; selftests checkseats and the bare rails in fastencheck --- application/editormodeselftestsleepers.cpp | 29 +- world/SleeperFastenings.cpp | 331 ++++++++++++++++++++- world/SleeperFastenings.h | 27 +- world/SleeperRules.h | 2 + 4 files changed, 369 insertions(+), 20 deletions(-) diff --git a/application/editormodeselftestsleepers.cpp b/application/editormodeselftestsleepers.cpp index 930bc134..63972320 100644 --- a/application/editormodeselftestsleepers.cpp +++ b/application/editormodeselftestsleepers.cpp @@ -188,27 +188,46 @@ editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &c auto const verbose{mode == "all"}; auto const start{std::chrono::steady_clock::now()}; auto const rows{track_sleepers::check_fastenings()}; - std::size_t beams{0}, overlaps{0}, tracks{0}; + std::size_t beams{0}, overlaps{0}, tracks{0}, bare{0}; std::array parts{}; for (auto const &row : rows) { beams += row.beams; overlaps += row.overlaps; + bare += row.bares.size(); tracks += (row.overlaps > 0 ? 1 : 0); for (int part = 0; part < track_sleepers::part_count; ++part) parts[part] += row.parts[part]; - WriteLog(format("SELFTEST fastencheck %s: %zu timbers, %zu plates, %zu chairs, %zu checks, %zu overlaps%s", - row.track.c_str(), row.beams, row.parts[track_sleepers::part_plate], row.parts[track_sleepers::part_chair], row.parts[track_sleepers::part_check], row.overlaps, + WriteLog(format("SELFTEST fastencheck %s: %zu timbers, %zu plates, %zu chairs, %zu checks, %zu bare rails, %zu overlaps%s", + row.track.c_str(), row.beams, row.parts[track_sleepers::part_plate], row.parts[track_sleepers::part_chair], row.parts[track_sleepers::part_check], row.bares.size(), row.overlaps, row.overlaps > 0 ? format(", worst %.3f m %s at %.2f %.2f", row.worst, row.worstparts.c_str(), row.worstat.x, row.worstat.z).c_str() : "")); + if (verbose) + for (auto const &at : row.bares) + WriteLog(format("SELFTEST fastencheck bare %s at %.3f %.3f", row.track.c_str(), at.x, at.y)); if (verbose) for (auto const &overlap : row.cases) WriteLog(format("SELFTEST fastencheck overlap %s %d %d apart %.3f over %.3f at %.2f %.2f", row.track.c_str(), overlap.parts[0], overlap.parts[1], overlap.apart, overlap.over, overlap.at.x, overlap.at.z)); } - WriteLog(format("SELFTEST fastencheck: %zu tracks, %zu timbers, %zu plates, %zu chairs, %zu checks, %zu overlaps on %zu tracks; in %.1f ms", - rows.size(), beams, parts[track_sleepers::part_plate], parts[track_sleepers::part_chair], parts[track_sleepers::part_check], overlaps, tracks, + WriteLog(format("SELFTEST fastencheck: %zu tracks, %zu timbers, %zu plates, %zu chairs, %zu checks, %zu bare rails, %zu overlaps on %zu tracks; in %.1f ms", + rows.size(), beams, parts[track_sleepers::part_plate], parts[track_sleepers::part_chair], parts[track_sleepers::part_check], bare, overlaps, tracks, std::chrono::duration(std::chrono::steady_clock::now() - start).count())); return selftest_result::handled; } + if (command == "checkseats") + { + // the check rails with timbers under them and no fastening seating them, along how much of them + auto const gaps{track_sleepers::check_rail_seats()}; + double bare{0.0}; + std::size_t unsupported{0}; + for (auto const &gap : gaps) + { + bare += gap.bare; + unsupported += gap.unsupported; + WriteLog(format("SELFTEST checkseats %s path %d side %+d %.2f-%.2f: %.0f of %zu timbers bare, %zu with nothing under, first at %.2f %.2f", gap.track.c_str(), gap.path, gap.side, gap.from, gap.to, gap.bare, gap.timbers, gap.unsupported, gap.at.x, gap.at.y)); + } + WriteLog(format("SELFTEST checkseats: %zu check rails with %.0f timbers bare, %zu with nothing under", gaps.size(), bare, unsupported)); + return selftest_result::handled; + } if (command == "sleepercheck") { auto const start{std::chrono::steady_clock::now()}; diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp index c9cd5945..b18ac69b 100644 --- a/world/SleeperFastenings.cpp +++ b/world/SleeperFastenings.cpp @@ -106,13 +106,12 @@ fastened_rails::fastened_rails( TTrack &Track ) { } } -std::vector fastened_rails::on( beam const &Beam, beam_model const &Model ) const { +std::vector fastened_rails::rails_on( beam const &Beam ) const { - std::vector result; auto const span { Beam.to - Beam.from }; auto const length { glm::length( plan( span ) ) }; if( length < 0.1 ) { - return result; + return {}; } auto const start { plan( Beam.from ) }; auto const axis { plan( span ) / length }; @@ -173,12 +172,71 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M } index = next; } + return rails; +} + +std::vector fastened_rails::on( beam const &Beam, beam_model const &Model ) const { + + std::vector result; + auto const span { Beam.to - Beam.from }; + auto const length { glm::length( plan( span ) ) }; + if( length < 0.1 ) { + return result; + } + auto const axis { plan( span ) / length }; + auto const rails { rails_on( Beam ) }; auto const point = [&]( double const At ) { return Beam.from + span * ( At / length ); }; auto const has = [&]( int const Part ) { return Model.parts[ Part ] != nullptr; }; + // the rails from the first to the last under fastenings of two rails in turn, each with its seats under two rails side by side + // and over as many of the rails after them as it reaches + auto const crowd = [&]( std::size_t First, std::size_t const Last ) { + auto const &reach { Model.spans[ part_check ] }; + while( First <= Last ) { + std::size_t covered { 0 }; + auto origin { 0.0 }; + auto turn { 1.0 }; + for( auto seat { First }; seat < Last; ++seat ) { + for( auto const direction : { 1.0, -1.0 } ) { + auto const at { 0.5 * ( rails[ seat ].at + rails[ seat + 1 ].at - direction * check_seats ) }; + auto const from { at + std::min( direction * reach.x, direction * reach.y ) }; + auto const to { at + std::max( direction * reach.x, direction * reach.y ) }; + if( rails[ First ].at < from ) { + continue; + } + std::size_t count { 0 }; + while( First + count <= Last && rails[ First + count ].at <= to ) { + ++count; + } + if( count > covered ) { + covered = count; + origin = at; + turn = direction; + } + } + } + if( covered < 2 ) { + result.push_back( { part_plate, point( rails[ First ].at ), rails[ First ].inward } ); + ++First; + continue; + } + result.push_back( { part_check, point( origin ), axis * turn, 1.f, static_cast( 0.5 * check_seats ), 0.f, true } ); + First += covered; + } + }; for( std::size_t index = 0; index < rails.size(); ++index ) { auto const &rail { rails[ index ] }; + // three rails or more close beside one another, as at the crossings of the slips, none a stock rail or with a check rail + auto last { index }; + while( last + 1 < rails.size() && rails[ last + 1 ].at - rails[ last ].at < fastening_pair && rails[ last ].kind == role::plain && rails[ last + 1 ].kind == role::plain ) { + ++last; + } + if( last >= index + 2 && has( part_check ) ) { + crowd( index, last ); + index = last; + continue; + } if( index + 1 < rails.size() && rails[ index + 1 ].at - rail.at < fastening_pair && rail.kind != role::stock && rails[ index + 1 ].kind != role::stock ) { auto const &other { rails[ index + 1 ] }; // a rail of the two with its check rail on the side away from the other one in the fastening with the check rail, @@ -202,7 +260,7 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M result.push_back( { part_plate, point( 0.5 * ( rail.at + other.at ) ), rail.inward } ); } else if( has( part_check ) ) { - result.push_back( { part_check, point( 0.5 * ( rail.at + other.at - check_seats ) ), axis, 1.f, static_cast( 0.5 * check_seats ) } ); + result.push_back( { part_check, point( 0.5 * ( rail.at + other.at - check_seats ) ), axis, 1.f, static_cast( 0.5 * check_seats ), 0.f, true } ); } else { result.push_back( { part_plate, point( rail.at ), rail.inward } ); @@ -214,6 +272,14 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M switch( rail.kind ) { case role::stock: { result.push_back( { has( part_chair ) ? part_chair : part_plate, point( rail.at ), -rail.inward } ); + if( has( part_chair ) ) { + // the rail on a plate as wide as its foot, between the brace and the slide plate + auto const &plate { Model.spans[ part_plate ] }; + auto const low { -rail.foot }; + auto const high { std::min( rail.foot, Model.spans[ part_chair ].x ) }; + auto const scale { ( high - low ) / ( plate.y - plate.x ) }; + result.push_back( { part_plate, point( rail.at ), -rail.inward, static_cast( scale ), 0.f, static_cast( low - scale * plate.x ) } ); + } if( has( part_slide ) ) { // the slide plate from the edge of the foot of the stock rail on, not under it auto const &slide { Model.spans[ part_slide ] }; @@ -236,6 +302,25 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M return result; } +std::vector fastened_rails::bare( beam const &Beam, beam_model const &Model, std::vector const &Fastenings ) const { + + auto const span { plan( Beam.to - Beam.from ) }; + auto const length { glm::length( span ) }; + std::vector result; + for( auto const &rail : rails_on( Beam ) ) { + auto const at { plan( Beam.from ) + span * ( rail.at / length ) }; + auto const under { std::any_of( Fastenings.begin(), Fastenings.end(), [&]( fastening const &Fastening ) { + auto const along { glm::dot( at - plan( Fastening.point ), Fastening.across ) }; + auto const a { Fastening.placed( Model.spans[ Fastening.part ].x, Fastening.scale ) }; + auto const b { Fastening.placed( Model.spans[ Fastening.part ].y, Fastening.scale ) }; + return along >= std::min( a, b ) - 0.01 && along <= std::max( a, b ) + 0.01; } ) }; + if( false == under ) { + result.push_back( at ); + } + } + return result; +} + void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, beam_model const &Model ) { auto const start { plan( Beam.from ) }; @@ -324,6 +409,22 @@ void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, } } return false; }; + auto const checks = [&]( std::pair const &Rail ) { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + auto const &fastening { Fastenings[ order[ member ] ] }; + if( fastening.part == part_check && false == fastening.both ) { + return true; + } + } + return false; }; + auto const faces = [&]( std::pair const &Rail, double const Toward ) { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + auto const &fastening { Fastenings[ order[ member ] ] }; + if( fastening.part == part_check && false == fastening.both && glm::dot( fastening.across, axis ) * Toward > 0.0 ) { + return true; + } + } + return false; }; auto const plates = [&]( std::pair const &Rail ) { for( auto member { Rail.first }; member < Rail.second; ++member ) { if( Fastenings[ order[ member ] ].part != part_plate ) { @@ -356,6 +457,37 @@ void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, for( auto member { Rail.first }; member < Rail.second; ++member ) { dropped[ order[ member ] ] = true; } }; + // the plates of the rail with their end toward the rail beside Room from it, the other end kept where it is + auto const clip = [&]( std::pair const &Rail, double const Toward, double const Room ) { + if( Room < 0.0 ) { + return false; + } + for( auto member { Rail.first }; member < Rail.second; ++member ) { + auto &fastening { Fastenings[ order[ member ] ] }; + auto const &span { Model.spans[ fastening.part ] }; + auto const turn { glm::dot( fastening.across, axis ) }; + auto const toward { turn * Toward > 0.0 ? span.y : span.x }; + auto const away { turn * Toward > 0.0 ? span.x : span.y }; + auto const stays { fastening.placed( away, 1.0 ) }; + auto const scale { ( Toward * turn * Room - stays ) / ( toward - away ) }; + if( scale < clip_least ) { + return false; + } + fastening.pivot = static_cast( away ); + fastening.shift = static_cast( stays - away ); + fastening.scale = static_cast( scale ); + } + return true; }; + // the fastenings of the rail made plates of their own + auto const replate = [&]( std::pair const &Rail ) -> std::pair const & { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + auto &fastening { Fastenings[ order[ member ] ] }; + fastening.part = part_plate; + fastening.scale = 1.f; + fastening.pivot = 0.f; + fastening.shift = 0.f; + } + return Rail; }; // the brace of a chair where the rail beside comes too close for it, its slide plate kept auto const brace = [&]( std::pair const &Rail, int const Side ) { for( auto member { Rail.first }; member < Rail.second; ++member ) { @@ -366,11 +498,28 @@ void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, } } return false; }; - if( false == chair( second ) && apart <= firstreach.y ) { + // of two rails with check rails, the one with its check rail toward the other one, lying on the fastening of the other + // one then, goes in a plate cut short at that fastening, or without its own, the other one keeping its check rail seated + if( checks( first ) && checks( second ) && faces( first, 1.0 ) != faces( second, -1.0 ) ) { + if( faces( first, 1.0 ) && apart - check_seats <= secondreach.x ) { + if( false == clip( replate( first ), 1.0, apart - secondreach.x - fastening_gap ) ) { + drop( first ); + } + kept = index; + continue; + } + if( faces( second, -1.0 ) && apart - check_seats <= firstreach.y ) { + if( false == clip( replate( second ), -1.0, apart - firstreach.y - fastening_gap ) ) { + drop( second ); + } + continue; + } + } + if( false == chair( second ) && apart <= firstreach.y && ( false == checks( second ) || checks( first ) ) ) { drop( second ); continue; } - if( false == chair( first ) && apart <= secondreach.x ) { + if( false == chair( first ) && apart <= secondreach.x && ( false == checks( first ) || checks( second ) ) ) { drop( first ); } else { @@ -379,14 +528,44 @@ void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, brace( second, 0 ); } } - // a rail in a plate still lying over the fastening of a rail beside held with more than a plate goes without its own + // a rail in a plate still lying over the fastening of a rail beside held with more than a plate gets its plate cut short + // at the edge of that fastening, as on one plate with it, or goes without its own if the plate would no longer be under it if( overlaps() ) { if( plates( second ) && false == plates( first ) ) { - drop( second ); - continue; + if( false == clip( second, -1.0, apart - extent( first ).y - fastening_gap ) ) { + drop( second ); + continue; + } } - if( plates( first ) && false == plates( second ) ) { - drop( first ); + else if( plates( first ) && false == plates( second ) ) { + if( false == clip( first, 1.0, apart - extent( second ).x - fastening_gap ) ) { + drop( first ); + } + } + } + // the plates of two rails still lying over each other, as of the stock rails back to back, one plate under both + auto const plate = [&]( std::pair const &Rail ) { + for( auto member { Rail.first }; member < Rail.second; ++member ) { + if( Fastenings[ order[ member ] ].part == part_plate && false == dropped[ order[ member ] ] ) { + return order[ member ]; + } + } + return Fastenings.size(); }; + auto const firstplate { plate( first ) }; + auto const secondplate { plate( second ) }; + if( firstplate < Fastenings.size() && secondplate < Fastenings.size() ) { + auto &one { Fastenings[ firstplate ] }; + auto const &other { Fastenings[ secondplate ] }; + auto const from { -reach( one, one.scale ).x }; + auto const to { apart + reach( other, other.scale ).y }; + if( reach( one, one.scale ).y + reach( other, other.scale ).x > apart ) { + auto const &span { Model.spans[ part_plate ] }; + auto const turn { glm::dot( one.across, axis ) }; + auto const scale { ( to - from ) / ( span.y - span.x ) }; + one.pivot = 0.f; + one.scale = static_cast( scale ); + one.shift = static_cast( ( turn > 0.0 ? from : -to ) - scale * span.x ); + dropped[ secondplate ] = true; } } kept = index; @@ -431,7 +610,11 @@ std::vector check_fastenings() { glm::dvec3 point; }; std::vector footprints; - for( auto const &fastening : rails->on( beam, *model ) ) { + auto const fastenings { rails->on( beam, *model ) }; + for( auto const &at : rails->bare( beam, *model, fastenings ) ) { + row.bares.push_back( at ); + } + for( auto const &fastening : fastenings ) { ++row.parts[ fastening.part ]; auto const at { glm::dot( plan( fastening.point ) - start, axis ) }; auto const turn { glm::dot( fastening.across, axis ) }; @@ -461,4 +644,128 @@ std::vector check_fastenings() { return result; } +std::vector check_rail_seats() { + + // the check rails, by points along their middle + struct check_line { + check_rail_gap gap; + std::vector points; + glm::dvec2 low { std::numeric_limits::max() }; + glm::dvec2 high { std::numeric_limits::lowest() }; + }; + std::vector lines; + for( auto *track : simulation::Paths.sequence() ) { + if( track == nullptr || track->m_editorremoved || false == bedded( *track ) || false == track_rails::cut_rails( *track ) ) { + continue; + } + auto const size { track->rail_dimensions() }; + auto const middle { 0.5 * std::abs( track->fTrackWidth ) + size.web }; + auto const &checks { track_rails::rail_checks( *track ) }; + for( int path = 0; path < path_count( *track ); ++path ) { + auto const *segment { path_segment( *track, path ) }; + if( segment == nullptr ) { + continue; + } + auto const length { segment->GetLength() }; + for( auto const side : { -1, 1 } ) { + for( auto const &check : checks[ path ][ side > 0 ? 1 : 0 ] ) { + check_line line { { track->name(), path, side, check.from, check.to } }; + auto const from { std::max( 0.0, check.from ) }; + auto const to { std::min( length, check.to ) }; + auto const count { static_cast( std::ceil( ( to - from ) / fastening_step ) ) }; + for( int step = 0; step <= count && to > from; ++step ) { + glm::dvec3 point, across; + segment->plan_frame( from + ( to - from ) * step / std::max( 1, count ), point, across ); + auto const flat { plan( across ) }; + if( glm::length( flat ) < 1e-9 ) { + continue; + } + auto const inward { glm::normalize( flat ) * static_cast( -side ) }; + auto const position { plan( point ) - inward * ( middle - check_seats ) }; + line.points.push_back( position ); + line.low = glm::min( line.low, position ); + line.high = glm::max( line.high, position ); + } + if( line.points.size() > 1 ) { + lines.push_back( std::move( line ) ); + } + } + } + } + } + // where each timber with the fastenings crosses a check rail, a seat of a fastening of the timber there + std::vector timbers( lines.size(), 0 ); + for( auto *track : simulation::Paths.sequence() ) { + if( track == nullptr || track->m_editorremoved || false == track->m_sleepers3d.active ) { + continue; + } + auto const *model { timber_model( *track ) }; + if( model == nullptr || false == model->fastened() ) { + continue; + } + std::optional rails; + for( auto const &beam : place( *track ) ) { + if( beam.plain ) { + continue; + } + if( false == rails.has_value() ) { + rails.emplace( *track ); + } + auto const start { plan( beam.from ) }; + auto const span { plan( beam.to - beam.from ) }; + auto const low { glm::min( start, start + span ) }; + auto const high { glm::max( start, start + span ) }; + std::vector seats; + auto const fastenings { rails->on( beam, *model ) }; + auto const under = [&]( glm::dvec2 const &Point ) { + return std::any_of( fastenings.begin(), fastenings.end(), [&]( fastening const &Fastening ) { + auto const along { glm::dot( Point - plan( Fastening.point ), Fastening.across ) }; + auto const a { Fastening.placed( model->spans[ Fastening.part ].x, Fastening.scale ) }; + auto const b { Fastening.placed( model->spans[ Fastening.part ].y, Fastening.scale ) }; + return along >= std::min( a, b ) && along <= std::max( a, b ); } ); }; + for( auto const &fastening : fastenings ) { + seats.push_back( plan( fastening.point ) + fastening.across * fastening.placed( 0.0, fastening.scale ) ); + if( fastening.part == part_check ) { + seats.push_back( plan( fastening.point ) + fastening.across * fastening.placed( check_seats, fastening.scale ) ); + } + } + for( std::size_t index = 0; index < lines.size(); ++index ) { + auto &line { lines[ index ] }; + if( line.high.x < low.x || line.low.x > high.x || line.high.y < low.y || line.low.y > high.y ) { + continue; + } + for( std::size_t point = 1; point < line.points.size(); ++point ) { + auto const chord { line.points[ point ] - line.points[ point - 1 ] }; + double u, v; + if( false == track_rails::meet( start, span, line.points[ point - 1 ], chord, 1e-12, u, v ) || u < 0.0 || u > 1.0 || v < 0.0 || v >= 1.0 ) { + continue; + } + auto const at { start + span * u }; + ++timbers[ index ]; + auto const seated { std::any_of( seats.begin(), seats.end(), [&]( glm::dvec2 const &Seat ) { + return glm::length( Seat - at ) < 0.03; } ) }; + if( false == seated ) { + if( line.gap.bare == 0.0 ) { + line.gap.at = at; + } + line.gap.bare += 1.0; + if( false == under( at ) ) { + ++line.gap.unsupported; + } + } + break; + } + } + } + } + std::vector result; + for( std::size_t index = 0; index < lines.size(); ++index ) { + if( lines[ index ].gap.bare > 0.0 ) { + lines[ index ].gap.timbers = timbers[ index ]; + result.push_back( lines[ index ].gap ); + } + } + return result; +} + } // namespace track_sleepers diff --git a/world/SleeperFastenings.h b/world/SleeperFastenings.h index c4f36a82..98a52fc4 100644 --- a/world/SleeperFastenings.h +++ b/world/SleeperFastenings.h @@ -27,9 +27,10 @@ struct fastening { int part { part_plate }; glm::dvec3 point { 0.0 }; // under the middle of the rail, on the top of the timber glm::dvec2 across { 1.0, 0.0 }; // unit vector in plan square to the rail, the way the +x of the part goes - float scale { 1.f }; // along its x, a plate or a slide plate narrowed where it'd lie over the fastenings of rails close beside + float scale { 1.f }; // along its x, a plate or a slide plate narrowed where it'd lie over the fastenings of rails close beside, a plate stretched under two float pivot { 0.f }; // the x of the part kept in place as it's narrowed float shift { 0.f }; // along its x, how far the pivot is moved + bool both { false }; // the fastening of two rails close beside each other, neither with a check rail of its own seated // where an x of the part lands along its across, at the scale double placed( double const X, double const Scale ) const { return pivot + shift + Scale * ( X - pivot ); } @@ -42,6 +43,8 @@ public: explicit fastened_rails( TTrack &Track ); // the fastenings of the rails on the timber, of the parts the model has std::vector on( beam const &Beam, beam_model const &Model ) const; + // where the rails on the timber lie over none of the fastenings + std::vector bare( beam const &Beam, beam_model const &Model, std::vector const &Fastenings ) const; private: // what a rail is where it lies on a timber @@ -77,8 +80,10 @@ private: double foot { 0.0 }; }; - // the fastenings of rails close by fitted to the room between them: narrowed across, and a rail lying on the fastening of the - // rail beside it left without one of its own + // the rails lying on the timber, in the order along it, one where several lie on one another, but the blades + std::vector rails_on( beam const &Beam ) const; + // the fastenings of rails close by fitted to the room between them: narrowed across, a plate cut short at the fastening of the + // rail beside it or left out, the plates of two rails lying over each other made one static void fit( std::vector &Fastenings, beam const &Beam, beam_model const &Model ); std::vector m_rails; @@ -90,6 +95,7 @@ struct fastening_row { std::size_t beams { 0 }; std::array parts {}; std::size_t overlaps { 0 }; // fastenings lying over one another on a timber, along it + std::vector bares; // where rails lie on a timber over no fastening double worst { 0.0 }; glm::dvec3 worstat { 0.0 }; std::string worstparts; @@ -104,4 +110,19 @@ struct fastening_row { // those of all the tracks laying timbers with fastenings std::vector check_fastenings(); +// a check rail of a track with timbers under it and no fastening seating it there, along some of its length +struct check_rail_gap { + std::string track; + int path { 0 }; + int side { 1 }; + double from { 0.0 }; // the check rail, along the path + double to { 0.0 }; + std::size_t timbers { 0 }; // with the fastenings, under it + double bare { 0.0 }; // of those, with no seat under it + std::size_t unsupported { 0 }; // of those, with no fastening under it at all + glm::dvec2 at { 0.0 }; // where the first of those crosses it +}; +// those of all the tracks with fastenings in the model of their timbers +std::vector check_rail_seats(); + } // namespace track_sleepers diff --git a/world/SleeperRules.h b/world/SleeperRules.h index d4573e9d..b1e3062b 100644 --- a/world/SleeperRules.h +++ b/world/SleeperRules.h @@ -62,6 +62,8 @@ double constexpr check_seats { 0.113 }; // the room left between the fastenings of rails close by, and how narrow their plates get to keep it double constexpr fastening_gap { 0.01 }; double constexpr plate_least { 0.6 }; +// a plate cut short at the fastening of the rail beside, as on one plate with it, no narrower than this +double constexpr clip_least { 0.4 }; double constexpr slide_least { 0.25 }; // a fastening of two rails narrowed no more than this, the rails lying a little off its seats then double constexpr check_least { 0.8 }; From f7821eea9936846cf3acb229f141bdcdb0fd5c88 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 19:24:47 +0200 Subject: [PATCH 05/19] fix(sleepers): the wing rails on the timbers as they're bent out: the rails of a track taken in their parts between the cuts at the crossings, as they're laid, a part going on over the gap at an obtuse crossing where its check rail lies; three rails or more close beside held by the fastening of two rails covering the most of them first, then those before and after it, one holding more rails than its seats not narrowed; a rail lying on the brace of a chair beside takes the brace away; a plate cut short at the fastening beside from where its narrowing left it --- world/SleeperFastenings.cpp | 183 ++++++++++++++++++++++-------------- world/SleeperFastenings.h | 1 + world/SleeperRules.h | 2 + world/Track.cpp | 5 + world/Track.h | 1 + 5 files changed, 120 insertions(+), 72 deletions(-) diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp index b18ac69b..b31318ad 100644 --- a/world/SleeperFastenings.cpp +++ b/world/SleeperFastenings.cpp @@ -18,6 +18,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include +#include #include #include @@ -57,50 +58,67 @@ fastened_rails::fastened_rails( TTrack &Track ) { auto const bladestretch = [&]( int const Path ) { auto const &shape { track->blade( Path ) }; return glm::dvec2 { shape.valid ? shape.tip : 0.0, track_rails::blade_length( *track, Path ) }; }; + // what the rail is along the path: a blade, the stock rail of a blade, checked + auto const add = [&]( rail &&Line, int const Path, int const Side ) { + if( switched ) { + if( Side == bladeside( Path ) ) { + Line.blade = { 0.0, bladestretch( Path ).y }; + } + if( Side == bladeside( 1 - Path ) ) { + auto const stretch { bladestretch( 1 - Path ) }; + Line.stock = { std::max( 0.0, stretch.x - chair_lead ), stretch.y }; + } + } + if( cut ) { + for( auto const &check : track_rails::rail_checks( *track )[ Path ][ Side > 0 ? 1 : 0 ] ) { + Line.checks.emplace_back( check.from, check.to ); + } + } + m_rails.emplace_back( std::move( Line ) ); }; for( int path = 0; path < path_count( *track ); ++path ) { auto const *segment { path_segment( *track, path ) }; if( segment == nullptr ) { continue; } auto const length { segment->GetLength() }; - auto const count { std::max( 1, static_cast( std::ceil( length / fastening_step ) ) ) }; for( auto const side : { -1, 1 } ) { - rail line; - line.track = track; - line.path = path; - line.side = side; - line.foot = size.web - size.low; - line.low = glm::dvec2 { std::numeric_limits::max() }; - line.high = glm::dvec2 { std::numeric_limits::lowest() }; - for( int step = 0; step <= count; ++step ) { - auto const distance { length * step / count }; - glm::dvec3 point, across; - segment->plan_frame( distance, point, across ); - auto const flat { plan( across ) }; - auto const inward { glm::length( flat ) > 1e-9 ? glm::normalize( flat ) * static_cast( -side ) : glm::dvec2 { 1.0, 0.0 } }; - auto const position { plan( point ) - inward * middle }; - line.points.push_back( position ); - line.heights.push_back( point.y ); - line.inward.push_back( inward ); - line.distances.push_back( distance ); - line.low = glm::min( line.low, position ); - line.high = glm::max( line.high, position ); - } - if( switched ) { - if( side == bladeside( path ) ) { - line.blade = { 0.0, bladestretch( path ).y }; + // the rail in its parts between the cuts at the crossings, as they're laid: a wing rail bent out from the path; on over the + // gap at an obtuse crossing, where its check rail lies + auto cuts { cut ? track->m_railplans[ path ][ side > 0 ].cuts : std::vector {} }; + std::stable_sort( cuts.begin(), cuts.end(), []( track_rails::rail_cut const &A, track_rails::rail_cut const &B ) { return A.at < B.at; } ); + for( std::size_t part = 0; part <= cuts.size(); ++part ) { + auto const *before { part > 0 ? &cuts[ part - 1 ] : nullptr }; + auto const *after { part < cuts.size() ? &cuts[ part ] : nullptr }; + auto const from { std::max( 0.0, before != nullptr ? before->start : 0.0 ) }; + auto const to { std::min( length, after != nullptr ? std::max( after->end, after->start ) : length ) }; + if( to - from < fastening_merge ) { + continue; } - if( side == bladeside( 1 - path ) ) { - auto const stretch { bladestretch( 1 - path ) }; - line.stock = { std::max( 0.0, stretch.x - chair_lead ), stretch.y }; + auto const offsets { knots::sum( before != nullptr ? before->startoffsets : knots::line {}, after != nullptr ? after->endoffsets : knots::line {} ) }; + auto const count { std::max( 1, static_cast( std::ceil( ( to - from ) / fastening_step ) ) ) }; + rail line; + line.track = track; + line.path = path; + line.side = side; + line.foot = size.web - size.low; + line.low = glm::dvec2 { std::numeric_limits::max() }; + line.high = glm::dvec2 { std::numeric_limits::lowest() }; + for( int step = 0; step <= count; ++step ) { + auto const distance { from + ( to - from ) * step / count }; + glm::dvec3 point, across; + segment->plan_frame( distance, point, across ); + auto const flat { plan( across ) }; + auto const inward { glm::length( flat ) > 1e-9 ? glm::normalize( flat ) * static_cast( -side ) : glm::dvec2 { 1.0, 0.0 } }; + auto const position { plan( point ) - inward * ( middle + knots::sample( offsets, distance, 0.0 ) ) }; + line.points.push_back( position ); + line.heights.push_back( point.y ); + line.inward.push_back( inward ); + line.distances.push_back( distance ); + line.low = glm::min( line.low, position ); + line.high = glm::max( line.high, position ); } + add( std::move( line ), path, side ); } - if( cut ) { - for( auto const &check : track_rails::rail_checks( *track )[ path ][ side > 0 ? 1 : 0 ] ) { - line.checks.emplace_back( check.from, check.to ); - } - } - m_rails.emplace_back( std::move( line ) ); } } } @@ -189,47 +207,54 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M return Beam.from + span * ( At / length ); }; auto const has = [&]( int const Part ) { return Model.parts[ Part ] != nullptr; }; - // the rails from the first to the last under fastenings of two rails in turn, each with its seats under two rails side by side - // and over as many of the rails after them as it reaches - auto const crowd = [&]( std::size_t First, std::size_t const Last ) { + // the rails from the first to the last under fastenings of two rails, each with its seats under two rails side by side and over as + // many of the rails around as it reaches: the one holding the most first, the rails before and after it in turn + std::function crowd = [&]( std::size_t const First, std::size_t const Last ) { auto const &reach { Model.spans[ part_check ] }; - while( First <= Last ) { - std::size_t covered { 0 }; - auto origin { 0.0 }; - auto turn { 1.0 }; - for( auto seat { First }; seat < Last; ++seat ) { - for( auto const direction : { 1.0, -1.0 } ) { - auto const at { 0.5 * ( rails[ seat ].at + rails[ seat + 1 ].at - direction * check_seats ) }; - auto const from { at + std::min( direction * reach.x, direction * reach.y ) }; - auto const to { at + std::max( direction * reach.x, direction * reach.y ) }; - if( rails[ First ].at < from ) { - continue; - } - std::size_t count { 0 }; - while( First + count <= Last && rails[ First + count ].at <= to ) { - ++count; - } - if( count > covered ) { - covered = count; - origin = at; - turn = direction; - } + std::size_t covered { 0 }; + std::size_t from { First }; + auto origin { 0.0 }; + auto turn { 1.0 }; + for( auto seat { First }; seat < Last; ++seat ) { + for( auto const direction : { 1.0, -1.0 } ) { + auto const at { 0.5 * ( rails[ seat ].at + rails[ seat + 1 ].at - direction * check_seats ) }; + auto const low { at + std::min( direction * reach.x, direction * reach.y ) }; + auto const high { at + std::max( direction * reach.x, direction * reach.y ) }; + auto start { seat }; + while( start > First && rails[ start - 1 ].at >= low ) { + --start; + } + auto end { seat + 1 }; + while( end < Last && rails[ end + 1 ].at <= high ) { + ++end; + } + if( end - start + 1 > covered ) { + covered = end - start + 1; + from = start; + origin = at; + turn = direction; } } - if( covered < 2 ) { - result.push_back( { part_plate, point( rails[ First ].at ), rails[ First ].inward } ); - ++First; - continue; + } + if( covered < 2 ) { + for( auto rail { First }; rail <= Last; ++rail ) { + result.push_back( { part_plate, point( rails[ rail ].at ), rails[ rail ].inward } ); } - result.push_back( { part_check, point( origin ), axis * turn, 1.f, static_cast( 0.5 * check_seats ), 0.f, true } ); - First += covered; + return; + } + result.push_back( { part_check, point( origin ), axis * turn, 1.f, static_cast( 0.5 * check_seats ), 0.f, true, covered > 2 } ); + if( from > First ) { + crowd( First, from - 1 ); + } + if( from + covered <= Last ) { + crowd( from + covered, Last ); } }; for( std::size_t index = 0; index < rails.size(); ++index ) { auto const &rail { rails[ index ] }; // three rails or more close beside one another, as at the crossings of the slips, none a stock rail or with a check rail auto last { index }; - while( last + 1 < rails.size() && rails[ last + 1 ].at - rails[ last ].at < fastening_pair && rails[ last ].kind == role::plain && rails[ last + 1 ].kind == role::plain ) { + while( last + 1 < rails.size() && rails[ last + 1 ].at - rails[ last ].at < fastening_crowd && rails[ last ].kind == role::plain && rails[ last + 1 ].kind == role::plain ) { ++last; } if( last >= index + 2 && has( part_check ) ) { @@ -338,7 +363,7 @@ void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, switch( Fastening.part ) { case part_plate: { return plate_least; } case part_slide: { return slide_least; } - case part_check: { return check_least; } + case part_check: { return Fastening.whole ? 1.0 : check_least; } default: { return 1.0; } } }; // those of one rail together, the rails in the order along the timber @@ -451,8 +476,8 @@ void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, kept = index; continue; } - auto const firstreach { extent( first ) }; - auto const secondreach { extent( second ) }; + auto firstreach { extent( first ) }; + auto secondreach { extent( second ) }; auto const drop = [&]( std::pair const &Rail ) { for( auto member { Rail.first }; member < Rail.second; ++member ) { dropped[ order[ member ] ] = true; @@ -468,8 +493,13 @@ void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, auto const turn { glm::dot( fastening.across, axis ) }; auto const toward { turn * Toward > 0.0 ? span.y : span.x }; auto const away { turn * Toward > 0.0 ? span.x : span.y }; - auto const stays { fastening.placed( away, 1.0 ) }; - auto const scale { ( Toward * turn * Room - stays ) / ( toward - away ) }; + // the other end where the narrowing left it, or as the plate was if that leaves too little of it + auto stays { fastening.placed( away, fastening.scale ) }; + auto scale { ( Toward * turn * Room - stays ) / ( toward - away ) }; + if( scale < clip_least ) { + stays = fastening.placed( away, 1.0 ); + scale = ( Toward * turn * Room - stays ) / ( toward - away ); + } if( scale < clip_least ) { return false; } @@ -489,15 +519,24 @@ void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, } return Rail; }; // the brace of a chair where the rail beside comes too close for it, its slide plate kept - auto const brace = [&]( std::pair const &Rail, int const Side ) { + auto const brace = [&]( std::pair const &Rail, int const Side, double const Reaching = fastening_gap ) { for( auto member { Rail.first }; member < Rail.second; ++member ) { auto const &fastening { Fastenings[ order[ member ] ] }; - if( fastening.part == part_chair && reach( fastening, fastening.scale )[ Side ] > fastening_gap ) { + if( fastening.part == part_chair && false == dropped[ order[ member ] ] && reach( fastening, fastening.scale )[ Side ] > Reaching ) { dropped[ order[ member ] ] = true; return true; } } return false; }; + // a rail lying on the brace of a chair beside takes the brace away + if( brace( first, 1, apart ) || brace( second, 0, apart ) ) { + if( false == overlaps() ) { + kept = index; + continue; + } + firstreach = extent( first ); + secondreach = extent( second ); + } // of two rails with check rails, the one with its check rail toward the other one, lying on the fastening of the other // one then, goes in a plate cut short at that fastening, or without its own, the other one keeping its check rail seated if( checks( first ) && checks( second ) && faces( first, 1.0 ) != faces( second, -1.0 ) ) { diff --git a/world/SleeperFastenings.h b/world/SleeperFastenings.h index 98a52fc4..9ad6ea24 100644 --- a/world/SleeperFastenings.h +++ b/world/SleeperFastenings.h @@ -31,6 +31,7 @@ struct fastening { float pivot { 0.f }; // the x of the part kept in place as it's narrowed float shift { 0.f }; // along its x, how far the pivot is moved bool both { false }; // the fastening of two rails close beside each other, neither with a check rail of its own seated + bool whole { false }; // holding more rails than its seats, out to its ends: not narrowed // where an x of the part lands along its across, at the scale double placed( double const X, double const Scale ) const { return pivot + shift + Scale * ( X - pivot ); } diff --git a/world/SleeperRules.h b/world/SleeperRules.h index b1e3062b..6b823438 100644 --- a/world/SleeperRules.h +++ b/world/SleeperRules.h @@ -57,6 +57,8 @@ double constexpr fastening_merge { 0.03 }; // one, as the legs of the nose where they're joined, in one plate between them double constexpr fastening_pair { 0.2 }; double constexpr fastening_nose { 0.05 }; +// three rails or more with less than this between each and the next, as the legs of a nose with the wing rails beside, are held together +double constexpr fastening_crowd { 0.3 }; // how far apart the rails lie in the fastening of both, the check part of the model double constexpr check_seats { 0.113 }; // the room left between the fastenings of rails close by, and how narrow their plates get to keep it diff --git a/world/Track.cpp b/world/Track.cpp index e0d0ac12..9ca80383 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -1595,6 +1595,11 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { auto const layout { plan_rails( Bank != 0 ) }; if( Bank != 0 ) { m_crossings = layout.crossed; + for( int path = 0; path < 2; ++path ) { + for( int side = 0; side < 2; ++side ) { + m_railplans[ path ][ side ] = layout.plans[ path ][ side ]; + } + } } auto const lay_rail = [&]( int const Path, gfx::vertex_array const &Profile, int const Side, float const Texturelength, double const From, bool const Openstart, bool const Openend, geometryhandle_sequence &Geometry, geometryhandle_sequence &Cuts ) { return this->lay_rail( layout, drawing, Path, Profile, Side, Texturelength, From, Openstart, Openend, Geometry, Cuts ); }; diff --git a/world/Track.h b/world/Track.h index 5c491758..9d18f06c 100644 --- a/world/Track.h +++ b/world/Track.h @@ -217,6 +217,7 @@ public: std::vector m_paths; // source data for owned paths bool m_editorremoved { false }; std::vector m_crossings; // tracks whose rails cross the rails of this one, found when its geometry was made + track_rails::rail_plan m_railplans[ 2 ][ 2 ]; // the cuts of its rails, by path and right side, as its geometry was made scene::basic_node *m_road { nullptr }; // road or road junction which generated this path. such path is one-way, and isn't saved nor edited on its own std::array m_routeweights { 0.f, 0.f, 0.f }; // odds of each route through a crossroads being picked by a driver, by the number of the route. all zero: even odds int iterate_stamp = 0; From 9478f69203c0fab598016f0c808dc5d8a6e2c356 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 19:24:47 +0200 Subject: [PATCH 06/19] feat(rails): the feet of two rails face to face across a flangeway, as a wing rail and the leg of the nose beside it or the two wing rails at the throat, planed off where they'd lie on each other, meeting in the middle of the flangeway as those of two rails back to back do; the heads left whole --- world/RailCarve.cpp | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/world/RailCarve.cpp b/world/RailCarve.cpp index 59c5a392..bdd1c94c 100644 --- a/world/RailCarve.cpp +++ b/world/RailCarve.cpp @@ -671,6 +671,7 @@ void trim( carve_rail &Rail, carve_part &Part ) { struct body_hit { bool in { false }; bool heads { false }; // or only the feet of two rails back to back + bool faces { false }; // the feet of two rails face to face across a flangeway, ourfoot and theirfoot how far in they reach double removed[ 2 ] {}; glm::dvec2 ours { 0.0 }; glm::dvec2 theirs { 0.0 }; @@ -693,7 +694,8 @@ struct body_hit { // the bounds of ours where its head lies under theirs, Above, or over it: the two meet in the middle of where they lie over each other, as far as // the wheels of each leave room; a head giving way runs on past the meeting over the rounded shoulder of the other one, facing it with its outer edge. -// Two rails back to back meet with their feet in the middle between them too, and the wheels of theirs roll on ours planed flush against it +// Two rails back to back meet with their feet in the middle between them too, and the wheels of theirs roll on ours planed flush against it; +// two face to face, as a wing rail and the leg of the nose, with their feet in the middle of the flangeway, the heads left whole void settle( body_hit const &Hit, bool const Above, double &Upper, double &Lower, double &Worn ) { auto const slant { std::max( std::abs( Hit.facing ), cos_wide ) }; @@ -713,6 +715,11 @@ void settle( body_hit const &Hit, bool const Above, double &Upper, double &Lower // as far from both bounds, whichever way they run: the same line from either rail auto const between = []( bound const &A, bound const &B ) { return bound { ( A.at * A.slant + B.at * B.slant ) / ( A.slant + B.slant ), 1.0 }; }; + if( Hit.faces ) { + auto const feet { between( { Hit.ourfoot, Hit.footslant.x }, { Hit.theirfoot, Hit.footslant.y } ).at }; + Lower = std::max( Lower, std::min( feet + slack, Hit.ours.x - slack ) ); + return; + } auto const a { higher( { Hit.ours.x, Hit.ourslant.x }, { Hit.theirs.x, Hit.theirslant.x } ) }; auto const b { lower( { Hit.ours.y, Hit.ourslant.y }, { Hit.theirs.y, Hit.theirslant.y } ) }; auto const overlap { Hit.heads && b.at > a.at }; @@ -860,6 +867,7 @@ carved_lines carve( carve_rail &Rail, carve_part &Part ) { // how the bounds of ours run, once something of theirs is in their way glm::dvec2 ourslant { 1.0 }; auto footslant { 1.0 }; + auto innerslant { 1.0 }; auto slanted { false }; if( theirs.track == Rail.track && theirs.side == Rail.side && distance < theirs.heel + heel_margin ) { continue; @@ -870,18 +878,19 @@ carved_lines carve( carve_rail &Rail, carve_part &Part ) { continue; } trim( theirs, part ); - // the head of theirs and how far out its profile reaches, out from its gauge line: the bound Which (from, to, out to) - // and how it runs on + // the head of theirs and how far its profile reaches, out from its gauge line: the bound Which (from, to, out to, the + // crown, in to) and how it runs on auto const trimmed { distance >= part.trimfrom && distance <= part.trimto }; auto const theirclearup { theirsize.high + 0.01 }; auto const theirclearlow { theirsize.low - 0.01 }; auto const bound = [&]( int const Which ) { return [&, Which]( double const At, bool &Bounding ) { auto const offset { knots::sample( part.carve, At, 0.0 ) }; - if( Which == 0 ) { + if( Which == 0 || Which == 4 ) { + auto const reach { Which == 0 ? 0.0 : theirsize.low }; auto const theirlow { trimmed ? knots::sample( part.lower, At, theirclearlow ) : theirclearlow }; - Bounding = theirlow > 0.0; - return offset + std::max( 0.0, theirlow ); + Bounding = theirlow > reach; + return offset + std::max( reach, theirlow ); } auto const reach { Which == 1 ? theirsize.head : Which == 2 ? theirsize.high : theirsize.head - theirsize.shoulder }; auto const theirup { trimmed ? knots::sample( part.upper, At, theirclearup ) : theirclearup }; @@ -905,16 +914,18 @@ carved_lines carve( carve_rail &Rail, carve_part &Part ) { glm::dvec2 const heads { std::min( m0, m1 ), std::max( m0, m1 ) }; auto const overlap { std::min( ours.y, heads.y ) - std::max( ours.x, heads.x ) }; auto const above { heads.x + heads.y > ours.x + ours.y }; - auto const ourfoot { here.offset + std::min( size.high, up ) }; auto const backs { facing < 0.0 && above }; - auto const theirfoot { ( overlap > 2.0 * slack || backs ) ? meets( bound( 2 ) ) : 0.0 }; - auto const feet { backs && ourfoot > theirfoot + 2.0 * slack }; + auto const faces { facing < 0.0 && false == above && overlap <= 2.0 * slack }; + auto const ourfoot { faces ? here.offset + std::max( size.low, low ) : here.offset + std::min( size.high, up ) }; + auto const theirfoot { faces ? meets( bound( 4 ) ) : ( overlap > 2.0 * slack || backs ) ? meets( bound( 2 ) ) : 0.0 }; + auto const feet { faces ? theirfoot > ourfoot + 2.0 * slack : backs && ourfoot > theirfoot + 2.0 * slack }; if( overlap <= 2.0 * slack && false == feet ) { continue; } if( false == slanted ) { ourslant = { square( run_of( ourbound( Part.lower, clearlow, 0.0, false ), s ) ), square( run_of( ourbound( Part.upper, clearup, size.head, true ), s ) ) }; footslant = square( run_of( ourbound( Part.upper, clearup, size.high, true ), s ) ); + innerslant = square( run_of( ourbound( Part.lower, clearlow, size.low, false ), s ) ); slanted = true; } auto &hit { hits[ j ][ static_cast( k - a ) ] }; @@ -927,7 +938,8 @@ carved_lines carve( carve_rail &Rail, carve_part &Part ) { hit.theirshoulder = theirsize.shoulder; hit.ourfoot = ourfoot; hit.theirfoot = theirfoot; - hit.footslant = { footslant, slant( bound( 2 ), theirfoot ) }; + hit.footslant = ( faces ? glm::dvec2 { innerslant, slant( bound( 4 ), theirfoot ) } : glm::dvec2 { footslant, slant( bound( 2 ), theirfoot ) } ); + hit.faces = faces; hit.ourcrown = here.offset + std::min( size.head - size.shoulder, up ); hit.theircrown = meets( bound( 3 ) ); hit.heads = ( overlap > 2.0 * slack ); @@ -939,7 +951,7 @@ carved_lines carve( carve_rail &Rail, carve_part &Part ) { hit.removed[ 1 ] = b - ours.x; } else { - hit.removed[ 0 ] = hit.ourfoot - hit.theirfoot; + hit.removed[ 0 ] = std::abs( hit.ourfoot - hit.theirfoot ); hit.removed[ 1 ] = unbounded; } if( false == here.runon ) { From 153cdc550c142325162668b81600321a29f450ae Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 19:39:07 +0200 Subject: [PATCH 07/19] fix(sleepers): the fastenings of a rail part between two carved cuts where the part is laid, not twice as far out: both cuts carry its carved lines, taken once as the rails are laid, by one rule for both --- world/RailLayer.cpp | 9 ++------- world/RailLayout.h | 11 +++++++++++ world/SleeperFastenings.cpp | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp index 9ba6c119..4dfe7f3d 100644 --- a/world/RailLayer.cpp +++ b/world/RailLayer.cpp @@ -785,16 +785,12 @@ void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vector tread_brightness( A, m_track.tread_band() ) ? B : A ); }; - knots::line offsets, extents, inner; + knots::line 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 }; - if( own ) { - pieceoffsets.insert( pieceoffsets.end(), Cuts[ i ].endoffsets.begin(), Cuts[ i ].endoffsets.end() ); - } - knots::sort( pieceoffsets ); + auto const pieceoffsets { part_offsets( i > 0 ? &Cuts[ i - 1 ] : nullptr, i < Cuts.size() ? &Cuts[ i ] : nullptr ) }; // 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 }; @@ -861,7 +857,6 @@ void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vector cuts; }; +// how far out a part of a rail between two cuts is laid: as it goes on from the one and comes to the other, once where both are carved, which +// gives both its lines +inline knots::line part_offsets( rail_cut const *Before, rail_cut const *After ) { + + knots::line result { Before != nullptr ? Before->startoffsets : knots::line {} }; + if( After != nullptr && false == ( Before != nullptr && Before->carved && After->carved ) ) { + result.insert( result.end(), After->endoffsets.begin(), After->endoffsets.end() ); + } + knots::sort( result ); + return result; +} struct rail_tread { double from { 0.0 }; double to { 0.0 }; diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp index b31318ad..9e282efa 100644 --- a/world/SleeperFastenings.cpp +++ b/world/SleeperFastenings.cpp @@ -94,7 +94,7 @@ fastened_rails::fastened_rails( TTrack &Track ) { if( to - from < fastening_merge ) { continue; } - auto const offsets { knots::sum( before != nullptr ? before->startoffsets : knots::line {}, after != nullptr ? after->endoffsets : knots::line {} ) }; + auto const offsets { track_rails::part_offsets( before, after ) }; auto const count { std::max( 1, static_cast( std::ceil( ( to - from ) / fastening_step ) ) ) }; rail line; line.track = track; From d3aef502578fe8372c76679b3dccf026328e0e34 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 20:21:13 +0200 Subject: [PATCH 08/19] feat(rails): the wing rails of the frogs of the low section of the blades, as Zu1-60, where the timbers have slide chairs: from 1.5 m before the throat to their ends the foot raised as much as the blades ride, the web shortened, the heads level with the rails beside, forged down to the plain rail along 0.5 m; a wing rail which is the stock rail of a blade too, as in the slips with the blades outside of the diamond, whole along its chairs; the fastening holding only rails raised as much moved up on a pad as wide as it, a raised rail on a fastening with others on a pad of its own --- world/RailLayer.cpp | 14 ++++- world/RailLayout.h | 33 ++++++++++++ world/Segment.cpp | 13 ++--- world/Segment.h | 1 + world/SleeperFastenings.cpp | 105 ++++++++++++++++++++++++++++++++---- world/SleeperFastenings.h | 9 ++++ world/SleeperModels.cpp | 4 ++ world/SleeperRules.h | 3 ++ world/TrackSleepers.cpp | 21 ++++++-- world/TrackSleepers.h | 7 +++ 10 files changed, 189 insertions(+), 21 deletions(-) diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp index 4dfe7f3d..9192dd6f 100644 --- a/world/RailLayer.cpp +++ b/world/RailLayer.cpp @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "world/TrackRails.h" +#include "world/TrackSleepers.h" #include "world/TrackBlend.h" #include "rendering/renderer.h" @@ -381,6 +382,7 @@ struct rail_piece { bool walls { false }; float lift { 0.f }; float wallu { -1.f }; + knots::line rises; }; struct top_band { @@ -605,6 +607,8 @@ private: TSegment const &m_segment; double const m_length; double const m_guardoffset; + double const m_rise; // of the feet of the wing rails of the low section + glm::dvec2 const m_stock; // the stretch the rail is the stock rail of a blade along std::size_t m_topfirst; std::size_t m_toplast { 0 }; std::vector m_tints; @@ -626,6 +630,8 @@ TTrack::rail_layer::rail_layer( TTrack &Track, rail_layout const &Layout, rail_d m_segment { path_segment( Track, Path ) }, m_length { m_segment.GetLength() }, m_guardoffset { -( flangeway + Drawing.size.head ) }, + m_rise { track_sleepers::wing_rise( Track ) }, + m_stock { track_sleepers::stock_stretch( Track, Path, Side ) }, m_topfirst { Profile.size() } { // the running surface on top of the head is split along the edge of the wheel treads, the part of it out of their reach goes matte; the rest of the profile and the end faces are laid whole for( std::size_t j = 0; j < Profile.size(); ++j ) { @@ -791,6 +797,7 @@ void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vector 0 && Cuts[ i - 1 ].carved && Cuts[ i ].carved ) }; auto const pieceoffsets { part_offsets( i > 0 ? &Cuts[ i - 1 ] : nullptr, i < Cuts.size() ? &Cuts[ i ] : nullptr ) }; + auto const piecerises { part_rises( i > 0 ? &Cuts[ i - 1 ] : nullptr, i < Cuts.size() ? &Cuts[ i ] : nullptr, Wheels ? m_rise : 0.0, m_stock ) }; // 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 }; @@ -838,7 +845,8 @@ void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vectoredge.empty() && m_topfirst < m_toplast ) { treaded( whole, *tread, ends ); } @@ -1270,6 +1278,10 @@ loft_span TTrack::rail_layer::span( rail_piece const &Piece, piece_ends const &E result.rampwidth = ( Piece.ramp.empty() ? 0.0 : Piece.fade ); result.walls = Piece.walls; result.wallu = Piece.wallu; + result.rises = Piece.rises; + if( false == Piece.rises.empty() ) { + result.head = { m_drawing.size.neck, m_drawing.size.crest }; + } if( std::abs( Piece.from - Ends.from ) < 1e-6 ) { result.fromend = Ends.fromend; } diff --git a/world/RailLayout.h b/world/RailLayout.h index ff667b82..4b222fe5 100644 --- a/world/RailLayout.h +++ b/world/RailLayout.h @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include #include #include #include @@ -49,6 +50,8 @@ 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 +double constexpr wing_low { 1.5 }; // how far before the throat the wing rails are of the low section of the blades, raised on their timbers as the blades +double constexpr wing_forged { 0.5 }; // how long the end of a wing rail forged to the plain rail is, going down to it // where the inner rails of the two paths of a switch cross, found from their gauge lines behind the blades struct switch_frog { @@ -175,6 +178,36 @@ inline knots::line part_offsets( rail_cut const *Before, rail_cut const *After ) knots::sort( result ); return result; } +// how far the foot of a part of a rail between two cuts is raised, Rise along a wing rail of the low section: from wing_low before the throat on, +// down to the plain rail past it along its forged end; the rail whole along Stock, the stock rail of a blade there +inline knots::line part_rises( rail_cut const *Before, rail_cut const *After, double const Rise, glm::dvec2 const &Stock ) { + + knots::line result; + auto const going { Before != nullptr && Before->startkind == rail_end::point }; + auto const coming { After != nullptr && After->endkind == rail_end::point }; + if( Rise <= 0.0 || ( false == going && false == coming ) ) { + return result; + } + auto const lowto { going ? Before->startanchor + wing_low : 0.0 }; + auto const lowfrom { coming ? After->endanchor - wing_low : 0.0 }; + auto const through { going && coming && lowto + 2.0 * wing_forged >= lowfrom }; + auto const stock { Stock.y > Stock.x }; + // the share of the rise: whole along the low section, none along the stock rail, between along the forged ends + auto const share = [&]( double const At ) { + auto const low { through ? 1.0 : std::max( + going ? std::clamp( ( lowto + wing_forged - At ) / wing_forged, 0.0, 1.0 ) : 0.0, + coming ? std::clamp( ( At - lowfrom + wing_forged ) / wing_forged, 0.0, 1.0 ) : 0.0 ) }; + auto const whole { stock ? std::clamp( std::max( Stock.x - At, At - Stock.y ) / wing_forged, 0.0, 1.0 ) : 1.0 }; + return std::min( low, whole ); }; + std::array places { lowto, lowto + wing_forged, lowfrom - wing_forged, lowfrom, Stock.x - wing_forged, Stock.x, Stock.y, Stock.y + wing_forged }; + std::sort( places.begin(), places.end() ); + for( auto const at : places ) { + if( result.empty() || at > result.back().x + 1e-6 ) { + result.emplace_back( at, Rise * share( at ) ); + } + } + return result; +} struct rail_tread { double from { 0.0 }; double to { 0.0 }; diff --git a/world/Segment.cpp b/world/Segment.cpp index afb2f2bc..9a2dc6c2 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -717,7 +717,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig for( auto distance { std::ceil( Span.from / 0.5 ) * 0.5 }; distance < std::min( Span.to, m_dense ); distance += 0.5 ) { stations.push_back( distance ); } - for( auto const *line : { &Span.offsets, &Span.extents, &Span.inner, &Span.drops } ) { + for( auto const *line : { &Span.offsets, &Span.extents, &Span.inner, &Span.drops, &Span.rises } ) { for( auto const &knot : *line ) { if( knot.x > Span.from && knot.x < Span.to ) { stations.push_back( knot.x ); @@ -759,11 +759,11 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig for( auto const &point : ShapePoints ) { bottom = std::min( bottom, point.position.y ); } - auto const raised = [&]( float const Height ) { - if( Span.rise <= 0.0 || Height >= Span.head.x || Span.head.x <= bottom ) { + auto const raised = [&]( float const Height, double const Rise ) { + if( Rise <= 0.0 || Height >= Span.head.x || Span.head.x <= bottom ) { return Height; } - return static_cast( Height + Span.rise * ( Span.head.x - Height ) / ( Span.head.x - bottom ) ); }; + return static_cast( Height + Rise * ( Span.head.x - Height ) / ( Span.head.x - bottom ) ); }; auto const ring = [&]( double const Distance, double const Middle, std::vector &Ring, std::vector &Wear ) { glm::dvec3 point; @@ -797,6 +797,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig } auto const worn { knots::sample( Span.ramp, Distance, -1e6 ) }; auto const drop { knots::sample( Span.drops, Distance, 0.0 ) }; + auto const rise { Span.rises.empty() ? Span.rise : knots::sample( Span.rises, Distance, 0.0 ) }; static loft_end const unshaped; auto const &shaped { Distance <= Span.from ? Span.fromend : Distance >= Span.to ? Span.toend : unshaped }; auto const bend { shaped.bend }; @@ -830,7 +831,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig // 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( Span.side * ( Span.halfwidth + outside + offset ) ) + position }; - pt.y += raised( ShapePoints.front().position.y ) + Span.lift; + pt.y += raised( ShapePoints.front().position.y, rise ) + 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 ); @@ -909,7 +910,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig if( drop > 0.0 && shape.position.y > Span.head.x && Span.head.y > Span.head.x ) { shape.position.y -= static_cast( drop * ( shape.position.y - Span.head.x ) / ( Span.head.y - Span.head.x ) ); } - shape.position.y = raised( shape.position.y ); + shape.position.y = raised( shape.position.y, rise ); auto pt { parallel * static_cast( Span.side * ( Span.halfwidth + outside + offset ) ) + position + along * static_cast( outside + offset ) }; pt.y += shape.position.y + Span.lift; auto norm { shape.normal.x * parallel }; diff --git a/world/Segment.h b/world/Segment.h index 69e47e7f..26fb1c35 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -48,6 +48,7 @@ struct loft_span { knots::line drops; glm::dvec2 head { 0.0 }; double rise { 0.0 }; // how far the foot of the profile is raised, the web below the head (head.x) shortened by as much + knots::line rises; // along the span, in place of rise where given }; struct segment_data { diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp index 9e282efa..8e4f4e1b 100644 --- a/world/SleeperFastenings.cpp +++ b/world/SleeperFastenings.cpp @@ -52,22 +52,17 @@ fastened_rails::fastened_rails( TTrack &Track ) { auto const middle { 0.5 * std::abs( track->fTrackWidth ) + size.web }; auto const cut { track_rails::cut_rails( *track ) }; auto const switched { track->eType == tt_Switch && track->SwitchExtension != nullptr }; - // the side of the blade of the path, and the stretch along it from the tip to the heel + auto const rise { wing_rise( *track ) }; + // the side of the blade of the path auto const bladeside = [&]( int const Path ) { return ( track->SwitchExtension->RightSwitch == ( Path == 0 ) ? 1 : -1 ); }; - auto const bladestretch = [&]( int const Path ) { - auto const &shape { track->blade( Path ) }; - return glm::dvec2 { shape.valid ? shape.tip : 0.0, track_rails::blade_length( *track, Path ) }; }; // what the rail is along the path: a blade, the stock rail of a blade, checked auto const add = [&]( rail &&Line, int const Path, int const Side ) { if( switched ) { if( Side == bladeside( Path ) ) { - Line.blade = { 0.0, bladestretch( Path ).y }; - } - if( Side == bladeside( 1 - Path ) ) { - auto const stretch { bladestretch( 1 - Path ) }; - Line.stock = { std::max( 0.0, stretch.x - chair_lead ), stretch.y }; + Line.blade = { 0.0, track_rails::blade_length( *track, Path ) }; } + Line.stock = stock_stretch( *track, Path, Side ); } if( cut ) { for( auto const &check : track_rails::rail_checks( *track )[ Path ][ Side > 0 ? 1 : 0 ] ) { @@ -100,6 +95,7 @@ fastened_rails::fastened_rails( TTrack &Track ) { line.track = track; line.path = path; line.side = side; + line.rises = track_rails::part_rises( before, after, rise, stock_stretch( *track, path, side ) ); line.foot = size.web - size.low; line.low = glm::dvec2 { std::numeric_limits::max() }; line.high = glm::dvec2 { std::numeric_limits::lowest() }; @@ -159,11 +155,13 @@ std::vector fastened_rails::rails_on( beam const &Beam ) co if( std::abs( height - glm::mix( Beam.from.y, Beam.to.y, u ) ) > level_tolerance ) { continue; } + auto const distance { glm::mix( line.distances[ index - 1 ], line.distances[ index ], v ) }; hits.push_back( { u * length, - line.at( glm::mix( line.distances[ index - 1 ], line.distances[ index ], v ) ), + line.at( distance ), glm::normalize( glm::mix( line.inward[ index - 1 ], line.inward[ index ], v ) ), - line.foot } ); + line.foot, + knots::sample( line.rises, distance, 0.0 ) } ); } } std::sort( hits.begin(), hits.end(), []( hit const &Left, hit const &Right ) { return Left.at < Right.at; } ); @@ -180,11 +178,14 @@ std::vector fastened_rails::rails_on( beam const &Beam ) co for( std::size_t index = 0; index < hits.size(); ) { auto best { hits[ index ] }; auto next { index + 1 }; + auto rise { best.rise }; for( ; next < hits.size() && hits[ next ].at - hits[ next - 1 ].at < fastening_merge; ++next ) { if( rank( hits[ next ].kind ) > rank( best.kind ) ) { best = hits[ next ]; } + rise = std::max( rise, hits[ next ].rise ); } + best.rise = rise; if( best.kind != role::blade ) { rails.push_back( best ); } @@ -324,6 +325,7 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M } } fit( result, Beam, Model ); + lift( result, rails, Beam, Model ); return result; } @@ -618,6 +620,81 @@ void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, Fastenings = std::move( fitted ); } +void fastened_rails::lift( std::vector &Fastenings, std::vector const &Rails, beam const &Beam, beam_model const &Model ) { + + if( std::none_of( Rails.begin(), Rails.end(), []( hit const &Rail ) { return Rail.rise > 0.0; } ) ) { + return; + } + auto const start { plan( Beam.from ) }; + auto const axis { glm::normalize( plan( Beam.to - Beam.from ) ) }; + auto const padpart { Model.parts[ part_slide ] != nullptr ? part_slide : part_plate }; + auto const &padspan { Model.spans[ padpart ] }; + auto const &padheights { Model.heights[ padpart ] }; + // a pad on the timber under the fastening, along its across from Low to High, up from Bottom to Top in the model + auto const pad = [&]( fastening const &Over, double const Low, double const High, float const Bottom, float const Top ) { + fastening result { padpart, Over.point, Over.across }; + result.scale = static_cast( ( High - Low ) / ( padspan.y - padspan.x ) ); + result.shift = static_cast( Low - result.scale * padspan.x ); + result.lift = Bottom - padheights.x; + result.height = ( Top - Bottom ) / std::max( 0.001f, padheights.y - padheights.x ); + result.pad = true; + return result; }; + // where along the timber the fastening reaches, from and to + auto const footprint = [&]( fastening const &Fastening ) { + auto const at { glm::dot( plan( Fastening.point ) - start, axis ) }; + auto const turn { glm::dot( Fastening.across, axis ) }; + auto const &span { Model.spans[ Fastening.part ] }; + auto const a { at + turn * Fastening.placed( span.x, Fastening.scale ) }; + auto const b { at + turn * Fastening.placed( span.y, Fastening.scale ) }; + return glm::dvec2 { std::min( a, b ), std::max( a, b ) }; }; + auto const holds = [&]( fastening const &Fastening, hit const &Rail ) { + auto const reach { footprint( Fastening ) }; + return Fastening.part != part_chair && Rail.at >= reach.x && Rail.at <= reach.y; }; + std::vector pads; + std::vector lifted( Fastenings.size(), false ); + for( std::size_t index = 0; index < Rails.size(); ++index ) { + auto const &rail { Rails[ index ] }; + if( rail.rise < pad_least ) { + continue; + } + auto const under { std::find_if( Fastenings.begin(), Fastenings.end(), [&]( fastening const &Fastening ) { return holds( Fastening, rail ); } ) }; + if( under == Fastenings.end() ) { + continue; + } + auto const which { static_cast( under - Fastenings.begin() ) }; + if( lifted[ which ] ) { + continue; + } + auto const alike { std::all_of( Rails.begin(), Rails.end(), [&]( hit const &Other ) { + return false == holds( *under, Other ) || std::abs( Other.rise - rail.rise ) < pad_least; } ) }; + if( alike ) { + // the fastening holding only the rails raised as much, moved up as far on a pad as wide as it + auto const &span { Model.spans[ under->part ] }; + auto const a { under->placed( span.x, under->scale ) }; + auto const b { under->placed( span.y, under->scale ) }; + pads.push_back( pad( *under, std::min( a, b ), std::max( a, b ), Model.bed, Model.heights[ under->part ].x + static_cast( rail.rise ) ) ); + under->lift = static_cast( rail.rise ); + lifted[ which ] = true; + continue; + } + // the rail on a pad of its own on the fastening, as wide as its foot, short of the rails beside + auto low { rail.foot }; + auto high { rail.foot }; + if( index > 0 ) { + low = std::min( low, 0.5 * ( rail.at - Rails[ index - 1 ].at ) - fastening_gap ); + } + if( index + 1 < Rails.size() ) { + high = std::min( high, 0.5 * ( Rails[ index + 1 ].at - rail.at ) - fastening_gap ); + } + if( low + high < pad_narrowest ) { + continue; + } + fastening const at { padpart, Beam.from + ( Beam.to - Beam.from ) * ( rail.at / glm::length( plan( Beam.to - Beam.from ) ) ), axis }; + pads.push_back( pad( at, -low, high, Model.seat, Model.seat + static_cast( rail.rise ) ) ); + } + Fastenings.insert( Fastenings.end(), pads.begin(), pads.end() ); +} + std::vector check_fastenings() { std::vector result; @@ -654,6 +731,9 @@ std::vector check_fastenings() { row.bares.push_back( at ); } for( auto const &fastening : fastenings ) { + if( fastening.pad ) { + continue; + } ++row.parts[ fastening.part ]; auto const at { glm::dot( plan( fastening.point ) - start, axis ) }; auto const turn { glm::dot( fastening.across, axis ) }; @@ -763,6 +843,9 @@ std::vector check_rail_seats() { auto const b { Fastening.placed( model->spans[ Fastening.part ].y, Fastening.scale ) }; return along >= std::min( a, b ) && along <= std::max( a, b ); } ); }; for( auto const &fastening : fastenings ) { + if( fastening.pad ) { + continue; + } seats.push_back( plan( fastening.point ) + fastening.across * fastening.placed( 0.0, fastening.scale ) ); if( fastening.part == part_check ) { seats.push_back( plan( fastening.point ) + fastening.across * fastening.placed( check_seats, fastening.scale ) ); diff --git a/world/SleeperFastenings.h b/world/SleeperFastenings.h index 9ad6ea24..59ebf641 100644 --- a/world/SleeperFastenings.h +++ b/world/SleeperFastenings.h @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include +#include "world/Knots.h" #include "world/TrackSleepers.h" // the fastenings of the rails on the timbers: where a rail lies on a timber, the part of the model of the timbers which holds it @@ -32,6 +33,9 @@ struct fastening { float shift { 0.f }; // along its x, how far the pivot is moved bool both { false }; // the fastening of two rails close beside each other, neither with a check rail of its own seated bool whole { false }; // holding more rails than its seats, out to its ends: not narrowed + float lift { 0.f }; // how far up it's moved, under a rail of the low section raised as a blade + float height { 1.f }; // along its y, from the bottom of the part: a pad stretched to fill under a raised rail + bool pad { false }; // under a raised rail or its fastening, not holding a rail itself // where an x of the part lands along its across, at the scale double placed( double const X, double const Scale ) const { return pivot + shift + Scale * ( X - pivot ); } @@ -60,6 +64,7 @@ private: TTrack *track { nullptr }; int path { 0 }; int side { 1 }; + knots::line rises; // of its foot, along the path, a wing rail of the low section raised as the blades std::vector points; std::vector heights; std::vector inward; // unit vectors in plan from the rail toward the middle of its path @@ -79,6 +84,7 @@ private: role kind { role::plain }; glm::dvec2 inward { 1.0, 0.0 }; double foot { 0.0 }; + double rise { 0.0 }; }; // the rails lying on the timber, in the order along it, one where several lie on one another, but the blades @@ -86,6 +92,9 @@ private: // the fastenings of rails close by fitted to the room between them: narrowed across, a plate cut short at the fastening of the // rail beside it or left out, the plates of two rails lying over each other made one static void fit( std::vector &Fastenings, beam const &Beam, beam_model const &Model ); + // the fastenings under the raised rails: one holding only rails raised as much moved up as far on a pad, a rail raised + // over a fastening with others on a pad of its own on it + static void lift( std::vector &Fastenings, std::vector const &Rails, beam const &Beam, beam_model const &Model ); std::vector m_rails; }; diff --git a/world/SleeperModels.cpp b/world/SleeperModels.cpp index 1dc13567..003dab2c 100644 --- a/world/SleeperModels.cpp +++ b/world/SleeperModels.cpp @@ -122,13 +122,16 @@ beam_model const *find_model( std::string const &Name, std::string const &Skinna continue; } glm::vec2 span { std::numeric_limits::max(), std::numeric_limits::lowest() }; + glm::vec2 heights { std::numeric_limits::max(), std::numeric_limits::lowest() }; auto const timber { part < timber_parts }; vertices_of( model.parts[ part ], [&]( glm::vec3 const &Position ) { span = { std::min( span.x, Position.x ), std::max( span.y, Position.x ) }; + heights = { std::min( heights.x, Position.y ), std::max( heights.y, Position.y ) }; if( timber ) { top = std::max( top, Position.y ); } } ); model.spans[ part ] = span; + model.heights[ part ] = heights; if( timber ) { model.span = { std::min( model.span.x, span.x ), std::max( model.span.y, span.y ) }; } @@ -155,6 +158,7 @@ beam_model const *find_model( std::string const &Name, std::string const &Skinna } std::sort( model.bands.begin(), model.bands.end() ); // the rails sit on the seat of the sleeper, below the fastenings sticking up over it, or on the plate of the fastening + model.bed = top; model.seat = ( Seat >= 0.f ? Seat : top ); if( Seat < 0.f && model.fastened() ) { model.seat = std::max( top, surface_at( model.parts[ part_plate ], 0.f, 0.f ).value_or( top ) ); diff --git a/world/SleeperRules.h b/world/SleeperRules.h index 6b823438..4248b4be 100644 --- a/world/SleeperRules.h +++ b/world/SleeperRules.h @@ -73,6 +73,9 @@ double constexpr check_least { 0.8 }; double constexpr chair_lead { 0.3 }; // how far from the stock rail toward its blade the slide plate of a chair is measured double constexpr slide_probe { 0.12 }; +// a rail raised less than this goes without a pad under it, and a pad narrower than the second one is left out +double constexpr pad_least { 0.002 }; +double constexpr pad_narrowest { 0.02 }; // how far two fastenings on a timber may reach over one another before the check counts them double constexpr fastening_overlap { 0.01 }; // how far from the camera the fastenings are drawn at most diff --git a/world/TrackSleepers.cpp b/world/TrackSleepers.cpp index 94f239d5..b4563475 100644 --- a/world/TrackSleepers.cpp +++ b/world/TrackSleepers.cpp @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include "world/SleeperRules.h" #include "world/SleeperWorker.h" #include "world/Track.h" +#include "world/TrackRails.h" #include "simulation/simulation.h" #include "scene/scene.h" #include "utilities/utilities.h" @@ -67,7 +68,21 @@ bool lays( TTrack const &Track ) { double blade_rise( TTrack const &Track ) { - return ( Track.eType == tt_Switch && lays( Track ) ? timber_model( Track )->slide : 0.0 ); + return ( Track.eType == tt_Switch ? wing_rise( Track ) : 0.0 ); +} + +double wing_rise( TTrack const &Track ) { + + return ( lays( Track ) ? timber_model( Track )->slide : 0.0 ); +} + +glm::dvec2 stock_stretch( TTrack const &Track, int const Path, int const Side ) { + + if( Track.eType != tt_Switch || Track.SwitchExtension == nullptr || Side != ( Track.SwitchExtension->RightSwitch == ( Path == 1 ) ? 1 : -1 ) ) { + return glm::dvec2 { -1.0 }; + } + auto const &shape { Track.blade( 1 - Path ) }; + return { std::max( 0.0, ( shape.valid ? shape.tip : 0.0 ) - chair_lead ), track_rails::blade_length( Track, 1 - Path ) }; } std::vector> shared_zones( TTrack const &Track, int const Path ) { @@ -255,9 +270,9 @@ void lay( TTrack &Track ) { place[ 1 ] = glm::vec4 { glm::vec3 { y }, 0.f }; place[ 2 ] = glm::vec4 { glm::vec3 { glm::cross( across, y ) }, 0.f }; place[ 3 ] = glm::vec4 { glm::vec3 { fastening.point - Track.m_origin }, 1.f }; - auto const pivot { glm::vec3 { fastening.pivot, 0.f, 0.f } }; + auto const pivot { glm::vec3 { fastening.pivot, parts.heights[ fastening.part ].x, 0.f } }; laid.instances[ fastening.part ].emplace_back( - glm::translate( glm::scale( glm::translate( place, glm::vec3 { fastening.shift, -parts.seat, 0.f } + pivot ), glm::vec3 { fastening.scale, 1.f, 1.f } ), -pivot ) ); + glm::translate( glm::scale( glm::translate( place, glm::vec3 { fastening.shift, fastening.lift - parts.seat, 0.f } + pivot ), glm::vec3 { fastening.scale, fastening.height, 1.f } ), -pivot ) ); } } runbeams.emplace_back( middle, half ); diff --git a/world/TrackSleepers.h b/world/TrackSleepers.h index 683a451b..7efa1681 100644 --- a/world/TrackSleepers.h +++ b/world/TrackSleepers.h @@ -72,6 +72,8 @@ struct beam_model { 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 float slide { 0.f }; // how far over the seat the blades of a switch slide, on the slide plates of the chairs; 0 without chairs + float bed { 0.f }; // the height in the model of the top of the timber + std::array heights {}; // from and to along y, of the parts std::vector 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; } // the model has the fastenings of the rails, put where the rails lie on its timbers @@ -123,6 +125,11 @@ bool lays( TTrack const &Track ); // how far the feet of the blades of the switch ride over those of its other rails, on the slide plates of the chairs of its // timbers; 0 where it lays none with chairs double blade_rise( TTrack const &Track ); +// how far the feet of its wing rails of the low section of the blades are raised as the blades; 0 where it lays no timbers with chairs +double wing_rise( TTrack const &Track ); +// the stretch along the path its rail of Side is the stock rail of the blade of the other path along, with the slide chairs from +// a little before the tip of the blade to its heel; from past to where there's none +glm::dvec2 stock_stretch( TTrack const &Track, int const Path, int const Side ); // 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 From 890ebebebde1b4c96c2f87cd993a7b9db854ecd2 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 21:19:14 +0200 Subject: [PATCH 09/19] feat(rails): the wing rails of the low section drawn of its profile, Zu1-60 (60E1A1), not the plain one shortened: below the head the web thickened to 44 mm, the foot 55 mm inwards and 85 mm outwards of the middle, the edges of the foot and the root of the web higher, of the measures of the profile drawn, made over as far as the foot is raised along the forged ends; the end faces of the rails of the same --- world/RailLayer.cpp | 1 + world/RailLayout.h | 7 +++ world/Segment.cpp | 101 ++++++++++++++++++++++++++++++++++++++++---- world/Segment.h | 12 ++++++ 4 files changed, 112 insertions(+), 9 deletions(-) diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp index 9192dd6f..816e6c51 100644 --- a/world/RailLayer.cpp +++ b/world/RailLayer.cpp @@ -1281,6 +1281,7 @@ loft_span TTrack::rail_layer::span( rail_piece const &Piece, piece_ends const &E result.rises = Piece.rises; if( false == Piece.rises.empty() ) { result.head = { m_drawing.size.neck, m_drawing.size.crest }; + result.low = { m_rise, low_web, low_inward, low_outward, low_edge, low_root }; } if( std::abs( Piece.from - Ends.from ) < 1e-6 ) { result.fromend = Ends.fromend; diff --git a/world/RailLayout.h b/world/RailLayout.h index 4b222fe5..99b13ba8 100644 --- a/world/RailLayout.h +++ b/world/RailLayout.h @@ -52,6 +52,13 @@ 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 double constexpr wing_low { 1.5 }; // how far before the throat the wing rails are of the low section of the blades, raised on their timbers as the blades double constexpr wing_forged { 0.5 }; // how long the end of a wing rail forged to the plain rail is, going down to it +// the low section of the blades, Zu1-60 (60E1A1) against 60E1: its web 44 mm thick against 16.5, its foot 55 mm inwards and 85 mm outwards +// of the middle against 75 both ways, the edges of its foot 20 mm high against 11.5, its web from 36 mm up against 30.6 +double constexpr low_web { 44.0 / 16.5 }; +double constexpr low_inward { 55.0 / 75.0 }; +double constexpr low_outward { 85.0 / 75.0 }; +double constexpr low_edge { 20.0 / 11.5 }; +double constexpr low_root { 36.0 / 30.6 }; // where the inner rails of the two paths of a switch cross, found from their gauge lines behind the blades struct switch_frog { diff --git a/world/Segment.cpp b/world/Segment.cpp index 9a2dc6c2..c87b5853 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -764,6 +764,87 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig return Height; } return static_cast( Height + Rise * ( Span.head.x - Height ) / ( Span.head.x - bottom ) ); }; + // the profile below its head as drawn, about the middle of its foot: how far the foot reaches inwards and outwards, how thick the web + // is, how wide the head, where its underside is, how high the top of the edges of the foot and the root of the web are + struct { + double middle { 0.0 }, inward { 0.0 }, outward { 0.0 }, web { 1e6 }, head { 0.0 }, underside { 1e6 }, edge { 0.0 }, root { 1e6 }; + } drawn; + auto const across = [&]( gfx::basic_vertex const &Point ) { + return static_cast( Span.side * Point.position.x - Span.halfwidth ); }; + auto low { Span.low.full > 0.0 && Span.head.x > bottom }; + if( low ) { + glm::dvec2 foot { 1e6, -1e6 }; + for( auto const &point : ShapePoints ) { + if( point.position.y < bottom + 0.0005f ) { + foot = { std::min( foot.x, across( point ) ), std::max( foot.y, across( point ) ) }; + } + } + drawn.middle = 0.5 * ( foot.x + foot.y ); + drawn.inward = drawn.middle - foot.x; + drawn.outward = foot.y - drawn.middle; + for( auto const &point : ShapePoints ) { + auto const off { std::abs( across( point ) - drawn.middle ) }; + if( point.position.y >= Span.head.x ) { + drawn.head = std::max( drawn.head, off ); + } + else if( point.position.y > bottom + 0.25 * ( Span.head.x - bottom ) ) { + drawn.web = std::min( drawn.web, off ); + } + } + for( auto const &point : ShapePoints ) { + auto const off { std::abs( across( point ) - drawn.middle ) }; + auto const height { static_cast( point.position.y - bottom ) }; + if( point.position.y >= Span.head.x && off >= drawn.head - 0.001 ) { + drawn.underside = std::min( drawn.underside, static_cast( point.position.y ) ); + } + if( point.position.y < Span.head.x && off <= drawn.web + 0.0005 ) { + drawn.root = std::min( drawn.root, height ); + } + if( off >= 0.9 * std::min( drawn.inward, drawn.outward ) ) { + drawn.edge = std::max( drawn.edge, height ); + } + } + low = ( drawn.web < std::min( drawn.inward, drawn.outward ) && drawn.web < drawn.head && drawn.edge > 0.0 && drawn.edge < drawn.root && drawn.root < Span.head.x - bottom && drawn.underside < 1e6 ); + } + // the profile made over into the low section with its foot raised by Rise, by as much of it as the rise is of the full one: below the + // head the web thickened, the foot reshaped, the web shortened + std::vector lowpoints; + auto const lowered = [&]( double const Rise ) -> gfx::vertex_array const & { + auto const share { std::clamp( Rise / Span.low.full, 0.0, 1.0 ) }; + auto const web { drawn.web * glm::mix( 1.0, Span.low.web, share ) }; + auto const inward { drawn.inward * glm::mix( 1.0, Span.low.inward, share ) }; + auto const outward { drawn.outward * glm::mix( 1.0, Span.low.outward, share ) }; + auto const edge { drawn.edge * glm::mix( 1.0, Span.low.edge, share ) }; + auto const root { drawn.root * glm::mix( 1.0, Span.low.root, share ) }; + auto const below { Span.head.x - bottom }; + auto const top { std::max( below - Rise, root + 0.001 ) }; + lowpoints = ShapePoints; + for( auto &point : lowpoints ) { + if( point.position.y >= drawn.underside ) { + continue; + } + auto const off { across( point ) - drawn.middle }; + auto const distance { std::abs( off ) }; + auto moved { distance }; + if( point.position.y >= Span.head.x ) { + moved = web + ( std::max( distance, drawn.web ) - drawn.web ) * ( drawn.head - web ) / std::max( drawn.head - drawn.web, 1e-6 ); + } + else { + auto const was { off < 0.0 ? drawn.inward : drawn.outward }; + auto const reach { off < 0.0 ? inward : outward }; + moved = ( distance <= drawn.web ? + distance * web / drawn.web : + web + ( distance - drawn.web ) * ( reach - web ) / std::max( was - drawn.web, 1e-6 ) ); + auto const height { static_cast( point.position.y - bottom ) }; + auto const made { + height <= drawn.edge ? height * edge / drawn.edge : + height <= drawn.root ? edge + ( height - drawn.edge ) * ( root - edge ) / ( drawn.root - drawn.edge ) : + root + ( height - drawn.root ) * ( top - root ) / ( below - drawn.root ) }; + point.position.y = static_cast( bottom + below - top + made ); + } + point.position.x = static_cast( Span.side * ( Span.halfwidth + drawn.middle + std::copysign( moved, off ) ) ); + } + return lowpoints; }; auto const ring = [&]( double const Distance, double const Middle, std::vector &Ring, std::vector &Wear ) { glm::dvec3 point; @@ -797,7 +878,9 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig } auto const worn { knots::sample( Span.ramp, Distance, -1e6 ) }; auto const drop { knots::sample( Span.drops, Distance, 0.0 ) }; - auto const rise { Span.rises.empty() ? Span.rise : knots::sample( Span.rises, Distance, 0.0 ) }; + auto const sampled { Span.rises.empty() ? Span.rise : knots::sample( Span.rises, Distance, 0.0 ) }; + auto const &points { low && sampled > 0.0 ? lowered( sampled ) : ShapePoints }; + auto const rise { low ? 0.0 : sampled }; static loft_end const unshaped; auto const &shaped { Distance <= Span.from ? Span.fromend : Distance >= Span.to ? Span.toend : unshaped }; auto const bend { shaped.bend }; @@ -813,11 +896,11 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig } Ring.clear(); Wear.clear(); - auto const count { ShapePoints.size() }; + auto const count { points.size() }; auto const reach = [&]( std::size_t const Index ) { - return static_cast( Span.side * ShapePoints[ Index ].position.x - Span.halfwidth ); }; + return static_cast( Span.side * points[ Index ].position.x - Span.halfwidth ); }; auto const twin = [&]( std::size_t const A, std::size_t const B ) { - return ShapePoints[ A ].position.x == ShapePoints[ B ].position.x && ShapePoints[ A ].position.y == ShapePoints[ B ].position.y; }; + return points[ A ].position.x == points[ B ].position.x && points[ A ].position.y == points[ B ].position.y; }; auto const clamped = [&]( std::size_t const Index ) { auto const outside { reach( Index ) }; return ( outside > middleextent ? 1 : outside < middleinner ? -1 : 0 ); }; @@ -831,15 +914,15 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig // 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( Span.side * ( Span.halfwidth + outside + offset ) ) + position }; - pt.y += raised( ShapePoints.front().position.y, rise ) + Span.lift; + pt.y += raised( points.front().position.y, rise ) + 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 } ); + Ring.emplace_back( pt, glm::vec3{ 0.f, 1.f, 0.f }, glm::vec2{ points[ 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 shape { points[ j ] }; auto outside { reach( j ) }; auto wall { glm::vec3{ 0.f } }; auto const side { clamped( j ) }; @@ -866,7 +949,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig if( false == open( neighbour ) ) { continue; } - auto const &other { ShapePoints[ neighbour ] }; + auto const &other { points[ neighbour ] }; auto const t { std::clamp( static_cast( ( limit - reach( neighbour ) ) / ( side > 0 ? std::max( outside - reach( neighbour ), 1e-6 ) : std::min( outside - reach( neighbour ), -1e-6 ) ) ), 0.f, 1.f ) }; shape.position.y = glm::mix( other.position.y, shape.position.y, t ); shape.texture.x = glm::mix( other.texture.x, shape.texture.x, t ); @@ -889,7 +972,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig continue; } auto const t { static_cast( ( limit - reach( static_cast( neighbour ) ) ) / ( reach( static_cast( index ) ) - reach( static_cast( neighbour ) ) ) ) }; - auto const height { glm::mix( ShapePoints[ neighbour ].position.y, ShapePoints[ index ].position.y, t ) }; + auto const height { glm::mix( points[ neighbour ].position.y, points[ index ].position.y, t ) }; top = std::max( top, height ); bottom = std::min( bottom, height ); ++ends; diff --git a/world/Segment.h b/world/Segment.h index 26fb1c35..2aa17fb0 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -28,6 +28,17 @@ struct loft_end { double bend { 0.0 }; }; +// a rail profile made over below its head into a lower section, as the wing rails of a frog of the section of the blades: its web, its +// foot inwards and outwards as many times as thick and wide, the top of the edges of its foot and the root of its web as many times as high +struct loft_low { + double full { 0.0 }; // the rise of the foot the profile is the whole low section at; none where it's 0 + double web { 1.0 }; + double inward { 1.0 }; + double outward { 1.0 }; + double edge { 1.0 }; + double root { 1.0 }; +}; + struct loft_span { int side { 1 }; float halfwidth { 0.f }; @@ -49,6 +60,7 @@ struct loft_span { glm::dvec2 head { 0.0 }; double rise { 0.0 }; // how far the foot of the profile is raised, the web below the head (head.x) shortened by as much knots::line rises; // along the span, in place of rise where given + loft_low low; // what the profile is made over into as its foot is raised }; struct segment_data { From 0513a92bc3b44f3ee30aa8502aa22c3619509ac5 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 21:19:14 +0200 Subject: [PATCH 10/19] fix(sleepers): the fastening of the rails of a frog, holding more of them than its seats, stretched under them all as the plate of the frog, its ends with the clips past the feet of the wing rails, short of the feet of the rails beside, and not narrowed then; the wing rails no longer lying off its ends with no clip --- world/SleeperFastenings.cpp | 26 ++++++++++++++++++++++++-- world/SleeperRules.h | 2 ++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp index 8e4f4e1b..c80b6d9e 100644 --- a/world/SleeperFastenings.cpp +++ b/world/SleeperFastenings.cpp @@ -243,7 +243,29 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M } return; } - result.push_back( { part_check, point( origin ), axis * turn, 1.f, static_cast( 0.5 * check_seats ), 0.f, true, covered > 2 } ); + fastening part { part_check, point( origin ), axis * turn, 1.f, static_cast( 0.5 * check_seats ), 0.f, true, covered > 2 }; + if( part.whole ) { + // holding more rails than its seats, as the rails of a frog: stretched under them all, its ends with the clips past the feet + // of the outer ones, short of the feet of the rails beside + auto const last { from + covered - 1 }; + auto const a { origin + turn * reach.x }; + auto const b { origin + turn * reach.y }; + auto low { rails[ from ].at - rails[ from ].foot - fastening_clip }; + auto high { rails[ last ].at + rails[ last ].foot + fastening_clip }; + if( from > 0 ) { + low = std::max( low, rails[ from - 1 ].at + rails[ from - 1 ].foot + fastening_gap ); + } + if( last + 1 < rails.size() ) { + high = std::min( high, rails[ last + 1 ].at - rails[ last + 1 ].foot - fastening_gap ); + } + low = std::min( low, std::min( a, b ) ); + high = std::max( high, std::max( a, b ) ); + auto const scale { ( high - low ) / ( reach.y - reach.x ) }; + part.pivot = 0.f; + part.scale = static_cast( scale ); + part.shift = static_cast( turn * ( 0.5 * ( low + high ) - origin ) - scale * 0.5 * ( reach.x + reach.y ) ); + } + result.push_back( part ); if( from > First ) { crowd( First, from - 1 ); } @@ -365,7 +387,7 @@ void fastened_rails::fit( std::vector &Fastenings, beam const &Beam, switch( Fastening.part ) { case part_plate: { return plate_least; } case part_slide: { return slide_least; } - case part_check: { return Fastening.whole ? 1.0 : check_least; } + case part_check: { return Fastening.whole ? static_cast( Fastening.scale ) : check_least; } default: { return 1.0; } } }; // those of one rail together, the rails in the order along the timber diff --git a/world/SleeperRules.h b/world/SleeperRules.h index 4248b4be..bba9b833 100644 --- a/world/SleeperRules.h +++ b/world/SleeperRules.h @@ -61,6 +61,8 @@ double constexpr fastening_nose { 0.05 }; double constexpr fastening_crowd { 0.3 }; // how far apart the rails lie in the fastening of both, the check part of the model double constexpr check_seats { 0.113 }; +// how far past the edge of the foot of a rail its fastening reaches, with the clip holding it +double constexpr fastening_clip { 0.09 }; // the room left between the fastenings of rails close by, and how narrow their plates get to keep it double constexpr fastening_gap { 0.01 }; double constexpr plate_least { 0.6 }; From 997a8da41d84357ce8bfc6be9ad4fa56d6979987 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 22:00:11 +0200 Subject: [PATCH 11/19] fix(rails): the rail going on from the heel of a blade raised as the blade at the heel and forged down to the plain rail along 0.5 m, no step at the heel; the wing rails made over into the low section by a share of their own along them, the rise apart from it; a fastening holding raised rails, as the plate of a frog under the wing rails and the nose, moved up on a pad as far as the least raised of them, its clips with them, a rail not raised sunk in it, a rail raised more on a pad of its own on it; the fastenings selftest lists the lifts and the pads --- application/editormodeselftestsleepers.cpp | 7 ++- world/RailLayer.cpp | 12 +++-- world/RailLayout.h | 38 +++++++++++--- world/Segment.cpp | 17 ++++--- world/Segment.h | 4 +- world/SleeperFastenings.cpp | 58 +++++++++++----------- world/SleeperFastenings.h | 6 +-- world/TrackSleepers.cpp | 8 +++ world/TrackSleepers.h | 2 + 9 files changed, 98 insertions(+), 54 deletions(-) diff --git a/application/editormodeselftestsleepers.cpp b/application/editormodeselftestsleepers.cpp index 63972320..3f795bb0 100644 --- a/application/editormodeselftestsleepers.cpp +++ b/application/editormodeselftestsleepers.cpp @@ -172,8 +172,11 @@ editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &c std::string text{format("SELFTEST fastenings %s at %.2f %.2f:", track->name().c_str(), middle.x, middle.z)}; for (auto const &fastening : rails.on(beam, *model)) { - text += format(" %s %.3f %.3f %+.0f", track_sleepers::part_names[fastening.part], fastening.point.x, fastening.point.z, glm::degrees(std::atan2(fastening.across.y, fastening.across.x))); - ++counts[fastening.part]; + text += format(" %s%s %.3f %.3f %+.0f", fastening.pad ? "pad:" : "", track_sleepers::part_names[fastening.part], fastening.point.x, fastening.point.z, glm::degrees(std::atan2(fastening.across.y, fastening.across.x))); + if (fastening.lift != 0.f) + text += format(" ^%.3f", fastening.lift); + if (false == fastening.pad) + ++counts[fastening.part]; } WriteLog(text); } diff --git a/world/RailLayer.cpp b/world/RailLayer.cpp index 816e6c51..3f7a855b 100644 --- a/world/RailLayer.cpp +++ b/world/RailLayer.cpp @@ -383,6 +383,7 @@ struct rail_piece { float lift { 0.f }; float wallu { -1.f }; knots::line rises; + knots::line lows; }; struct top_band { @@ -609,6 +610,7 @@ private: double const m_guardoffset; double const m_rise; // of the feet of the wing rails of the low section glm::dvec2 const m_stock; // the stretch the rail is the stock rail of a blade along + double const m_heel; // of the blade the rail goes on from std::size_t m_topfirst; std::size_t m_toplast { 0 }; std::vector m_tints; @@ -632,6 +634,7 @@ TTrack::rail_layer::rail_layer( TTrack &Track, rail_layout const &Layout, rail_d m_guardoffset { -( flangeway + Drawing.size.head ) }, m_rise { track_sleepers::wing_rise( Track ) }, m_stock { track_sleepers::stock_stretch( Track, Path, Side ) }, + m_heel { track_sleepers::blade_heel( Track, Path, Side ) }, m_topfirst { Profile.size() } { // the running surface on top of the head is split along the edge of the wheel treads, the part of it out of their reach goes matte; the rest of the profile and the end faces are laid whole for( std::size_t j = 0; j < Profile.size(); ++j ) { @@ -797,7 +800,8 @@ void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vector 0 && Cuts[ i - 1 ].carved && Cuts[ i ].carved ) }; auto const pieceoffsets { part_offsets( i > 0 ? &Cuts[ i - 1 ] : nullptr, i < Cuts.size() ? &Cuts[ i ] : nullptr ) }; - auto const piecerises { part_rises( i > 0 ? &Cuts[ i - 1 ] : nullptr, i < Cuts.size() ? &Cuts[ i ] : nullptr, Wheels ? m_rise : 0.0, m_stock ) }; + auto const pieceshares { Wheels && m_rise > 0.0 ? wing_shares( i > 0 ? &Cuts[ i - 1 ] : nullptr, i < Cuts.size() ? &Cuts[ i ] : nullptr, m_stock ) : knots::line {} }; + auto const piecerises { part_rises( pieceshares, Wheels ? m_rise : 0.0, m_heel ) }; // 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 }; @@ -846,7 +850,8 @@ void TTrack::rail_layer::lay_cuts( gfx::vertex_array const &Shape, std::vectoredge.empty() && m_topfirst < m_toplast ) { treaded( whole, *tread, ends ); } @@ -1279,9 +1284,10 @@ loft_span TTrack::rail_layer::span( rail_piece const &Piece, piece_ends const &E result.walls = Piece.walls; result.wallu = Piece.wallu; result.rises = Piece.rises; + result.lows = Piece.lows; if( false == Piece.rises.empty() ) { result.head = { m_drawing.size.neck, m_drawing.size.crest }; - result.low = { m_rise, low_web, low_inward, low_outward, low_edge, low_root }; + result.low = { low_web, low_inward, low_outward, low_edge, low_root }; } if( std::abs( Piece.from - Ends.from ) < 1e-6 ) { result.fromend = Ends.fromend; diff --git a/world/RailLayout.h b/world/RailLayout.h index 99b13ba8..925d6fa4 100644 --- a/world/RailLayout.h +++ b/world/RailLayout.h @@ -185,21 +185,21 @@ inline knots::line part_offsets( rail_cut const *Before, rail_cut const *After ) knots::sort( result ); return result; } -// how far the foot of a part of a rail between two cuts is raised, Rise along a wing rail of the low section: from wing_low before the throat on, -// down to the plain rail past it along its forged end; the rail whole along Stock, the stock rail of a blade there -inline knots::line part_rises( rail_cut const *Before, rail_cut const *After, double const Rise, glm::dvec2 const &Stock ) { +// how much of the low section of the blades a part of a rail between two cuts is of along it, 0 to 1: a wing rail from wing_low before the throat +// on, down to the plain rail past it along its forged end; the rail whole along Stock, the stock rail of a blade there +inline knots::line wing_shares( rail_cut const *Before, rail_cut const *After, glm::dvec2 const &Stock ) { knots::line result; auto const going { Before != nullptr && Before->startkind == rail_end::point }; auto const coming { After != nullptr && After->endkind == rail_end::point }; - if( Rise <= 0.0 || ( false == going && false == coming ) ) { + if( false == going && false == coming ) { return result; } auto const lowto { going ? Before->startanchor + wing_low : 0.0 }; auto const lowfrom { coming ? After->endanchor - wing_low : 0.0 }; auto const through { going && coming && lowto + 2.0 * wing_forged >= lowfrom }; auto const stock { Stock.y > Stock.x }; - // the share of the rise: whole along the low section, none along the stock rail, between along the forged ends + // whole along the low section, none along the stock rail, between along the forged ends auto const share = [&]( double const At ) { auto const low { through ? 1.0 : std::max( going ? std::clamp( ( lowto + wing_forged - At ) / wing_forged, 0.0, 1.0 ) : 0.0, @@ -210,7 +210,33 @@ inline knots::line part_rises( rail_cut const *Before, rail_cut const *After, do std::sort( places.begin(), places.end() ); for( auto const at : places ) { if( result.empty() || at > result.back().x + 1e-6 ) { - result.emplace_back( at, Rise * share( at ) ); + result.emplace_back( at, share( at ) ); + } + } + return result; +} +// how far the foot of the part is raised: by Rise along the low section of a wing rail of Shares, and from the heel of a blade at Heel, the +// rail going on from it, down to the plain rail along the forged end of the blade; not along the blade itself, raised on its own +inline knots::line part_rises( knots::line const &Shares, double const Rise, double const Heel ) { + + knots::line result; + if( Rise <= 0.0 || ( Shares.empty() && Heel <= 0.0 ) ) { + return result; + } + std::vector places; + for( auto const &knot : Shares ) { + places.push_back( knot.x ); + } + if( Heel > 0.0 ) { + places.push_back( Heel - 1e-3 ); + places.push_back( Heel ); + places.push_back( Heel + wing_forged ); + } + std::sort( places.begin(), places.end() ); + for( auto const at : places ) { + if( result.empty() || at > result.back().x + 1e-6 ) { + auto const heel { Heel > 0.0 && at >= Heel ? std::clamp( ( Heel + wing_forged - at ) / wing_forged, 0.0, 1.0 ) : 0.0 }; + result.emplace_back( at, Rise * std::max( knots::sample( Shares, at, 0.0 ), heel ) ); } } return result; diff --git a/world/Segment.cpp b/world/Segment.cpp index c87b5853..10f8b361 100644 --- a/world/Segment.cpp +++ b/world/Segment.cpp @@ -717,7 +717,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig for( auto distance { std::ceil( Span.from / 0.5 ) * 0.5 }; distance < std::min( Span.to, m_dense ); distance += 0.5 ) { stations.push_back( distance ); } - for( auto const *line : { &Span.offsets, &Span.extents, &Span.inner, &Span.drops, &Span.rises } ) { + for( auto const *line : { &Span.offsets, &Span.extents, &Span.inner, &Span.drops, &Span.rises, &Span.lows } ) { for( auto const &knot : *line ) { if( knot.x > Span.from && knot.x < Span.to ) { stations.push_back( knot.x ); @@ -771,7 +771,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig } drawn; auto const across = [&]( gfx::basic_vertex const &Point ) { return static_cast( Span.side * Point.position.x - Span.halfwidth ); }; - auto low { Span.low.full > 0.0 && Span.head.x > bottom }; + auto low { false == Span.lows.empty() && Span.head.x > bottom }; if( low ) { glm::dvec2 foot { 1e6, -1e6 }; for( auto const &point : ShapePoints ) { @@ -806,11 +806,11 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig } low = ( drawn.web < std::min( drawn.inward, drawn.outward ) && drawn.web < drawn.head && drawn.edge > 0.0 && drawn.edge < drawn.root && drawn.root < Span.head.x - bottom && drawn.underside < 1e6 ); } - // the profile made over into the low section with its foot raised by Rise, by as much of it as the rise is of the full one: below the - // head the web thickened, the foot reshaped, the web shortened + // the profile with its foot raised by Rise made over into the low section by Share of it: below the head the web thickened, the foot + // reshaped, the web shortened; with none of it, only raised std::vector lowpoints; - auto const lowered = [&]( double const Rise ) -> gfx::vertex_array const & { - auto const share { std::clamp( Rise / Span.low.full, 0.0, 1.0 ) }; + auto const lowered = [&]( double const Rise, double const Share ) -> gfx::vertex_array const & { + auto const share { std::clamp( Share, 0.0, 1.0 ) }; auto const web { drawn.web * glm::mix( 1.0, Span.low.web, share ) }; auto const inward { drawn.inward * glm::mix( 1.0, Span.low.inward, share ) }; auto const outward { drawn.outward * glm::mix( 1.0, Span.low.outward, share ) }; @@ -840,7 +840,7 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig height <= drawn.edge ? height * edge / drawn.edge : height <= drawn.root ? edge + ( height - drawn.edge ) * ( root - edge ) / ( drawn.root - drawn.edge ) : root + ( height - drawn.root ) * ( top - root ) / ( below - drawn.root ) }; - point.position.y = static_cast( bottom + below - top + made ); + point.position.y = static_cast( glm::mix( static_cast( raised( point.position.y, Rise ) ), bottom + below - top + made, share ) ); } point.position.x = static_cast( Span.side * ( Span.halfwidth + drawn.middle + std::copysign( moved, off ) ) ); } @@ -879,7 +879,8 @@ bool TSegment::RenderLoftSpan( gfx::vertex_array &Output, glm::dvec3 const &Orig auto const worn { knots::sample( Span.ramp, Distance, -1e6 ) }; auto const drop { knots::sample( Span.drops, Distance, 0.0 ) }; auto const sampled { Span.rises.empty() ? Span.rise : knots::sample( Span.rises, Distance, 0.0 ) }; - auto const &points { low && sampled > 0.0 ? lowered( sampled ) : ShapePoints }; + auto const share { low ? knots::sample( Span.lows, Distance, 0.0 ) : 0.0 }; + auto const &points { low && ( sampled > 0.0 || share > 0.0 ) ? lowered( sampled, share ) : ShapePoints }; auto const rise { low ? 0.0 : sampled }; static loft_end const unshaped; auto const &shaped { Distance <= Span.from ? Span.fromend : Distance >= Span.to ? Span.toend : unshaped }; diff --git a/world/Segment.h b/world/Segment.h index 2aa17fb0..24665ddd 100644 --- a/world/Segment.h +++ b/world/Segment.h @@ -31,7 +31,6 @@ struct loft_end { // a rail profile made over below its head into a lower section, as the wing rails of a frog of the section of the blades: its web, its // foot inwards and outwards as many times as thick and wide, the top of the edges of its foot and the root of its web as many times as high struct loft_low { - double full { 0.0 }; // the rise of the foot the profile is the whole low section at; none where it's 0 double web { 1.0 }; double inward { 1.0 }; double outward { 1.0 }; @@ -60,7 +59,8 @@ struct loft_span { glm::dvec2 head { 0.0 }; double rise { 0.0 }; // how far the foot of the profile is raised, the web below the head (head.x) shortened by as much knots::line rises; // along the span, in place of rise where given - loft_low low; // what the profile is made over into as its foot is raised + knots::line lows; // along the span, how much of the low section the profile is made over into below its head, 0 to 1 + loft_low low; // what the profile is made over into along lows }; struct segment_data { diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp index c80b6d9e..b11dea4a 100644 --- a/world/SleeperFastenings.cpp +++ b/world/SleeperFastenings.cpp @@ -53,14 +53,11 @@ fastened_rails::fastened_rails( TTrack &Track ) { auto const cut { track_rails::cut_rails( *track ) }; auto const switched { track->eType == tt_Switch && track->SwitchExtension != nullptr }; auto const rise { wing_rise( *track ) }; - // the side of the blade of the path - auto const bladeside = [&]( int const Path ) { - return ( track->SwitchExtension->RightSwitch == ( Path == 0 ) ? 1 : -1 ); }; // what the rail is along the path: a blade, the stock rail of a blade, checked auto const add = [&]( rail &&Line, int const Path, int const Side ) { if( switched ) { - if( Side == bladeside( Path ) ) { - Line.blade = { 0.0, track_rails::blade_length( *track, Path ) }; + if( auto const heel { blade_heel( *track, Path, Side ) }; heel > 0.0 ) { + Line.blade = { 0.0, heel }; } Line.stock = stock_stretch( *track, Path, Side ); } @@ -95,7 +92,7 @@ fastened_rails::fastened_rails( TTrack &Track ) { line.track = track; line.path = path; line.side = side; - line.rises = track_rails::part_rises( before, after, rise, stock_stretch( *track, path, side ) ); + line.rises = track_rails::part_rises( rise > 0.0 ? track_rails::wing_shares( before, after, stock_stretch( *track, path, side ) ) : knots::line {}, rise, blade_heel( *track, path, side ) ); line.foot = size.web - size.low; line.low = glm::dvec2 { std::numeric_limits::max() }; line.high = glm::dvec2 { std::numeric_limits::lowest() }; @@ -673,33 +670,34 @@ void fastened_rails::lift( std::vector &Fastenings, std::vector auto const reach { footprint( Fastening ) }; return Fastening.part != part_chair && Rail.at >= reach.x && Rail.at <= reach.y; }; std::vector pads; - std::vector lifted( Fastenings.size(), false ); + for( auto &fastening : Fastenings ) { + if( fastening.pad || fastening.part == part_chair ) { + continue; + } + // moved up on a pad as wide as it as far as the least raised of the raised rails it holds, a rail not raised sunk in it + auto lift { 0.0 }; + for( auto const &rail : Rails ) { + if( rail.rise >= pad_least && holds( fastening, rail ) ) { + lift = ( lift > 0.0 ? std::min( lift, rail.rise ) : rail.rise ); + } + } + if( lift <= 0.0 ) { + continue; + } + auto const &span { Model.spans[ fastening.part ] }; + auto const a { fastening.placed( span.x, fastening.scale ) }; + auto const b { fastening.placed( span.y, fastening.scale ) }; + pads.push_back( pad( fastening, std::min( a, b ), std::max( a, b ), Model.bed, Model.heights[ fastening.part ].x + static_cast( lift ) ) ); + fastening.lift = static_cast( lift ); + } + // a rail raised more than its fastening on a pad of its own on it, as wide as its foot, short of the rails beside for( std::size_t index = 0; index < Rails.size(); ++index ) { auto const &rail { Rails[ index ] }; - if( rail.rise < pad_least ) { + auto const under { std::find_if( Fastenings.begin(), Fastenings.end(), [&]( fastening const &Fastening ) { + return false == Fastening.pad && holds( Fastening, rail ); } ) }; + if( under == Fastenings.end() || rail.rise - under->lift < pad_least ) { continue; } - auto const under { std::find_if( Fastenings.begin(), Fastenings.end(), [&]( fastening const &Fastening ) { return holds( Fastening, rail ); } ) }; - if( under == Fastenings.end() ) { - continue; - } - auto const which { static_cast( under - Fastenings.begin() ) }; - if( lifted[ which ] ) { - continue; - } - auto const alike { std::all_of( Rails.begin(), Rails.end(), [&]( hit const &Other ) { - return false == holds( *under, Other ) || std::abs( Other.rise - rail.rise ) < pad_least; } ) }; - if( alike ) { - // the fastening holding only the rails raised as much, moved up as far on a pad as wide as it - auto const &span { Model.spans[ under->part ] }; - auto const a { under->placed( span.x, under->scale ) }; - auto const b { under->placed( span.y, under->scale ) }; - pads.push_back( pad( *under, std::min( a, b ), std::max( a, b ), Model.bed, Model.heights[ under->part ].x + static_cast( rail.rise ) ) ); - under->lift = static_cast( rail.rise ); - lifted[ which ] = true; - continue; - } - // the rail on a pad of its own on the fastening, as wide as its foot, short of the rails beside auto low { rail.foot }; auto high { rail.foot }; if( index > 0 ) { @@ -712,7 +710,7 @@ void fastened_rails::lift( std::vector &Fastenings, std::vector continue; } fastening const at { padpart, Beam.from + ( Beam.to - Beam.from ) * ( rail.at / glm::length( plan( Beam.to - Beam.from ) ) ), axis }; - pads.push_back( pad( at, -low, high, Model.seat, Model.seat + static_cast( rail.rise ) ) ); + pads.push_back( pad( at, -low, high, Model.seat + under->lift, Model.seat + static_cast( rail.rise ) ) ); } Fastenings.insert( Fastenings.end(), pads.begin(), pads.end() ); } diff --git a/world/SleeperFastenings.h b/world/SleeperFastenings.h index 59ebf641..f6d22003 100644 --- a/world/SleeperFastenings.h +++ b/world/SleeperFastenings.h @@ -64,7 +64,7 @@ private: TTrack *track { nullptr }; int path { 0 }; int side { 1 }; - knots::line rises; // of its foot, along the path, a wing rail of the low section raised as the blades + knots::line rises; // of its foot, along the path: a wing rail of the low section raised as the blades, the rail going on from the heel of a blade std::vector points; std::vector heights; std::vector inward; // unit vectors in plan from the rail toward the middle of its path @@ -92,8 +92,8 @@ private: // the fastenings of rails close by fitted to the room between them: narrowed across, a plate cut short at the fastening of the // rail beside it or left out, the plates of two rails lying over each other made one static void fit( std::vector &Fastenings, beam const &Beam, beam_model const &Model ); - // the fastenings under the raised rails: one holding only rails raised as much moved up as far on a pad, a rail raised - // over a fastening with others on a pad of its own on it + // the fastenings under the raised rails: one holding them moved up on a pad as far as the least raised of them, a rail not raised + // sunk in it, a rail raised more on a pad of its own on it static void lift( std::vector &Fastenings, std::vector const &Rails, beam const &Beam, beam_model const &Model ); std::vector m_rails; diff --git a/world/TrackSleepers.cpp b/world/TrackSleepers.cpp index b4563475..4f3502d2 100644 --- a/world/TrackSleepers.cpp +++ b/world/TrackSleepers.cpp @@ -85,6 +85,14 @@ glm::dvec2 stock_stretch( TTrack const &Track, int const Path, int const Side ) return { std::max( 0.0, ( shape.valid ? shape.tip : 0.0 ) - chair_lead ), track_rails::blade_length( Track, 1 - Path ) }; } +double blade_heel( TTrack const &Track, int const Path, int const Side ) { + + if( Track.eType != tt_Switch || Track.SwitchExtension == nullptr || Side != ( Track.SwitchExtension->RightSwitch == ( Path == 0 ) ? 1 : -1 ) ) { + return 0.0; + } + return track_rails::blade_length( Track, Path ); +} + std::vector> shared_zones( TTrack const &Track, int const Path ) { std::vector> result; diff --git a/world/TrackSleepers.h b/world/TrackSleepers.h index 7efa1681..1e3c831b 100644 --- a/world/TrackSleepers.h +++ b/world/TrackSleepers.h @@ -130,6 +130,8 @@ double wing_rise( TTrack const &Track ); // the stretch along the path its rail of Side is the stock rail of the blade of the other path along, with the slide chairs from // a little before the tip of the blade to its heel; from past to where there's none glm::dvec2 stock_stretch( TTrack const &Track, int const Path, int const Side ); +// how far along the path its rail of Side is the blade of the path, to its heel, the rail going on from there; 0 where it's none +double blade_heel( TTrack const &Track, int const Path, int const Side ); // 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 From 2e00619d7f8e5a93f35523be53bca7a6589d36a0 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 23 Oct 2026 23:37:28 +0200 Subject: [PATCH 12/19] feat(sleepers): the crossings at wide angles laid on timbers: where two tracks cross so wide they're laid as a grid, the one track carries timbers square to it through the crossing, two side by side under each rail of the other, which lays none there; the rail running along the two held on their seam by fastenings as far apart as the beams at most, clear of the fastenings of the rails across; the guard rails of the crossings at wide and middle angles held as the rails beside are, a running rail with its guard rail in the fastening of both, the rails crowding at the corners under one plate; the beams under the guard rails timbers as far as the guard rails reach; the fastenings selftest lists the rails on each timber with detail rails --- application/editormodeselftestsleepers.cpp | 18 +- world/SleeperFastenings.cpp | 221 ++++++++++++++++++--- world/SleeperFastenings.h | 40 ++-- world/SleeperLayout.cpp | 51 ++++- world/SleeperRules.h | 5 + world/Track.cpp | 4 + world/Track.h | 1 + 7 files changed, 290 insertions(+), 50 deletions(-) diff --git a/application/editormodeselftestsleepers.cpp b/application/editormodeselftestsleepers.cpp index 3f795bb0..cf9d6c51 100644 --- a/application/editormodeselftestsleepers.cpp +++ b/application/editormodeselftestsleepers.cpp @@ -153,8 +153,8 @@ editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &c if (command == "fastenings") { // the fastenings on the timbers of the track: along the track, the part and where across it, which way its +x goes - std::string name; - words >> name; + std::string name, detail; + words >> name >> detail; auto *track{simulation::Paths.find(name)}; auto const *model{track != nullptr ? track_sleepers::timber_model(*track) : nullptr}; if (model == nullptr || false == model->fastened()) @@ -170,11 +170,25 @@ editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &c continue; auto const middle{0.5 * (beam.from + beam.to)}; std::string text{format("SELFTEST fastenings %s at %.2f %.2f:", track->name().c_str(), middle.x, middle.z)}; + if (detail == "rails") + { + auto const span{beam.to - beam.from}; + auto const length{glm::length(glm::dvec2{span.x, span.z})}; + text += " rails"; + for (auto const &rail : rails.lying(beam)) + { + auto const at{beam.from + span * (rail.at / length)}; + text += format(" %.3f %.3f k%d r%.3f;", at.x, at.z, static_cast(rail.kind), rail.rise); + } + text += " parts"; + } for (auto const &fastening : rails.on(beam, *model)) { text += format(" %s%s %.3f %.3f %+.0f", fastening.pad ? "pad:" : "", track_sleepers::part_names[fastening.part], fastening.point.x, fastening.point.z, glm::degrees(std::atan2(fastening.across.y, fastening.across.x))); if (fastening.lift != 0.f) text += format(" ^%.3f", fastening.lift); + if (detail == "rails" && (fastening.scale != 1.f || fastening.shift != 0.f)) + text += format(" s%.3f/%.3f/%.3f", fastening.scale, fastening.pivot, fastening.shift); if (false == fastening.pad) ++counts[fastening.part]; } diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp index b11dea4a..dfae41f8 100644 --- a/world/SleeperFastenings.cpp +++ b/world/SleeperFastenings.cpp @@ -40,6 +40,11 @@ fastened_rails::role fastened_rails::rail::at( double const Distance ) const { return role::plain; } +bool fastened_rails::rail::guarded( double const Distance ) const { + + return std::any_of( guards.begin(), guards.end(), [&]( glm::dvec2 const &Stretch ) { return Distance >= Stretch.x && Distance <= Stretch.y; } ); +} + fastened_rails::fastened_rails( TTrack &Track ) { std::vector tracks; @@ -65,8 +70,35 @@ fastened_rails::fastened_rails( TTrack &Track ) { for( auto const &check : track_rails::rail_checks( *track )[ Path ][ Side > 0 ? 1 : 0 ] ) { Line.checks.emplace_back( check.from, check.to ); } + Line.guards = track->m_railguards[ Path ][ Side > 0 ? 1 : 0 ]; } m_rails.emplace_back( std::move( Line ) ); }; + // the rail of the side of the path from and to along it, by points the given way out from the middle of the path + auto const sampled = [&]( int const Path, int const Side, double const From, double const To, double const Middle, knots::line const &Offsets ) { + auto const *segment { path_segment( *track, Path ) }; + auto const count { std::max( 1, static_cast( std::ceil( ( To - From ) / fastening_step ) ) ) }; + rail line; + line.track = track; + line.path = Path; + line.side = Side; + line.foot = size.web - size.low; + line.low = glm::dvec2 { std::numeric_limits::max() }; + line.high = glm::dvec2 { std::numeric_limits::lowest() }; + for( int step = 0; step <= count; ++step ) { + auto const distance { From + ( To - From ) * step / count }; + glm::dvec3 point, across; + segment->plan_frame( distance, point, across ); + auto const flat { plan( across ) }; + auto const inward { glm::length( flat ) > 1e-9 ? glm::normalize( flat ) * static_cast( -Side ) : glm::dvec2 { 1.0, 0.0 } }; + auto const position { plan( point ) - inward * ( Middle + knots::sample( Offsets, distance, 0.0 ) ) }; + line.points.push_back( position ); + line.heights.push_back( point.y ); + line.inward.push_back( inward ); + line.distances.push_back( distance ); + line.low = glm::min( line.low, position ); + line.high = glm::max( line.high, position ); + } + return line; }; for( int path = 0; path < path_count( *track ); ++path ) { auto const *segment { path_segment( *track, path ) }; if( segment == nullptr ) { @@ -86,32 +118,21 @@ fastened_rails::fastened_rails( TTrack &Track ) { if( to - from < fastening_merge ) { continue; } - auto const offsets { track_rails::part_offsets( before, after ) }; - auto const count { std::max( 1, static_cast( std::ceil( ( to - from ) / fastening_step ) ) ) }; - rail line; - line.track = track; - line.path = path; - line.side = side; + auto line { sampled( path, side, from, to, middle, track_rails::part_offsets( before, after ) ) }; line.rises = track_rails::part_rises( rise > 0.0 ? track_rails::wing_shares( before, after, stock_stretch( *track, path, side ) ) : knots::line {}, rise, blade_heel( *track, path, side ) ); - line.foot = size.web - size.low; - line.low = glm::dvec2 { std::numeric_limits::max() }; - line.high = glm::dvec2 { std::numeric_limits::lowest() }; - for( int step = 0; step <= count; ++step ) { - auto const distance { from + ( to - from ) * step / count }; - glm::dvec3 point, across; - segment->plan_frame( distance, point, across ); - auto const flat { plan( across ) }; - auto const inward { glm::length( flat ) > 1e-9 ? glm::normalize( flat ) * static_cast( -side ) : glm::dvec2 { 1.0, 0.0 } }; - auto const position { plan( point ) - inward * ( middle + knots::sample( offsets, distance, 0.0 ) ) }; - line.points.push_back( position ); - line.heights.push_back( point.y ); - line.inward.push_back( inward ); - line.distances.push_back( distance ); - line.low = glm::min( line.low, position ); - line.high = glm::max( line.high, position ); - } add( std::move( line ), path, side ); } + // the guard rails of the crossings at wide angles inside the rail, the flangeway from it + for( auto const &guard : cut ? track->m_railguards[ path ][ side > 0 ] : std::vector {} ) { + auto const from { std::max( 0.0, guard.x ) }; + auto const to { std::min( length, guard.y ) }; + if( to - from < fastening_merge ) { + continue; + } + auto line { sampled( path, side, from, to, middle - ( track_rails::flangeway + size.head ), {} ) }; + line.guard = true; + m_rails.emplace_back( std::move( line ) ); + } } } } @@ -345,9 +366,152 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M } fit( result, Beam, Model ); lift( result, rails, Beam, Model ); + along( result, Beam, Model ); return result; } +void fastened_rails::along( std::vector &Fastenings, beam const &Beam, beam_model const &Model ) const { + + auto const span { Beam.to - Beam.from }; + auto const length { glm::length( plan( span ) ) }; + auto const start { plan( Beam.from ) }; + auto const axis { plan( span ) / length }; + auto const side { glm::normalize( plan( Beam.along ) ) }; + auto const low { glm::min( start, plan( Beam.to ) ) - 2.0 * half_width }; + auto const high { glm::max( start, plan( Beam.to ) ) + 2.0 * half_width }; + // where the rail lies over the timber, along it: a stretch of a chord of the rail + struct stretch { + rail const *line; + std::size_t index; // of the end of the chord + double from; // along the timber + double to; + }; + std::vector stretches; + for( auto const &line : m_rails ) { + // a guard rail along the timber lies in the fastening of its running rail + if( line.guard || line.high.x < low.x || line.low.x > high.x || line.high.y < low.y || line.low.y > high.y ) { + continue; + } + for( std::size_t index = 1; index < line.points.size(); ++index ) { + auto const chord { line.points[ index ] - line.points[ index - 1 ] }; + if( glm::length( chord ) < 1e-6 || std::abs( cross( axis, glm::normalize( chord ) ) ) >= crossing_sine_min ) { + continue; + } + if( std::abs( line.heights[ index ] - glm::mix( Beam.from.y, Beam.to.y, std::clamp( glm::dot( line.points[ index ] - start, axis ) / length, 0.0, 1.0 ) ) ) > level_tolerance ) { + continue; + } + // the share of the chord over the middle of the timber or its side toward the next one, and over its length + auto const u0 { glm::dot( line.points[ index - 1 ] - start, axis ) }; + auto const u1 { glm::dot( line.points[ index ] - start, axis ) }; + auto const w0 { glm::dot( line.points[ index - 1 ] - start, side ) }; + auto const w1 { glm::dot( line.points[ index ] - start, side ) }; + auto from { 0.0 }; + auto to { 1.0 }; + auto const clip = [&]( double const A, double const B, double const Low, double const High ) { + if( std::abs( B - A ) < 1e-9 ) { + if( A <= Low || A > High ) { + to = -1.0; + } + return; + } + auto const a { ( Low - A ) / ( B - A ) }; + auto const b { ( High - A ) / ( B - A ) }; + from = std::max( from, std::min( a, b ) ); + to = std::min( to, std::max( a, b ) ); }; + clip( w0, w1, -half_width + 2.0 * fastening_gap, half_width + 2.0 * fastening_gap ); + clip( u0, u1, 0.0, length ); + if( to <= from ) { + continue; + } + auto const a { glm::mix( u0, u1, from ) }; + auto const b { glm::mix( u0, u1, to ) }; + stretches.push_back( { &line, index, std::min( a, b ), std::max( a, b ) } ); + } + } + if( stretches.empty() ) { + return; + } + // kept clear of the fastenings of the rails across the timber, and of its ends + std::vector taken; + for( auto const &fastening : Fastenings ) { + auto const at { glm::dot( plan( fastening.point ) - start, axis ) }; + auto const turn { glm::dot( fastening.across, axis ) }; + auto const &reach { Model.spans[ fastening.part ] }; + auto const a { at + turn * fastening.placed( reach.x, fastening.scale ) }; + auto const b { at + turn * fastening.placed( reach.y, fastening.scale ) }; + taken.emplace_back( std::min( a, b ) - 0.5 * fastening_length - fastening_gap, std::max( a, b ) + 0.5 * fastening_length + fastening_gap ); + } + auto const end { 0.5 * fastening_length + fastening_gap }; + std::sort( stretches.begin(), stretches.end(), []( stretch const &Left, stretch const &Right ) { return Left.line != Right.line ? Left.line < Right.line : Left.from < Right.from; } ); + for( std::size_t first = 0; first < stretches.size(); ) { + // the stretches of one rail going on one from another + auto last { first }; + auto to { stretches[ first ].to }; + while( last + 1 < stretches.size() && stretches[ last + 1 ].line == stretches[ first ].line && stretches[ last + 1 ].from <= to + 0.01 ) { + to = std::max( to, stretches[ ++last ].to ); + } + std::vector free { glm::dvec2 { std::max( stretches[ first ].from, end ), std::min( to, length - end ) } }; + for( auto const &blocked : taken ) { + std::vector left; + for( auto const &room : free ) { + if( blocked.y <= room.x || blocked.x >= room.y ) { + left.push_back( room ); + continue; + } + if( blocked.x > room.x ) { + left.emplace_back( room.x, blocked.x ); + } + if( blocked.y < room.y ) { + left.emplace_back( blocked.y, room.y ); + } + } + free = std::move( left ); + } + auto const plate = [&]( double const At ) { + for( auto index { first }; index <= last; ++index ) { + auto const &piece { stretches[ index ] }; + if( At < piece.from - 1e-6 || At > piece.to + 1e-6 ) { + continue; + } + auto const &line { *piece.line }; + auto const u0 { glm::dot( line.points[ piece.index - 1 ] - start, axis ) }; + auto const u1 { glm::dot( line.points[ piece.index ] - start, axis ) }; + auto const share { std::abs( u1 - u0 ) > 1e-9 ? std::clamp( ( At - u0 ) / ( u1 - u0 ), 0.0, 1.0 ) : 0.0 }; + auto const offset { glm::dot( glm::mix( line.points[ piece.index - 1 ], line.points[ piece.index ], share ) - start, side ) }; + auto const inward { glm::normalize( glm::mix( line.inward[ piece.index - 1 ], line.inward[ piece.index ], share ) ) }; + auto const point { Beam.from + span * ( At / length ) + glm::dvec3 { side.x, 0.0, side.y } * offset }; + // with its check rail, or the guard rail of the crossing, in the fastening of both + auto const distance { glm::mix( line.distances[ piece.index - 1 ], line.distances[ piece.index ], share ) }; + if( Model.parts[ part_check ] != nullptr && ( line.at( distance ) == role::checked || line.guarded( distance ) ) ) { + Fastenings.push_back( { part_check, point, inward, 1.f, static_cast( 0.5 * check_seats ) } ); + } + else { + Fastenings.push_back( { part_plate, point, inward } ); + } + return; + } }; + for( auto const &room : free ) { + if( room.y < room.x ) { + continue; + } + if( room.y - room.x < 0.5 * pitch ) { + plate( 0.5 * ( room.x + room.y ) ); + continue; + } + auto const count { static_cast( std::ceil( ( room.y - room.x ) / pitch ) ) + 1 }; + for( int index = 0; index < count; ++index ) { + plate( glm::mix( room.x, room.y, static_cast( index ) / ( count - 1 ) ) ); + } + } + first = last + 1; + } +} + +std::vector fastened_rails::lying( beam const &Beam ) const { + + return rails_on( Beam ); +} + std::vector fastened_rails::bare( beam const &Beam, beam_model const &Model, std::vector const &Fastenings ) const { auto const span { plan( Beam.to - Beam.from ) }; @@ -807,10 +971,15 @@ std::vector check_rail_seats() { } auto const length { segment->GetLength() }; for( auto const side : { -1, 1 } ) { + // the check rails and the guard rails of the crossings at wide angles + auto spans { track->m_railguards[ path ][ side > 0 ? 1 : 0 ] }; for( auto const &check : checks[ path ][ side > 0 ? 1 : 0 ] ) { - check_line line { { track->name(), path, side, check.from, check.to } }; - auto const from { std::max( 0.0, check.from ) }; - auto const to { std::min( length, check.to ) }; + spans.emplace_back( check.from, check.to ); + } + for( auto const &check : spans ) { + check_line line { { track->name(), path, side, check.x, check.y } }; + auto const from { std::max( 0.0, check.x ) }; + auto const to { std::min( length, check.y ) }; auto const count { static_cast( std::ceil( ( to - from ) / fastening_step ) ) }; for( int step = 0; step <= count && to > from; ++step ) { glm::dvec3 point, across; diff --git a/world/SleeperFastenings.h b/world/SleeperFastenings.h index f6d22003..dc8d88da 100644 --- a/world/SleeperFastenings.h +++ b/world/SleeperFastenings.h @@ -45,13 +45,6 @@ struct fastening { class fastened_rails { public: - explicit fastened_rails( TTrack &Track ); - // the fastenings of the rails on the timber, of the parts the model has - std::vector on( beam const &Beam, beam_model const &Model ) const; - // where the rails on the timber lie over none of the fastenings - std::vector bare( beam const &Beam, beam_model const &Model, std::vector const &Fastenings ) const; - -private: // what a rail is where it lies on a timber enum class role { plain, @@ -59,6 +52,24 @@ private: stock, // the stock rail of a switch with the blade beside it checked // a rail with a check rail beside it }; + // where a rail crosses a timber + struct hit { + double at { 0.0 }; // along the timber, from its from end + role kind { role::plain }; + glm::dvec2 inward { 1.0, 0.0 }; + double foot { 0.0 }; + double rise { 0.0 }; + }; + + explicit fastened_rails( TTrack &Track ); + // the fastenings of the rails on the timber, of the parts the model has + std::vector on( beam const &Beam, beam_model const &Model ) const; + // the rails lying on the timber, in the order along it + std::vector lying( beam const &Beam ) const; + // where the rails on the timber lie over none of the fastenings + std::vector bare( beam const &Beam, beam_model const &Model, std::vector const &Fastenings ) const; + +private: // a rail by points along its middle struct rail { TTrack *track { nullptr }; @@ -76,15 +87,10 @@ private: glm::dvec2 blade { -1.0 }; glm::dvec2 stock { -1.0 }; std::vector checks; + std::vector guards; // with the guard rail of a crossing at a wide angle inside it + bool guard { false }; // the guard rail itself role at( double const Distance ) const; - }; - // where a rail crosses a timber - struct hit { - double at { 0.0 }; // along the timber, from its from end - role kind { role::plain }; - glm::dvec2 inward { 1.0, 0.0 }; - double foot { 0.0 }; - double rise { 0.0 }; + bool guarded( double const Distance ) const; }; // the rails lying on the timber, in the order along it, one where several lie on one another, but the blades @@ -95,6 +101,10 @@ private: // the fastenings under the raised rails: one holding them moved up on a pad as far as the least raised of them, a rail not raised // sunk in it, a rail raised more on a pad of its own on it static void lift( std::vector &Fastenings, std::vector const &Rails, beam const &Beam, beam_model const &Model ); + // the plates of the rails lying along the timber rather than across it, as of a track crossing another square to it, on two + // timbers side by side under each of its rails: as far apart as the beams at most, in the room the fastenings of the rails + // across leave, on the timber with the rail over its middle or over its side toward the next one + void along( std::vector &Fastenings, beam const &Beam, beam_model const &Model ) const; std::vector m_rails; }; diff --git a/world/SleeperLayout.cpp b/world/SleeperLayout.cpp index 23bca2fa..f22623f8 100644 --- a/world/SleeperLayout.cpp +++ b/world/SleeperLayout.cpp @@ -203,6 +203,12 @@ private: return result; } + // the sine of the angle the paths of a grid cross at, no less than of 30 degrees + static double grid_sine( feature const &Feature ) { + + return std::max( 0.5, std::sin( 2.0 * std::asin( Feature.sine ) ) ); + } + // the pins of the lines led along the path: the rails of a path crossing this one so wide it's laid on a grid of beams; elsewhere // the lines are spread evenly std::vector pins( sampled_path const &Path ) const { @@ -216,17 +222,46 @@ private: if( feature.paths[ side ] != &Path ) { continue; } - // under the rails of the other path, which runs across the beams + // under the middles of the rails of the other path, which runs across the beams auto const *other { feature.paths[ 1 - side ] }; - auto const sine { std::max( 0.5, std::sin( 2.0 * std::asin( feature.sine ) ) ) }; - result.push_back( feature.at[ side ] - 0.5 * other->gauge / sine ); - result.push_back( feature.at[ side ] + 0.5 * other->gauge / sine ); + auto const rail { ( 0.5 * other->gauge + rail_web ) / grid_sine( feature ) }; + result.push_back( feature.at[ side ] - rail ); + result.push_back( feature.at[ side ] + rail ); } } std::sort( result.begin(), result.end() ); return result; } + // whether the beam of the path at the distance is a timber of a crossing at a wide angle: where it crosses another path so wide + // they're laid as a grid, the other path laying none there, or under the guard rails of the crossing, as far as they reach + bool crossing_timber( sampled_path const &Path, double const Distance ) const { + + return std::any_of( features.begin(), features.end(), [&]( feature const &Feature ) { + if( Feature.frog ) { + return false; + } + auto const angle { 2.0 * std::asin( std::min( 1.0, Feature.sine ) ) }; + auto const acute { std::min( angle, glm::pi() - angle ) }; + for( int side = 0; side < 2; ++side ) { + if( Feature.paths[ side ] != &Path ) { + continue; + } + auto const *other { Feature.paths[ 1 - side ] }; + auto reach { Feature.grid ? ( other->half + half_width + end_gap ) / grid_sine( Feature ) : 0.0 }; + if( acute > track_rails::wide_angle ) { + // past where the rails cross, as far as the guard rails go on along them, a beam reaching under their ends too + reach = std::max( + reach, + ( 0.5 * other->gauge + 0.5 * Path.gauge * std::cos( acute ) + track_rails::flangeway + rail_head + track_rails::guard_reach ) / std::sin( acute ) + half_width ); + } + if( std::abs( Distance - Feature.at[ side ] ) < reach ) { + return true; + } + } + return false; } ); + } + // the line of the timbers carrying the path nearest to the distance on the side, between the distance and the reach from it std::optional record_near( std::size_t const Index, double const Distance, double const Side, double const Reach ) const { @@ -343,8 +378,10 @@ private: } std::vector anchors { start }; for( auto const pin : pins( path ) ) { - if( pin > start.at + 0.5 * pitch_min && pin < end.at - 0.5 * pitch_min && pin - anchors.back().at >= pitch_min ) { - anchors.push_back( { pin, 0.0 } ); + // two timbers side by side under the rail running along them, its fastenings on both + if( pin - half_width > start.at + 0.5 * pitch_min && pin + half_width < end.at - 0.5 * pitch_min && pin - half_width - anchors.back().at >= pitch_min ) { + anchors.push_back( { pin - half_width, 0.0 } ); + anchors.push_back( { pin + half_width, 0.0 } ); } } anchors.push_back( end ); @@ -515,7 +552,7 @@ private: auto const top { point - rolledup * static_cast( path.track->rail_height() ) }; // a sleeper of a plain track running alone and square to it, the rest are the timbers of the switches and crossings auto const carried { std::count_if( bands.begin(), bands.end(), []( band const &Band ) { return Band.carried; } ) }; - auto const plain { path.track->eType == tt_Normal && carried == 1 && pieces.size() == 1 && false == pieces.front().clipped && std::abs( glm::dot( forward, along ) ) > 0.9998 }; + auto const plain { path.track->eType == tt_Normal && carried == 1 && pieces.size() == 1 && false == pieces.front().clipped && std::abs( glm::dot( forward, along ) ) > 0.9998 && false == crossing_timber( path, Distance ) }; for( std::size_t index = 0; index < pieces.size(); ++index ) { auto const &piece { pieces[ index ] }; auto const &way { turned[ index ] }; diff --git a/world/SleeperRules.h b/world/SleeperRules.h index bba9b833..30267cfa 100644 --- a/world/SleeperRules.h +++ b/world/SleeperRules.h @@ -20,6 +20,9 @@ double constexpr pitch_min { 0.83 * pitch }; double constexpr pitch_max { 1.08 * pitch }; // how far out from the middle of a track its rail reaches past half the gauge: the foot of the rail outside the gauge line double constexpr rail_reach { 0.1115 }; +// how far out from the gauge line the middle of the rail is, and how wide its head is +double constexpr rail_web { 0.0365 }; +double constexpr rail_head { 0.0735 }; // how far a beam reaches out past the foot of the rail, 2.6 m sleepers under the standard gauge, and how far at least double constexpr overhang { 0.471 }; double constexpr overhang_min { 0.25 }; @@ -61,6 +64,8 @@ double constexpr fastening_nose { 0.05 }; double constexpr fastening_crowd { 0.3 }; // how far apart the rails lie in the fastening of both, the check part of the model double constexpr check_seats { 0.113 }; +// how long a fastening is along its rail, at most +double constexpr fastening_length { 0.19 }; // how far past the edge of the foot of a rail its fastening reaches, with the clip holding it double constexpr fastening_clip { 0.09 }; // the room left between the fastenings of rails close by, and how narrow their plates get to keep it diff --git a/world/Track.cpp b/world/Track.cpp index 9ca80383..82e8a954 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -1598,6 +1598,10 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { for( int path = 0; path < 2; ++path ) { for( int side = 0; side < 2; ++side ) { m_railplans[ path ][ side ] = layout.plans[ path ][ side ]; + m_railguards[ path ][ side ].clear(); + for( auto const &guard : layout.guards[ path ][ side ] ) { + m_railguards[ path ][ side ].emplace_back( guard.from, guard.to ); + } } } } diff --git a/world/Track.h b/world/Track.h index 9d18f06c..64426404 100644 --- a/world/Track.h +++ b/world/Track.h @@ -218,6 +218,7 @@ public: bool m_editorremoved { false }; std::vector m_crossings; // tracks whose rails cross the rails of this one, found when its geometry was made track_rails::rail_plan m_railplans[ 2 ][ 2 ]; // the cuts of its rails, by path and right side, as its geometry was made + std::vector m_railguards[ 2 ][ 2 ]; // the guard rails inside its rails at the crossings at wide angles, from and to along the path, as its geometry was made scene::basic_node *m_road { nullptr }; // road or road junction which generated this path. such path is one-way, and isn't saved nor edited on its own std::array m_routeweights { 0.f, 0.f, 0.f }; // odds of each route through a crossroads being picked by a driver, by the number of the route. all zero: even odds int iterate_stamp = 0; From f155956669b603c491e375534c51356c7f646763 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 24 Oct 2026 00:23:50 +0200 Subject: [PATCH 13/19] perf(sleepers): the rails of each track for the fastenings worked out once and kept until its geometry is made again, it goes far or the crossings change, not again for every track laid around it; the fastenings laid only out to 30 m past where they're drawn, the tracks further out keeping their beams to lay them on as they come near; out past 50 m the tracks laid while the laying in a frame took under 2 ms, the rest in the frames after --- rendering/opengl46/opengl46renderer.cpp | 27 +++- rendering/opengl46/opengl46renderer.h | 2 + world/SleeperFastenings.cpp | 185 ++++++++++++++---------- world/SleeperFastenings.h | 13 +- world/SleeperRules.h | 6 + world/Track.cpp | 1 + world/TrackSleepers.cpp | 95 ++++++++---- world/TrackSleepers.h | 10 +- 8 files changed, 225 insertions(+), 114 deletions(-) diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 992b43c7..8bd4c0fb 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -6203,6 +6203,12 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ m_timbersweepframe = m_framestamp; track_sleepers::release_far( camera, range + 600.0 ); } + if( m_timberlayframe != m_framestamp ) { + m_timberlayframe = m_framestamp; + m_timberlaying = 0.0; + } + // the fastenings laid out to a little past where they're drawn + auto const fasteningrange = std::min( static_cast( Global.SleeperDistance ), track_sleepers::fastening_range ) + track_sleepers::fastening_lead; glm::mat4 const originmodelview = OpenGLMatrices.data( GL_MODELVIEW ); for( auto &bucket : m_timberbuckets ) { bucket.instances.clear(); @@ -6243,15 +6249,28 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ for( auto first { First }; first != Last; ++first ) { auto *track = *first; if( false == track->m_visible || false == track->m_sleepers3d.active ) { continue; } - if( glm::length( track->location() - camera ) > range + track->radius() ) { + auto const trackdistance = glm::length( track->location() - camera ); + if( trackdistance > range + track->radius() ) { // worked out ahead, in the background, before it comes in range - if( false == shadows && track->m_sleepers3d.dirty && glm::length( track->location() - camera ) < range + track->radius() + 300.0 ) { + if( false == shadows && track->m_sleepers3d.dirty && trackdistance < range + track->radius() + 300.0 ) { track_sleepers::prefetch( *track ); } continue; } - if( track->m_sleepers3d.dirty ) { - track_sleepers::lay( *track ); + auto const fastened = ( trackdistance < fasteningrange + track->radius() ); + if( track->m_sleepers3d.dirty || ( fastened && false == track->m_sleepers3d.fastened ) ) { + // the nearest at once, the others while the frame has the time, the cells nearest coming first; till then a track draws + // what it had + if( trackdistance < track_sleepers::lay_near + track->radius() || m_timberlaying < track_sleepers::lay_budget ) { + auto const start = std::chrono::steady_clock::now(); + if( track->m_sleepers3d.dirty ) { + track_sleepers::lay( *track, fastened ); + } + else { + track_sleepers::fasten( *track ); + } + m_timberlaying += std::chrono::duration( std::chrono::steady_clock::now() - start ).count(); + } } auto const &laid = track->m_sleepers3d; if( laid.timbers == nullptr ) { continue; } diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index 20f90777..0c269640 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -610,6 +610,8 @@ class opengl46_renderer : public gfx_renderer { unsigned int m_framestamp; // id of currently rendered gfx frame unsigned int m_timbersweepframe { 0 }; // the frame the beams of the tracks far away were last let go in + unsigned int m_timberlayframe { 0 }; // the frame the time spent laying the beams is counted for + double m_timberlaying { 0.0 }; // in ms float m_framerate; double m_updateaccumulator{0.0}; std::string m_debugtimestext; diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp index dfae41f8..e047230d 100644 --- a/world/SleeperFastenings.cpp +++ b/world/SleeperFastenings.cpp @@ -52,90 +52,119 @@ fastened_rails::fastened_rails( TTrack &Track ) { if( bedded( *Other ) && std::find( tracks.begin(), tracks.end(), Other ) == tracks.end() ) { tracks.push_back( Other ); } } ); + auto &kept { fastened_rails::kept() }; for( auto *track : tracks ) { - auto const size { track->rail_dimensions() }; - auto const middle { 0.5 * std::abs( track->fTrackWidth ) + size.web }; - auto const cut { track_rails::cut_rails( *track ) }; - auto const switched { track->eType == tt_Switch && track->SwitchExtension != nullptr }; - auto const rise { wing_rise( *track ) }; - // what the rail is along the path: a blade, the stock rail of a blade, checked - auto const add = [&]( rail &&Line, int const Path, int const Side ) { - if( switched ) { - if( auto const heel { blade_heel( *track, Path, Side ) }; heel > 0.0 ) { - Line.blade = { 0.0, heel }; - } - Line.stock = stock_stretch( *track, Path, Side ); + auto &rails { kept[ track ] }; + if( rails == nullptr ) { + rails = std::make_shared const>( rails_of( *track ) ); + } + m_kept.push_back( rails ); + m_rails.insert( m_rails.end(), rails->begin(), rails->end() ); + } +} + +fastened_rails::kept_rails &fastened_rails::kept() { + + static kept_rails tracks; + static unsigned int stamp { 0 }; + if( stamp != track_rails::memo_stamp() ) { + tracks.clear(); + stamp = track_rails::memo_stamp(); + } + return tracks; +} + +void forget_rails( TTrack const &Track ) { + + fastened_rails::kept().erase( &Track ); +} + +std::vector fastened_rails::rails_of( TTrack &Track ) { + + std::vector result; + auto const size { Track.rail_dimensions() }; + auto const middle { 0.5 * std::abs( Track.fTrackWidth ) + size.web }; + auto const cut { track_rails::cut_rails( Track ) }; + auto const switched { Track.eType == tt_Switch && Track.SwitchExtension != nullptr }; + auto const rise { wing_rise( Track ) }; + // what the rail is along the path: a blade, the stock rail of a blade, checked + auto const add = [&]( rail &&Line, int const Path, int const Side ) { + if( switched ) { + if( auto const heel { blade_heel( Track, Path, Side ) }; heel > 0.0 ) { + Line.blade = { 0.0, heel }; } - if( cut ) { - for( auto const &check : track_rails::rail_checks( *track )[ Path ][ Side > 0 ? 1 : 0 ] ) { - Line.checks.emplace_back( check.from, check.to ); - } - Line.guards = track->m_railguards[ Path ][ Side > 0 ? 1 : 0 ]; + Line.stock = stock_stretch( Track, Path, Side ); + } + if( cut ) { + for( auto const &check : track_rails::rail_checks( Track )[ Path ][ Side > 0 ? 1 : 0 ] ) { + Line.checks.emplace_back( check.from, check.to ); } - m_rails.emplace_back( std::move( Line ) ); }; - // the rail of the side of the path from and to along it, by points the given way out from the middle of the path - auto const sampled = [&]( int const Path, int const Side, double const From, double const To, double const Middle, knots::line const &Offsets ) { - auto const *segment { path_segment( *track, Path ) }; - auto const count { std::max( 1, static_cast( std::ceil( ( To - From ) / fastening_step ) ) ) }; - rail line; - line.track = track; - line.path = Path; - line.side = Side; - line.foot = size.web - size.low; - line.low = glm::dvec2 { std::numeric_limits::max() }; - line.high = glm::dvec2 { std::numeric_limits::lowest() }; - for( int step = 0; step <= count; ++step ) { - auto const distance { From + ( To - From ) * step / count }; - glm::dvec3 point, across; - segment->plan_frame( distance, point, across ); - auto const flat { plan( across ) }; - auto const inward { glm::length( flat ) > 1e-9 ? glm::normalize( flat ) * static_cast( -Side ) : glm::dvec2 { 1.0, 0.0 } }; - auto const position { plan( point ) - inward * ( Middle + knots::sample( Offsets, distance, 0.0 ) ) }; - line.points.push_back( position ); - line.heights.push_back( point.y ); - line.inward.push_back( inward ); - line.distances.push_back( distance ); - line.low = glm::min( line.low, position ); - line.high = glm::max( line.high, position ); - } - return line; }; - for( int path = 0; path < path_count( *track ); ++path ) { - auto const *segment { path_segment( *track, path ) }; - if( segment == nullptr ) { - continue; - } - auto const length { segment->GetLength() }; - for( auto const side : { -1, 1 } ) { - // the rail in its parts between the cuts at the crossings, as they're laid: a wing rail bent out from the path; on over the - // gap at an obtuse crossing, where its check rail lies - auto cuts { cut ? track->m_railplans[ path ][ side > 0 ].cuts : std::vector {} }; - std::stable_sort( cuts.begin(), cuts.end(), []( track_rails::rail_cut const &A, track_rails::rail_cut const &B ) { return A.at < B.at; } ); - for( std::size_t part = 0; part <= cuts.size(); ++part ) { - auto const *before { part > 0 ? &cuts[ part - 1 ] : nullptr }; - auto const *after { part < cuts.size() ? &cuts[ part ] : nullptr }; - auto const from { std::max( 0.0, before != nullptr ? before->start : 0.0 ) }; - auto const to { std::min( length, after != nullptr ? std::max( after->end, after->start ) : length ) }; - if( to - from < fastening_merge ) { - continue; - } - auto line { sampled( path, side, from, to, middle, track_rails::part_offsets( before, after ) ) }; - line.rises = track_rails::part_rises( rise > 0.0 ? track_rails::wing_shares( before, after, stock_stretch( *track, path, side ) ) : knots::line {}, rise, blade_heel( *track, path, side ) ); - add( std::move( line ), path, side ); + Line.guards = Track.m_railguards[ Path ][ Side > 0 ? 1 : 0 ]; + } + result.emplace_back( std::move( Line ) ); }; + // the rail of the side of the path from and to along it, by points the given way out from the middle of the path + auto const sampled = [&]( int const Path, int const Side, double const From, double const To, double const Middle, knots::line const &Offsets ) { + auto const *segment { path_segment( Track, Path ) }; + auto const count { std::max( 1, static_cast( std::ceil( ( To - From ) / fastening_step ) ) ) }; + rail line; + line.track = &Track; + line.path = Path; + line.side = Side; + line.foot = size.web - size.low; + line.low = glm::dvec2 { std::numeric_limits::max() }; + line.high = glm::dvec2 { std::numeric_limits::lowest() }; + for( int step = 0; step <= count; ++step ) { + auto const distance { From + ( To - From ) * step / count }; + glm::dvec3 point, across; + segment->plan_frame( distance, point, across ); + auto const flat { plan( across ) }; + auto const inward { glm::length( flat ) > 1e-9 ? glm::normalize( flat ) * static_cast( -Side ) : glm::dvec2 { 1.0, 0.0 } }; + auto const position { plan( point ) - inward * ( Middle + knots::sample( Offsets, distance, 0.0 ) ) }; + line.points.push_back( position ); + line.heights.push_back( point.y ); + line.inward.push_back( inward ); + line.distances.push_back( distance ); + line.low = glm::min( line.low, position ); + line.high = glm::max( line.high, position ); + } + return line; }; + for( int path = 0; path < path_count( Track ); ++path ) { + auto const *segment { path_segment( Track, path ) }; + if( segment == nullptr ) { + continue; + } + auto const length { segment->GetLength() }; + for( auto const side : { -1, 1 } ) { + // the rail in its parts between the cuts at the crossings, as they're laid: a wing rail bent out from the path; on over the + // gap at an obtuse crossing, where its check rail lies + auto cuts { cut ? Track.m_railplans[ path ][ side > 0 ].cuts : std::vector {} }; + std::stable_sort( cuts.begin(), cuts.end(), []( track_rails::rail_cut const &A, track_rails::rail_cut const &B ) { return A.at < B.at; } ); + for( std::size_t part = 0; part <= cuts.size(); ++part ) { + auto const *before { part > 0 ? &cuts[ part - 1 ] : nullptr }; + auto const *after { part < cuts.size() ? &cuts[ part ] : nullptr }; + auto const from { std::max( 0.0, before != nullptr ? before->start : 0.0 ) }; + auto const to { std::min( length, after != nullptr ? std::max( after->end, after->start ) : length ) }; + if( to - from < fastening_merge ) { + continue; } - // the guard rails of the crossings at wide angles inside the rail, the flangeway from it - for( auto const &guard : cut ? track->m_railguards[ path ][ side > 0 ] : std::vector {} ) { - auto const from { std::max( 0.0, guard.x ) }; - auto const to { std::min( length, guard.y ) }; - if( to - from < fastening_merge ) { - continue; - } - auto line { sampled( path, side, from, to, middle - ( track_rails::flangeway + size.head ), {} ) }; - line.guard = true; - m_rails.emplace_back( std::move( line ) ); + auto line { sampled( path, side, from, to, middle, track_rails::part_offsets( before, after ) ) }; + line.rises = track_rails::part_rises( rise > 0.0 ? track_rails::wing_shares( before, after, stock_stretch( Track, path, side ) ) : knots::line {}, rise, blade_heel( Track, path, side ) ); + add( std::move( line ), path, side ); + } + // the guard rails of the crossings at wide angles inside the rail, the flangeway from it + for( auto const &guard : cut ? Track.m_railguards[ path ][ side > 0 ] : std::vector {} ) { + auto const from { std::max( 0.0, guard.x ) }; + auto const to { std::min( length, guard.y ) }; + if( to - from < fastening_merge ) { + continue; } + auto line { sampled( path, side, from, to, middle - ( track_rails::flangeway + size.head ), {} ) }; + line.guard = true; + result.emplace_back( std::move( line ) ); } } } + return result; } std::vector fastened_rails::rails_on( beam const &Beam ) const { @@ -150,7 +179,7 @@ std::vector fastened_rails::rails_on( beam const &Beam ) co auto const low { glm::min( start, plan( Beam.to ) ) }; auto const high { glm::max( start, plan( Beam.to ) ) }; std::vector hits; - for( auto const &line : m_rails ) { + for( rail const &line : m_rails ) { if( line.high.x < low.x || line.low.x > high.x || line.high.y < low.y || line.low.y > high.y ) { continue; } @@ -387,7 +416,7 @@ void fastened_rails::along( std::vector &Fastenings, beam const &Beam double to; }; std::vector stretches; - for( auto const &line : m_rails ) { + for( rail const &line : m_rails ) { // a guard rail along the timber lies in the fastening of its running rail if( line.guard || line.high.x < low.x || line.low.x > high.x || line.high.y < low.y || line.low.y > high.y ) { continue; diff --git a/world/SleeperFastenings.h b/world/SleeperFastenings.h index dc8d88da..41c1ee80 100644 --- a/world/SleeperFastenings.h +++ b/world/SleeperFastenings.h @@ -10,7 +10,10 @@ http://mozilla.org/MPL/2.0/. #pragma once #include +#include +#include #include +#include #include #include @@ -92,6 +95,13 @@ private: role at( double const Distance ) const; bool guarded( double const Distance ) const; }; + // the rails of each track, worked out as the first track around it is laid and kept until it's laid anew or goes far, or + // the crossings change + using kept_rails = std::unordered_map const>>; + static kept_rails &kept(); + friend void forget_rails( TTrack const &Track ); + // the rails of the track, in their parts between the cuts, and its guard rails + static std::vector rails_of( TTrack &Track ); // the rails lying on the timber, in the order along it, one where several lie on one another, but the blades std::vector rails_on( beam const &Beam ) const; @@ -106,7 +116,8 @@ private: // across leave, on the timber with the rail over its middle or over its side toward the next one void along( std::vector &Fastenings, beam const &Beam, beam_model const &Model ) const; - std::vector m_rails; + std::vector const>> m_kept; + std::vector> m_rails; }; // the fastenings of a track with them in the model of its timbers, as laid on the timbers laid with it diff --git a/world/SleeperRules.h b/world/SleeperRules.h index 30267cfa..34e85b47 100644 --- a/world/SleeperRules.h +++ b/world/SleeperRules.h @@ -87,6 +87,12 @@ double constexpr pad_narrowest { 0.02 }; double constexpr fastening_overlap { 0.01 }; // how far from the camera the fastenings are drawn at most double constexpr fastening_range { 120.0 }; +// how much further out the fastenings are laid, there as the camera comes near +double constexpr fastening_lead { 30.0 }; +// how near the camera the beams of a track are laid at once as they're needed; further out while the laying in a frame took less +// than the budget, in ms, the rest left for the frames after +double constexpr lay_near { 50.0 }; +double constexpr lay_budget { 2.0 }; // how many tracks a cluster takes at most std::size_t constexpr cluster_cap { 256 }; diff --git a/world/Track.cpp b/world/Track.cpp index 82e8a954..00c8a3e9 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -1604,6 +1604,7 @@ void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { } } } + track_sleepers::forget_rails( *this ); } auto const lay_rail = [&]( int const Path, gfx::vertex_array const &Profile, int const Side, float const Texturelength, double const From, bool const Openstart, bool const Openend, geometryhandle_sequence &Geometry, geometryhandle_sequence &Cuts ) { return this->lay_rail( layout, drawing, Path, Profile, Side, Texturelength, From, Openstart, Openend, Geometry, Cuts ); }; diff --git a/world/TrackSleepers.cpp b/world/TrackSleepers.cpp index 4f3502d2..f819d8c1 100644 --- a/world/TrackSleepers.cpp +++ b/world/TrackSleepers.cpp @@ -123,29 +123,22 @@ std::pair shared_zone( TTrack const &Track, int const Path ) { return { zones.front().first, zones.back().second }; } -void lay( TTrack &Track ) { +namespace { + +// the beams as instances of the parts of the model of the track: those of the fastenings with them, or left for later and the beams +// kept for them meanwhile +void instance( TTrack &Track, std::vector &&Beams, bool const Fastened ) { auto &laid { Track.m_sleepers3d }; - std::vector placed; - if( laid.active && in_background() && timber_model( Track ) != nullptr ) { - // worked out in the background; till then the track keeps what it has, and tries again - auto beams { laid_out( Track ) }; - if( false == beams.has_value() ) { - ask_layout( Track ); - return; - } - placed = std::move( *beams ); - } - laid.dirty = false; for( auto &instances : laid.instances ) { instances.clear(); } laid.whole.clear(); laid.plain.clear(); laid.chunks.clear(); - laid.timbers = timber_model( Track ); - laid.plains = plain_model( Track ); - if( false == laid.active || laid.timbers == nullptr ) { + laid.fastened = true; + std::vector {}.swap( laid.beams ); + if( laid.timbers == nullptr ) { return; } // the sleepers of the model of the track itself (sleepermodel) at the height they're drawn at without the 3D sleepers: the @@ -153,16 +146,6 @@ void lay( TTrack &Track ) { auto const ownplains { laid.plains != nullptr && Track.m_sleeper_enabled && laid.plains != plain_model() }; auto const plainlift { ownplains ? Track.rail_height() + Track.m_sleeper_offset.z : 0.f }; auto const &parts { *laid.timbers }; - if( auto const panel { panel_off_plane( Track ) }; panel.first > coplanar_tolerance ) { - WriteLog( - "sleepers3d: switch \"" + Track.name() + "\" has its paths " + to_string( panel.first * 1000.0, 1 ) + " mm off one plane " - + to_string( panel.second, 2 ) + " m along, where they share the timbers of its panel" ); - } - laid.laying = true; - auto const beams { in_background() ? std::move( placed ) : place( Track ) }; - laid.laying = false; - // the layout of the cluster isn't needed by this track any more, the instances below are what's drawn - forget_layout( Track ); // the rails around, where they lie on the timbers, for the fastenings of the model; found at the first timber, none for plain sleepers std::optional rails; @@ -199,7 +182,7 @@ void lay( TTrack &Track ) { runbeams.clear(); }; - for( auto const &beam : beams ) { + for( auto const &beam : Beams ) { if( runbeams.empty() ) { for( int part = 0; part < part_count; ++part ) { run.first[ part ] = static_cast( laid.instances[ part ].size() ); @@ -265,7 +248,7 @@ void lay( TTrack &Track ) { put( part_end, scale, parts.spans[ part_end ].x, -half ); put( part_begin, scale, parts.spans[ part_begin ].y, half ); } - if( parts.fastened() && false == ( beam.plain && laid.plains != nullptr ) ) { + if( Fastened && parts.fastened() && false == ( beam.plain && laid.plains != nullptr ) ) { if( false == rails.has_value() ) { rails.emplace( Track ); } @@ -296,6 +279,55 @@ void lay( TTrack &Track ) { laid.whole.shrink_to_fit(); laid.plain.shrink_to_fit(); laid.chunks.shrink_to_fit(); + if( false == Fastened && parts.fastened() ) { + laid.fastened = false; + laid.beams = std::move( Beams ); + } +} + +} // namespace + +void lay( TTrack &Track, bool const Fastened ) { + + auto &laid { Track.m_sleepers3d }; + std::vector placed; + if( laid.active && in_background() && timber_model( Track ) != nullptr ) { + // worked out in the background; till then the track keeps what it has, and tries again + auto beams { laid_out( Track ) }; + if( false == beams.has_value() ) { + ask_layout( Track ); + return; + } + placed = std::move( *beams ); + } + laid.dirty = false; + laid.timbers = timber_model( Track ); + laid.plains = plain_model( Track ); + if( false == laid.active || laid.timbers == nullptr ) { + instance( Track, {}, true ); + return; + } + if( auto const panel { panel_off_plane( Track ) }; panel.first > coplanar_tolerance ) { + WriteLog( + "sleepers3d: switch \"" + Track.name() + "\" has its paths " + to_string( panel.first * 1000.0, 1 ) + " mm off one plane " + + to_string( panel.second, 2 ) + " m along, where they share the timbers of its panel" ); + } + laid.laying = true; + auto beams { in_background() ? std::move( placed ) : place( Track ) }; + laid.laying = false; + // the layout of the cluster isn't needed by this track any more, the instances below are what's drawn + forget_layout( Track ); + instance( Track, std::move( beams ), Fastened ); +} + +void fasten( TTrack &Track ) { + + auto &laid { Track.m_sleepers3d }; + if( laid.fastened || laid.dirty ) { + return; + } + auto beams { std::move( laid.beams ) }; + instance( Track, std::move( beams ), true ); } void release_far( glm::dvec3 const &Camera, double const Range ) { @@ -309,15 +341,19 @@ void release_far( glm::dvec3 const &Camera, double const Range ) { next = 0; } auto *track { tracks[ next++ ] }; - if( track == nullptr || false == track->m_sleepers3d.active || track->m_sleepers3d.laying ) { + if( track == nullptr || track->m_sleepers3d.laying ) { continue; } if( glm::length( track->location() - Camera ) - track->radius() <= Range ) { continue; } + forget_rails( *track ); + if( false == track->m_sleepers3d.active ) { + continue; + } auto &laid { track->m_sleepers3d }; forget_layout( *track ); - auto const holds { laid.chunks.capacity() > 0 || laid.whole.capacity() > 0 || laid.plain.capacity() > 0 + auto const holds { laid.chunks.capacity() > 0 || laid.whole.capacity() > 0 || laid.plain.capacity() > 0 || laid.beams.capacity() > 0 || std::any_of( laid.instances.begin(), laid.instances.end(), []( std::vector const &Instances ) { return Instances.capacity() > 0; } ) }; if( false == holds ) { continue; @@ -328,6 +364,7 @@ void release_far( glm::dvec3 const &Camera, double const Range ) { std::vector {}.swap( laid.whole ); std::vector {}.swap( laid.plain ); std::vector {}.swap( laid.chunks ); + std::vector {}.swap( laid.beams ); laid.dirty = true; } } diff --git a/world/TrackSleepers.h b/world/TrackSleepers.h index 1e3c831b..ba9772e2 100644 --- a/world/TrackSleepers.h +++ b/world/TrackSleepers.h @@ -103,6 +103,8 @@ struct laid { std::vector whole; // transforms of the timbers of a model without parts, drawn whole std::vector plain; // transforms of the plain sleepers, whole models, relative to the origin of the track std::vector chunks; + bool fastened { false }; // the fastenings are laid, else they're laid as the track comes near where they're drawn + std::vector beams; // kept for the fastenings till they're laid }; // how far the ballast of a track with beams is lowered, so they stick out of it @@ -147,10 +149,14 @@ struct pack_model { // 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 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 track placed anew, as instances of the parts of the model; with the fastenings, or those left for later +void lay( TTrack &Track, bool const Fastened = true ); +// the fastenings left out laid on the beams of the track +void fasten( 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 rails of the track kept for the fastenings of the tracks around let go, as they're laid anew or the track goes far +void forget_rails( TTrack const &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 ); From 85b76acca54f6f8aa8773072d0cf23ad1ee6fb4f Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 24 Oct 2026 01:40:34 +0200 Subject: [PATCH 14/19] perf(sleepers): the instances of the sleepers and timbers of a track sent to the gpu once, as it's laid, and kept there in a buffer of their own; in each pass the runs of them in sight turned by the modelviews of their cells into the instance buffer of the frame by a compute pass, one dispatch and one barrier a pass, and drawn in one instanced draw for each model, part and level of detail, in place of the cpu turning every instance every frame --- CMakeLists.txt | 1 + gl/glsl_common.cpp | 5 + gl/streambuffer.cpp | 16 +- gl/streambuffer.h | 2 + gl/ubo.h | 18 +++ rendering/opengl46/opengl46renderer.cpp | 204 ++++++++++++++---------- rendering/opengl46/opengl46renderer.h | 21 ++- rendering/opengl46/opengl46timbers.cpp | 152 ++++++++++++++++++ rendering/opengl46/opengl46timbers.h | 66 ++++++++ shaders/timber_instances.comp | 46 ++++++ world/TrackSleepers.cpp | 4 + world/TrackSleepers.h | 1 + 12 files changed, 444 insertions(+), 92 deletions(-) create mode 100644 rendering/opengl46/opengl46timbers.cpp create mode 100644 rendering/opengl46/opengl46timbers.h create mode 100644 shaders/timber_instances.comp diff --git a/CMakeLists.txt b/CMakeLists.txt index e53d4aa5..f856e705 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -393,6 +393,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl46/opengl46localshadows.cpp" "rendering/opengl46/opengl46instancecull.cpp" "rendering/opengl46/opengl46hiz.cpp" + "rendering/opengl46/opengl46timbers.cpp" "rendering/opengl46/opengl46shadowfit.cpp" "rendering/opengl46/opengl46precipitation.cpp" "rendering/opengl46/opengl46particles.cpp" diff --git a/gl/glsl_common.cpp b/gl/glsl_common.cpp index 16fcb3f3..3fa04174 100644 --- a/gl/glsl_common.cpp +++ b/gl/glsl_common.cpp @@ -29,6 +29,11 @@ void gl::glsl_common_setup() "#define HIZ_GROUP_SIZE " + std::to_string(HIZ_GROUP_SIZE) + "\n" + "#define SHADOW_FIT_GROUP_SIZE " + std::to_string(SHADOW_FIT_GROUP_SIZE) + "\n" + "#define SHADOW_FIT_SSBO_BINDING " + std::to_string(SHADOW_FIT_SSBO_BINDING) + "\n" + + "#define TIMBER_GROUP_SIZE " + std::to_string(TIMBER_GROUP_SIZE) + "\n" + + "#define TIMBER_LOCAL_SSBO_BINDING " + std::to_string(TIMBER_LOCAL_SSBO_BINDING) + "\n" + + "#define TIMBER_RANGE_SSBO_BINDING " + std::to_string(TIMBER_RANGE_SSBO_BINDING) + "\n" + + "#define TIMBER_OUTPUT_SSBO_BINDING " + std::to_string(TIMBER_OUTPUT_SSBO_BINDING) + "\n" + + "#define TIMBER_ORIGIN_SSBO_BINDING " + std::to_string(TIMBER_ORIGIN_SSBO_BINDING) + "\n" + "#define PARTICLE_GROUP_SIZE " + std::to_string(PARTICLE_GROUP_SIZE) + "\n" + "#define PARTICLE_SSBO_BINDING " + std::to_string(PARTICLE_SSBO_BINDING) + "\n" + "#define PARTICLE_SOURCE_SSBO_BINDING " + std::to_string(PARTICLE_SOURCE_SSBO_BINDING) + "\n" + diff --git a/gl/streambuffer.cpp b/gl/streambuffer.cpp index 38c6dbe2..070723aa 100644 --- a/gl/streambuffer.cpp +++ b/gl/streambuffer.cpp @@ -141,6 +141,17 @@ void gl::frame_stream::next_frame() } GLintptr gl::frame_stream::push(void const *Data, GLsizeiptr Size) +{ + auto const offset{reserve(Size)}; + auto const ¤t{m_slots[m_slot]}; + if (current.data != nullptr) + std::memcpy(current.data + offset, Data, static_cast(Size)); + else + glNamedBufferSubData(current.buffer, offset, Size, Data); + return offset; +} + +GLintptr gl::frame_stream::reserve(GLsizeiptr Size) { auto *current{&m_slots[m_slot]}; auto offset{align_up(m_fill, m_alignment)}; @@ -167,13 +178,8 @@ GLintptr gl::frame_stream::push(void const *Data, GLsizeiptr Size) glFinish(); ++m_waits; } - current = &m_slots[m_slot]; offset = m_start; } - if (current->data != nullptr) - std::memcpy(current->data + offset, Data, static_cast(Size)); - else - glNamedBufferSubData(current->buffer, offset, Size, Data); m_fill = offset + Size; return offset; } diff --git a/gl/streambuffer.h b/gl/streambuffer.h index e8363750..51efefca 100644 --- a/gl/streambuffer.h +++ b/gl/streambuffer.h @@ -34,6 +34,8 @@ namespace gl // bigger one (generation() changes, buffer() tells which to bind); the old one stays until the frame is done with, so // what was written to it can still be bound and drawn from later in the frame GLintptr push(void const *Data, GLsizeiptr Size); + // the room for Size bytes in the buffer of the frame, as push() makes it, left for the gpu to write (a compute pass) + GLintptr reserve(GLsizeiptr Size); GLuint buffer() const { diff --git a/gl/ubo.h b/gl/ubo.h index a070b78f..502665fe 100644 --- a/gl/ubo.h +++ b/gl/ubo.h @@ -506,6 +506,24 @@ namespace gl const GLuint PARTICLE_OUTPUT_SSBO_BINDING = 12; // what the draw reads, a particle_draw per particle drawn const GLuint PARTICLE_COMMAND_SSBO_BINDING = 13; // the indirect draw command, whose instances the passes count + // the instances of the timbers of the tracks (opengl46_timbers, timber_instances.comp): work group of the pass and the binding + // points of its buffers, ones of the instance culling, which binds its buffers before every use; the pass binds its own + const GLuint TIMBER_GROUP_SIZE = 64; + const GLuint TIMBER_LOCAL_SSBO_BINDING = 9; // the transforms of the instances of the tracks, relative to the origins of the tracks + const GLuint TIMBER_RANGE_SSBO_BINDING = 11; // the runs of them drawn, a timber_range each + const GLuint TIMBER_OUTPUT_SSBO_BINDING = 12; // the instance buffer of the frame, their modelviews written there + const GLuint TIMBER_ORIGIN_SSBO_BINDING = 13; // the modelviews of the origins of the cells of the runs + + // layout std430: GLSL timber_range. a run of instances: count of them from source in the transforms, to target in the output, + // turned by the modelview of origin + struct timber_range + { + uint32_t source; + uint32_t count; + uint32_t target; + uint32_t origin; + }; + // layout std430: GLSL smoke_particle_s struct particle_smoke { diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 8bd4c0fb..73c1809e 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -256,6 +256,7 @@ bool opengl46_renderer::Init(GLFWwindow *Window) shadowcull.init(&m_instancecull); m_reflectioncull.init(&m_instancecull); m_hiz.init(); + m_timbers.init(); m_shadowfit.init(); m_exposure.init(); m_ao.init(); @@ -739,6 +740,7 @@ bool opengl46_renderer::Render() // the same for the model blocks of the draws m_instancestream->next_frame(); Bind_Instance_Stream(); + m_timbers.next_frame(); model_ubo->next_frame(); if (m_drawblockstream) m_drawblockstream->next_frame(); @@ -3716,6 +3718,8 @@ void opengl46_renderer::Render(cell_sequence::iterator First, cell_sequence::ite ++First; } + // the sleepers and timbers of the tracks of all the cells at once + Flush_Timbers(); // second pass draws elements with their own vbos while (first != Last) { @@ -4413,6 +4417,16 @@ std::uint32_t opengl46_renderer::Upload_Instance_Modelviews( glm::mat4 const *Mo return static_cast( offset / static_cast( sizeof( glm::mat4 ) ) ); } +std::uint32_t opengl46_renderer::Reserve_Instance_Modelviews( std::size_t const Count ) +{ + auto const generation { m_instancestream->generation() }; + auto const offset { m_instancestream->reserve( static_cast( Count * sizeof( glm::mat4 ) ) ) }; + if( m_instancestream->generation() != generation ) { + Bind_Instance_Stream(); + } + return static_cast( offset / static_cast( sizeof( glm::mat4 ) ) ); +} + void opengl46_renderer::Bind_Instance_Stream() const { GLuint const buffer { m_instancestream->buffer() }; @@ -6186,10 +6200,11 @@ void opengl46_renderer::Render_Sleepers( TTrack *Track ) } } -// Renders the sleepers and switch timbers of a run of tracks of a cell. Each beam is made of instances of the parts of a model or -// of a whole model, kept by its track relative to the origin of the track. The runs of beams in range and in sight are gathered -// for the whole run of tracks by the model and its part or its level of detail, and each is drawn in one instanced batch. -// In the shadow pass only those near the camera are drawn, their shadows falling where they can be seen. +// Gathers the sleepers and switch timbers of a run of tracks of a cell. Each beam is made of instances of the parts of a model or +// of a whole model, kept by its track relative to the origin of the track, on the cpu and on the gpu (m_timbers). The runs of beams +// in range and in sight are gathered by the model and its part or its level of detail, for all the cells of the pass, which +// Flush_Timbers() draws each in one instanced batch. In the shadow pass only those near the camera are drawn, their shadows falling +// where they can be seen. void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last ) { if( Global.SleeperDistance <= 0.f ) { return; } @@ -6209,19 +6224,15 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ } // the fastenings laid out to a little past where they're drawn auto const fasteningrange = std::min( static_cast( Global.SleeperDistance ), track_sleepers::fastening_range ) + track_sleepers::fastening_lead; - glm::mat4 const originmodelview = OpenGLMatrices.data( GL_MODELVIEW ); - for( auto &bucket : m_timberbuckets ) { - bucket.instances.clear(); - } - auto const bucket_for = [&]( track_sleepers::beam_model const *Model, int const Part, float const Band ) -> std::vector & { + auto const bucket_for = [&]( track_sleepers::beam_model const *Model, int const Part, float const Band ) -> timber_bucket & { for( auto &bucket : m_timberbuckets ) { if( bucket.model == Model && bucket.part == Part && bucket.band == Band ) { - return bucket.instances; + return bucket; } } - m_timberbuckets.push_back( { Model, Part, Band, {} } ); - return m_timberbuckets.back().instances; }; - // the level of detail of the whole model a beam is seen at, from its distance + m_timberbuckets.push_back( { Model, Part, Band, {}, 0, 0 } ); + return m_timberbuckets.back(); }; + // the level of detail of the whole model a run of beams is seen at, from its distance auto const band_of = []( track_sleepers::beam_model const &Model, double const Distance ) { auto const squaredistance { static_cast( Distance * Distance ) }; auto band { Model.bands.empty() ? 0.f : Model.bands.front() }; @@ -6231,21 +6242,7 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ } } return band; }; - // the beams of a run made of a whole model, each at the level of detail of its own distance; the run is culled as one - auto const put_by_band = [&]( track_sleepers::beam_model const &Model, std::vector const &Transforms, std::uint32_t const From, std::uint32_t const To, glm::dvec3 const &Origin ) { - std::vector *instances { nullptr }; - auto current { 0.f }; - for( auto index = From; index < To; ++index ) { - auto const &local = Transforms[ index ]; - auto const band = band_of( Model, glm::length( Origin + glm::dvec3( glm::vec3( local[ 3 ] ) ) - camera ) ); - if( instances == nullptr || band != current ) { - // looked up again only as the band changes; a bucket added on the way moves the others, so the pointer goes with it - instances = &bucket_for( &Model, -1, band ); - current = band; - } - instances->emplace_back( originmodelview * local ); - } - }; + m_timbertracks.clear(); for( auto first { First }; first != Last; ++first ) { auto *track = *first; if( false == track->m_visible || false == track->m_sleepers3d.active ) { continue; } @@ -6272,10 +6269,33 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ m_timberlaying += std::chrono::duration( std::chrono::steady_clock::now() - start ).count(); } } + if( track->m_sleepers3d.timbers == nullptr || track->m_sleepers3d.chunks.empty() ) { continue; } + m_timbertracks.push_back( track ); + } + if( m_timbertracks.empty() ) { return; } + // the instances of the tracks on the gpu; those of the cells before expanded first when the buffer is to be started over + if( false == m_timbers.place( m_timbertracks, false ) ) { + Flush_Timbers(); + m_timbers.place( m_timbertracks, true ); + } + // the runs of them in range and in sight, turned by the modelview of the cell + auto const origin { static_cast( m_timberorigins.size() ) }; + m_timberorigins.push_back( OpenGLMatrices.data( GL_MODELVIEW ) ); + // a run of instances of a track for the bucket; the reference to it goes with the next bucket added + auto const add_run = [&]( timber_bucket &Bucket, std::uint32_t const Source, std::uint32_t const Count ) { + Bucket.runs.push_back( { Source, Count, 0, origin } ); + Bucket.count += Count; }; + for( auto *track : m_timbertracks ) { auto const &laid = track->m_sleepers3d; - if( laid.timbers == nullptr ) { continue; } - // the transforms of the beams are relative to the origin of the track, which the modelview is translated to already - auto const &trackmodelview = originmodelview; + // where the instances of the parts, of the whole timbers and of the plain sleepers of the track start on the gpu + std::array partfirst; + auto next { m_timbers.first( *track ) }; + for( int part = 0; part < track_sleepers::part_count; ++part ) { + partfirst[ part ] = next; + next += static_cast( laid.instances[ part ].size() ); + } + auto const wholefirst { next }; + auto const plainfirst { wholefirst + static_cast( laid.whole.size() ) }; for( auto const &chunk : laid.chunks ) { auto const centre = track->m_origin + glm::dvec3( chunk.centre ); auto const distance = glm::length( centre - camera ); @@ -6286,66 +6306,86 @@ void opengl46_renderer::Render_Timbers( scene::basic_cell::path_sequence::const_ if( chunk.last[ part ] <= chunk.first[ part ] ) { continue; } // the fastenings only near, they're small if( part >= track_sleepers::timber_parts && distance > std::min( range, track_sleepers::fastening_range ) + chunk.radius ) { continue; } - auto &instances = bucket_for( laid.timbers, part, 0.f ); - for( auto index = chunk.first[ part ]; index < chunk.last[ part ]; ++index ) { - instances.emplace_back( trackmodelview * laid.instances[ part ][ index ] ); + add_run( bucket_for( laid.timbers, part, 0.f ), partfirst[ part ] + chunk.first[ part ], chunk.last[ part ] - chunk.first[ part ] ); + } + } + // the beams made of a whole model at the level of detail of the distance of the run + if( chunk.wholelast > chunk.wholefirst ) { + add_run( bucket_for( laid.timbers, -1, band_of( *laid.timbers, distance ) ), wholefirst + chunk.wholefirst, chunk.wholelast - chunk.wholefirst ); + } + if( laid.plains != nullptr && chunk.plainlast > chunk.plainfirst ) { + add_run( bucket_for( laid.plains, -1, band_of( *laid.plains, distance ) ), plainfirst + chunk.plainfirst, chunk.plainlast - chunk.plainfirst ); + } + } + } +} + +// the runs of the sleepers and timbers gathered by Render_Timbers() over the cells of the pass made into modelviews on the gpu at +// once, and each bucket drawn in one instanced batch +void opengl46_renderer::Flush_Timbers() +{ + std::size_t total { 0 }; + for( auto const &bucket : m_timberbuckets ) { + total += bucket.count; + } + if( total > 0 ) { + // in the instance buffer of the frame bucket after bucket, as in Render_Sleepers(), which the recorded walk of the depth + // pass keeps for its replay as well + auto target { Reserve_Instance_Modelviews( total ) }; + m_timberranges.clear(); + for( auto &bucket : m_timberbuckets ) { + bucket.first = target; + for( auto const &run : bucket.runs ) { + m_timberranges.push_back( { run.source, run.count, target, run.origin } ); + target += run.count; + } + } + m_timbers.expand( m_timberorigins, m_timberranges, m_instancestream->buffer() ); + auto const alpha = ( 0x30300030 ^ 0x0F0F000F ); + for( auto &bucket : m_timberbuckets ) { + if( bucket.count == 0 ) { continue; } + auto const &beammodel = *bucket.model; + auto *model = beammodel.model; + if( bucket.part >= 0 ) { + // only the part of the model is visible, its siblings are skipped as the model is walked + for( int part = 0; part < track_sleepers::part_count; ++part ) { + if( beammodel.parts[ part ] != nullptr ) { + beammodel.parts[ part ]->iVisible = ( part == bucket.part ? 1 : 0 ); } } } - if( chunk.wholelast > chunk.wholefirst ) { - put_by_band( *laid.timbers, laid.whole, chunk.wholefirst, chunk.wholelast, track->m_origin ); + material_data skins {}; + if( beammodel.skin != null_handle ) { + skins.replacable_skins[ 1 ] = beammodel.skin; } - if( laid.plains != nullptr && chunk.plainlast > chunk.plainfirst ) { - put_by_band( *laid.plains, laid.plain, chunk.plainfirst, chunk.plainlast, track->m_origin ); + ::glPushMatrix(); + ::glLoadIdentity(); + m_current_instance_count = bucket.count; + m_current_instance_base = bucket.first; + // inside the range of the level of detail the beams are at + model->Root->fSquareDist = ( bucket.part >= 0 ? 0.f : bucket.band + 1.f ); + model->Root->ReplacableSet( ( beammodel.skin != null_handle ? skins.replacable_skins : nullptr ), alpha ); + model->Root->pRoot = model; + Render( model->Root ); + m_current_instance_count = 0; + m_current_instance_base = 0; + ::glPopMatrix(); + ++m_renderpass.draw_stats.instanced_drawcalls; + if( bucket.part >= 0 ) { + for( int part = 0; part < track_sleepers::part_count; ++part ) { + if( beammodel.parts[ part ] != nullptr ) { + beammodel.parts[ part ]->iVisible = 1; + } + } } } + m_renderpass.draw_stats.instances += static_cast( total ); } - - auto const alpha = ( 0x30300030 ^ 0x0F0F000F ); - std::size_t total { 0 }; for( auto &bucket : m_timberbuckets ) { - auto const &instances = bucket.instances; - if( instances.empty() ) { continue; } - total += instances.size(); - auto const &beammodel = *bucket.model; - auto *model = beammodel.model; - if( bucket.part >= 0 ) { - // only the part of the model is visible, its siblings are skipped as the model is walked - for( int part = 0; part < track_sleepers::part_count; ++part ) { - if( beammodel.parts[ part ] != nullptr ) { - beammodel.parts[ part ]->iVisible = ( part == bucket.part ? 1 : 0 ); - } - } - } - material_data skins {}; - if( beammodel.skin != null_handle ) { - skins.replacable_skins[ 1 ] = beammodel.skin; - } - // the instances go to the instance stream of the frame, as in Render_Sleepers(), which the recorded walk of the depth - // pass keeps for its replay as well - auto const baseinstance { Upload_Instance_Modelviews( instances.data(), instances.size() ) }; - ::glPushMatrix(); - ::glLoadIdentity(); - m_current_instance_count = instances.size(); - m_current_instance_base = baseinstance; - // inside the range of the level of detail the beams are at - model->Root->fSquareDist = ( bucket.part >= 0 ? 0.f : bucket.band + 1.f ); - model->Root->ReplacableSet( ( beammodel.skin != null_handle ? skins.replacable_skins : nullptr ), alpha ); - model->Root->pRoot = model; - Render( model->Root ); - m_current_instance_count = 0; - m_current_instance_base = 0; - ::glPopMatrix(); - ++m_renderpass.draw_stats.instanced_drawcalls; - if( bucket.part >= 0 ) { - for( int part = 0; part < track_sleepers::part_count; ++part ) { - if( beammodel.parts[ part ] != nullptr ) { - beammodel.parts[ part ]->iVisible = 1; - } - } - } + bucket.runs.clear(); + bucket.count = 0; } - m_renderpass.draw_stats.instances += static_cast( total ); + m_timberorigins.clear(); } bool opengl46_renderer::Render(TDynamicObject *Dynamic) diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index 0c269640..26c45d4f 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -44,6 +44,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/opengl46/opengl46localshadows.h" #include "rendering/opengl46/opengl46instancecull.h" #include "rendering/opengl46/opengl46hiz.h" +#include "rendering/opengl46/opengl46timbers.h" #include "rendering/opengl46/opengl46shadowfit.h" // bare-bones render controller, in lack of anything better yet @@ -376,6 +377,8 @@ class opengl46_renderer : public gfx_renderer { static scene::bounding_area Bucket_Area( scene::basic_cell::instance_bucket const &Bucket ); // places supplied modelview matrices in the instance buffer of the frame. returns: base instance of the first of them std::uint32_t Upload_Instance_Modelviews( glm::mat4 const *Modelviews, std::size_t const Count ); + // the room for Count modelviews in the instance buffer of the frame, for the gpu to write. returns: base instance of the first + std::uint32_t Reserve_Instance_Modelviews( std::size_t const Count ); // binds the instance buffer of the frame for the vertex shaders (INSTANCE_SSBO_BINDING) void Bind_Instance_Stream() const; // draws the instances queued in the pass from the result of the gpu culling (Global.gfx_gpucull 1): makes the depth @@ -491,9 +494,11 @@ class opengl46_renderer : public gfx_renderer { // queues the sleepers of a track in the gpu culling of the pass (Global.gfx_gpucull_sleepers), with the gates of // Render_Sleepers(), making their records first when they're missing or out of date void Queue_Sleepers( TTrack *Track ); - // renders the sleepers and switch timbers of the tracks in range (TTrack::m_sleepers3d), laying them first where they're out of date. - // the parts of the model are drawn in instanced batches for the whole run of tracks, the runs of beams out of sight left out + // gathers the sleepers and switch timbers of the tracks in range (TTrack::m_sleepers3d), laying them first where they're out of + // date. the parts of the model are drawn in instanced batches for all the cells of the pass, the runs of beams out of sight left out void Render_Timbers( scene::basic_cell::path_sequence::const_iterator First, scene::basic_cell::path_sequence::const_iterator Last ); + // draws what Render_Timbers() gathered in the pass since the last time + void Flush_Timbers(); // queues a model instance drawn on its own in the gpu culling of the pass (Global.gfx_gpucull_models), as a bucket of // its own, making its record first when it's missing or out of date. Inrange: the cell of the instance lies within the // range of the pass. returns true when the instance isn't drawn by the cpu in the pass: it's drawn from the result of the @@ -709,6 +714,7 @@ class opengl46_renderer : public gfx_renderer { std::unordered_map, scene::basic_cell::instance_bucket_key_hash> m_sleeperbandcounts; // depth pyramid for the occlusion test of the gpu culling opengl46_hiz m_hiz; + opengl46_timbers m_timbers; // the instances of the sleepers and timbers of the tracks on the gpu // the cascades of the sun shadows fitted to the depth of the main view (Global.gfx_shadow_sdsm) opengl46_shadow_fit m_shadowfit; // with a cascade fitted: the corners of the area its map is to cover, for setup_pass() @@ -989,15 +995,20 @@ class opengl46_renderer : public gfx_renderer { // per-instance modelview from instance_ssbo[gl_BaseInstance + gl_InstanceID]. Reset to 0 on exit. std::size_t m_current_instance_count { 0 }; std::uint32_t m_current_instance_base { 0 }; - // camera-space transforms of the sleepers and timbers drawn by Render_Timbers, by the model and its part or its level of - // detail; kept from frame to frame to reuse their allocations + // the runs of the instances of the sleepers and timbers drawn by Render_Timbers, by the model and its part or its level of + // detail, where they lie in m_timbers; kept from frame to frame to reuse their allocations struct timber_bucket { track_sleepers::beam_model const *model { nullptr }; int part { -1 }; // of a model put together from parts; -1: the whole model float band { 0.f }; // the squared distance the level of detail of the whole model is seen from - std::vector instances; + std::vector runs; + std::uint32_t count { 0 }; // instances of the runs + std::uint32_t first { 0 }; // base instance of their modelviews in the instance buffer of the frame }; std::vector m_timberbuckets; + std::vector m_timbertracks; // the tracks of a cell with beams to draw + std::vector m_timberranges; // the runs of all the buckets, as they're expanded + std::vector m_timberorigins; // the modelviews of the cells of the runs // persistent scratch buffer for Render_Instanced(): holds the per-instance // camera-space root modelview matrices for the batch currently being built. // Kept as a member rather than a function-local so its heap allocation is diff --git a/rendering/opengl46/opengl46timbers.cpp b/rendering/opengl46/opengl46timbers.cpp new file mode 100644 index 00000000..e6d122ff --- /dev/null +++ b/rendering/opengl46/opengl46timbers.cpp @@ -0,0 +1,152 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "rendering/opengl46/opengl46timbers.h" + +#include "utilities/Globals.h" +#include "world/Track.h" + +#include + +namespace { + +// the buffer to start with, and how soon after being started over it's too small for the tracks in sight +std::uint32_t constexpr initial_capacity{65536}; +unsigned int constexpr restart_frames{600}; + +// the instances of a track, in the order they lie in the buffer +std::uint32_t count_of(track_sleepers::laid const &Laid) +{ + std::size_t count{Laid.whole.size() + Laid.plain.size()}; + for (auto const &instances : Laid.instances) + count += instances.size(); + return static_cast(count); +} + +} // namespace + +opengl46_timbers::~opengl46_timbers() +{ + if (m_buffer != 0) + glDeleteBuffers(1, &m_buffer); +} + +void opengl46_timbers::init() +{ + gl::shader shader("timber_instances.comp"); + m_program = std::make_unique(std::vector>{shader}); + // the modelviews of the origins and the runs, both placed at whole matrices + m_ranges = std::make_unique( + static_cast(4096 * sizeof(gl::timber_range)), static_cast(sizeof(glm::mat4)), Global.gfx_bufferstreaming); + restart(0); +} + +void opengl46_timbers::restart(std::uint32_t const Needed) +{ + auto capacity{std::max(m_capacity, initial_capacity)}; + if ((m_capacity > 0) && (m_frame - m_restartframe < restart_frames)) + capacity *= 2; + while (capacity < 2 * Needed) + capacity *= 2; + // the draws and passes issued with the old buffer keep it until the gpu is done with them + if (m_buffer != 0) + glDeleteBuffers(1, &m_buffer); + glCreateBuffers(1, &m_buffer); + glNamedBufferStorage(m_buffer, static_cast(capacity) * static_cast(sizeof(glm::mat4)), nullptr, GL_DYNAMIC_STORAGE_BIT); + m_capacity = capacity; + m_fill = 0; + m_restartframe = m_frame; + m_tracks.clear(); +} + +bool opengl46_timbers::place(std::vector const &Tracks, bool const Restart) +{ + auto const placed_now = [&](TTrack const &Track) { + auto const found{m_tracks.find(&Track)}; + return (found != m_tracks.end()) && (found->second.made == Track.m_sleepers3d.made); + }; + std::uint32_t needed{0}; + std::uint32_t all{0}; + for (auto const *track : Tracks) + { + auto const count{count_of(track->m_sleepers3d)}; + all += count; + if (false == placed_now(*track)) + needed += count; + } + if (needed == 0) + return true; + if (m_fill + needed > m_capacity) + { + if (false == Restart) + return false; + restart(all); + } + for (auto const *track : Tracks) + { + if (placed_now(*track)) + continue; + auto const &laid{track->m_sleepers3d}; + auto const first{m_fill}; + auto const send = [&](std::vector const &Instances) { + if (false == Instances.empty()) + glNamedBufferSubData(m_buffer, static_cast(m_fill) * static_cast(sizeof(glm::mat4)), + static_cast(Instances.size() * sizeof(glm::mat4)), Instances.data()); + m_fill += static_cast(Instances.size()); + }; + for (auto const &instances : laid.instances) + send(instances); + send(laid.whole); + send(laid.plain); + m_tracks[track] = {laid.made, first}; + } + return true; +} + +std::uint32_t opengl46_timbers::first(TTrack const &Track) const +{ + auto const found{m_tracks.find(&Track)}; + return (found != m_tracks.end() ? found->second.first : 0); +} + +void opengl46_timbers::expand(std::vector const &Origins, std::vector const &Ranges, GLuint const Output) +{ + if (Ranges.empty()) + return; + // sent together, in one buffer of the stream + auto const originbytes{Origins.size() * sizeof(glm::mat4)}; + m_upload.resize(originbytes + Ranges.size() * sizeof(gl::timber_range)); + std::memcpy(m_upload.data(), Origins.data(), originbytes); + std::memcpy(m_upload.data() + originbytes, Ranges.data(), Ranges.size() * sizeof(gl::timber_range)); + auto const origins{m_ranges->push(m_upload.data(), static_cast(m_upload.size()))}; + auto const ranges{origins + static_cast(originbytes)}; + GLuint const buffers[]{m_buffer, m_ranges->buffer()}; + ::glBindBuffersBase(GL_SHADER_STORAGE_BUFFER, gl::TIMBER_LOCAL_SSBO_BINDING, 1, &buffers[0]); + ::glBindBuffersBase(GL_SHADER_STORAGE_BUFFER, gl::TIMBER_RANGE_SSBO_BINDING, 1, &buffers[1]); + ::glBindBuffersBase(GL_SHADER_STORAGE_BUFFER, gl::TIMBER_OUTPUT_SSBO_BINDING, 1, &Output); + ::glBindBuffersBase(GL_SHADER_STORAGE_BUFFER, gl::TIMBER_ORIGIN_SSBO_BINDING, 1, &buffers[1]); + // a dispatch takes at most 65535 work groups + auto const first{static_cast(ranges / static_cast(sizeof(gl::timber_range)))}; + auto const origin{static_cast(origins / static_cast(sizeof(glm::mat4)))}; + for (std::size_t done = 0; done < Ranges.size();) + { + auto const count{std::min(Ranges.size() - done, 65535)}; + ::glProgramUniform4ui(*m_program, 0, first + static_cast(done), origin, 0, 0); + m_program->dispatch(static_cast(count)); + done += count; + } + ::glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); +} + +void opengl46_timbers::next_frame() +{ + ++m_frame; + m_ranges->next_frame(); +} diff --git a/rendering/opengl46/opengl46timbers.h b/rendering/opengl46/opengl46timbers.h new file mode 100644 index 00000000..5790e8f2 --- /dev/null +++ b/rendering/opengl46/opengl46timbers.h @@ -0,0 +1,66 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include + +#include "gl/shader.h" +#include "gl/streambuffer.h" +#include "gl/ubo.h" + +class TTrack; + +// the instances of the timbers and sleepers of the tracks (track_sleepers::laid) kept on the gpu, those of a track sent once as +// it's laid, and the runs of them drawn made into the modelviews of the instance buffer of the frame by a compute pass +// (timber_instances.comp): for a run the cpu gives only where it lies and where it goes, not its matrices +class opengl46_timbers +{ + public: + opengl46_timbers() = default; + opengl46_timbers(opengl46_timbers const &) = delete; + opengl46_timbers &operator=(opengl46_timbers const &) = delete; + ~opengl46_timbers(); + + // throws gl::shader_exception + void init(); + // the instances of the tracks on the gpu, sent again for a track laid anew since; when those to send don't fit in what's left + // of the buffer, it's started over for all the tracks, a bigger one when it was started over not long before. without + // Restart nothing is sent then, and false returned: the runs of the instances placed before are to be expanded first + bool place(std::vector const &Tracks, bool const Restart); + // where the instances of a track placed lie in the buffer: the first of the parts in turn, then of the whole timbers, then of + // the plain sleepers + std::uint32_t first(TTrack const &Track) const; + // the runs written to the instance buffer Output, each as the modelview of its origin times its transforms, for the draws after + void expand(std::vector const &Origins, std::vector const &Ranges, GLuint const Output); + void next_frame(); + + private: + struct placed + { + std::uint64_t made; // the making of the instances of the track sent + std::uint32_t first; + }; + + // a new buffer, room for Needed instances at least, the tracks to be sent again + void restart(std::uint32_t const Needed); + + std::unique_ptr m_program; + std::unique_ptr m_ranges; + GLuint m_buffer{0}; + std::uint32_t m_capacity{0}; // in instances + std::uint32_t m_fill{0}; + unsigned int m_frame{0}; + unsigned int m_restartframe{0}; + std::unordered_map m_tracks; + std::vector m_upload; // the origins and the runs of an expand(), as they're sent +}; diff --git a/shaders/timber_instances.comp b/shaders/timber_instances.comp new file mode 100644 index 00000000..e1f7d6c9 --- /dev/null +++ b/shaders/timber_instances.comp @@ -0,0 +1,46 @@ +// the modelviews of the instances of the timbers and sleepers of the tracks drawn in a pass (opengl46_timbers): their transforms +// are kept relative to the origins of the tracks, each run of them is turned by the modelview of the origin of its cell and written +// to the instance buffer of the frame, where the instanced draws read it. a work group a run + +#include + +layout(local_size_x = TIMBER_GROUP_SIZE) in; + +struct timber_range +{ + uint source; + uint count; + uint target; + uint origin; +}; + +layout(std430, binding = TIMBER_LOCAL_SSBO_BINDING) readonly buffer timber_local_ssbo +{ + mat4 timber_local[]; +}; + +layout(std430, binding = TIMBER_RANGE_SSBO_BINDING) readonly buffer timber_range_ssbo +{ + timber_range timber_ranges[]; +}; + +layout(std430, binding = TIMBER_OUTPUT_SSBO_BINDING) writeonly buffer timber_output_ssbo +{ + mat4 timber_output[]; +}; + +layout(std430, binding = TIMBER_ORIGIN_SSBO_BINDING) readonly buffer timber_origin_ssbo +{ + mat4 timber_origins[]; +}; + +// x: the first run of the dispatch, y: the first modelview of the origins +layout(location = 0) uniform uvec4 params; + +void main() +{ + timber_range range = timber_ranges[params.x + gl_WorkGroupID.x]; + mat4 origin = timber_origins[params.y + range.origin]; + for (uint index = gl_LocalInvocationID.x; index < range.count; index += TIMBER_GROUP_SIZE) + timber_output[range.target + index] = origin * timber_local[range.source + index]; +} diff --git a/world/TrackSleepers.cpp b/world/TrackSleepers.cpp index f819d8c1..56843763 100644 --- a/world/TrackSleepers.cpp +++ b/world/TrackSleepers.cpp @@ -125,11 +125,15 @@ std::pair shared_zone( TTrack const &Track, int const Path ) { namespace { +// the instances made so far, of all the tracks +std::uint64_t makings { 0 }; + // the beams as instances of the parts of the model of the track: those of the fastenings with them, or left for later and the beams // kept for them meanwhile void instance( TTrack &Track, std::vector &&Beams, bool const Fastened ) { auto &laid { Track.m_sleepers3d }; + laid.made = ++makings; for( auto &instances : laid.instances ) { instances.clear(); } diff --git a/world/TrackSleepers.h b/world/TrackSleepers.h index ba9772e2..f6c3730b 100644 --- a/world/TrackSleepers.h +++ b/world/TrackSleepers.h @@ -103,6 +103,7 @@ struct laid { std::vector whole; // transforms of the timbers of a model without parts, drawn whole std::vector plain; // transforms of the plain sleepers, whole models, relative to the origin of the track std::vector chunks; + std::uint64_t made { 0 }; // which making of the instances they are, none other of any track the same: a copy of them tells by it bool fastened { false }; // the fastenings are laid, else they're laid as the track comes near where they're drawn std::vector beams; // kept for the fastenings till they're laid }; From 81a6b855d05be0ccaa57d5f9b85c8cfca4558ee6 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 24 Oct 2026 01:40:47 +0200 Subject: [PATCH 15/19] feat(editor): buttons in the switch tool setting a model of timbers on all the switches and crossings of the scenery at once, one for each model of timbers of the scenery data and one for the model of eu07.ini, in one step of the undo; the selftest switchtimbers does the same --- application/editormode.h | 4 ++++ application/editormodeselftest.cpp | 1 + application/editormodeselftestrails.cpp | 9 +++++++ application/editormodeswitchtool.cpp | 31 +++++++++++++++++++++++++ lang/editor_pl.po | 12 ++++++++++ 5 files changed, 57 insertions(+) diff --git a/application/editormode.h b/application/editormode.h index d2ccab4e..117566e9 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1411,6 +1411,7 @@ class editor_mode : public application_mode, private editor_track::observer std::vector lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const; alignment::result lay_shape(std::vector const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &Design) const; void render_switch_ui(); + void render_switch_timbers(); void render_rail_profile_hint(TTrack &Track); bool start_switch_placement(); void finish_switch_placement(); @@ -1780,6 +1781,9 @@ class editor_mode : public application_mode, private editor_track::observer void select_tracks(std::vector const &Tracks); // changes each track which can be changed, in one step of the undo; false from Change: the track stays as it was std::size_t tracks_apply(std::vector const &Tracks, std::function const &Change, bool const Rebuild, char const *What); + // the model of the timbers, with the fastenings where it has them, set on all the switches and crossings of the scenery, in one + // step of the undo; empty: the one of eu07.ini. returns: the tracks changed + std::size_t set_switch_timbers(std::string const &Model); int track_property_index(std::string const &Key) const; std::size_t track_property_apply(int const Property, std::vector const &Tracks, std::string const &Text); void track_field_settle(); diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index e163b632..cb8095d4 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -68,6 +68,7 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c // railtime lays out the rails of every track, as when its geometry is made, and logs how long it took // throw <0|1> sets every switch to the path and moves its blades there at once // crossings [name] logs where rails of tracks cross rails of other tracks and of what kind (acute, obtuse, middle acute or obtuse, wide), checks that each acute crossing has one partner and one nose +// switchtimbers the model of the timbers set on all the switches and crossings, as the buttons of the switch tool do it // frogkey name [on|off] sets or clears the frogs key of a switch (one undo step); logs the key, whether it is written, the frog and the crossings // geomhash [name] logs the number of vertices and a hash of the drawn geometry of each rail track (rails, trackbed, cut faces), to compare builds; exact: the bits of every value, userdata and tread materials too // sweep =... model, params (separated by commas), lateral, height, side (origin, near, far, diff --git a/application/editormodeselftestrails.cpp b/application/editormodeselftestrails.cpp index 86d69280..ee1e7b4f 100644 --- a/application/editormodeselftestrails.cpp +++ b/application/editormodeselftestrails.cpp @@ -324,6 +324,15 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm WriteLog(format("SELFTEST geom total: tracks %d, vertices %zu, hash %016llx, exact %016llx", tracks, vertices, static_cast(total), static_cast(exacttotal))); return selftest_result::handled; } + if (command == "switchtimbers") + { + std::string model; + words >> model; + if (model == "none") + model.clear(); + WriteLog(format("SELFTEST switchtimbers %s: %zu tracks", model.c_str(), set_switch_timbers(model))); + return selftest_result::handled; + } if (command == "frogkey") { std::string name, value; diff --git a/application/editormodeswitchtool.cpp b/application/editormodeswitchtool.cpp index 169dde34..41f163a5 100644 --- a/application/editormodeswitchtool.cpp +++ b/application/editormodeswitchtool.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "utilities/translation.h" #include "world/Track.h" +#include "world/TrackSleepers.h" #include "imgui/imgui.h" #include @@ -390,6 +391,7 @@ void editor_mode::render_switch_ui() arm_switch(); } item_tooltip("Looks for the switches of the scenery, to use their shapes as the templates"); + render_switch_timbers(); ImGui::TreePop(); } ImGui::PopID(); @@ -405,6 +407,35 @@ void editor_mode::render_switch_ui() item_tooltip("Own switch templates: diverging tracks of straights, arcs and transition curves, double switches"); } +// the timbers of all the switches and crossings of the scenery at once: a button for each model of timbers of the scenery data +void editor_mode::render_switch_timbers() +{ + ImGui::TextDisabled("%s", STR_C("Timbers of all the switches")); + auto first{true}; + auto const button = [&](std::string const &Model, std::string const &Label) { + if (false == first) + ImGui::SameLine(); + first = false; + if (ImGui::Button((Label + "##switchtimbers" + Model).c_str())) + m_switch.status = format(STR_C("Timbers %s set on %d tracks"), Label.c_str(), static_cast(set_switch_timbers(Model))); + item_tooltip("Sets the model of the timbers, with the fastenings where it has them, on all the switches and crossings of the scenery, in one step of the undo"); + }; + for (auto const &model : track_sleepers::pack_models()) + if (model.timber) + button(model.name, model.name.substr(model.name.find_last_of('/') + 1)); + button({}, STR_C("As in eu07.ini")); +} + +std::size_t editor_mode::set_switch_timbers(std::string const &Model) +{ + std::vector tracks; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && (track->iCategoryFlag & 15) == 1 && (track->eType == tt_Switch || false == track->m_crossings.empty()) + && track->m_timber_model_name != Model) + tracks.push_back(track); + return tracks_apply(tracks, [&](TTrack &Track) { Track.m_timber_model_name = Model; return true; }, true, "Timber model"); +} + // a track with no rail profile, as the sceneries of the older editors have them: the templates of all the rails are offered. // the profile set at once on all the tracks laid with the same rails and no profile, so a scenery is put right in a few clicks void editor_mode::render_rail_profile_hint(TTrack &Track) diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 5f29c7e4..df157d80 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10378,3 +10378,15 @@ msgstr "Odczytaj ponownie" msgid "Pick a memory cell from the list to set what's in it" msgstr "Wybierz komórkę pamięci z listy, aby ustawić jej zawartość" + +msgid "Timbers of all the switches" +msgstr "Podrozjazdnice wszystkich rozjazdów" + +msgid "Timbers %s set on %d tracks" +msgstr "Podrozjazdnice %s ustawione na %d torach" + +msgid "As in eu07.ini" +msgstr "Jak w eu07.ini" + +msgid "Sets the model of the timbers, with the fastenings where it has them, on all the switches and crossings of the scenery, in one step of the undo" +msgstr "Ustawia model podrozjazdnic, razem z przytwierdzeniami tam, gdzie je ma, na wszystkich rozjazdach i skrzyżowaniach scenerii, jednym krokiem cofania" From 540dae420f4e1a9fd06965fcd2f14370203fee82 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 24 Oct 2026 02:18:58 +0200 Subject: [PATCH 16/19] fix(sleepers): the timbers a switch shares with a track with no model of timbers of its own laid of the model of the switch, not of the one of eu07.ini without the fastenings: the model of a track with none of its own that of the tracks most of its timbers carry with one, and a timber carrying none of them, as under the short tracks between the halves of a slip, of the model of the nearest timber with one up to 1.5 m away; the fastenings and checkseats selftests take the model the same way --- application/editormodeselftestsleepers.cpp | 5 ++-- world/SleeperFastenings.cpp | 10 ++++--- world/SleeperLayout.cpp | 35 ++++++++++++++++++++-- world/SleeperModels.cpp | 21 +++++++++++++ world/SleeperRules.h | 3 ++ world/TrackSleepers.cpp | 1 + world/TrackSleepers.h | 5 ++++ 7 files changed, 72 insertions(+), 8 deletions(-) diff --git a/application/editormodeselftestsleepers.cpp b/application/editormodeselftestsleepers.cpp index cf9d6c51..2188df66 100644 --- a/application/editormodeselftestsleepers.cpp +++ b/application/editormodeselftestsleepers.cpp @@ -156,7 +156,8 @@ editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &c std::string name, detail; words >> name >> detail; auto *track{simulation::Paths.find(name)}; - auto const *model{track != nullptr ? track_sleepers::timber_model(*track) : nullptr}; + auto const beams{track != nullptr ? track_sleepers::place(*track) : std::vector{}}; + auto const *model{track != nullptr ? track_sleepers::timber_model(*track, beams) : nullptr}; if (model == nullptr || false == model->fastened()) { WriteLog("SELFTEST fastenings: no track " + name + " with the fastenings in the model of its timbers"); @@ -164,7 +165,7 @@ editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &c } track_sleepers::fastened_rails const rails{*track}; std::array counts{}; - for (auto const &beam : track_sleepers::place(*track)) + for (auto const &beam : beams) { if (beam.plain) continue; diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp index e047230d..3a616712 100644 --- a/world/SleeperFastenings.cpp +++ b/world/SleeperFastenings.cpp @@ -915,14 +915,15 @@ std::vector check_fastenings() { if( track == nullptr || track->m_editorremoved || false == track->m_sleepers3d.active ) { continue; } - auto const *model { timber_model( *track ) }; + auto const beams { place( *track ) }; + auto const *model { timber_model( *track, beams ) }; if( model == nullptr || false == model->fastened() ) { continue; } fastening_row row; row.track = track->name(); std::optional rails; - for( auto const &beam : place( *track ) ) { + for( auto const &beam : beams ) { if( beam.plain ) { continue; } @@ -1036,12 +1037,13 @@ std::vector check_rail_seats() { if( track == nullptr || track->m_editorremoved || false == track->m_sleepers3d.active ) { continue; } - auto const *model { timber_model( *track ) }; + auto const beams { place( *track ) }; + auto const *model { timber_model( *track, beams ) }; if( model == nullptr || false == model->fastened() ) { continue; } std::optional rails; - for( auto const &beam : place( *track ) ) { + for( auto const &beam : beams ) { if( beam.plain ) { continue; } diff --git a/world/SleeperLayout.cpp b/world/SleeperLayout.cpp index f22623f8..5fc40183 100644 --- a/world/SleeperLayout.cpp +++ b/world/SleeperLayout.cpp @@ -152,6 +152,7 @@ public: keep_apart(); fill_gaps(); keep_apart(); + share_panels(); for( auto const &laid : beams ) { result->beams[ laid.owner ].push_back( laid.value ); } @@ -553,17 +554,47 @@ private: // a sleeper of a plain track running alone and square to it, the rest are the timbers of the switches and crossings auto const carried { std::count_if( bands.begin(), bands.end(), []( band const &Band ) { return Band.carried; } ) }; auto const plain { path.track->eType == tt_Normal && carried == 1 && pieces.size() == 1 && false == pieces.front().clipped && std::abs( glm::dot( forward, along ) ) > 0.9998 && false == crossing_timber( path, Distance ) }; + auto const owner { std::find_if( bands.begin(), bands.end(), []( band const &Band ) { + return Band.carried && false == Band.path->track->m_timber_model_name.empty(); } ) }; + TTrack const *panel { owner != bands.end() ? owner->path->track : nullptr }; for( std::size_t index = 0; index < pieces.size(); ++index ) { auto const &piece { pieces[ index ] }; auto const &way { turned[ index ] }; if( way == along ) { - beams.push_back( { path.track, { top + rolledacross * piece.from, top + rolledacross * piece.to, glm::dvec3 { along.x, 0.0, along.y }, plain, piece.corefrom - piece.from, piece.coreto - piece.from } } ); + beams.push_back( { path.track, { top + rolledacross * piece.from, top + rolledacross * piece.to, glm::dvec3 { along.x, 0.0, along.y }, plain, piece.corefrom - piece.from, piece.coreto - piece.from, panel } } ); continue; } // turned about where the middle of its track crosses the line auto const centre { top + rolledacross * piece.single->centre }; glm::dvec3 const turnedacross { glm::dvec3 { -way.y, 0.0, way.x } * std::cos( roll ) - up * std::sin( roll ) }; - beams.push_back( { path.track, { centre + turnedacross * ( piece.from - piece.single->centre ), centre + turnedacross * ( piece.to - piece.single->centre ), glm::dvec3 { way.x, 0.0, way.y }, false, piece.corefrom - piece.from, piece.coreto - piece.from } } ); + beams.push_back( { path.track, { centre + turnedacross * ( piece.from - piece.single->centre ), centre + turnedacross * ( piece.to - piece.single->centre ), glm::dvec3 { way.x, 0.0, way.y }, false, piece.corefrom - piece.from, piece.coreto - piece.from, panel } } ); + } + } + + // the timbers whose lines carry no track with a model of timbers of its own of the model of the nearest timber with one + void share_panels() { + + std::vector> panels; + for( auto const &laid : beams ) { + if( false == laid.value.plain && laid.value.panel != nullptr ) { + panels.emplace_back( plan( 0.5 * ( laid.value.from + laid.value.to ) ), laid.value.panel ); + } + } + if( panels.empty() ) { + return; + } + for( auto &laid : beams ) { + if( laid.value.plain || laid.value.panel != nullptr ) { + continue; + } + auto const middle { plan( 0.5 * ( laid.value.from + laid.value.to ) ) }; + auto nearest { panel_reach }; + for( auto const &[ at, panel ] : panels ) { + if( auto const distance { glm::length( at - middle ) }; distance < nearest ) { + nearest = distance; + laid.value.panel = panel; + } + } } } diff --git a/world/SleeperModels.cpp b/world/SleeperModels.cpp index 003dab2c..9138450d 100644 --- a/world/SleeperModels.cpp +++ b/world/SleeperModels.cpp @@ -216,6 +216,27 @@ beam_model const *timber_model( TTrack const &Track ) { return timber_model(); } +beam_model const *timber_model( TTrack const &Track, std::vector const &Beams ) { + + if( false == Track.m_timber_model_name.empty() ) { + return timber_model( Track ); + } + std::vector> panels; + for( auto const &beam : Beams ) { + if( beam.plain || beam.panel == nullptr ) { + continue; + } + auto found { std::find_if( panels.begin(), panels.end(), [&]( auto const &Panel ) { return Panel.first == beam.panel; } ) }; + if( found == panels.end() ) { + panels.emplace_back( beam.panel, 0 ); + found = std::prev( panels.end() ); + } + ++found->second; + } + auto const most { std::max_element( panels.begin(), panels.end(), []( auto const &Left, auto const &Right ) { return Left.second < Right.second; } ) }; + return ( most != panels.end() ? timber_model( *most->first ) : timber_model( Track ) ); +} + beam_model const *plain_model( TTrack const &Track ) { if( Track.m_sleeper_enabled && false == Track.m_sleeper_model_name.empty() ) { diff --git a/world/SleeperRules.h b/world/SleeperRules.h index 34e85b47..b6fa0b8d 100644 --- a/world/SleeperRules.h +++ b/world/SleeperRules.h @@ -50,6 +50,9 @@ double constexpr level_tolerance { 1.0 }; // how far around a track the tracks are taken in to lay its beams: those whose beams it carries, or which carry its own, and // those which set where the beams of these go double constexpr around_margin { 20.0 }; +// a timber whose line carries no track with a model of timbers of its own, as under the short tracks between the halves of a slip, +// is of the model of the nearest timber with one so far from it +double constexpr panel_reach { 1.5 }; // how many beams make a run drawn or left out together std::size_t constexpr chunk_beams { 16 }; // how far apart the points of the rails are, as the places they lie on the timbers are found diff --git a/world/TrackSleepers.cpp b/world/TrackSleepers.cpp index 56843763..34709d4b 100644 --- a/world/TrackSleepers.cpp +++ b/world/TrackSleepers.cpp @@ -319,6 +319,7 @@ void lay( TTrack &Track, bool const Fastened ) { laid.laying = true; auto beams { in_background() ? std::move( placed ) : place( Track ) }; laid.laying = false; + laid.timbers = timber_model( Track, beams ); // the layout of the cluster isn't needed by this track any more, the instances below are what's drawn forget_layout( Track ); instance( Track, std::move( beams ), Fastened ); diff --git a/world/TrackSleepers.h b/world/TrackSleepers.h index f6c3730b..596544d6 100644 --- a/world/TrackSleepers.h +++ b/world/TrackSleepers.h @@ -56,6 +56,8 @@ struct beam { 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 }; + TTrack const *panel { nullptr }; // of the tracks its line carries, the one with a model of timbers of its own: its own track first; else + // that of the nearest timber with one }; // a model sleepers or timbers are made of, as found in it @@ -119,6 +121,9 @@ 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 ); +// the model the track lays the timbers of the beams of: its own, else that of the tracks most of its timbers carry with one of their +// own, as a switch it shares its timbers with, else the one of eu07.ini +beam_model const *timber_model( TTrack const &Track, std::vector const &Beams ); // 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 From f36fa81fd1f398bf1ad946772cb9089cc12595d6 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 24 Oct 2026 02:19:03 +0200 Subject: [PATCH 17/19] fix(sleepers): the fastenings of the rails at the obtuse crossings of the slips: the rails and check rails along a timber each with its foot over the foot of the one beside, with a check rail between two of them, in one fastening stretched under them all, a check rail where a rail lies being that rail; of the rails close beside one another, as of a frog, two in a fastening of both only where its ends reach them both, else each in a plate of its own; a check rail on a fastening holding all the rails over it seated for the checkseats selftest --- world/SleeperFastenings.cpp | 121 +++++++++++++++++++++++++++--------- 1 file changed, 93 insertions(+), 28 deletions(-) diff --git a/world/SleeperFastenings.cpp b/world/SleeperFastenings.cpp index 3a616712..ea382ae4 100644 --- a/world/SleeperFastenings.cpp +++ b/world/SleeperFastenings.cpp @@ -255,6 +255,31 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M return Beam.from + span * ( At / length ); }; auto const has = [&]( int const Part ) { return Model.parts[ Part ] != nullptr; }; + // a fastening of two rails with its seats at the origin the way given, holding more rails than its seats, as the rails of a frog + // or two rails with a check rail between: stretched under the rails from the first to the last and their check rails, its ends with + // the clips past the feet of the outer ones, short of the feet of the rails beside + auto const whole = [&]( std::size_t const First, std::size_t const Last, double const Origin, double const Turn ) { + auto const &reach { Model.spans[ part_check ] }; + auto low { std::numeric_limits::max() }; + auto high { std::numeric_limits::lowest() }; + for( auto index { First }; index <= Last; ++index ) { + auto const &rail { rails[ index ] }; + for( auto const at : { rail.at, rail.kind == role::checked ? rail.at + check_seats / glm::dot( rail.inward, axis ) : rail.at } ) { + low = std::min( low, at - rail.foot - fastening_clip ); + high = std::max( high, at + rail.foot + fastening_clip ); + } + } + if( First > 0 ) { + low = std::max( low, rails[ First - 1 ].at + rails[ First - 1 ].foot + fastening_gap ); + } + if( Last + 1 < rails.size() ) { + high = std::min( high, rails[ Last + 1 ].at - rails[ Last + 1 ].foot - fastening_gap ); + } + low = std::min( low, Origin + std::min( Turn * reach.x, Turn * reach.y ) ); + high = std::max( high, Origin + std::max( Turn * reach.x, Turn * reach.y ) ); + auto const scale { ( high - low ) / ( reach.y - reach.x ) }; + return fastening { part_check, point( Origin ), axis * Turn, static_cast( scale ), 0.f, + static_cast( Turn * ( 0.5 * ( low + high ) - Origin ) - scale * 0.5 * ( reach.x + reach.y ) ), true, true }; }; // the rails from the first to the last under fastenings of two rails, each with its seats under two rails side by side and over as // many of the rails around as it reaches: the one holding the most first, the rails before and after it in turn std::function crowd = [&]( std::size_t const First, std::size_t const Last ) { @@ -268,6 +293,9 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M auto const at { 0.5 * ( rails[ seat ].at + rails[ seat + 1 ].at - direction * check_seats ) }; auto const low { at + std::min( direction * reach.x, direction * reach.y ) }; auto const high { at + std::max( direction * reach.x, direction * reach.y ) }; + if( rails[ seat ].at < low || rails[ seat + 1 ].at > high ) { + continue; + } auto start { seat }; while( start > First && rails[ start - 1 ].at >= low ) { --start; @@ -290,29 +318,9 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M } return; } - fastening part { part_check, point( origin ), axis * turn, 1.f, static_cast( 0.5 * check_seats ), 0.f, true, covered > 2 }; - if( part.whole ) { - // holding more rails than its seats, as the rails of a frog: stretched under them all, its ends with the clips past the feet - // of the outer ones, short of the feet of the rails beside - auto const last { from + covered - 1 }; - auto const a { origin + turn * reach.x }; - auto const b { origin + turn * reach.y }; - auto low { rails[ from ].at - rails[ from ].foot - fastening_clip }; - auto high { rails[ last ].at + rails[ last ].foot + fastening_clip }; - if( from > 0 ) { - low = std::max( low, rails[ from - 1 ].at + rails[ from - 1 ].foot + fastening_gap ); - } - if( last + 1 < rails.size() ) { - high = std::min( high, rails[ last + 1 ].at - rails[ last + 1 ].foot - fastening_gap ); - } - low = std::min( low, std::min( a, b ) ); - high = std::max( high, std::max( a, b ) ); - auto const scale { ( high - low ) / ( reach.y - reach.x ) }; - part.pivot = 0.f; - part.scale = static_cast( scale ); - part.shift = static_cast( turn * ( 0.5 * ( low + high ) - origin ) - scale * 0.5 * ( reach.x + reach.y ) ); - } - result.push_back( part ); + result.push_back( covered > 2 ? + whole( from, from + covered - 1, origin, turn ) : + fastening { part_check, point( origin ), axis * turn, 1.f, static_cast( 0.5 * check_seats ), 0.f, true } ); if( from > First ) { crowd( First, from - 1 ); } @@ -320,11 +328,64 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M crowd( from + covered, Last ); } }; + // the rails and the check rails along the timber, each with its foot over the foot of the one beside, with a check rail between two + // of them, as at the obtuse crossings of the slips, none a stock rail: from the first rail to the last, held by one fastening. a + // check rail where a rail lies is that rail + std::vector runs( rails.size(), rails.size() ); // of the first rail of each, the last + std::vector run( rails.size(), false ); + if( has( part_check ) ) { + struct line { + double at; + double foot; + std::size_t rail; + bool check; + }; + std::vector lines; + for( std::size_t index = 0; index < rails.size(); ++index ) { + auto const &rail { rails[ index ] }; + lines.push_back( { rail.at, rail.foot, index, false } ); + if( rail.kind != role::checked ) { + continue; + } + auto const at { rail.at + check_seats / glm::dot( rail.inward, axis ) }; + if( std::none_of( rails.begin(), rails.end(), [&]( hit const &Other ) { return std::abs( Other.at - at ) < fastening_nose; } ) ) { + lines.push_back( { at, rail.foot, index, true } ); + } + } + std::stable_sort( lines.begin(), lines.end(), []( line const &Left, line const &Right ) { return Left.at < Right.at; } ); + for( std::size_t first = 0; first < lines.size(); ) { + auto last { first }; + while( last + 1 < lines.size() && lines[ last + 1 ].at - lines[ last ].at < lines[ last ].foot + lines[ last + 1 ].foot ) { + ++last; + } + auto const between { last > first + 1 && std::any_of( lines.begin() + first + 1, lines.begin() + last, []( line const &Line ) { return Line.check; } ) }; + auto low { rails.size() }; + auto high { std::size_t { 0 } }; + auto stock { false }; + for( auto index { first }; index <= last; ++index ) { + low = std::min( low, lines[ index ].rail ); + high = std::max( high, lines[ index ].rail ); + stock = stock || rails[ lines[ index ].rail ].kind == role::stock; + } + if( between && false == stock && false == run[ low ] ) { + runs[ low ] = high; + std::fill( run.begin() + low, run.begin() + high + 1, true ); + } + first = last + 1; + } + } for( std::size_t index = 0; index < rails.size(); ++index ) { auto const &rail { rails[ index ] }; + if( runs[ index ] < rails.size() ) { + auto const &reach { Model.spans[ part_check ] }; + auto const last { runs[ index ] }; + result.push_back( whole( index, last, 0.5 * ( rail.at + rails[ last ].at - reach.x - reach.y ), 1.0 ) ); + index = last; + continue; + } // three rails or more close beside one another, as at the crossings of the slips, none a stock rail or with a check rail auto last { index }; - while( last + 1 < rails.size() && rails[ last + 1 ].at - rails[ last ].at < fastening_crowd && rails[ last ].kind == role::plain && rails[ last + 1 ].kind == role::plain ) { + while( last + 1 < rails.size() && rails[ last + 1 ].at - rails[ last ].at < fastening_crowd && rails[ last ].kind == role::plain && rails[ last + 1 ].kind == role::plain && false == run[ last + 1 ] ) { ++last; } if( last >= index + 2 && has( part_check ) ) { @@ -332,7 +393,7 @@ std::vector fastened_rails::on( beam const &Beam, beam_model const &M index = last; continue; } - if( index + 1 < rails.size() && rails[ index + 1 ].at - rail.at < fastening_pair && rail.kind != role::stock && rails[ index + 1 ].kind != role::stock ) { + if( index + 1 < rails.size() && rails[ index + 1 ].at - rail.at < fastening_pair && rail.kind != role::stock && rails[ index + 1 ].kind != role::stock && false == run[ index + 1 ] ) { auto const &other { rails[ index + 1 ] }; // a rail of the two with its check rail on the side away from the other one in the fastening with the check rail, // the other one in a plate of its own @@ -1056,8 +1117,11 @@ std::vector check_rail_seats() { auto const high { glm::max( start, start + span ) }; std::vector seats; auto const fastenings { rails->on( beam, *model ) }; - auto const under = [&]( glm::dvec2 const &Point ) { + auto const under = [&]( glm::dvec2 const &Point, bool const Whole ) { return std::any_of( fastenings.begin(), fastenings.end(), [&]( fastening const &Fastening ) { + if( Whole && false == Fastening.whole ) { + return false; + } auto const along { glm::dot( Point - plan( Fastening.point ), Fastening.across ) }; auto const a { Fastening.placed( model->spans[ Fastening.part ].x, Fastening.scale ) }; auto const b { Fastening.placed( model->spans[ Fastening.part ].y, Fastening.scale ) }; @@ -1084,14 +1148,15 @@ std::vector check_rail_seats() { } auto const at { start + span * u }; ++timbers[ index ]; - auto const seated { std::any_of( seats.begin(), seats.end(), [&]( glm::dvec2 const &Seat ) { + // in a seat, or on a fastening holding all the rails over it + auto const seated { under( at, true ) || std::any_of( seats.begin(), seats.end(), [&]( glm::dvec2 const &Seat ) { return glm::length( Seat - at ) < 0.03; } ) }; if( false == seated ) { if( line.gap.bare == 0.0 ) { line.gap.at = at; } line.gap.bare += 1.0; - if( false == under( at ) ) { + if( false == under( at, false ) ) { ++line.gap.unsupported; } } From 8375891c41558fc623cdf69db192e996d738e30b Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 24 Oct 2026 02:19:06 +0200 Subject: [PATCH 18/19] feat(editor): the selftest fastentour, the camera over each cell of a grid with switches or crossings laying timbers in turn, for a fastencheck of the whole scenery after it --- application/editormodeselftest.cpp | 1 + application/editormodeselftestsleepers.cpp | 26 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index cb8095d4..9aa32171 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -68,6 +68,7 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c // railtime lays out the rails of every track, as when its geometry is made, and logs how long it took // throw <0|1> sets every switch to the path and moves its blades there at once // crossings [name] logs where rails of tracks cross rails of other tracks and of what kind (acute, obtuse, middle acute or obtuse, wide), checks that each acute crossing has one partner and one nose +// fastentour [cell] [frames] the camera over each cell of the grid with switches laying timbers in turn, for a fastencheck after of the whole scenery // switchtimbers the model of the timbers set on all the switches and crossings, as the buttons of the switch tool do it // frogkey name [on|off] sets or clears the frogs key of a switch (one undo step); logs the key, whether it is written, the frog and the crossings // geomhash [name] logs the number of vertices and a hash of the drawn geometry of each rail track (rails, trackbed, cut faces), to compare builds; exact: the bits of every value, userdata and tread materials too diff --git a/application/editormodeselftestsleepers.cpp b/application/editormodeselftestsleepers.cpp index 2188df66..d8e1e7b1 100644 --- a/application/editormodeselftestsleepers.cpp +++ b/application/editormodeselftestsleepers.cpp @@ -198,6 +198,32 @@ editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &c WriteLog(format("SELFTEST fastenings %s: %d plates, %d chairs, %d checks", track->name().c_str(), counts[track_sleepers::part_plate], counts[track_sleepers::part_chair], counts[track_sleepers::part_check])); return selftest_result::handled; } + if (command == "fastentour") + { + // the camera over each cell of the grid with switches or crossings laying timbers, in turn, as the tracks get their beams only + // near it: the steps of the tour go in after this one, a fastencheck after them takes the whole scenery + double cell{400.0}; + int frames{40}; + words >> cell >> frames; + std::map, glm::dvec3> stops; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->eType != tt_Switch && track->m_crossings.empty()) || false == track_sleepers::lays(*track)) + continue; + auto const at{track->location()}; + stops.emplace(std::make_pair(std::lround(std::floor(at.x / cell)), std::lround(std::floor(at.z / cell))), at); + } + std::vector steps; + for (auto const &stop : stops) + { + auto const &at{stop.second}; + steps.push_back(format("camera %.2f %.2f %.2f %.2f %.2f %.2f", at.x, at.y + 60.0, at.z + 1.0, at.x, at.y, at.z)); + steps.push_back("wait " + std::to_string(frames)); + } + m_selftest.lines.insert(m_selftest.lines.begin() + static_cast(m_selftest.next), steps.begin(), steps.end()); + WriteLog(format("SELFTEST fastentour: %zu stops of %.0f m", stops.size(), cell)); + return selftest_result::handled; + } if (command == "fastencheck") { // the fastenings of all the tracks laying timbers with them: of each kind, and those over one another on a timber From 2d7eb966df4031795b9d9cacb01e8e04f0f54466 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 24 Oct 2026 03:04:10 +0200 Subject: [PATCH 19/19] fix(rails): the points of obtuse crossings on short tracks planed as on long ones: two rails only go on from each other across a joint less than 2.6 degrees apart, not the knuckle and the point whose ends lie within 5 cm at a slip centre (1:9 is 6.4 degrees); the flangeway of a rail around keeps ours out 1 cm past its ends, over the gap of a joint; a point beside its knuckle always keeps the gauge side of its head, whichever track of the crossing it lies on --- world/RailCarve.cpp | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/world/RailCarve.cpp b/world/RailCarve.cpp index bdd1c94c..3653e1b1 100644 --- a/world/RailCarve.cpp +++ b/world/RailCarve.cpp @@ -26,6 +26,7 @@ double constexpr window { 0.25 }; // how far around a station with a rail near t double constexpr nearby { 0.4 }; // how far along past the ends of a rail around it's near enough to be looked at closer double constexpr across_margin { 0.05 }; // and how far across from its reach, more where it runs at an angle double constexpr outside { 10.0 }; // how far past the ends of the track a part is looked at for where it ends +double constexpr joint_gap { 0.01 }; // how far past its ends the flangeway of a rail around keeps ours out, over the gap of a joint double constexpr slack { 0.0002 }; // how far a head reaches into a flangeway or another head unnoticed, and how far two heads planed against each other are kept apart double constexpr point_width { 0.001 }; // a wing rail ends where less than this is left of its head double constexpr blunt_width { 0.5 * nose_tip }; // the legs of a nose and the points of an obtuse crossing begin where this much is left of their heads @@ -251,7 +252,7 @@ bool continues( carve_rail const &A, carve_rail const &B ) { } for( int a = 0; a < 2; ++a ) { for( int b = 0; b < 2; ++b ) { - if( glm::length( plan( A.ends[ a ] - B.ends[ b ] ) ) < 0.05 && std::abs( glm::dot( A.directions[ a ], B.directions[ b ] ) ) > 0.99 ) { + if( glm::length( plan( A.ends[ a ] - B.ends[ b ] ) ) < 0.05 && std::abs( glm::dot( A.directions[ a ], B.directions[ b ] ) ) > 0.999 ) { return true; } } @@ -529,12 +530,13 @@ double run_of( Value const &Bound, double const At ) { } // how a run of stations along a part, where it runs into a flangeway or a head, planes it: from the side its head leaves that at the end of the -// run nearer the middle of the part; running into it all the way, from the side taking less of it where it takes least +// run nearer the middle of the part; running into it all the way, from the side taking less of it where it takes least; Outer, always from +// the outer side template -void settle_runs( std::vector const &Hits, long const First, double const Middle, Apply const &Applied ) { +void settle_runs( std::vector const &Hits, long const First, double const Middle, Apply const &Applied, bool const Outer = false ) { auto const upper_at = [&]( std::size_t const I ) { - return Hits[ I ].removed[ 0 ] <= Hits[ I ].removed[ 1 ]; }; + return Outer || Hits[ I ].removed[ 0 ] <= Hits[ I ].removed[ 1 ]; }; std::size_t i { 0 }; while( i < Hits.size() ) { if( false == Hits[ i ].in ) { @@ -586,7 +588,7 @@ band_hit band_at( station const &Station, carve_rail const &Ours, carve_rail con auto const centre { Station.offset + 0.5 * Ours.size.head }; glm::dvec2 outwards; auto const lateral { project( Theirs, across( Station, centre ), Distance, outwards ) }; - if( Distance < 0.0 || Distance > Theirs.length ) { + if( Distance < -joint_gap || Distance > Theirs.length + joint_gap ) { return hit; } // where the blades of a switch are, the rails of both paths on a side lie at each other as they're made to @@ -614,6 +616,25 @@ band_hit band_at( station const &Station, carve_rail const &Ours, carve_rail con return hit; } +// the part is the point of an obtuse crossing and theirs the knuckle it lies at, the rest of its head under the knuckle: the point keeps the +// gauge side of its head, which tracks the crossing is split among +bool beside_knuckle( carve_part const &Part, carve_rail const &Theirs ) { + + for( int end = 0; end < 2; ++end ) { + if( false == Part.obtuse[ end ] || Part.kinds[ end ] != rail_end::blunt ) { + continue; + } + for( auto const &other : Theirs.parts ) { + for( int otherend = 0; otherend < 2; ++otherend ) { + if( other.obtuse[ otherend ] && glm::length( Part.marks[ end ] - other.marks[ otherend ] ) < 0.1 ) { + return true; + } + } + } + } + return false; +} + // the part kept out of the flangeways of the rails around void trim( carve_rail &Rail, carve_part &Part ) { @@ -633,10 +654,12 @@ void trim( carve_rail &Rail, carve_part &Part ) { std::vector lower( scan.count(), -unbounded ); auto const around { around_of( *Rail.track ) }; for( auto *other : around ) { + design( *other, nullptr ); for( auto &theirs : track_of( *other ).rails ) { if( false == flangeway_counts( Rail, Part, theirs ) ) { continue; } + auto const knuckle { beside_knuckle( Part, theirs ) }; auto const windows { scan.stations_near( theirs, [&]( double ) { return glm::dvec2 { -flangeway - 0.5 * size.head, 0.5 * size.head }; }, { -nearby, theirs.length + nearby }, flangeway + size.head + nearby ) }; for( auto const &[ a, b ] : windows ) { if( debugging( Rail ) ) { @@ -657,7 +680,7 @@ void trim( carve_rail &Rail, carve_part &Part ) { } else { lower[ index ] = std::max( lower[ index ], hits[ I ].lower ); - } } ); + } }, knuckle ); } } }