mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-23 08:51:11 +02:00
refactor(track): the sleeper layout split by concern instead of one TrackSleepers.cpp
- SleeperRules: the rules beams are laid by and the faults counted where the layout breaks them - SleeperGeometry: plan helpers, beam boxes with their overlap and support tests and a grid of cells, shared by laying, filling gaps, keeping beams apart and checking instead of three copies of each - SleeperPaths: sampled paths, how near they run and whether one carries another; the tracks near a track and the stretches of a path where a test holds, shared by paths_around/unsettle_around and shared_zones/zones_of - SleeperFeatures: frogs and crossings the timbers turn to; SleeperLines: lines of beams, the rails they carry, where they part - SleeperLayout: clusters and their layout, given the epoch instead of reading it; SleeperModels: the models of the beams - SleeperWorker: the layouts kept per epoch and worked out in the background, behind a small interface - TrackSleepers: the facade, laying the instances, the shared zones and the check - sleepercheck reports a digest of all beams; geomhash skips empty chunks instead of crashing The layout is unchanged: geomhash, sleepercheck with the digest, wings, space, guards, crossings and switches give the same output on baltyk, zz_rozjazdy_id4, zz_rf_slips, zz_rf_wide, zz_slips_s42, zz_demo_przechylka and zz_connections.
This commit is contained in:
@@ -97,6 +97,12 @@ set(SOURCES
|
||||
"utilities/Timer.cpp"
|
||||
"world/Track.cpp"
|
||||
"world/TrackSleepers.cpp"
|
||||
"world/SleeperPaths.cpp"
|
||||
"world/SleeperFeatures.cpp"
|
||||
"world/SleeperLines.cpp"
|
||||
"world/SleeperLayout.cpp"
|
||||
"world/SleeperModels.cpp"
|
||||
"world/SleeperWorker.cpp"
|
||||
"world/GaugeLine.cpp"
|
||||
"world/RailCrossings.cpp"
|
||||
"world/RailMaterials.cpp"
|
||||
|
||||
@@ -280,8 +280,8 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm
|
||||
{
|
||||
auto const start{std::chrono::steady_clock::now()};
|
||||
auto const result{track_sleepers::check()};
|
||||
WriteLog(format("SELFTEST sleepercheck: %zu beams, %zu overlaps, %zu gaps, %zu bare rails, %zu too long (longest %.2f m), widest spacing %.2f m at %s; %zu shared off one plane, %zu switch panels off one plane, %zu ends clipped, %zu beams dropped, slowest cluster %.1f ms of %zu tracks; in %.1f ms",
|
||||
result.beams, result.overlaps, result.gaps, result.bare, result.toolong, result.longest, result.widest, result.widestat.c_str(), result.offplane, result.panels, result.clipped, result.dropped, result.slowest, result.biggest,
|
||||
WriteLog(format("SELFTEST sleepercheck: %zu beams, %zu overlaps, %zu gaps, %zu bare rails, %zu too long (longest %.2f m), widest spacing %.2f m at %s; %zu shared off one plane, %zu switch panels off one plane, %zu ends clipped, %zu beams dropped, slowest cluster %.1f ms of %zu tracks; digest %016llx; in %.1f ms",
|
||||
result.beams, result.overlaps, result.gaps, result.bare, result.toolong, result.longest, result.widest, result.widestat.c_str(), result.offplane, result.panels, result.clipped, result.dropped, result.slowest, result.biggest, static_cast<unsigned long long>(result.digest),
|
||||
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count()));
|
||||
for (auto const &example : result.examples)
|
||||
WriteLog("SELFTEST sleepercheck " + example);
|
||||
@@ -3247,6 +3247,9 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
|
||||
{
|
||||
for (auto const &geometry : *sequence)
|
||||
{
|
||||
// an empty piece of a rail gets no chunk, as the renderer skips it
|
||||
if (geometry == null_handle)
|
||||
continue;
|
||||
++chunks;
|
||||
for (auto const &vertex : GfxRenderer->Vertices(geometry))
|
||||
{
|
||||
|
||||
255
world/SleeperFeatures.cpp
Normal file
255
world/SleeperFeatures.cpp
Normal file
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
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/SleeperFeatures.h"
|
||||
#include "world/SleeperGeometry.h"
|
||||
#include "world/SleeperRules.h"
|
||||
#include "world/Track.h"
|
||||
|
||||
namespace track_sleepers {
|
||||
|
||||
namespace {
|
||||
|
||||
// how far past the timbers the sleepers turn back square to the tracks, by the angle the timbers are turned off them
|
||||
double turn_length( double const Angle ) {
|
||||
|
||||
return ( Angle <= turn_max ? 0.05 * pitch : pitch * std::ceil( Angle / turn_max ) );
|
||||
}
|
||||
|
||||
// the frog of the switch: where its paths are a gauge apart, from the samples of the diverging path against the main one
|
||||
void find_frog( sampled_path const &Main, sampled_path const &Diverging, std::vector<feature> &Features ) {
|
||||
|
||||
auto previousgap { 0.0 };
|
||||
for( std::size_t index = 1; index < Diverging.points.size(); ++index ) {
|
||||
double gap;
|
||||
glm::dvec2 direction;
|
||||
if( false == beside( Main, Diverging.points[ index ], Main.gauge + 1.0, gap, direction ) ) {
|
||||
return;
|
||||
}
|
||||
if( gap < Main.gauge ) {
|
||||
previousgap = gap;
|
||||
continue;
|
||||
}
|
||||
// between the samples, where the paths are a gauge apart
|
||||
auto const share { ( Main.gauge - previousgap ) / std::max( 1e-6, gap - previousgap ) };
|
||||
auto const distance { glm::mix( Diverging.distances[ index - 1 ], Diverging.distances[ index ], share ) };
|
||||
glm::dvec3 point;
|
||||
glm::vec3 angles;
|
||||
Diverging.segment->RaPositionGet( distance, point, angles );
|
||||
glm::dvec2 mainpoint, maindirection;
|
||||
auto const mainat { nearest_on( Main, plan( point ), mainpoint, maindirection ) };
|
||||
auto const divergingdirection { heading( Diverging, distance ) };
|
||||
if( glm::dot( maindirection, divergingdirection ) < 0.0 ) {
|
||||
maindirection = -maindirection;
|
||||
}
|
||||
auto const halfangle { 0.5 * angle_between( maindirection, divergingdirection ) };
|
||||
if( halfangle < 0.002 ) {
|
||||
return;
|
||||
}
|
||||
feature frog;
|
||||
// the frog lies midway between the paths
|
||||
frog.point = 0.5 * ( mainpoint + plan( point ) );
|
||||
frog.bisector = glm::normalize( maindirection + divergingdirection );
|
||||
frog.sine = std::sin( halfangle );
|
||||
frog.spacing = Main.gauge;
|
||||
frog.from = -frog_ramp;
|
||||
frog.full = 0.0;
|
||||
// till the plain sleepers of the paths going apart wouldn't touch any more
|
||||
frog.until = std::max( 0.0, ( Main.half + Diverging.half + end_gap - Main.gauge ) / ( 2.0 * frog.sine ) );
|
||||
frog.back = frog.until + turn_length( halfangle );
|
||||
frog.paths = { &Main, &Diverging };
|
||||
frog.at = { mainat, distance };
|
||||
frog.frog = true;
|
||||
Features.push_back( frog );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// where two paths of different tracks cross, from their samples
|
||||
void find_crossing( sampled_path const &Left, sampled_path const &Right, std::vector<feature> &Features ) {
|
||||
|
||||
if( Left.low.x > Right.high.x || Right.low.x > Left.high.x || Left.low.y > Right.high.y || Right.low.y > Left.high.y ) {
|
||||
return;
|
||||
}
|
||||
for( std::size_t left = 1; left < Left.points.size(); ++left ) {
|
||||
auto const a0 { plan( Left.points[ left - 1 ] ) };
|
||||
auto const a { plan( Left.points[ left ] ) - a0 };
|
||||
if( std::max( a0.x, a0.x + a.x ) < Right.low.x || std::min( a0.x, a0.x + a.x ) > Right.high.x
|
||||
|| std::max( a0.y, a0.y + a.y ) < Right.low.y || std::min( a0.y, a0.y + a.y ) > Right.high.y ) {
|
||||
continue;
|
||||
}
|
||||
for( std::size_t right = 1; right < Right.points.size(); ++right ) {
|
||||
auto const b0 { plan( Right.points[ right - 1 ] ) };
|
||||
auto const b { plan( Right.points[ right ] ) - b0 };
|
||||
auto const denominator { cross( a, b ) };
|
||||
if( std::abs( denominator ) < 1e-9 ) {
|
||||
continue;
|
||||
}
|
||||
auto const u { cross( b0 - a0, b ) / denominator };
|
||||
auto const v { cross( b0 - a0, a ) / denominator };
|
||||
if( u < 0.0 || u >= 1.0 || v < 0.0 || v >= 1.0 ) {
|
||||
continue;
|
||||
}
|
||||
auto const leftat { glm::mix( Left.distances[ left - 1 ], Left.distances[ left ], u ) };
|
||||
auto const rightat { glm::mix( Right.distances[ right - 1 ], Right.distances[ right ], v ) };
|
||||
if( leftat < 0.05 || leftat > Left.length - 0.05 || rightat < 0.05 || rightat > Right.length - 0.05 ) {
|
||||
// tracks joined end to end, or one ending on the other
|
||||
continue;
|
||||
}
|
||||
auto const levels { glm::mix( Left.points[ left - 1 ].y, Left.points[ left ].y, u ) - glm::mix( Right.points[ right - 1 ].y, Right.points[ right ].y, v ) };
|
||||
if( std::abs( levels ) > level_tolerance ) {
|
||||
continue;
|
||||
}
|
||||
auto const leftdirection { glm::normalize( a ) };
|
||||
auto rightdirection { glm::normalize( b ) };
|
||||
if( glm::dot( leftdirection, rightdirection ) < 0.0 ) {
|
||||
rightdirection = -rightdirection;
|
||||
}
|
||||
auto const angle { angle_between( leftdirection, rightdirection ) };
|
||||
if( angle < 0.01 ) {
|
||||
continue;
|
||||
}
|
||||
feature crossing;
|
||||
crossing.point = a0 + a * u;
|
||||
crossing.bisector = glm::normalize( leftdirection + rightdirection );
|
||||
crossing.sine = std::sin( 0.5 * angle );
|
||||
crossing.until = std::max( touch_spacing( Left, Right, angle ), touch_spacing( Right, Left, angle ) ) / ( 2.0 * crossing.sine );
|
||||
crossing.back = crossing.until + turn_length( 0.5 * angle );
|
||||
crossing.from = -crossing.back;
|
||||
crossing.full = -crossing.until;
|
||||
crossing.paths = { &Left, &Right };
|
||||
crossing.at = { leftat, rightat };
|
||||
crossing.grid = ( angle >= grid_angle );
|
||||
Features.push_back( crossing );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// whether another path goes on from the end of the path, so the beams there go on too
|
||||
bool goes_on( sampled_path const &Path, glm::dvec3 const &End, std::vector<sampled_path> const &Around ) {
|
||||
|
||||
for( auto const &other : Around ) {
|
||||
if( other.track == Path.track ) {
|
||||
continue;
|
||||
}
|
||||
if( glm::length( plan( other.points.front() - End ) ) < 0.05 || glm::length( plan( other.points.back() - End ) ) < 0.05 ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// the stretch of the timbers square to the bisector ends where either path ends with nothing going on from it
|
||||
void clamp_to_ends( feature &Feature, std::vector<sampled_path> const &Around ) {
|
||||
|
||||
for( auto const *path : Feature.paths ) {
|
||||
for( auto const *end : { &path->points.front(), &path->points.back() } ) {
|
||||
if( goes_on( *path, *end, Around ) ) {
|
||||
continue;
|
||||
}
|
||||
auto const along { glm::dot( plan( *end ) - Feature.point, Feature.bisector ) };
|
||||
if( along > 0.0 ) {
|
||||
Feature.until = std::max( Feature.full, std::min( Feature.until, along ) );
|
||||
}
|
||||
else if( false == Feature.frog ) {
|
||||
Feature.full = std::min( Feature.until, std::max( Feature.full, along ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
auto const turn { turn_length( std::asin( Feature.sine ) ) };
|
||||
Feature.back = Feature.until + turn;
|
||||
if( false == Feature.frog ) {
|
||||
Feature.from = Feature.full - turn;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// the frogs of the switches and the crossings of the paths around
|
||||
std::vector<feature> find_features( std::vector<sampled_path> const &Around ) {
|
||||
|
||||
std::vector<feature> result;
|
||||
for( std::size_t index = 0; index < Around.size(); ++index ) {
|
||||
auto const &path { Around[ index ] };
|
||||
if( path.track->eType == tt_Switch && path.path == 0 && index + 1 < Around.size() && Around[ index + 1 ].track == path.track ) {
|
||||
find_frog( path, Around[ index + 1 ], result );
|
||||
}
|
||||
for( std::size_t other = index + 1; other < Around.size(); ++other ) {
|
||||
if( Around[ other ].track != path.track ) {
|
||||
find_crossing( path, Around[ other ], result );
|
||||
}
|
||||
}
|
||||
}
|
||||
for( auto &feature : result ) {
|
||||
clamp_to_ends( feature, Around );
|
||||
}
|
||||
// the frogs of the switches of a crossover between tracks running alongside each other: the diverging tracks of both lie on one
|
||||
// line, the main ones side by side
|
||||
auto const way = []( sampled_path const &Path, double const Distance ) {
|
||||
return heading( Path, Distance ); };
|
||||
for( auto &left : result ) {
|
||||
for( auto &right : result ) {
|
||||
if( &left == &right || false == left.frog || false == right.frog || left.paths[ 0 ]->track == right.paths[ 0 ]->track ) {
|
||||
continue;
|
||||
}
|
||||
auto const leftmain { way( *left.paths[ 0 ], left.at[ 0 ] ) };
|
||||
auto const rightmain { way( *right.paths[ 0 ], right.at[ 0 ] ) };
|
||||
auto const leftdiverging { way( *left.paths[ 1 ], left.at[ 1 ] ) };
|
||||
auto const rightdiverging { way( *right.paths[ 1 ], right.at[ 1 ] ) };
|
||||
auto const apart { std::abs( cross( leftmain, right.point - left.point ) ) };
|
||||
if( angle_between( leftmain, rightmain ) < 0.035 && angle_between( leftdiverging, rightdiverging ) < 0.035 && apart > 3.0 && apart < 8.0 ) {
|
||||
left.crossover = true;
|
||||
right.crossover = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// which way the beam at the point lies square to: the path laying it, turned toward the bisectors of the frogs and crossings
|
||||
// about the point. this depends only on where the point is, so the beams of paths laid one after another turn smoothly
|
||||
glm::dvec2 beam_direction( glm::dvec3 const &Point, glm::dvec2 const &Forward, std::vector<feature> const &Features ) {
|
||||
|
||||
auto const point { plan( Point ) };
|
||||
auto weight { 0.0 };
|
||||
glm::dvec2 bisectors { 0.0 };
|
||||
for( auto const &feature : Features ) {
|
||||
if( feature.grid || feature.crossover ) {
|
||||
continue;
|
||||
}
|
||||
auto const offset { point - feature.point };
|
||||
auto const along { glm::dot( offset, feature.bisector ) };
|
||||
if( along <= feature.from || along >= feature.back ) {
|
||||
continue;
|
||||
}
|
||||
auto turned {
|
||||
along < feature.full ? glm::smoothstep( feature.from, feature.full, along ) :
|
||||
along > feature.until ? 1.0 - glm::smoothstep( feature.until, feature.back, along ) :
|
||||
1.0 };
|
||||
// only the beams under the paths of the frog or the crossing turn, not those of the tracks beside them
|
||||
auto const spacing { feature.spacing + 2.0 * feature.sine * ( feature.frog ? std::max( 0.0, along ) : std::abs( along ) ) };
|
||||
auto const reach { 0.5 * spacing + feature.paths[ 0 ]->reach + 0.3 };
|
||||
auto const lateral { std::abs( cross( feature.bisector, offset ) ) };
|
||||
turned *= 1.0 - glm::smoothstep( reach, reach + 0.5, lateral );
|
||||
if( turned <= 0.0 ) {
|
||||
continue;
|
||||
}
|
||||
weight = std::max( weight, turned );
|
||||
bisectors += turned * ( glm::dot( feature.bisector, Forward ) >= 0.0 ? feature.bisector : -feature.bisector );
|
||||
}
|
||||
if( weight <= 0.0 || glm::length( bisectors ) < 1e-9 ) {
|
||||
return Forward;
|
||||
}
|
||||
return glm::normalize( Forward * ( 1.0 - weight ) + glm::normalize( bisectors ) * weight );
|
||||
}
|
||||
|
||||
|
||||
} // namespace track_sleepers
|
||||
47
world/SleeperFeatures.h
Normal file
47
world/SleeperFeatures.h
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "world/SleeperPaths.h"
|
||||
|
||||
// the frogs of the switches and the crossings of the tracks, which the timbers about them turn to
|
||||
namespace track_sleepers {
|
||||
|
||||
// where two paths meet at an angle, the timbers which carry both lie square to the bisector of the angle: past the frog of
|
||||
// a switch, where the paths go apart, and about the point where two paths cross. the timbers turn to the bisector before
|
||||
// and back square to the tracks after, by the distance along the bisector from the point
|
||||
struct feature {
|
||||
glm::dvec2 point; // the frog, or where the paths cross, in plan
|
||||
glm::dvec2 bisector; // unit
|
||||
double sine { 0.0 }; // of half the angle between the paths
|
||||
double spacing { 0.0 }; // how far apart the paths run at the point
|
||||
double from { 0.0 }; // along the bisector, where the timbers start turning to it
|
||||
double full { 0.0 }; // where they're square to it
|
||||
double until { 0.0 }; // till where they stay so
|
||||
double back { 0.0 }; // where they're square to the tracks again
|
||||
std::array<sampled_path const *, 2> paths {};
|
||||
std::array<double, 2> at {}; // the distance of the point along each path
|
||||
bool frog { false }; // the paths go apart from the point, rather than cross there
|
||||
bool grid { false }; // the paths cross so wide the timbers keep square to each
|
||||
bool crossover { false }; // the frog of a switch whose diverging track leads to another switch of a track alongside: the timbers keep square to the tracks
|
||||
};
|
||||
|
||||
// the frogs of the switches and the crossings of the paths around
|
||||
std::vector<feature> find_features( std::vector<sampled_path> const &Around );
|
||||
// which way the beam at the point lies square to: the path laying it, turned toward the bisectors of the frogs and crossings
|
||||
// about the point. this depends only on where the point is, so the beams of paths laid one after another turn smoothly
|
||||
glm::dvec2 beam_direction( glm::dvec3 const &Point, glm::dvec2 const &Forward, std::vector<feature> const &Features );
|
||||
|
||||
} // namespace track_sleepers
|
||||
148
world/SleeperGeometry.h
Normal file
148
world/SleeperGeometry.h
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
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 <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "world/SleeperRules.h"
|
||||
#include "world/TrackSleepers.h"
|
||||
|
||||
// geometry in plan, x and z of the world, the sleepers are laid out in
|
||||
namespace track_sleepers {
|
||||
|
||||
inline glm::dvec2 plan( glm::dvec3 const &Vector ) {
|
||||
return { Vector.x, Vector.z };
|
||||
}
|
||||
|
||||
inline double cross( glm::dvec2 const &Left, glm::dvec2 const &Right ) {
|
||||
return Left.x * Right.y - Left.y * Right.x;
|
||||
}
|
||||
|
||||
// the angle between two directions in plan, either way along them
|
||||
inline double angle_between( glm::dvec2 const &Left, glm::dvec2 const &Right ) {
|
||||
|
||||
return std::acos( std::min( 1.0, std::abs( glm::dot( Left, Right ) ) ) );
|
||||
}
|
||||
|
||||
// the direction turned by the angle, counterclockwise from x to z
|
||||
inline glm::dvec2 rotated( glm::dvec2 const &Direction, double const Angle ) {
|
||||
|
||||
return { Direction.x * std::cos( Angle ) - Direction.y * std::sin( Angle ), Direction.x * std::sin( Angle ) + Direction.y * std::cos( Angle ) };
|
||||
}
|
||||
|
||||
// the angle the direction is turned off the forward way, either way along it
|
||||
inline double turn_off( glm::dvec2 const &Forward, glm::dvec2 Direction ) {
|
||||
|
||||
if( glm::dot( Forward, Direction ) < 0.0 ) {
|
||||
Direction = -Direction;
|
||||
}
|
||||
return std::atan2( cross( Forward, Direction ), glm::dot( Forward, Direction ) );
|
||||
}
|
||||
|
||||
// a beam in plan, by its middle, the unit vector along it and half its length
|
||||
struct beam_box {
|
||||
glm::dvec2 centre { 0.0 };
|
||||
glm::dvec2 axis { 1.0, 0.0 };
|
||||
double half { 0.0 };
|
||||
};
|
||||
|
||||
inline beam_box box_of( beam const &Beam ) {
|
||||
|
||||
auto const span { plan( Beam.to - Beam.from ) };
|
||||
auto const length { glm::length( span ) };
|
||||
return { plan( 0.5 * ( Beam.from + Beam.to ) ), length > 1e-9 ? span / length : glm::dvec2 { 1.0, 0.0 }, 0.5 * length };
|
||||
}
|
||||
|
||||
// whether two beams keep clear of each other, with a little room left between them: an axis of either keeps them apart
|
||||
inline bool apart( beam_box const &Left, beam_box const &Right ) {
|
||||
|
||||
auto const offset { Right.centre - Left.centre };
|
||||
for( auto const &axis : { Left.axis, glm::dvec2 { -Left.axis.y, Left.axis.x }, Right.axis, glm::dvec2 { -Right.axis.y, Right.axis.x } } ) {
|
||||
auto const reach = [&]( beam_box const &Box ) {
|
||||
return Box.half * std::abs( glm::dot( Box.axis, axis ) ) + ( half_width - 0.01 ) * std::abs( cross( Box.axis, axis ) ); };
|
||||
if( std::abs( glm::dot( offset, axis ) ) > reach( Left ) + reach( Right ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// whether the point lies on the beam
|
||||
inline bool holds( beam_box const &Box, glm::dvec2 const &Point ) {
|
||||
|
||||
auto const offset { Point - Box.centre };
|
||||
return std::abs( glm::dot( offset, Box.axis ) ) <= Box.half && std::abs( cross( Box.axis, offset ) ) <= half_width;
|
||||
}
|
||||
|
||||
// values put in the square cells of a grid in plan that a square about a point reaches into, to find those near a place
|
||||
template <typename Value>
|
||||
class plan_grid {
|
||||
|
||||
public:
|
||||
using cell = std::pair<std::int64_t, std::int64_t>;
|
||||
|
||||
explicit plan_grid( double const Size ) :
|
||||
m_size( Size ) {}
|
||||
|
||||
// the value in every cell the square of the reach about the centre touches
|
||||
void insert( glm::dvec2 const &Centre, double const Reach, Value const &Inserted ) {
|
||||
|
||||
auto const low { key( Centre - glm::dvec2 { Reach } ) };
|
||||
auto const high { key( Centre + glm::dvec2 { Reach } ) };
|
||||
for( auto x { low.first }; x <= high.first; ++x ) {
|
||||
for( auto z { low.second }; z <= high.second; ++z ) {
|
||||
m_cells[ { x, z } ].push_back( Inserted );
|
||||
}
|
||||
}
|
||||
}
|
||||
// the values in the cell of the point, null where there are none
|
||||
std::vector<Value> const *at( glm::dvec2 const &Point ) const {
|
||||
|
||||
auto const found { m_cells.find( key( Point ) ) };
|
||||
return ( found != m_cells.end() ? &found->second : nullptr );
|
||||
}
|
||||
// whether the test holds for every value in the cells the square of the reach about the centre touches
|
||||
template <typename Test>
|
||||
bool all_about( glm::dvec2 const &Centre, double const Reach, Test const &Holds ) const {
|
||||
|
||||
auto const low { key( Centre - glm::dvec2 { Reach } ) };
|
||||
auto const high { key( Centre + glm::dvec2 { Reach } ) };
|
||||
for( auto x { low.first }; x <= high.first; ++x ) {
|
||||
for( auto z { low.second }; z <= high.second; ++z ) {
|
||||
auto const found { m_cells.find( { x, z } ) };
|
||||
if( found != m_cells.end() && false == std::all_of( found->second.begin(), found->second.end(), Holds ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::map<cell, std::vector<Value>> const &cells() const {
|
||||
return m_cells;
|
||||
}
|
||||
|
||||
private:
|
||||
cell key( glm::dvec2 const &Point ) const {
|
||||
return { static_cast<std::int64_t>( std::floor( Point.x / m_size ) ), static_cast<std::int64_t>( std::floor( Point.y / m_size ) ) };
|
||||
}
|
||||
|
||||
double m_size;
|
||||
std::map<cell, std::vector<Value>> m_cells;
|
||||
};
|
||||
|
||||
|
||||
} // namespace track_sleepers
|
||||
709
world/SleeperLayout.cpp
Normal file
709
world/SleeperLayout.cpp
Normal file
@@ -0,0 +1,709 @@
|
||||
/*
|
||||
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/SleeperLayout.h"
|
||||
#include "world/SleeperFeatures.h"
|
||||
#include "world/SleeperGeometry.h"
|
||||
#include "world/SleeperLines.h"
|
||||
#include "world/SleeperPaths.h"
|
||||
#include "world/SleeperRules.h"
|
||||
#include "world/Track.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace track_sleepers {
|
||||
|
||||
namespace {
|
||||
|
||||
// whether the plain sleepers of the two paths would meet anywhere, so they lie on timbers together there
|
||||
bool share_anywhere( sampled_path const &Left, sampled_path const &Right ) {
|
||||
|
||||
auto const margin { Left.half + Right.half + half_width + end_gap };
|
||||
if( Left.low.x > Right.high.x + margin || Right.low.x > Left.high.x + margin || Left.low.y > Right.high.y + margin || Right.low.y > Left.high.y + margin ) {
|
||||
return false;
|
||||
}
|
||||
for( auto const &pair : { std::make_pair( &Left, &Right ), std::make_pair( &Right, &Left ) } ) {
|
||||
auto const &path { *pair.first };
|
||||
for( std::size_t index = 0; index < path.points.size(); ++index ) {
|
||||
if( shares_with( path, path.points[ index ], heading( path, path.distances[ index ] ), *pair.second ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// the stretches of the path where another path of the cluster runs so near that their plain sleepers would meet
|
||||
std::vector<glm::dvec2> zones_of( sampled_path const &Path, std::vector<sampled_path> const &Paths, std::vector<feature> const &Crossings ) {
|
||||
|
||||
return stretches_where( Path, [&]( glm::dvec3 const &Point, glm::dvec2 const &Direction ) {
|
||||
return std::any_of( Paths.begin(), Paths.end(), [&]( sampled_path const &Other ) {
|
||||
if( &Other == &Path ) {
|
||||
return false;
|
||||
}
|
||||
// tracks crossing share the timbers where their sleepers would meet, others only where they're too close to part them
|
||||
auto const crossed { std::any_of( Crossings.begin(), Crossings.end(), [&]( feature const &Feature ) {
|
||||
return ( Feature.paths[ 0 ] == &Path && Feature.paths[ 1 ] == &Other ) || ( Feature.paths[ 0 ] == &Other && Feature.paths[ 1 ] == &Path ); } ) };
|
||||
if( crossed ) {
|
||||
return shares_with( Path, Point, Direction, Other );
|
||||
}
|
||||
double gap;
|
||||
glm::dvec2 otherdirection { Direction };
|
||||
return beside( Other, Point, shared_spacing( Path, Other ), gap, otherdirection ) && gap < shared_spacing( Path, Other ); } ); } );
|
||||
}
|
||||
|
||||
// the stretches left of the first ones once the second ones are taken out, those shorter than the least length left out too
|
||||
std::vector<glm::dvec2> without( std::vector<glm::dvec2> const &Stretches, std::vector<glm::dvec2> const &Taken, double const Least ) {
|
||||
|
||||
std::vector<glm::dvec2> result { Stretches };
|
||||
for( auto const &taken : Taken ) {
|
||||
std::vector<glm::dvec2> left;
|
||||
for( auto const &stretch : result ) {
|
||||
if( stretch.y <= taken.x || stretch.x >= taken.y ) {
|
||||
left.push_back( stretch );
|
||||
continue;
|
||||
}
|
||||
if( stretch.x < taken.x ) {
|
||||
left.emplace_back( stretch.x, taken.x );
|
||||
}
|
||||
if( stretch.y > taken.y ) {
|
||||
left.emplace_back( taken.y, stretch.y );
|
||||
}
|
||||
}
|
||||
result = std::move( left );
|
||||
}
|
||||
result.erase( std::remove_if( result.begin(), result.end(), [&]( glm::dvec2 const &Stretch ) { return Stretch.y - Stretch.x < Least; } ), result.end() );
|
||||
return result;
|
||||
}
|
||||
|
||||
// the beams of a cluster of tracks, laid together: lines of timbers led along the paths near other paths one after another, each
|
||||
// line across all the rails of the cluster it reaches, and the plain sleepers of every path where no line carries it, in step
|
||||
// with the last timber carrying it
|
||||
class cluster_layout {
|
||||
|
||||
public:
|
||||
cluster_layout( std::vector<TTrack *> const &Tracks, std::uint64_t const Epoch ) :
|
||||
tracks( Tracks ), epoch( Epoch ) {}
|
||||
|
||||
std::shared_ptr<cluster_beams> lay() {
|
||||
|
||||
auto result { std::make_shared<cluster_beams>() };
|
||||
result->epoch = epoch;
|
||||
for( auto *track : tracks ) {
|
||||
result->beams[ track ];
|
||||
for( int path = 0; path < path_count( *track ); ++path ) {
|
||||
paths.emplace_back( sample( *track, path ) );
|
||||
}
|
||||
}
|
||||
paths.erase( std::remove_if( paths.begin(), paths.end(), []( sampled_path const &Path ) { return Path.points.size() < 2; } ), paths.end() );
|
||||
for( auto *track : tracks ) {
|
||||
for( auto &other : paths_around( *track ) ) {
|
||||
if( std::find( tracks.begin(), tracks.end(), other.track ) != tracks.end()
|
||||
|| std::any_of( context.begin(), context.end(), [&]( sampled_path const &Known ) { return Known.track == other.track && Known.path == other.path; } ) ) {
|
||||
continue;
|
||||
}
|
||||
context.emplace_back( std::move( other ) );
|
||||
}
|
||||
}
|
||||
features = find_features( paths );
|
||||
for( auto const &feature : features ) {
|
||||
if( false == feature.frog && false == feature.grid ) {
|
||||
crossings.push_back( feature );
|
||||
}
|
||||
}
|
||||
zones.resize( paths.size() );
|
||||
records.resize( paths.size() );
|
||||
done.resize( paths.size() );
|
||||
for( std::size_t index = 0; index < paths.size(); ++index ) {
|
||||
zones[ index ] = ( tracks.size() > 1 || paths.size() > 1 ? zones_of( paths[ index ], paths, crossings ) : std::vector<glm::dvec2> {} );
|
||||
}
|
||||
// the lines of timbers, led along the path first by priority with a stretch near another path no line carries yet
|
||||
for( std::size_t round = 0; round < 1000; ++round ) {
|
||||
auto best { paths.size() };
|
||||
glm::dvec2 stretch { 0.0 };
|
||||
for( std::size_t index = 0; index < paths.size(); ++index ) {
|
||||
auto const left { without( zones[ index ], covered( index ), 0.3 ) };
|
||||
if( left.empty() ) {
|
||||
continue;
|
||||
}
|
||||
if( best == paths.size() || before( paths[ index ], paths[ best ] ) ) {
|
||||
best = index;
|
||||
stretch = *std::max_element( left.begin(), left.end(), []( glm::dvec2 const &Left, glm::dvec2 const &Right ) { return Left.y - Left.x < Right.y - Right.x; } );
|
||||
}
|
||||
}
|
||||
if( best == paths.size() ) {
|
||||
break;
|
||||
}
|
||||
lead( best, stretch );
|
||||
done[ best ].push_back( stretch );
|
||||
}
|
||||
// the plain sleepers of each path, between the timbers carrying it and its ends
|
||||
for( std::size_t index = 0; index < paths.size(); ++index ) {
|
||||
lay_plain( index );
|
||||
}
|
||||
keep_apart();
|
||||
fill_gaps();
|
||||
keep_apart();
|
||||
for( auto const &laid : beams ) {
|
||||
result->beams[ laid.owner ].push_back( laid.value );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
struct owned {
|
||||
TTrack const *owner;
|
||||
beam value;
|
||||
};
|
||||
|
||||
std::vector<TTrack *> const &tracks;
|
||||
std::uint64_t const epoch; // of the layout the tracks are laid out for
|
||||
std::vector<sampled_path> paths; // of the tracks of the cluster
|
||||
std::vector<sampled_path> context; // of the tracks about them, whose rails and sleepers the beams keep clear of
|
||||
std::vector<feature> features;
|
||||
std::vector<feature> crossings; // the features where tracks cross, without the frogs
|
||||
std::vector<std::vector<glm::dvec2>> zones; // of each path, where its plain sleepers would meet those of another one
|
||||
// a line of timbers carrying a path: where along the path, and which way the line runs in plan
|
||||
struct record {
|
||||
double at;
|
||||
glm::dvec2 along;
|
||||
};
|
||||
std::vector<std::vector<record>> records; // the lines of timbers carrying each path
|
||||
std::vector<std::vector<glm::dvec2>> done; // the stretches of each path lines were led along
|
||||
std::vector<owned> beams;
|
||||
|
||||
// the stretches of the path the lines laid so far carry it on, and those lines were led along
|
||||
std::vector<glm::dvec2> covered( std::size_t const Index ) const {
|
||||
|
||||
auto result { done[ Index ] };
|
||||
auto const at { distances( Index ) };
|
||||
if( at.empty() ) {
|
||||
return result;
|
||||
}
|
||||
auto start { at.front() };
|
||||
auto last { at.front() };
|
||||
for( std::size_t index = 1; index <= at.size(); ++index ) {
|
||||
if( index < at.size() && at[ index ] - last <= 1.6 * pitch ) {
|
||||
last = at[ index ];
|
||||
continue;
|
||||
}
|
||||
result.emplace_back( start - 0.5 * pitch, last + 0.5 * pitch );
|
||||
if( index < at.size() ) {
|
||||
start = last = at[ index ];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 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<double> pins( sampled_path const &Path ) const {
|
||||
|
||||
std::vector<double> result;
|
||||
for( auto const &feature : features ) {
|
||||
if( false == feature.grid ) {
|
||||
continue;
|
||||
}
|
||||
for( int side = 0; side < 2; ++side ) {
|
||||
if( feature.paths[ side ] != &Path ) {
|
||||
continue;
|
||||
}
|
||||
// under 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 );
|
||||
}
|
||||
}
|
||||
std::sort( result.begin(), result.end() );
|
||||
return result;
|
||||
}
|
||||
|
||||
// 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> record_near( std::size_t const Index, double const Distance, double const Side, double const Reach ) const {
|
||||
|
||||
std::optional<record> result;
|
||||
for( auto const &line : records[ Index ] ) {
|
||||
auto const along { ( line.at - Distance ) * Side };
|
||||
if( along < -0.25 * pitch || along > Reach ) {
|
||||
continue;
|
||||
}
|
||||
if( false == result.has_value() || std::abs( line.at - Distance ) < std::abs( result->at - Distance ) ) {
|
||||
result = line;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// where along the path the lines of timbers carry it, in order
|
||||
std::vector<double> distances( std::size_t const Index ) const {
|
||||
|
||||
std::vector<double> result;
|
||||
for( auto const &line : records[ Index ] ) {
|
||||
result.push_back( line.at );
|
||||
}
|
||||
std::sort( result.begin(), result.end() );
|
||||
result.erase( std::unique( result.begin(), result.end(), []( double const Left, double const Right ) { return Right - Left < 0.05; } ), result.end() );
|
||||
return result;
|
||||
}
|
||||
|
||||
// where the path is at the distance and which way it runs in plan; false where it can't tell
|
||||
bool frame_at( sampled_path const &Path, double const Distance, glm::dvec3 &Point, glm::vec3 &Angles, glm::dvec2 &Forward ) const {
|
||||
|
||||
Path.segment->RaPositionGet( Distance, Point, Angles );
|
||||
glm::dvec3 behind, ahead;
|
||||
glm::vec3 unused;
|
||||
Path.segment->RaPositionGet( std::max( 0.0, Distance - 0.1 ), behind, unused );
|
||||
Path.segment->RaPositionGet( std::min( Path.length, Distance + 0.1 ), ahead, unused );
|
||||
Forward = plan( ahead - behind );
|
||||
if( glm::length( Forward ) < 1e-6 ) {
|
||||
return false;
|
||||
}
|
||||
Forward = glm::normalize( Forward );
|
||||
return true;
|
||||
}
|
||||
|
||||
// the lines at the distances along the path, turned as the frogs and crossings about them ask, but by no more than the rate
|
||||
// from one line to the next, nor from the lines carrying the path at either end, so the ends of long timbers keep their spacing
|
||||
void lay_lines( std::size_t const Index, std::vector<double> const &Stations, std::optional<record> const &First, std::optional<record> const &Last, double const Rate, bool const Led ) {
|
||||
|
||||
auto const &path { paths[ Index ] };
|
||||
std::vector<double> distances;
|
||||
std::vector<glm::dvec2> forwards;
|
||||
std::vector<double> turns; // of the lines off square to the path, in plan
|
||||
for( auto const station : Stations ) {
|
||||
glm::dvec3 point;
|
||||
glm::vec3 angles;
|
||||
glm::dvec2 forward;
|
||||
if( false == frame_at( path, station, point, angles, forward ) ) {
|
||||
continue;
|
||||
}
|
||||
distances.push_back( station );
|
||||
forwards.push_back( forward );
|
||||
// the plain sleepers lie square to their track, turned only to meet the timbers next to them
|
||||
auto along { Led ? beam_direction( point, forward, features ) : forward };
|
||||
if( Led ) {
|
||||
// the timbers about a crossing lie square to the mean way of the tracks they carry, the frogs about it don't turn them:
|
||||
// square to the main tracks of a scissors crossing, to the bisector of a diamond
|
||||
if( auto const mean { whole_at( path, point, forward, along ) }; mean.has_value() ) {
|
||||
along = *mean;
|
||||
}
|
||||
}
|
||||
turns.push_back( turn_off( forward, along ) );
|
||||
}
|
||||
if( distances.empty() ) {
|
||||
return;
|
||||
}
|
||||
auto const limit = [&]( std::size_t const From, std::size_t const To, std::optional<record> const &Seed ) {
|
||||
auto const step { To > From ? 1 : -1 };
|
||||
std::optional<double> previous;
|
||||
auto rate { Rate };
|
||||
if( Seed.has_value() ) {
|
||||
previous = turn_off( forwards[ From ], Seed->along );
|
||||
if( false == Led ) {
|
||||
// square after three sleepers
|
||||
rate = std::max( 0.004, std::abs( *previous ) / 3.0 );
|
||||
}
|
||||
}
|
||||
for( auto index { From }; ; index += step ) {
|
||||
if( previous.has_value() ) {
|
||||
turns[ index ] = std::clamp( turns[ index ], *previous - rate, *previous + rate );
|
||||
}
|
||||
previous = turns[ index ];
|
||||
if( index == To ) {
|
||||
break;
|
||||
}
|
||||
} };
|
||||
limit( 0, distances.size() - 1, First );
|
||||
limit( distances.size() - 1, 0, Last );
|
||||
for( std::size_t index = 0; index < distances.size(); ++index ) {
|
||||
lay_line( Index, distances[ index ], Led, rotated( forwards[ index ], turns[ index ] ) );
|
||||
}
|
||||
}
|
||||
|
||||
// the lines of timbers led along the stretch of the path, spread between the lines carrying it at either end, or the ends of the
|
||||
// stretch, and the pins on it
|
||||
void lead( std::size_t const Index, glm::dvec2 const &Stretch ) {
|
||||
|
||||
auto const &path { paths[ Index ] };
|
||||
auto const back { record_near( Index, Stretch.x, -1.0, 2.0 * pitch ) };
|
||||
auto const fore { record_near( Index, Stretch.y, 1.0, 2.0 * pitch ) };
|
||||
anchor const start { back.has_value() ? anchor { back->at, 1.0 } : anchor { Stretch.x <= 0.01 ? 0.0 : Stretch.x, 0.5 } };
|
||||
anchor const end { fore.has_value() ? anchor { fore->at, 1.0 } : anchor { Stretch.y >= path.length - 0.01 ? path.length : Stretch.y, 0.5 } };
|
||||
if( end.at - start.at <= 0.0 ) {
|
||||
return;
|
||||
}
|
||||
std::vector<anchor> 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 } );
|
||||
}
|
||||
}
|
||||
anchors.push_back( end );
|
||||
std::vector<double> stations;
|
||||
for( std::size_t index = 1; index < anchors.size(); ++index ) {
|
||||
spread( anchors[ index - 1 ], anchors[ index ], stations );
|
||||
}
|
||||
lay_lines( Index, stations, back, fore, timber_turn, true );
|
||||
}
|
||||
|
||||
// where the timber led along the path through the point, lying the given way, carries both tracks of a crossing: the way it lies
|
||||
// then, square to the middle between the ways of the tracks it carries turned furthest either way
|
||||
std::optional<glm::dvec2> whole_at( sampled_path const &Path, glm::dvec3 const &Point, glm::dvec2 const &Forward, glm::dvec2 const &Along ) const {
|
||||
|
||||
if( crossings.empty() ) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::vector<crossing> members;
|
||||
find_crossings( Path, Point, Along, glm::dvec2 { -Along.y, Along.x }, 0.5 * length_cap + Path.reach, paths, members );
|
||||
carry( Path, Forward, Along, members, false, false );
|
||||
if( false == whole_with( Path, members ) ) {
|
||||
return std::nullopt;
|
||||
}
|
||||
// the outermost tracks it carries, either way along it: the main tracks of a scissors crossing, the crossing ones of a diamond
|
||||
std::pair<double, glm::dvec2> low { 0.0, Forward };
|
||||
std::pair<double, glm::dvec2> high { 0.0, Forward };
|
||||
for( auto const &crossing : members ) {
|
||||
if( false == crossing.carried ) {
|
||||
continue;
|
||||
}
|
||||
if( crossing.across < low.first ) {
|
||||
low = { crossing.across, crossing.direction };
|
||||
}
|
||||
if( crossing.across > high.first ) {
|
||||
high = { crossing.across, crossing.direction };
|
||||
}
|
||||
}
|
||||
return rotated( Forward, 0.5 * ( turn_off( Forward, low.second ) + turn_off( Forward, high.second ) ) );
|
||||
}
|
||||
|
||||
bool whole_with( sampled_path const &Path, std::vector<crossing> const &Members ) const {
|
||||
|
||||
auto const carries = [&]( sampled_path const *Carried ) {
|
||||
return Carried == &Path || std::any_of( Members.begin(), Members.end(), [&]( crossing const &Crossing ) { return Crossing.carried && Crossing.path == Carried; } ); };
|
||||
return std::any_of( crossings.begin(), crossings.end(), [&]( feature const &Feature ) {
|
||||
return carries( Feature.paths[ 0 ] ) && carries( Feature.paths[ 1 ] ); } );
|
||||
}
|
||||
|
||||
// the plain sleepers of the path on the stretches no line of timbers carries it, in step with the timbers at their ends
|
||||
void lay_plain( std::size_t const Index ) {
|
||||
|
||||
auto const &path { paths[ Index ] };
|
||||
auto const at { distances( Index ) };
|
||||
std::vector<std::pair<anchor, anchor>> stretches;
|
||||
if( at.empty() ) {
|
||||
stretches.push_back( { { 0.0, 0.5 }, { path.length, 0.5 } } );
|
||||
}
|
||||
else {
|
||||
stretches.push_back( { { 0.0, 0.5 }, { at.front(), 1.0 } } );
|
||||
for( std::size_t index = 1; index < at.size(); ++index ) {
|
||||
if( at[ index ] - at[ index - 1 ] > 1.6 * pitch ) {
|
||||
stretches.push_back( { { at[ index - 1 ], 1.0 }, { at[ index ], 1.0 } } );
|
||||
}
|
||||
}
|
||||
stretches.push_back( { { at.back(), 1.0 }, { path.length, 0.5 } } );
|
||||
}
|
||||
for( auto const &stretch : stretches ) {
|
||||
auto const span { stretch.second.at - stretch.first.at };
|
||||
if( span <= 0.0 || span < ( stretch.first.offset + stretch.second.offset - 1.0 ) * pitch + 0.5 * pitch_min ) {
|
||||
continue;
|
||||
}
|
||||
std::vector<double> stations;
|
||||
spread( stretch.first, stretch.second, stations );
|
||||
// turned back square to the path from the timbers at the ends of the stretch
|
||||
auto const first { stretch.first.offset == 1.0 ? record_near( Index, stretch.first.at, 1.0, 0.05 ) : std::optional<record> {} };
|
||||
auto const last { stretch.second.offset == 1.0 ? record_near( Index, stretch.second.at, -1.0, 0.05 ) : std::optional<record> {} };
|
||||
lay_lines( Index, stations, first, last, sleeper_turn, false );
|
||||
}
|
||||
}
|
||||
|
||||
// the beams on the line through the point of the path at the distance: the timber of the line led along it, taking the rails of
|
||||
// the paths of the cluster it reaches, or the plain sleeper of the path alone
|
||||
void lay_line( std::size_t const Index, double const Distance, bool const Led, glm::dvec2 const &Along ) {
|
||||
|
||||
auto const &path { paths[ Index ] };
|
||||
glm::dvec3 point;
|
||||
glm::vec3 angles;
|
||||
glm::dvec2 forward;
|
||||
if( false == frame_at( path, Distance, point, angles, forward ) ) {
|
||||
return;
|
||||
}
|
||||
auto const along { Along };
|
||||
glm::dvec2 const across { -along.y, along.x };
|
||||
auto const reach { 0.5 * length_cap + path.reach };
|
||||
std::vector<crossing> members, others;
|
||||
find_crossings( path, point, along, across, reach, paths, members );
|
||||
find_crossings( path, point, along, across, reach, context, others );
|
||||
if( Led ) {
|
||||
// about a crossing the timbers take all the tracks whose sleepers would meet, elsewhere only those too close to part
|
||||
carry( path, forward, along, members, false );
|
||||
if( false == whole_with( path, members ) ) {
|
||||
for( auto &crossing : members ) {
|
||||
crossing.carried = false;
|
||||
}
|
||||
carry( path, forward, along, members, true );
|
||||
}
|
||||
}
|
||||
std::vector<band> bands { band_of( path, 0.0, std::max( crossing_sine_min, std::abs( glm::dot( forward, along ) ) ), forward ) };
|
||||
for( auto const *crossings : { &members, &others } ) {
|
||||
for( auto const &crossing : *crossings ) {
|
||||
if( crossing.carried ) {
|
||||
bands.push_back( band_of( *crossing.path, crossing.across, crossing.sine, crossing.direction ) );
|
||||
}
|
||||
else if( crossing.sine >= crossing_sine_min && angle_between( crossing.direction, forward ) < grid_angle ) {
|
||||
bands.push_back( band_of( *crossing.path, crossing.across, crossing.sine, crossing.direction, false ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
// a timber carrying both tracks of a crossing lies whole under all the tracks it carries
|
||||
auto const whole { Led && whole_with( path, members ) };
|
||||
std::vector<piece> pieces;
|
||||
emit_line( bands, pieces, whole );
|
||||
cut_long( bands, pieces );
|
||||
// the parts of a timber parted between tracks going apart turn square to their own track, the more the further apart the
|
||||
// tracks run, till square where their plain sleepers wouldn't touch any more
|
||||
std::vector<glm::dvec2> turned( pieces.size(), along );
|
||||
for( std::size_t index = 0; index < pieces.size() && pieces.size() > 1; ++index ) {
|
||||
auto const *own { pieces[ index ].single };
|
||||
if( own == nullptr ) {
|
||||
continue;
|
||||
}
|
||||
band const *nearest { nullptr };
|
||||
for( auto const &other : bands ) {
|
||||
if( &other != own && other.carried && ( nearest == nullptr || std::abs( other.centre - own->centre ) < std::abs( nearest->centre - own->centre ) ) ) {
|
||||
nearest = &other;
|
||||
}
|
||||
}
|
||||
if( nearest == nullptr ) {
|
||||
continue;
|
||||
}
|
||||
auto const spacing { std::abs( nearest->centre - own->centre ) * std::min( own->sine, nearest->sine ) };
|
||||
auto const parted { shared_spacing( *own->path, *nearest->path ) };
|
||||
auto const apart { own->path->half + nearest->path->half + end_gap };
|
||||
auto const share { apart > parted ? std::clamp( ( spacing - parted ) / ( apart - parted ), 0.0, 1.0 ) : 1.0 };
|
||||
turned[ index ] = rotated( along, share * turn_off( along, own->direction ) );
|
||||
}
|
||||
auto const turned_for = [&]( sampled_path const *Path ) {
|
||||
for( std::size_t index = 0; index < pieces.size(); ++index ) {
|
||||
if( pieces[ index ].single != nullptr && pieces[ index ].single->path == Path ) {
|
||||
return turned[ index ];
|
||||
}
|
||||
}
|
||||
return along; };
|
||||
if( Led ) {
|
||||
records[ Index ].push_back( { Distance, turned_for( &path ) } );
|
||||
for( auto const &crossing : members ) {
|
||||
if( crossing.carried ) {
|
||||
records[ crossing.path - paths.data() ].push_back( { crossing.at, turned_for( crossing.path ) } );
|
||||
}
|
||||
}
|
||||
}
|
||||
// the beam lies tilted with the roll of the path, as the ballast is
|
||||
glm::dvec3 const flatacross { across.x, 0.0, across.y };
|
||||
glm::dvec3 const up { 0.0, 1.0, 0.0 };
|
||||
auto const roll { static_cast<double>( angles.x ) };
|
||||
auto const rolledacross { flatacross * std::cos( roll ) - up * std::sin( roll ) };
|
||||
auto const rolledup { flatacross * std::sin( roll ) + up * std::cos( roll ) };
|
||||
auto const top { point - rolledup * static_cast<double>( 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 };
|
||||
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 } } );
|
||||
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 } } );
|
||||
}
|
||||
}
|
||||
|
||||
// a sleeper of the path put in where the rails of it go further without a beam under them than the pitch lets them, turned
|
||||
// as the beams about it are: where beams were left out or lines of timbers led along different paths meet
|
||||
void fill_gaps() {
|
||||
|
||||
auto const count { beams.size() };
|
||||
// the beams by the cells of a grid they reach into
|
||||
plan_grid<std::size_t> grid { 2.0 };
|
||||
for( std::size_t laid = 0; laid < count; ++laid ) {
|
||||
auto const &beam { beams[ laid ].value };
|
||||
grid.insert( plan( 0.5 * ( beam.from + beam.to ) ), 0.5 * glm::length( plan( beam.to - beam.from ) ) + half_width, laid );
|
||||
}
|
||||
for( std::size_t index = 0; index < paths.size(); ++index ) {
|
||||
auto const &path { paths[ index ] };
|
||||
// where along the path beams lie under each of its rails, and which way they run
|
||||
auto const steps { std::max( 2, static_cast<int>( std::ceil( path.length / 0.05 ) ) ) };
|
||||
std::vector<double> gaps;
|
||||
std::vector<glm::dvec2> turns;
|
||||
for( auto const side : { -1.0, 1.0 } ) {
|
||||
std::optional<std::pair<double, glm::dvec2>> last;
|
||||
for( int step = 0; step <= steps; ++step ) {
|
||||
auto const distance { path.length * step / steps };
|
||||
glm::dvec3 point, across;
|
||||
path.segment->plan_frame( distance, point, across );
|
||||
auto const rail { plan( point + across * ( side * 0.5 * path.gauge ) ) };
|
||||
std::optional<glm::dvec2> found;
|
||||
auto const *cell { grid.at( rail ) };
|
||||
if( cell == nullptr ) {
|
||||
continue;
|
||||
}
|
||||
for( auto const laid : *cell ) {
|
||||
auto const box { box_of( beams[ laid ].value ) };
|
||||
if( 2.0 * box.half >= 0.1 && holds( box, rail ) ) {
|
||||
found = glm::dvec2 { -box.axis.y, box.axis.x };
|
||||
break;
|
||||
}
|
||||
}
|
||||
if( false == found.has_value() ) {
|
||||
continue;
|
||||
}
|
||||
if( last.has_value() && distance - last->first > 0.06 && distance - last->first + 2.0 * half_width > 1.25 * pitch_max ) {
|
||||
// the middle of the stretch without one, and the way the beams at its ends run
|
||||
gaps.push_back( 0.5 * ( distance + last->first ) );
|
||||
auto along { last->second + ( glm::dot( last->second, *found ) < 0.0 ? -*found : *found ) };
|
||||
turns.push_back( glm::length( along ) > 1e-9 ? glm::normalize( along ) : heading( path, gaps.back() ) );
|
||||
}
|
||||
last = std::make_pair( distance, *found );
|
||||
}
|
||||
}
|
||||
// a sleeper in each, those of both rails about one place put in once
|
||||
for( std::size_t gap = 0; gap < gaps.size(); ++gap ) {
|
||||
if( std::any_of( gaps.begin(), gaps.begin() + gap, [&]( double const Other ) { return std::abs( Other - gaps[ gap ] ) < 0.3; } ) ) {
|
||||
continue;
|
||||
}
|
||||
lay_line( index, gaps[ gap ], false, turns[ gap ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a beam over one laid before it is left out: where tracks cross too wide for one timber to take both, or the lines of two
|
||||
// stretches meet at an angle
|
||||
void keep_apart() {
|
||||
|
||||
plan_grid<beam_box> grid { 3.0 };
|
||||
std::vector<owned> kept;
|
||||
auto const clear_of_laid = [&]( beam_box const &Box ) {
|
||||
return grid.all_about( Box.centre, Box.half + half_width, [&]( beam_box const &Other ) { return apart( Box, Other ); } ); };
|
||||
for( auto laid : beams ) {
|
||||
auto const length { glm::length( laid.value.to - laid.value.from ) };
|
||||
if( length < 0.1 ) {
|
||||
continue;
|
||||
}
|
||||
if( false == clear_of_laid( box_of( laid.value ) ) ) {
|
||||
// shortened from either end or both, down to the least overhang past the rails it carries, till it's clear
|
||||
auto const direction { ( laid.value.to - laid.value.from ) / length };
|
||||
auto const most { std::max( 0.0, laid.value.corefrom - overhang_min ) }; // off the from end
|
||||
auto const mostto { std::max( 0.0, length - laid.value.coreto - overhang_min ) };
|
||||
auto found { false };
|
||||
for( int step = 1; step <= 20 && false == found; ++step ) {
|
||||
auto const share { step / 20.0 };
|
||||
for( auto const ends : { glm::dvec2 { 0.0, mostto * share }, glm::dvec2 { most * share, 0.0 }, glm::dvec2 { most * share, mostto * share } } ) {
|
||||
auto shortened { laid.value };
|
||||
shortened.from = laid.value.from + direction * ends.x;
|
||||
shortened.to = laid.value.to - direction * ends.y;
|
||||
shortened.corefrom -= ends.x;
|
||||
shortened.coreto -= ends.x;
|
||||
if( clear_of_laid( box_of( shortened ) ) ) {
|
||||
shortened.plain = false;
|
||||
laid.value = shortened;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if( false == found ) {
|
||||
++faults().dropped;
|
||||
continue;
|
||||
}
|
||||
++faults().clipped;
|
||||
}
|
||||
auto const box { box_of( laid.value ) };
|
||||
grid.insert( box.centre, box.half + half_width, box );
|
||||
kept.push_back( laid );
|
||||
}
|
||||
beams = std::move( kept );
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
} // namespace
|
||||
|
||||
// the tracks whose timbers are laid together with those of the track: all those reached from it through paths whose plain
|
||||
// sleepers would meet, in the order of their names
|
||||
std::vector<TTrack *> cluster_of( TTrack &Track ) {
|
||||
|
||||
std::vector<TTrack *> result { &Track };
|
||||
for( std::size_t index = 0; index < result.size() && result.size() < cluster_cap; ++index ) {
|
||||
auto const around { paths_around( *result[ index ] ) };
|
||||
for( auto const &path : around ) {
|
||||
if( path.track != result[ index ] ) {
|
||||
continue;
|
||||
}
|
||||
for( auto const &other : around ) {
|
||||
if( other.track == result[ index ] || std::find( result.begin(), result.end(), other.track ) != result.end() ) {
|
||||
continue;
|
||||
}
|
||||
if( share_anywhere( path, other ) ) {
|
||||
result.push_back( other.track );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort( result.begin(), result.end(), []( TTrack const *Left, TTrack const *Right ) {
|
||||
return ( Left->name() != Right->name() ? Left->name() < Right->name() : Left < Right ); } );
|
||||
return result;
|
||||
}
|
||||
|
||||
std::shared_ptr<cluster_beams> lay_out( std::vector<TTrack *> const &Tracks, std::uint64_t const Epoch ) {
|
||||
|
||||
return cluster_layout( Tracks, Epoch ).lay();
|
||||
}
|
||||
|
||||
// how far the paths of the switch stand off one plane where they share the timbers of its panel, and where along the diverging
|
||||
// path; the switch itself is out of shape for its timbers where it's more than the tolerance
|
||||
std::pair<double, double> panel_off_plane( TTrack &Track ) {
|
||||
|
||||
std::pair<double, double> result { 0.0, 0.0 };
|
||||
if( Track.eType != tt_Switch ) {
|
||||
return result;
|
||||
}
|
||||
auto const main { sample( Track, 0 ) };
|
||||
auto const diverging { sample( Track, 1 ) };
|
||||
if( main.points.size() < 2 || diverging.points.size() < 2 ) {
|
||||
return result;
|
||||
}
|
||||
auto alongside { false };
|
||||
for( std::size_t index = 0; index < diverging.points.size(); ++index ) {
|
||||
double gap;
|
||||
glm::dvec2 direction;
|
||||
if( false == beside( main, diverging.points[ index ], shared_spacing( diverging, main ), gap, direction ) ) {
|
||||
if( alongside ) {
|
||||
// apart far enough to lay their own
|
||||
break;
|
||||
}
|
||||
// at the start, which both paths share
|
||||
continue;
|
||||
}
|
||||
alongside = true;
|
||||
auto const off { off_plane( diverging, diverging.points[ index ], main ) };
|
||||
if( off > result.first ) {
|
||||
result = { off, diverging.distances[ index ] };
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
} // namespace track_sleepers
|
||||
38
world/SleeperLayout.h
Normal file
38
world/SleeperLayout.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
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 <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "world/TrackSleepers.h"
|
||||
|
||||
// the beams of a cluster of tracks laid out together
|
||||
namespace track_sleepers {
|
||||
|
||||
// the beams of a cluster of tracks laid together, by the track each is laid with
|
||||
struct cluster_beams {
|
||||
std::uint64_t epoch { 0 };
|
||||
std::map<TTrack const *, std::vector<beam>> beams;
|
||||
};
|
||||
|
||||
// the tracks whose timbers are laid together with those of the track: all those reached from it through paths whose plain
|
||||
// sleepers would meet, in the order of their names
|
||||
std::vector<TTrack *> cluster_of( TTrack &Track );
|
||||
// the beams of the tracks of a cluster, laid out together for the layout of the given epoch
|
||||
std::shared_ptr<cluster_beams> lay_out( std::vector<TTrack *> const &Tracks, std::uint64_t const Epoch );
|
||||
// how far the paths of the switch stand off one plane where they share the timbers of its panel, and where along the diverging
|
||||
// path; the switch itself is out of shape for its timbers where it's more than the tolerance
|
||||
std::pair<double, double> panel_off_plane( TTrack &Track );
|
||||
|
||||
} // namespace track_sleepers
|
||||
276
world/SleeperLines.cpp
Normal file
276
world/SleeperLines.cpp
Normal file
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
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/SleeperLines.h"
|
||||
#include "world/SleeperGeometry.h"
|
||||
#include "world/SleeperRules.h"
|
||||
|
||||
namespace track_sleepers {
|
||||
|
||||
// the beams spread evenly between two anchors, the pin at the first one included and the one at the last left to the next stretch
|
||||
void spread( anchor const &From, anchor const &To, std::vector<double> &Stations ) {
|
||||
|
||||
auto const span { To.at - From.at };
|
||||
if( span <= 0.0 ) {
|
||||
return;
|
||||
}
|
||||
auto count { static_cast<int>( std::round( span / pitch + 1.0 - From.offset - To.offset ) ) };
|
||||
if( From.offset == 0.5 && To.offset == 0.5 ) {
|
||||
// a track between two others gets a beam however short it is
|
||||
count = std::max( 1, count );
|
||||
}
|
||||
// the rails are carried at most so far apart, the beams pulled closer where it takes one more
|
||||
while( span / ( count - 1 + From.offset + To.offset ) > pitch_max ) {
|
||||
++count;
|
||||
}
|
||||
auto const intervals { count - 1 + From.offset + To.offset };
|
||||
if( count < 1 || intervals <= 0.0 ) {
|
||||
if( From.offset == 0.0 ) {
|
||||
Stations.push_back( From.at );
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto const step { span / intervals };
|
||||
auto const last { To.offset == 0.0 ? count - 1 : count };
|
||||
for( int index = 0; index < last; ++index ) {
|
||||
Stations.push_back( From.at + ( From.offset + index ) * step );
|
||||
}
|
||||
}
|
||||
|
||||
void find_crossings( sampled_path const &Path, glm::dvec3 const &Point, glm::dvec2 const &Along, glm::dvec2 const &Across, double const Reach, std::vector<sampled_path> const &Around, std::vector<crossing> &Crossings ) {
|
||||
|
||||
for( auto const &other : Around ) {
|
||||
if( &other == &Path || false == near_bounds( other, Point, Reach ) ) {
|
||||
continue;
|
||||
}
|
||||
for( std::size_t index = 0; index + 1 < other.points.size(); ++index ) {
|
||||
auto const &from { other.points[ index ] };
|
||||
auto const &to { other.points[ index + 1 ] };
|
||||
auto const fromside { glm::dot( plan( from - Point ), Along ) };
|
||||
auto const toside { glm::dot( plan( to - Point ), Along ) };
|
||||
if( ( fromside < 0.0 ) == ( toside < 0.0 ) ) {
|
||||
continue;
|
||||
}
|
||||
auto const share { fromside / ( fromside - toside ) };
|
||||
auto const point { from + ( to - from ) * share };
|
||||
auto const across { glm::dot( plan( point - Point ), Across ) };
|
||||
if( std::abs( across ) > Reach || std::abs( point.y - Point.y ) > level_tolerance ) {
|
||||
continue;
|
||||
}
|
||||
auto const direction { plan( to - from ) };
|
||||
if( glm::dot( direction, direction ) < 1e-12 ) {
|
||||
continue;
|
||||
}
|
||||
auto const unit { glm::normalize( direction ) };
|
||||
auto const at { glm::mix( other.distances[ index ], other.distances[ index + 1 ], share ) };
|
||||
Crossings.push_back( { &other, point, unit, at, across, std::abs( glm::dot( unit, Along ) ), false } );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the paths whose rails the line of the beam through the point of the path takes, marked among the paths of its cluster crossing
|
||||
// the line: those whose plain sleepers would meet the sleepers of the path or of any the line takes already, at a narrow enough
|
||||
// angle to the line, and in one plane with the path unless they run too close to be parted
|
||||
// the timber takes the rails of a path whose plain sleepers would meet those of a path it takes already; where Tight, only of one so
|
||||
// close that their sleepers couldn't be laid apart even cut short
|
||||
void carry( sampled_path const &Path, glm::dvec2 const &Forward, glm::dvec2 const &Along, std::vector<crossing> &Crossings, bool const Tight, bool const Count ) {
|
||||
|
||||
struct carried_path {
|
||||
double across;
|
||||
double half;
|
||||
double rails;
|
||||
double sine;
|
||||
};
|
||||
std::vector<carried_path> carried { { 0.0, Path.half, Path.rails, std::max( crossing_sine_min, std::abs( glm::dot( Forward, Along ) ) ) } };
|
||||
for( auto added { true }; added; ) {
|
||||
added = false;
|
||||
for( auto &crossing : Crossings ) {
|
||||
if( crossing.carried || crossing.sine < crossing_sine_min || angle_between( crossing.direction, Forward ) >= grid_angle ) {
|
||||
continue;
|
||||
}
|
||||
auto const close { std::any_of( carried.begin(), carried.end(), [&]( carried_path const &Carried ) {
|
||||
auto const reach { Tight ? crossing.path->rails + Carried.rails + 2.0 * overhang_min + end_gap : crossing.path->half + Carried.half + end_gap };
|
||||
return std::abs( crossing.across - Carried.across ) * std::min( crossing.sine, Carried.sine ) < reach; } ) };
|
||||
if( false == close ) {
|
||||
continue;
|
||||
}
|
||||
if( off_plane( *crossing.path, crossing.point, Path ) > coplanar_tolerance ) {
|
||||
if( std::abs( crossing.across ) * crossing.sine >= shared_spacing( *crossing.path, Path ) ) {
|
||||
continue;
|
||||
}
|
||||
// too close to the path to be parted, out of the plane of its rails
|
||||
if( Count ) {
|
||||
++faults().offplane;
|
||||
}
|
||||
}
|
||||
crossing.carried = true;
|
||||
added = true;
|
||||
carried.push_back( { crossing.across, crossing.path->half, crossing.path->rails, crossing.sine } );
|
||||
}
|
||||
}
|
||||
// the line doesn't carry the rails of a track past those of one it doesn't, that one lays its own beams there
|
||||
auto const blocks = [&]( crossing const &Other ) {
|
||||
return false == Other.carried && Other.sine >= crossing_sine_min && angle_between( Other.direction, Forward ) < grid_angle; };
|
||||
for( auto demoted { true }; demoted; ) {
|
||||
demoted = false;
|
||||
for( auto &crossing : Crossings ) {
|
||||
if( false == crossing.carried ) {
|
||||
continue;
|
||||
}
|
||||
if( std::any_of( Crossings.begin(), Crossings.end(), [&]( struct crossing const &Other ) {
|
||||
return blocks( Other ) && Other.across * crossing.across > 0.0 && std::abs( Other.across ) < std::abs( crossing.across ); } ) ) {
|
||||
crossing.carried = false;
|
||||
demoted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
band band_of( sampled_path const &Path, double const Across, double const Sine, glm::dvec2 const &Direction, bool const Carried ) {
|
||||
|
||||
return { Across - Path.rails / Sine, Across + Path.rails / Sine, overhang / Sine, overhang_min / Sine, Carried, &Path, Across, Direction, Sine };
|
||||
}
|
||||
|
||||
// the beams on the line through the point: one timber under all the rails it carries, the ends keeping clear of the rails of the
|
||||
// tracks the line doesn't carry and of the sleepers under them. the timber is parted between the rails of two tracks where each
|
||||
// keeps at least its least overhang past its rails, unless it's whole: about a crossing of the tracks, as in a scissors crossing
|
||||
void emit_line( std::vector<band> &Bands, std::vector<piece> &Pieces, bool const Whole ) {
|
||||
|
||||
std::sort( Bands.begin(), Bands.end(), []( band const &Left, band const &Right ) { return Left.from < Right.from; } );
|
||||
auto open { false };
|
||||
piece current;
|
||||
auto end { 0.0 }, endoverhang { 0.0 }, endoverhangmin { 0.0 };
|
||||
// the far side of the last rails not carried, and of the sleepers under them
|
||||
auto fenced { false };
|
||||
auto fencerails { 0.0 }, fencereach { 0.0 };
|
||||
for( auto const &next : Bands ) {
|
||||
if( false == next.carried ) {
|
||||
if( open ) {
|
||||
auto const kept { std::max( 0.0, std::clamp( next.from - next.overhang - end - end_gap, std::min( endoverhangmin, next.from - end - end_gap ), endoverhang ) ) };
|
||||
current.clipped = current.clipped || kept < endoverhang;
|
||||
if( kept < endoverhangmin ) {
|
||||
++faults().clipped;
|
||||
}
|
||||
current.to = end + kept;
|
||||
current.coreto = end;
|
||||
Pieces.push_back( current );
|
||||
open = false;
|
||||
}
|
||||
if( false == fenced || next.to > fencerails ) {
|
||||
fencerails = next.to;
|
||||
fencereach = next.to + next.overhang;
|
||||
}
|
||||
fenced = true;
|
||||
continue;
|
||||
}
|
||||
if( false == open ) {
|
||||
current = piece();
|
||||
auto startoverhang { next.overhang };
|
||||
if( fenced ) {
|
||||
startoverhang = std::max( 0.0, std::clamp( next.from - fencereach - end_gap, std::min( next.overhangmin, next.from - fencerails - end_gap ), next.overhang ) );
|
||||
current.clipped = startoverhang < next.overhang;
|
||||
if( startoverhang < next.overhangmin ) {
|
||||
++faults().clipped;
|
||||
}
|
||||
}
|
||||
current.from = next.from - startoverhang;
|
||||
current.corefrom = next.from;
|
||||
current.single = &next;
|
||||
end = next.to;
|
||||
endoverhang = next.overhang;
|
||||
endoverhangmin = next.overhangmin;
|
||||
open = true;
|
||||
continue;
|
||||
}
|
||||
if( false == Whole && next.from - end >= endoverhangmin + next.overhangmin + end_gap ) {
|
||||
// the room between them shared by their overhangs
|
||||
auto const room { next.from - end - end_gap };
|
||||
auto const ours { std::min( endoverhang, room * endoverhang / ( endoverhang + next.overhang ) ) };
|
||||
auto const theirs { std::min( next.overhang, room - ours ) };
|
||||
current.to = end + ours;
|
||||
current.coreto = end;
|
||||
current.clipped = current.clipped || ours < endoverhang;
|
||||
Pieces.push_back( current );
|
||||
current = piece();
|
||||
current.from = next.from - theirs;
|
||||
current.corefrom = next.from;
|
||||
current.clipped = theirs < next.overhang;
|
||||
current.single = &next;
|
||||
end = next.to;
|
||||
endoverhang = next.overhang;
|
||||
endoverhangmin = next.overhangmin;
|
||||
continue;
|
||||
}
|
||||
// one timber under the rails of all the tracks it carries
|
||||
current.single = nullptr;
|
||||
if( next.to > end ) {
|
||||
end = next.to;
|
||||
endoverhang = next.overhang;
|
||||
endoverhangmin = next.overhangmin;
|
||||
}
|
||||
}
|
||||
if( open ) {
|
||||
current.to = end + endoverhang;
|
||||
current.coreto = end;
|
||||
Pieces.push_back( current );
|
||||
}
|
||||
}
|
||||
|
||||
// a timber longer than the cap cut in two where the rails of the tracks it carries are furthest apart, the ends sharing the
|
||||
// room between them, till every piece is short enough or there's nowhere to cut
|
||||
void cut_long( std::vector<band> const &Bands, std::vector<piece> &Pieces ) {
|
||||
|
||||
for( std::size_t index = 0; index < Pieces.size(); ) {
|
||||
auto &whole { Pieces[ index ] };
|
||||
if( whole.to - whole.from <= length_cap ) {
|
||||
++index;
|
||||
continue;
|
||||
}
|
||||
// the widest gap between the rails under the timber
|
||||
band const *left { nullptr };
|
||||
band const *right { nullptr };
|
||||
band const *last { nullptr };
|
||||
auto widest { 0.0 };
|
||||
auto end { std::numeric_limits<double>::lowest() };
|
||||
for( auto const &band : Bands ) {
|
||||
if( false == band.carried || band.from < whole.from || band.to > whole.to ) {
|
||||
continue;
|
||||
}
|
||||
if( last != nullptr && band.from - end > widest ) {
|
||||
widest = band.from - end;
|
||||
left = last;
|
||||
right = &band;
|
||||
}
|
||||
if( band.to > end ) {
|
||||
end = band.to;
|
||||
last = &band;
|
||||
}
|
||||
}
|
||||
if( left == nullptr ) {
|
||||
++index;
|
||||
continue;
|
||||
}
|
||||
auto const room { std::max( 0.0, widest - end_gap ) };
|
||||
auto const ours { std::min( left->overhang, room * left->overhang / ( left->overhang + right->overhang ) ) };
|
||||
auto const theirs { std::min( right->overhang, room - ours ) };
|
||||
if( ours < left->overhangmin || theirs < right->overhangmin ) {
|
||||
++faults().clipped;
|
||||
}
|
||||
piece second { right->from - theirs, whole.to, true, right->from, whole.coreto, nullptr };
|
||||
whole.to = left->to + ours;
|
||||
whole.coreto = left->to;
|
||||
whole.clipped = true;
|
||||
Pieces.insert( Pieces.begin() + index + 1, second );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // namespace track_sleepers
|
||||
82
world/SleeperLines.h
Normal file
82
world/SleeperLines.h
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
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 <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include "world/SleeperPaths.h"
|
||||
|
||||
// the lines of beams across the tracks: where the beams go along the paths, which rails a line takes and where the beams on it
|
||||
// part and end
|
||||
namespace track_sleepers {
|
||||
|
||||
// a place along a path the beams are spread from: a pin with a beam at it, a free end of the track with the first beam half
|
||||
// a step from it, so the beams keep the pitch over the joints, or a timber of another path which carries the rails of this
|
||||
// one past it, with the first beam of this one a step from it
|
||||
struct anchor {
|
||||
double at { 0.0 };
|
||||
double offset { 0.5 }; // how many steps the first beam is from the anchor: 0 pin, 0.5 free end, 1 timber of another path
|
||||
};
|
||||
|
||||
// where another path crosses the line of a beam
|
||||
struct crossing {
|
||||
sampled_path const *path { nullptr };
|
||||
glm::dvec3 point;
|
||||
glm::dvec2 direction; // of the path, in plan
|
||||
double at { 0.0 }; // the distance of the point along the path
|
||||
double across { 0.0 }; // along the beam, from the path laying it
|
||||
double sine { 1.0 }; // of the angle between the path and the beam
|
||||
bool carried { false };
|
||||
};
|
||||
|
||||
// the stretch of a beam line the rails of a path take, and how far the beam may reach past them
|
||||
struct band {
|
||||
double from { 0.0 };
|
||||
double to { 0.0 };
|
||||
double overhang { 0.0 };
|
||||
double overhangmin { 0.0 };
|
||||
bool carried { true }; // the rails are laid on the line, else the beam keeps clear of them and of the sleepers under them
|
||||
sampled_path const *path { nullptr };
|
||||
double centre { 0.0 }; // where the middle of the path crosses the line
|
||||
glm::dvec2 direction { 0.0 }; // of the path there, in plan
|
||||
double sine { 1.0 }; // of the angle between the path and the line
|
||||
};
|
||||
|
||||
// a beam on a line, by where its ends are along the line
|
||||
struct piece {
|
||||
double from { 0.0 };
|
||||
double to { 0.0 };
|
||||
bool clipped { false }; // an end cut short to keep clear of the rails of another track
|
||||
double corefrom { 0.0 }; // the stretch the feet of the rails it carries take
|
||||
double coreto { 0.0 };
|
||||
band const *single { nullptr }; // the rails of the only track it carries, null where it carries more
|
||||
};
|
||||
|
||||
// the beams spread evenly between two anchors, the pin at the first one included and the one at the last left to the next stretch
|
||||
void spread( anchor const &From, anchor const &To, std::vector<double> &Stations );
|
||||
void find_crossings( sampled_path const &Path, glm::dvec3 const &Point, glm::dvec2 const &Along, glm::dvec2 const &Across, double const Reach, std::vector<sampled_path> const &Around, std::vector<crossing> &Crossings );
|
||||
// the paths whose rails the line of the beam through the point of the path takes, marked among the paths of its cluster crossing
|
||||
// the line: those whose plain sleepers would meet the sleepers of the path or of any the line takes already, at a narrow enough
|
||||
// angle to the line, and in one plane with the path unless they run too close to be parted
|
||||
// the timber takes the rails of a path whose plain sleepers would meet those of a path it takes already; where Tight, only of one so
|
||||
// close that their sleepers couldn't be laid apart even cut short
|
||||
void carry( sampled_path const &Path, glm::dvec2 const &Forward, glm::dvec2 const &Along, std::vector<crossing> &Crossings, bool const Tight, bool const Count = true );
|
||||
band band_of( sampled_path const &Path, double const Across, double const Sine, glm::dvec2 const &Direction, bool const Carried = true );
|
||||
// the beams on the line through the point: one timber under all the rails it carries, the ends keeping clear of the rails of the
|
||||
// tracks the line doesn't carry and of the sleepers under them. the timber is parted between the rails of two tracks where each
|
||||
// keeps at least its least overhang past its rails, unless it's whole: about a crossing of the tracks, as in a scissors crossing
|
||||
void emit_line( std::vector<band> &Bands, std::vector<piece> &Pieces, bool const Whole );
|
||||
// a timber longer than the cap cut in two where the rails of the tracks it carries are furthest apart, the ends sharing the
|
||||
// room between them, till every piece is short enough or there's nowhere to cut
|
||||
void cut_long( std::vector<band> const &Bands, std::vector<piece> &Pieces );
|
||||
|
||||
} // namespace track_sleepers
|
||||
229
world/SleeperModels.cpp
Normal file
229
world/SleeperModels.cpp
Normal file
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
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/TrackSleepers.h"
|
||||
#include "world/TrackFeatures.h"
|
||||
#include "world/SleeperRules.h"
|
||||
#include "world/Track.h"
|
||||
#include "model/Model3d.h"
|
||||
#include "model/MdlMngr.h"
|
||||
#include "rendering/renderer.h"
|
||||
#include "utilities/Globals.h"
|
||||
#include "utilities/Logs.h"
|
||||
#include "utilities/utilities.h"
|
||||
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
|
||||
namespace track_sleepers {
|
||||
|
||||
namespace {
|
||||
|
||||
// the models of the beams found so far, by their names and skins and seats
|
||||
std::map<std::string, beam_model> &known_models() {
|
||||
static std::map<std::string, beam_model> models;
|
||||
return models;
|
||||
}
|
||||
|
||||
// the vertices of the submodel in the space of the model, through its own transform
|
||||
template <typename Visit>
|
||||
void vertices_of( TSubModel *Submodel, Visit Visitor ) {
|
||||
|
||||
glm::mat4 transform { 1.f };
|
||||
if( ( Submodel->iFlags & 0xC000 ) && ( Submodel->GetMatrix() != nullptr ) ) {
|
||||
transform = glm::make_mat4( Submodel->GetMatrix()->readArray() );
|
||||
}
|
||||
auto const &vertices { Submodel->m_geometry.handle != null_handle ? GfxRenderer->Vertices( Submodel->m_geometry.handle ) : Submodel->Vertices };
|
||||
for( auto const &vertex : vertices ) {
|
||||
Visitor( glm::vec3 { transform * glm::vec4 { vertex.position, 1.f } } );
|
||||
}
|
||||
}
|
||||
|
||||
// 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 ) {
|
||||
|
||||
if( Name.empty() ) {
|
||||
return nullptr;
|
||||
}
|
||||
auto const Skin { Skinname == "none" ? std::string {} : Skinname };
|
||||
auto const key { Name + '|' + Skin + '|' + std::to_string( Seat ) };
|
||||
auto &models { known_models() };
|
||||
auto found { models.find( key ) };
|
||||
if( found == models.end() ) {
|
||||
auto &model { models[ key ] };
|
||||
model.name = Name;
|
||||
model.skinname = Skin;
|
||||
model.model = TModelsManager::GetModel( Name, false );
|
||||
if( model.model == nullptr ) {
|
||||
ErrorLog( "Bad sleepers3d: model \"" + Name + "\" failed to load" );
|
||||
return nullptr;
|
||||
}
|
||||
if( false == Skin.empty() ) {
|
||||
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<char const *, part_count> const names { "begin", "main", "end" };
|
||||
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;
|
||||
}
|
||||
auto top { std::numeric_limits<float>::lowest() };
|
||||
model.span = { std::numeric_limits<float>::max(), std::numeric_limits<float>::lowest() };
|
||||
if( model.stretchable ) {
|
||||
for( int part = 0; part < part_count; ++part ) {
|
||||
glm::vec2 span { std::numeric_limits<float>::max(), std::numeric_limits<float>::lowest() };
|
||||
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 ); } );
|
||||
model.spans[ part ] = span;
|
||||
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;
|
||||
}
|
||||
else {
|
||||
// a whole model, measured on its most detailed submodels, the others being the same beam seen from further
|
||||
model.parts = {};
|
||||
for( auto *submodel { model.model->Root }; submodel != nullptr; submodel = submodel->Next ) {
|
||||
if( submodel->fSquareMinDist > 0.f ) {
|
||||
continue;
|
||||
}
|
||||
vertices_of( submodel, [&]( glm::vec3 const &Position ) {
|
||||
model.span = { std::min( model.span.x, Position.x ), std::max( model.span.y, Position.x ) };
|
||||
top = std::max( top, Position.y ); } );
|
||||
}
|
||||
}
|
||||
// the levels of detail, by the distances they're seen from
|
||||
for( auto *submodel { model.model->Root }; submodel != nullptr; submodel = submodel->Next ) {
|
||||
if( std::find( model.bands.begin(), model.bands.end(), submodel->fSquareMinDist ) == model.bands.end() ) {
|
||||
model.bands.push_back( submodel->fSquareMinDist );
|
||||
}
|
||||
}
|
||||
std::sort( model.bands.begin(), model.bands.end() );
|
||||
// the rails sit on the seat of the sleeper, below the fastenings sticking up over it
|
||||
model.seat = ( Seat >= 0.f ? Seat : top );
|
||||
if( model.length() < 0.5f ) {
|
||||
ErrorLog( "Bad sleepers3d: model \"" + Name + "\" has no beam along x" );
|
||||
model.model = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
WriteLog(
|
||||
"sleepers3d: model \"" + Name + "\"" + ( model.stretchable ? std::string { " of parts" } : std::string { " whole" } )
|
||||
+ ", " + to_string( model.span.x, 3 ) + ".." + to_string( model.span.y, 3 )
|
||||
+ ( 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 ) );
|
||||
return &model;
|
||||
}
|
||||
return ( found->second.model != nullptr ? &found->second : nullptr );
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
beam_model const *timber_model() {
|
||||
|
||||
if( false == track_features::sleepers ) {
|
||||
return nullptr;
|
||||
}
|
||||
return find_model( Global.Sleepers3D, std::string {}, -1.f );
|
||||
}
|
||||
|
||||
beam_model const *plain_model() {
|
||||
|
||||
return find_model( Global.Sleepers3DPlain, Global.Sleepers3DPlainSkin, Global.Sleepers3DPlainSeat );
|
||||
}
|
||||
|
||||
beam_model const *timber_model( TTrack const &Track ) {
|
||||
|
||||
if( false == Track.m_timber_model_name.empty() ) {
|
||||
if( auto const *model { find_model( Track.m_timber_model_name, Track.m_timber_skin_name, -1.f ) }; model != nullptr ) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
return timber_model();
|
||||
}
|
||||
|
||||
beam_model const *plain_model( TTrack const &Track ) {
|
||||
|
||||
if( Track.m_sleeper_enabled && false == Track.m_sleeper_model_name.empty() ) {
|
||||
// the sleepers of the track drawn as they are in the model, their tops at the feet of the rails
|
||||
if( auto const *model { find_model( Track.m_sleeper_model_name, Track.m_sleeper_skin_name, -1.f ) }; model != nullptr ) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
return plain_model();
|
||||
}
|
||||
|
||||
double plain_length( TTrack const &Track ) {
|
||||
|
||||
if( auto const *model { plain_model( Track ) }; model != nullptr ) {
|
||||
return model->length();
|
||||
}
|
||||
return 2.0 * ( 0.5 * std::abs( Track.fTrackWidth ) + rail_reach + overhang );
|
||||
}
|
||||
|
||||
std::vector<pack_model> const &pack_models() {
|
||||
|
||||
static std::vector<pack_model> models;
|
||||
static auto scanned { false };
|
||||
if( scanned ) {
|
||||
return models;
|
||||
}
|
||||
scanned = true;
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code error;
|
||||
auto const root { fs::path { paths::models } };
|
||||
for( fs::recursive_directory_iterator entry { root, fs::directory_options::follow_directory_symlink | fs::directory_options::skip_permission_denied, error }, end;
|
||||
entry != end; entry.increment( error ) ) {
|
||||
if( error ) {
|
||||
break;
|
||||
}
|
||||
if( false == entry->is_regular_file( error ) ) {
|
||||
continue;
|
||||
}
|
||||
auto const extension { ToLower( entry->path().extension().string() ) };
|
||||
if( extension != ".t3d" && extension != ".e3d" ) {
|
||||
continue;
|
||||
}
|
||||
auto name { ToLower( fs::relative( entry->path(), root, error ).replace_extension().generic_string() ) };
|
||||
if( error || name.empty() ) {
|
||||
continue;
|
||||
}
|
||||
if( name.find( "podklad" ) == std::string::npos && name.find( "podrozjazd" ) == std::string::npos
|
||||
&& name.find( "sleeper" ) == std::string::npos && name.find( "timber" ) == std::string::npos ) {
|
||||
continue;
|
||||
}
|
||||
// a timber model has the parts it's put together from, told by their names in the file
|
||||
std::ifstream file { entry->path(), std::ios::binary };
|
||||
std::string const content { std::istreambuf_iterator<char>( file ), std::istreambuf_iterator<char>() };
|
||||
auto const has = [&]( std::string const &Part ) {
|
||||
return (
|
||||
extension == ".t3d" ?
|
||||
content.find( "Name: " + Part + "\n" ) != std::string::npos || content.find( "Name: " + Part + " " ) != std::string::npos
|
||||
|| content.find( "Name: " + Part + "\r" ) != std::string::npos :
|
||||
content.find( std::string { '\0' } + Part + '\0' ) != std::string::npos ); };
|
||||
auto const timber { has( "begin" ) && has( "main" ) && has( "end" ) };
|
||||
if( std::none_of( models.begin(), models.end(), [&]( pack_model const &Model ) { return Model.name == name; } ) ) {
|
||||
models.push_back( { name, timber } );
|
||||
}
|
||||
}
|
||||
std::sort( models.begin(), models.end(), []( pack_model const &Left, pack_model const &Right ) {
|
||||
return ( Left.timber != Right.timber ? Left.timber : Left.name < Right.name ); } );
|
||||
return models;
|
||||
}
|
||||
|
||||
|
||||
} // namespace track_sleepers
|
||||
384
world/SleeperPaths.cpp
Normal file
384
world/SleeperPaths.cpp
Normal file
@@ -0,0 +1,384 @@
|
||||
/*
|
||||
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/SleeperPaths.h"
|
||||
#include "world/SleeperGeometry.h"
|
||||
#include "world/SleeperRules.h"
|
||||
#include "world/TrackSleepers.h"
|
||||
#include "world/Track.h"
|
||||
#include "simulation/simulation.h"
|
||||
#include "scene/scene.h"
|
||||
|
||||
namespace track_sleepers {
|
||||
|
||||
int path_count( TTrack const &Track ) {
|
||||
return ( Track.eType == tt_Switch ? 2 : 1 );
|
||||
}
|
||||
|
||||
TSegment const *path_segment( TTrack const &Track, int const Path ) {
|
||||
return ( Track.eType == tt_Switch ? Track.SwitchExtension->Segments[ Path ].get() : Track.Segment.get() );
|
||||
}
|
||||
|
||||
// how long the line of tracks joined end to end the plain track is part of, so a line keeps laying the timbers where it runs
|
||||
// past the tracks of another, from one track of it to the next. the line goes on through the main path of a switch, and ends
|
||||
// where it comes to its diverging path
|
||||
namespace {
|
||||
|
||||
double line_length( TTrack const &Track ) {
|
||||
|
||||
if( Track.eType != tt_Normal || Track.Segment == nullptr ) {
|
||||
return 0.0;
|
||||
}
|
||||
auto result { Track.Segment->GetLength() };
|
||||
for( auto *next : { Track.trPrev, Track.trNext } ) {
|
||||
TTrack const *previous { &Track };
|
||||
auto const *current { next };
|
||||
for( int count = 0; current != nullptr && current != &Track && count < 1000; ++count ) {
|
||||
TTrack const *following { nullptr };
|
||||
if( current->eType == tt_Normal && current->Segment != nullptr ) {
|
||||
result += current->Segment->GetLength();
|
||||
following = ( current->trPrev == previous ? current->trNext : current->trPrev );
|
||||
}
|
||||
else if( current->eType == tt_Switch && current->SwitchExtension != nullptr && current->SwitchExtension->Segments[ 0 ] ) {
|
||||
auto const &links { *current->SwitchExtension };
|
||||
if( links.pNexts[ 0 ] == previous ) {
|
||||
following = links.pPrevs[ 0 ];
|
||||
}
|
||||
else if( links.pPrevs[ 0 ] == previous || links.pPrevs[ 1 ] == previous ) {
|
||||
following = links.pNexts[ 0 ];
|
||||
}
|
||||
else {
|
||||
// come to the diverging path
|
||||
break;
|
||||
}
|
||||
result += links.Segments[ 0 ]->GetLength();
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
previous = current;
|
||||
current = following;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
sampled_path sample( TTrack &Track, int const Path ) {
|
||||
|
||||
sampled_path result;
|
||||
result.track = &Track;
|
||||
result.path = Path;
|
||||
result.segment = path_segment( Track, Path );
|
||||
if( result.segment == nullptr ) {
|
||||
return result;
|
||||
}
|
||||
result.length = result.segment->GetLength();
|
||||
result.gauge = std::abs( Track.fTrackWidth );
|
||||
result.rails = 0.5 * result.gauge + rail_reach;
|
||||
result.reach = result.rails + overhang;
|
||||
result.half = 0.5 * plain_length( Track );
|
||||
result.line = line_length( Track );
|
||||
auto const count { std::max( 1, static_cast<int>( std::ceil( result.length / sample_step ) ) ) };
|
||||
result.low = glm::dvec2 { std::numeric_limits<double>::max() };
|
||||
result.high = glm::dvec2 { std::numeric_limits<double>::lowest() };
|
||||
for( int step = 0; step <= count; ++step ) {
|
||||
auto const distance { result.length * step / count };
|
||||
glm::dvec3 point;
|
||||
glm::vec3 angles;
|
||||
result.segment->RaPositionGet( distance, point, angles );
|
||||
if( std::abs( angles.x ) > 1e-6 || ( false == result.points.empty() && std::abs( point.y - result.points.front().y ) > 1e-4 ) ) {
|
||||
result.level = false;
|
||||
}
|
||||
result.points.emplace_back( point );
|
||||
result.distances.emplace_back( distance );
|
||||
result.low = glm::min( result.low, plan( point ) );
|
||||
result.high = glm::max( result.high, plan( point ) );
|
||||
}
|
||||
result.feet = result.points.front().y - Track.rail_height();
|
||||
return result;
|
||||
}
|
||||
|
||||
// the direction of the path in plan by its samples about the distance
|
||||
glm::dvec2 heading( sampled_path const &Path, double const Distance ) {
|
||||
|
||||
auto const count { Path.points.size() };
|
||||
auto index { static_cast<std::size_t>( std::upper_bound( Path.distances.begin(), Path.distances.end(), Distance ) - Path.distances.begin() ) };
|
||||
index = std::clamp<std::size_t>( index, 1, count - 1 );
|
||||
auto const direction { plan( Path.points[ index ] - Path.points[ index - 1 ] ) };
|
||||
auto const length { glm::length( direction ) };
|
||||
return ( length > 1e-9 ? direction / length : glm::dvec2 { 0.0, 1.0 } );
|
||||
}
|
||||
|
||||
// which of two paths lays the beams where they're merged: a switch before a plain track, the main path of a switch
|
||||
// before the diverging one, a plain track of a longer line before one of a shorter, the longer before the shorter, and
|
||||
// then by the name
|
||||
bool before( sampled_path const &Left, sampled_path const &Right ) {
|
||||
|
||||
auto const leftswitch { Left.track->eType == tt_Switch };
|
||||
auto const rightswitch { Right.track->eType == tt_Switch };
|
||||
if( leftswitch != rightswitch ) {
|
||||
return leftswitch;
|
||||
}
|
||||
if( Left.track == Right.track ) {
|
||||
return Left.path < Right.path;
|
||||
}
|
||||
if( Left.path != Right.path ) {
|
||||
return Left.path < Right.path;
|
||||
}
|
||||
if( std::abs( Left.line - Right.line ) > 0.001 ) {
|
||||
return Left.line > Right.line;
|
||||
}
|
||||
if( std::abs( Left.length - Right.length ) > 0.001 ) {
|
||||
return Left.length > Right.length;
|
||||
}
|
||||
if( Left.track->name() != Right.track->name() ) {
|
||||
return Left.track->name() < Right.track->name();
|
||||
}
|
||||
return Left.track < Right.track;
|
||||
}
|
||||
|
||||
bool near_bounds( sampled_path const &Path, glm::dvec3 const &Point, double const Distance ) {
|
||||
|
||||
return Point.x > Path.low.x - Distance && Point.x < Path.high.x + Distance
|
||||
&& Point.z > Path.low.y - Distance && Point.z < Path.high.y + Distance;
|
||||
}
|
||||
|
||||
// how far the point is from the path in plan and which way the path runs there, when it's beside the path closer than the
|
||||
// distance and at about the same level. a point past either end of the path isn't beside it, so a track going on from the end
|
||||
// of another doesn't count as running beside it
|
||||
bool beside( sampled_path const &Path, glm::dvec3 const &Point, double const Distance, double &Gap, glm::dvec2 &Direction ) {
|
||||
|
||||
if( Path.points.size() < 2 || false == near_bounds( Path, Point, Distance ) ) {
|
||||
return false;
|
||||
}
|
||||
auto nearest { Distance * Distance };
|
||||
auto found { false };
|
||||
auto past { false }; // the nearest place of the path is its end, with the point beyond it
|
||||
auto const last { Path.points.size() - 2 };
|
||||
for( std::size_t index = 0; index <= last; ++index ) {
|
||||
auto const &from { Path.points[ index ] };
|
||||
auto const &to { Path.points[ index + 1 ] };
|
||||
auto const chord { plan( to - from ) };
|
||||
auto const offset { plan( Point - from ) };
|
||||
auto const chordlength2 { glm::dot( chord, chord ) };
|
||||
auto const along { chordlength2 > 0.0 ? glm::dot( offset, chord ) / chordlength2 : 0.0 };
|
||||
auto const clamped { std::clamp( along, 0.0, 1.0 ) };
|
||||
auto const gap { offset - chord * clamped };
|
||||
auto const gap2 { glm::dot( gap, gap ) };
|
||||
if( gap2 >= nearest ) {
|
||||
continue;
|
||||
}
|
||||
auto const level { from.y + ( to.y - from.y ) * clamped };
|
||||
if( std::abs( level - Point.y ) > level_tolerance ) {
|
||||
continue;
|
||||
}
|
||||
nearest = gap2;
|
||||
found = true;
|
||||
past = ( ( index == 0 && along < 1e-6 ) || ( index == last && along > 1.0 - 1e-6 ) );
|
||||
if( chordlength2 > 0.0 ) {
|
||||
Direction = chord / std::sqrt( chordlength2 );
|
||||
}
|
||||
}
|
||||
Gap = std::sqrt( nearest );
|
||||
return found && false == past;
|
||||
}
|
||||
|
||||
// the place of the path nearest to the point in plan: its distance along the path, where it is and which way the path runs
|
||||
double nearest_on( sampled_path const &Path, glm::dvec2 const &Point, glm::dvec2 &Position, glm::dvec2 &Direction ) {
|
||||
|
||||
auto nearest { std::numeric_limits<double>::max() };
|
||||
auto result { 0.0 };
|
||||
for( std::size_t index = 1; index < Path.points.size(); ++index ) {
|
||||
auto const from { plan( Path.points[ index - 1 ] ) };
|
||||
auto const chord { plan( Path.points[ index ] ) - from };
|
||||
auto const chordlength2 { glm::dot( chord, chord ) };
|
||||
if( chordlength2 <= 0.0 ) {
|
||||
continue;
|
||||
}
|
||||
auto const along { std::clamp( glm::dot( Point - from, chord ) / chordlength2, 0.0, 1.0 ) };
|
||||
auto const position { from + chord * along };
|
||||
auto const gap2 { glm::dot( Point - position, Point - position ) };
|
||||
if( gap2 < nearest ) {
|
||||
nearest = gap2;
|
||||
result = glm::mix( Path.distances[ index - 1 ], Path.distances[ index ], along );
|
||||
Position = position;
|
||||
Direction = chord / std::sqrt( chordlength2 );
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// where the feet of the rails of the path are at the distance along it: the middle between them, the unit vector across to the
|
||||
// rail on the right, tilted with the cant, and the unit vector along the path, with its grade
|
||||
void rail_frame( sampled_path const &Path, double const Distance, glm::dvec3 &Middle, glm::dvec3 &Across, glm::dvec3 &Along ) {
|
||||
|
||||
glm::vec3 angles;
|
||||
Path.segment->RaPositionGet( Distance, Middle, angles );
|
||||
glm::dvec3 behind, ahead;
|
||||
glm::vec3 unused;
|
||||
Path.segment->RaPositionGet( std::max( 0.0, Distance - 0.1 ), behind, unused );
|
||||
Path.segment->RaPositionGet( std::min( Path.length, Distance + 0.1 ), ahead, unused );
|
||||
Along = ahead - behind;
|
||||
if( glm::length( Along ) < 1e-6 ) {
|
||||
Along = { 0.0, 0.0, 1.0 };
|
||||
}
|
||||
Along = glm::normalize( Along );
|
||||
auto flat { glm::dvec3 { -Along.z, 0.0, Along.x } };
|
||||
flat = ( glm::length( flat ) > 1e-6 ? glm::normalize( flat ) : glm::dvec3 { 1.0, 0.0, 0.0 } );
|
||||
auto const roll { static_cast<double>( angles.x ) };
|
||||
Across = flat * std::cos( roll ) - glm::dvec3 { 0.0, 1.0, 0.0 } * std::sin( roll );
|
||||
Middle.y -= Path.track->rail_height();
|
||||
}
|
||||
|
||||
// how far the feet of the rails of the path at the point stand off the plane of the feet of the rails of the other path where
|
||||
// it's nearest the point: a timber under both lies flat under all of them only if they're in one plane
|
||||
double off_plane( sampled_path const &Path, glm::dvec3 const &Point, sampled_path const &Other ) {
|
||||
|
||||
if( Path.level && Other.level ) {
|
||||
return std::abs( Path.feet - Other.feet );
|
||||
}
|
||||
glm::dvec2 position, direction;
|
||||
auto const at { nearest_on( Path, plan( Point ), position, direction ) };
|
||||
auto const otherat { nearest_on( Other, plan( Point ), position, direction ) };
|
||||
glm::dvec3 middle, across, along, othermiddle, otheracross, otheralong;
|
||||
rail_frame( Path, at, middle, across, along );
|
||||
rail_frame( Other, otherat, othermiddle, otheracross, otheralong );
|
||||
auto const normal { glm::normalize( glm::cross( otheracross, otheralong ) ) };
|
||||
auto result { 0.0 };
|
||||
for( auto const side : { -0.5, 0.5 } ) {
|
||||
result = std::max( result, std::abs( glm::dot( middle + across * ( side * Path.gauge ) - othermiddle, normal ) ) );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// how far apart the middles of two paths running at the angle are where their plain sleepers would just touch: the one square to
|
||||
// the other reaches out by half its length, the other by its half length foreshortened and half its width, with the gap
|
||||
// between their ends. closer, the paths share the timbers
|
||||
double touch_spacing( sampled_path const &Path, sampled_path const &Other, double const Angle ) {
|
||||
|
||||
return Other.half + Path.half * std::cos( Angle ) + half_width * std::sin( Angle ) + end_gap;
|
||||
}
|
||||
|
||||
// how close two paths run where their sleepers can't be laid apart, so they share the timbers whether they're in one plane or not
|
||||
double shared_spacing( sampled_path const &Path, sampled_path const &Other ) {
|
||||
|
||||
return Path.rails + Other.rails + 2.0 * overhang_min + end_gap;
|
||||
}
|
||||
|
||||
// whether the path at the point, running the given way, is carried by the beams of a path before it, which it runs beside so
|
||||
// close that their plain sleepers would touch. where the paths cross so wide that one timber can't carry both, the sleepers
|
||||
// of the path are only left out where they'd meet the timbers of the other. the paths share timbers only while their rails lie
|
||||
// in one plane, or run so close they can't be parted: where one rises, falls or cants apart, each lays its own
|
||||
bool shares_with( sampled_path const &Path, glm::dvec3 const &Point, glm::dvec2 const &Direction, sampled_path const &Other ) {
|
||||
|
||||
double gap;
|
||||
glm::dvec2 direction { Direction };
|
||||
if( false == beside( Other, Point, Path.half + Other.half + half_width + end_gap, gap, direction ) ) {
|
||||
return false;
|
||||
}
|
||||
auto const angle { angle_between( Direction, direction ) };
|
||||
if( angle >= grid_angle ) {
|
||||
return gap < Other.half + half_width + end_gap;
|
||||
}
|
||||
if( gap >= touch_spacing( Path, Other, angle ) ) {
|
||||
return false;
|
||||
}
|
||||
return gap < shared_spacing( Path, Other ) || off_plane( Path, Point, Other ) <= coplanar_tolerance;
|
||||
}
|
||||
|
||||
// the paths of the track, and of the tracks with beams around it; or of all those with the ballast the simulator makes, whether
|
||||
// they get beams or not
|
||||
std::vector<sampled_path> paths_around( TTrack &Track, bool const Bedded ) {
|
||||
|
||||
std::vector<sampled_path> result;
|
||||
for( int path = 0; path < path_count( Track ); ++path ) {
|
||||
result.emplace_back( sample( Track, path ) );
|
||||
}
|
||||
std::vector<TTrack *> others;
|
||||
tracks_near( Track, [&]( TTrack *Other ) {
|
||||
if( Other == &Track || Other->m_editorremoved ) {
|
||||
return;
|
||||
}
|
||||
if( std::find( others.begin(), others.end(), Other ) != others.end() || false == ( Bedded ? bedded( *Other ) : lays( *Other ) ) ) {
|
||||
return;
|
||||
}
|
||||
others.push_back( Other ); } );
|
||||
for( auto *other : others ) {
|
||||
for( int path = 0; path < path_count( *other ); ++path ) {
|
||||
result.emplace_back( sample( *other, path ) );
|
||||
}
|
||||
}
|
||||
result.erase(
|
||||
std::remove_if( result.begin(), result.end(), []( sampled_path const &Path ) { return Path.points.size() < 2; } ),
|
||||
result.end() );
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
void tracks_near( TTrack &Track, std::function<void( TTrack * )> const &Visit ) {
|
||||
|
||||
auto const sections { simulation::Region->sections( Track.location(), static_cast<float>( Track.radius() + 50.0 ) ) };
|
||||
for( auto *section : sections ) {
|
||||
for( auto const &cell : section->m_cells ) {
|
||||
if( glm::length( cell.area().center - Track.location() ) > cell.area().radius + Track.radius() + around_margin ) {
|
||||
continue;
|
||||
}
|
||||
for( auto *other : cell.m_directories.paths ) {
|
||||
if( other == nullptr || glm::length( other->location() - Track.location() ) > other->radius() + Track.radius() + around_margin ) {
|
||||
continue;
|
||||
}
|
||||
Visit( other );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<glm::dvec2> stretches_where( sampled_path const &Path, std::function<bool( glm::dvec3 const &, glm::dvec2 const & )> const &Test ) {
|
||||
|
||||
std::vector<glm::dvec2> result;
|
||||
auto const holds = [&]( double const Distance ) {
|
||||
auto const distance { std::clamp( Distance, std::min( 0.01, 0.5 * Path.length ), std::max( Path.length - 0.01, 0.5 * Path.length ) ) };
|
||||
glm::dvec3 point;
|
||||
glm::vec3 angles;
|
||||
Path.segment->RaPositionGet( distance, point, angles );
|
||||
return Test( point, heading( Path, distance ) ); };
|
||||
auto const count { std::max( 1, static_cast<int>( std::ceil( Path.length / ( 0.5 * sample_step ) ) ) ) };
|
||||
auto was { holds( 0.0 ) };
|
||||
auto start { 0.0 };
|
||||
auto previous { 0.0 };
|
||||
for( int step = 1; step <= count; ++step ) {
|
||||
auto const distance { Path.length * step / count };
|
||||
auto const is { holds( distance ) };
|
||||
if( is != was ) {
|
||||
auto low { previous };
|
||||
auto high { distance };
|
||||
for( int narrow = 0; narrow < 8; ++narrow ) {
|
||||
auto const middle { 0.5 * ( low + high ) };
|
||||
( holds( middle ) == was ? low : high ) = middle;
|
||||
}
|
||||
auto const boundary { 0.5 * ( low + high ) };
|
||||
if( was ) {
|
||||
result.emplace_back( start, boundary );
|
||||
}
|
||||
start = boundary;
|
||||
was = is;
|
||||
}
|
||||
previous = distance;
|
||||
}
|
||||
if( was ) {
|
||||
result.emplace_back( start, Path.length );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace track_sleepers
|
||||
85
world/SleeperPaths.h
Normal file
85
world/SleeperPaths.h
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
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 <functional>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
class TTrack;
|
||||
class TSegment;
|
||||
|
||||
// the paths of the tracks as the sleepers are laid out along them: sampled in plan, with how close they run to each other and
|
||||
// whether the beams of one carry the rails of another
|
||||
namespace track_sleepers {
|
||||
|
||||
// a path of a track by samples taken along it
|
||||
struct sampled_path {
|
||||
TTrack *track { nullptr };
|
||||
int path { 0 };
|
||||
TSegment const *segment { nullptr };
|
||||
double length { 0.0 };
|
||||
double gauge { 1.435 };
|
||||
double rails { 0.829 }; // how far out from the middle of the path its rails reach
|
||||
double reach { 1.3 }; // half the length of the sleepers of the path alone
|
||||
double half { 1.3 }; // half the length of the plain sleepers of the track, of the model it lays them of
|
||||
double line { 0.0 }; // the length of the line of plain tracks joined end to end the path is part of
|
||||
bool level { true }; // the path runs level and without cant, its rails all at the one height
|
||||
double feet { 0.0 }; // the height of the feet of its rails where it's level
|
||||
std::vector<glm::dvec3> points;
|
||||
std::vector<double> distances;
|
||||
glm::dvec2 low { 0.0 }; // the bounds of the samples in plan
|
||||
glm::dvec2 high { 0.0 };
|
||||
};
|
||||
|
||||
int path_count( TTrack const &Track );
|
||||
TSegment const *path_segment( TTrack const &Track, int const Path );
|
||||
sampled_path sample( TTrack &Track, int const Path );
|
||||
// the direction of the path in plan by its samples about the distance
|
||||
glm::dvec2 heading( sampled_path const &Path, double const Distance );
|
||||
// which of two paths lays the beams where they're merged: a switch before a plain track, the main path of a switch
|
||||
// before the diverging one, a plain track of a longer line before one of a shorter, the longer before the shorter, and
|
||||
// then by the name
|
||||
bool before( sampled_path const &Left, sampled_path const &Right );
|
||||
bool near_bounds( sampled_path const &Path, glm::dvec3 const &Point, double const Distance );
|
||||
// how far the point is from the path in plan and which way the path runs there, when it's beside the path closer than the
|
||||
// distance and at about the same level. a point past either end of the path isn't beside it, so a track going on from the end
|
||||
// of another doesn't count as running beside it
|
||||
bool beside( sampled_path const &Path, glm::dvec3 const &Point, double const Distance, double &Gap, glm::dvec2 &Direction );
|
||||
// the place of the path nearest to the point in plan: its distance along the path, where it is and which way the path runs
|
||||
double nearest_on( sampled_path const &Path, glm::dvec2 const &Point, glm::dvec2 &Position, glm::dvec2 &Direction );
|
||||
// where the feet of the rails of the path are at the distance along it: the middle between them, the unit vector across to the
|
||||
// rail on the right, tilted with the cant, and the unit vector along the path, with its grade
|
||||
void rail_frame( sampled_path const &Path, double const Distance, glm::dvec3 &Middle, glm::dvec3 &Across, glm::dvec3 &Along );
|
||||
// how far the feet of the rails of the path at the point stand off the plane of the feet of the rails of the other path where
|
||||
// it's nearest the point: a timber under both lies flat under all of them only if they're in one plane
|
||||
double off_plane( sampled_path const &Path, glm::dvec3 const &Point, sampled_path const &Other );
|
||||
// how far apart the middles of two paths running at the angle are where their plain sleepers would just touch: the one square to
|
||||
// the other reaches out by half its length, the other by its half length foreshortened and half its width, with the gap
|
||||
// between their ends. closer, the paths share the timbers
|
||||
double touch_spacing( sampled_path const &Path, sampled_path const &Other, double const Angle );
|
||||
// how close two paths run where their sleepers can't be laid apart, so they share the timbers whether they're in one plane or not
|
||||
double shared_spacing( sampled_path const &Path, sampled_path const &Other );
|
||||
// whether the path at the point, running the given way, is carried by the beams of a path before it, which it runs beside so
|
||||
// close that their plain sleepers would touch. where the paths cross so wide that one timber can't carry both, the sleepers
|
||||
// of the path are only left out where they'd meet the timbers of the other. the paths share timbers only while their rails lie
|
||||
// in one plane, or run so close they can't be parted: where one rises, falls or cants apart, each lays its own
|
||||
bool shares_with( sampled_path const &Path, glm::dvec3 const &Point, glm::dvec2 const &Direction, sampled_path const &Other );
|
||||
// the paths of the track, and of the tracks with beams around it; or of all those with the ballast the simulator makes, whether
|
||||
// they get beams or not
|
||||
std::vector<sampled_path> paths_around( TTrack &Track, bool const Bedded = false );
|
||||
// the tracks of the region near enough the track to take part in laying its beams, itself among them, some more than once
|
||||
void tracks_near( TTrack &Track, std::function<void( TTrack * )> const &Visit );
|
||||
// the stretches of the path where the test holds of the place at a distance along it and the way the path runs there, tested at
|
||||
// half the step of the samples, with the ends narrowed down between
|
||||
std::vector<glm::dvec2> stretches_where( sampled_path const &Path, std::function<bool( glm::dvec3 const &, glm::dvec2 const & )> const &Test );
|
||||
|
||||
} // namespace track_sleepers
|
||||
75
world/SleeperRules.h
Normal file
75
world/SleeperRules.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
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 <cstddef>
|
||||
|
||||
// the rules the sleepers and timbers are laid by, and the record of where the layout had to break them
|
||||
namespace track_sleepers {
|
||||
|
||||
// how far apart the beams are along the track, and how far closer or further they may go to put one where a pin is
|
||||
double constexpr pitch { 0.6 };
|
||||
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 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 };
|
||||
// how far the feet of the rails a timber carries may stand off one plane
|
||||
double constexpr coplanar_tolerance { 0.01 };
|
||||
// the gap left between the ends of two beams on one line
|
||||
double constexpr end_gap { 0.05 };
|
||||
// half the width of a beam, to keep the sleepers of a track crossing another at a wide angle clear of the timbers of the other
|
||||
double constexpr half_width { 0.13 };
|
||||
// a timber longer than this is a fault of the layout
|
||||
double constexpr length_cap { 8.5 }; // the longest prestressed concrete bearer of EN 13230-4, under both tracks of a scissors crossing about 5 m apart
|
||||
// the rails of a path crossing the line of a beam at a lower angle than this aren't laid on it
|
||||
double constexpr crossing_sine_min { 0.2588 }; // 15 degrees
|
||||
// two paths crossing at this angle or wider keep the beams square to each, laid as a grid
|
||||
double constexpr grid_angle { 1.2915 }; // 74 degrees
|
||||
// the timbers turn toward the bisector of a frog over as many beams before it
|
||||
double constexpr frog_ramp { 6.0 * pitch };
|
||||
// past where the tracks go apart the sleepers lie square to them at once, unless the last timber turns so far from them that
|
||||
// the ends of the next sleepers would meet it: then they turn back by at most so much a beam
|
||||
double constexpr turn_max { 0.2415 }; // 13.8 degrees: a sleeper 1.3 m out from the track keeps clear of one turned so far a pitch away
|
||||
// how far apart the samples of a path are, to tell how far the paths around run from it
|
||||
double constexpr sample_step { 0.5 };
|
||||
// two paths over each other, on a bridge, aren't merged
|
||||
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 };
|
||||
// how many beams make a run drawn or left out together
|
||||
std::size_t constexpr chunk_beams { 16 };
|
||||
|
||||
// how many tracks a cluster takes at most
|
||||
std::size_t constexpr cluster_cap { 256 };
|
||||
// how far the lines of the timbers turn from one to the next at most, so the far ends of the long ones keep their spacing, and how
|
||||
// far the plain sleepers turn back square to the track from one to the next
|
||||
double constexpr timber_turn { 0.0175 }; // a degree
|
||||
double constexpr sleeper_turn { 0.06 }; // three and a half degrees
|
||||
|
||||
// what was wrong with the beams laid since the counts were taken last
|
||||
struct fault_counts {
|
||||
std::size_t offplane { 0 }; // timbers shared by rails out of one plane, where the tracks run too close to part them
|
||||
std::size_t clipped { 0 }; // beam ends cut shorter than the least overhang, to keep clear of the beams of other tracks
|
||||
std::size_t dropped { 0 }; // beams left out where they'd lie over one laid before
|
||||
double slowest { 0.0 }; // the longest a cluster took to lay, in ms
|
||||
std::size_t biggest { 0 }; // the most tracks in a cluster
|
||||
};
|
||||
|
||||
inline fault_counts &faults() {
|
||||
static fault_counts counts;
|
||||
return counts;
|
||||
}
|
||||
|
||||
|
||||
} // namespace track_sleepers
|
||||
230
world/SleeperWorker.cpp
Normal file
230
world/SleeperWorker.cpp
Normal file
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
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/SleeperWorker.h"
|
||||
#include "world/SleeperLayout.h"
|
||||
#include "world/SleeperRules.h"
|
||||
#include "world/Track.h"
|
||||
#include "simulation/simulation.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
|
||||
namespace track_sleepers {
|
||||
|
||||
namespace {
|
||||
|
||||
// the version of the layout of the beams, moved on whenever a track changes, so the beams worked out for a cluster hold till then
|
||||
std::atomic<std::uint64_t> &layout_epoch() {
|
||||
static std::atomic<std::uint64_t> epoch { 1 };
|
||||
return epoch;
|
||||
}
|
||||
|
||||
std::map<TTrack const *, std::shared_ptr<cluster_beams>> &known_clusters() {
|
||||
static std::map<TTrack const *, std::shared_ptr<cluster_beams>> clusters;
|
||||
return clusters;
|
||||
}
|
||||
|
||||
std::mutex &clusters_mutex() {
|
||||
static std::mutex mutex;
|
||||
return mutex;
|
||||
}
|
||||
|
||||
// the beams of the cluster of the track worked out already for the current layout, null where they aren't
|
||||
std::shared_ptr<cluster_beams> cached_cluster( TTrack const &Track ) {
|
||||
|
||||
std::lock_guard<std::mutex> lock { clusters_mutex() };
|
||||
auto const found { known_clusters().find( &Track ) };
|
||||
return ( found != known_clusters().end() && found->second->epoch == layout_epoch() ? found->second : nullptr );
|
||||
}
|
||||
|
||||
// the beams of the cluster of the track worked out, and kept for each track of it unless the tracks changed meanwhile
|
||||
std::shared_ptr<cluster_beams> work_out( TTrack &Track ) {
|
||||
|
||||
auto const start { std::chrono::steady_clock::now() };
|
||||
auto const epoch { layout_epoch().load() };
|
||||
auto const tracks { cluster_of( Track ) };
|
||||
auto const laid { lay_out( tracks, epoch ) };
|
||||
faults().slowest = std::max( faults().slowest, std::chrono::duration<double, std::milli>( std::chrono::steady_clock::now() - start ).count() );
|
||||
faults().biggest = std::max( faults().biggest, tracks.size() );
|
||||
std::lock_guard<std::mutex> lock { clusters_mutex() };
|
||||
if( laid->epoch == layout_epoch() ) {
|
||||
for( auto *track : tracks ) {
|
||||
known_clusters()[ track ] = laid;
|
||||
}
|
||||
}
|
||||
return laid;
|
||||
}
|
||||
|
||||
std::vector<beam> beams_of( cluster_beams const &Cluster, TTrack const &Track ) {
|
||||
|
||||
auto const beams { Cluster.beams.find( &Track ) };
|
||||
return ( beams != Cluster.beams.end() ? beams->second : std::vector<beam> {} );
|
||||
}
|
||||
|
||||
// the clusters of the tracks coming near worked out in a thread of their own, one after another
|
||||
class background_worker {
|
||||
|
||||
public:
|
||||
~background_worker() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock { mutex };
|
||||
stop = true;
|
||||
queue.clear();
|
||||
}
|
||||
wake.notify_all();
|
||||
if( thread.joinable() ) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
bool enabled { true };
|
||||
|
||||
void request( TTrack &Track ) {
|
||||
|
||||
if( false == resolved ) {
|
||||
// the models the sleepers are made of loaded here, the thread can't load them
|
||||
resolved = true;
|
||||
for( auto *track : simulation::Paths.sequence() ) {
|
||||
if( track != nullptr && bedded( *track ) ) {
|
||||
plain_model( *track );
|
||||
timber_model( *track );
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock { mutex };
|
||||
if( queued.count( &Track ) ) {
|
||||
return;
|
||||
}
|
||||
queued.insert( &Track );
|
||||
queue.push_back( &Track );
|
||||
if( false == thread.joinable() ) {
|
||||
thread = std::thread( [ this ]() { run(); } );
|
||||
}
|
||||
}
|
||||
wake.notify_one();
|
||||
}
|
||||
|
||||
// nothing left to work out nor being worked out, the waiting ones dropped
|
||||
void settle() {
|
||||
|
||||
std::unique_lock<std::mutex> lock { mutex };
|
||||
queue.clear();
|
||||
queued.clear();
|
||||
idle.wait( lock, [ this ]() { return false == busy; } );
|
||||
}
|
||||
|
||||
private:
|
||||
void run() {
|
||||
|
||||
while( true ) {
|
||||
TTrack *track { nullptr };
|
||||
{
|
||||
std::unique_lock<std::mutex> lock { mutex };
|
||||
wake.wait( lock, [ this ]() { return stop || false == queue.empty(); } );
|
||||
if( stop ) {
|
||||
return;
|
||||
}
|
||||
track = queue.front();
|
||||
queue.pop_front();
|
||||
busy = true;
|
||||
}
|
||||
if( cached_cluster( *track ) == nullptr ) {
|
||||
work_out( *track );
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock { mutex };
|
||||
queued.erase( track );
|
||||
busy = false;
|
||||
}
|
||||
idle.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex mutex;
|
||||
std::condition_variable wake;
|
||||
std::condition_variable idle;
|
||||
std::deque<TTrack *> queue;
|
||||
std::set<TTrack const *> queued;
|
||||
std::thread thread;
|
||||
bool stop { false };
|
||||
bool busy { false };
|
||||
bool resolved { false };
|
||||
};
|
||||
|
||||
background_worker &worker() {
|
||||
static background_worker worker;
|
||||
return worker;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<beam> place( TTrack &Track ) {
|
||||
|
||||
auto cluster { cached_cluster( Track ) };
|
||||
if( cluster == nullptr ) {
|
||||
cluster = work_out( Track );
|
||||
}
|
||||
return beams_of( *cluster, Track );
|
||||
}
|
||||
|
||||
void lay_in_background( bool const Background ) {
|
||||
|
||||
if( false == Background ) {
|
||||
worker().settle();
|
||||
}
|
||||
worker().enabled = Background;
|
||||
}
|
||||
|
||||
void prefetch( TTrack &Track ) {
|
||||
|
||||
if( worker().enabled && cached_cluster( Track ) == nullptr ) {
|
||||
worker().request( Track );
|
||||
}
|
||||
}
|
||||
|
||||
void outdate_layouts() {
|
||||
|
||||
++layout_epoch();
|
||||
}
|
||||
|
||||
std::optional<std::vector<beam>> laid_out( TTrack const &Track ) {
|
||||
|
||||
if( auto const cluster { cached_cluster( Track ) }; cluster != nullptr ) {
|
||||
return beams_of( *cluster, Track );
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool in_background() {
|
||||
|
||||
return worker().enabled;
|
||||
}
|
||||
|
||||
void ask_layout( TTrack &Track ) {
|
||||
|
||||
worker().request( Track );
|
||||
}
|
||||
|
||||
void settle_background() {
|
||||
|
||||
worker().settle();
|
||||
}
|
||||
|
||||
} // namespace track_sleepers
|
||||
31
world/SleeperWorker.h
Normal file
31
world/SleeperWorker.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
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 <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "world/TrackSleepers.h"
|
||||
|
||||
// the layouts of the clusters of tracks kept till a track changes, and worked out in a thread of their own while the simulation runs
|
||||
namespace track_sleepers {
|
||||
|
||||
// the layouts worked out so far hold no more, as a track changed
|
||||
void outdate_layouts();
|
||||
// the beams of the track from the layout of its cluster worked out already for the tracks as they are, none where there's none yet
|
||||
std::optional<std::vector<beam>> laid_out( TTrack const &Track );
|
||||
// whether the layouts are worked out in the background
|
||||
bool in_background();
|
||||
// the layout of the cluster of the track asked of the thread working them out
|
||||
void ask_layout( TTrack &Track );
|
||||
// nothing left to work out in the background nor being worked out, the waiting ones dropped
|
||||
void settle_background();
|
||||
|
||||
} // namespace track_sleepers
|
||||
File diff suppressed because it is too large
Load Diff
@@ -145,6 +145,7 @@ struct layout_check {
|
||||
double slowest { 0.0 }; // the longest a cluster of tracks took to lay, in ms
|
||||
std::size_t biggest { 0 }; // the most tracks in a cluster
|
||||
std::size_t panels { 0 }; // switches with their paths out of one plane where they share the timbers of the panel
|
||||
std::uint64_t digest { 0 }; // of where all the beams lie, to tell whether a change of the code moved any
|
||||
double longest { 0.0 };
|
||||
double widest { 0.0 }; // the widest spacing of the beams under a rail
|
||||
std::string widestat;
|
||||
|
||||
Reference in New Issue
Block a user