17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-22 15:33:21 +02:00

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

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

View File

@@ -40,15 +40,29 @@ http://mozilla.org/MPL/2.0/.
#include "scene/sceneterrain.h"
#include "editor/editorSpeed.hpp"
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <map>
#include <sstream>
namespace {
// frames gone through the selftest, for the measurements of perf
std::size_t selftest_frames{0};
std::string one_line(std::string Text)
{
std::replace(Text.begin(), Text.end(), '\n', ';');
return Text;
}
} // namespace
editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &command, std::istringstream &words)
{
for (auto const group : {&editor_mode::selftest_scene, &editor_mode::selftest_sweep, &editor_mode::selftest_track, &editor_mode::selftest_switch, &editor_mode::selftest_rails, &editor_mode::selftest_join, &editor_mode::selftest_signal, &editor_mode::selftest_terrain})
for (auto const group : {&editor_mode::selftest_scene, &editor_mode::selftest_sweep, &editor_mode::selftest_track, &editor_mode::selftest_switch, &editor_mode::selftest_rails, &editor_mode::selftest_join, &editor_mode::selftest_signal, &editor_mode::selftest_terrain, &editor_mode::selftest_props})
if (auto const result{(this->*group)(command, words)}; result != selftest_result::unhandled)
return result;
return selftest_result::unhandled;
@@ -69,6 +83,13 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm
m_focus_active = false;
return selftest_result::handled;
}
if (command == "zoom")
{
float factor{1.0f};
words >> factor;
Global.ZoomFactor = std::max(factor, 0.01f);
return selftest_result::handled;
}
if (command == "selectat")
{
glm::dvec3 point{0.0};
@@ -102,6 +123,170 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm
WriteLog("SELFTEST select " + name + (m_node != nullptr ? "" : ": no such track"));
return selftest_result::handled;
}
if (command == "perf")
{
// the frames drawn and the time gone since the last perf, with the timings and the counts the renderer keeps
std::string label;
words >> label;
static auto last{std::chrono::steady_clock::now()};
static std::size_t lastframe{0};
auto const now{std::chrono::steady_clock::now()};
auto const frames{selftest_frames - lastframe};
auto const elapsed{std::chrono::duration<double, std::milli>(now - last).count()};
WriteLog(format("SELFTEST perf %s: %zu frames, %.3f ms per frame | %s | %s", label.c_str(), frames, frames > 0 ? elapsed / frames : 0.0, one_line(GfxRenderer->info_times()).c_str(), one_line(GfxRenderer->info_stats()).c_str()));
last = now;
lastframe = selftest_frames;
return selftest_result::handled;
}
if (command == "rebuildall")
{
// the geometry of every path made anew, as the editor does it after a change, timed
auto const start{std::chrono::steady_clock::now()};
auto count{0};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved)
continue;
auto &section{simulation::Region->section(track->location())};
if (false == section.m_geometrycreated)
continue;
track->rebuild_geometry(section.m_geometrybank);
++count;
}
WriteLog(format("SELFTEST rebuildall: %d paths in %.1f ms", count, std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count()));
// the sleepers laid again on all the tracks, which the renderer otherwise does as they come in sight, timed by section
std::map<scene::basic_section const *, double> sectiontimes;
auto laidcount{0};
std::size_t parts{0};
auto laytime{0.0};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || false == track->m_sleepers3d.active)
continue;
auto const laystart{std::chrono::steady_clock::now()};
track_sleepers::lay(*track);
auto const took{std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - laystart).count()};
laytime += took;
sectiontimes[&simulation::Region->section(track->location())] += took;
++laidcount;
for (auto const &instances : track->m_sleepers3d.instances)
parts += instances.size();
}
auto slowest{0.0};
for (auto const &sectiontime : sectiontimes)
slowest = std::max(slowest, sectiontime.second);
WriteLog(format("SELFTEST rebuildall sleepers3d: %d tracks laid in %.1f ms, %zu parts; %zu sections, %.2f ms per section, %.2f ms the slowest", laidcount, laytime, parts, sectiontimes.size(),
(sectiontimes.empty() ? 0.0 : laytime / sectiontimes.size()), slowest));
return selftest_result::handled;
}
if (command == "beams")
{
// the beams the track lays, between two distances along it: their middles, lengths and the way they lie square to
std::string name;
double from{-1e9}, to{1e9};
words >> name >> from >> to;
auto *track{simulation::Paths.find(name)};
if (track == nullptr || false == track->m_sleepers3d.active)
{
WriteLog("SELFTEST beams: no track with beams " + name);
return selftest_result::handled;
}
std::string text;
for (auto const &beam : track_sleepers::place(*track))
{
auto const middle{0.5 * (beam.from + beam.to)};
if (middle.z < from || middle.z > to)
continue;
text += format(" [%.2f %.2f L%.2f a%.2f%s]", middle.x, middle.z, glm::length(beam.to - beam.from), glm::degrees(std::atan2(beam.along.x, beam.along.z)), beam.plain ? " p" : "");
}
WriteLog("SELFTEST beams " + name + ":" + text);
return selftest_result::handled;
}
if (command == "sharedzones")
{
// how far the timbers shared with other tracks reach along the paths of the track and of the tracks joined to its ends
std::string name;
words >> name;
auto *track{simulation::Paths.find(name)};
if (track == nullptr)
{
WriteLog("SELFTEST sharedzones: no track " + name);
return selftest_result::handled;
}
auto const zones_text = [](TTrack const &Track, int const Path) {
std::string text;
for (auto const &zone : track_sleepers::shared_zones(Track, Path))
text += format(" %.2f..%.2f", zone.first, zone.second);
return text.empty() ? std::string{" none"} : text;
};
auto const paths{track->eType == tt_Switch ? 2 : 1};
for (int path = 0; path < paths; ++path)
{
auto const length{track->eType == tt_Switch ? track->SwitchExtension->Segments[path]->GetLength() : track->Segment->GetLength()};
WriteLog(format("SELFTEST sharedzones %s path %d (%.2f m):%s", name.c_str(), path, length, zones_text(*track, path).c_str()));
// the track going on from the end of the path, and how far past the end the shared timbers reach along it
auto *next{track->eType == tt_Switch ? track->SwitchExtension->pNexts[path] : track->trNext};
if (next == nullptr || next->eType != tt_Normal || next->Segment == nullptr)
continue;
glm::dvec3 end, start, unused;
glm::vec3 angles;
(track->eType == tt_Switch ? track->SwitchExtension->Segments[path] : track->Segment)->RaPositionGet(length, end, angles);
next->Segment->RaPositionGet(0.0, start, angles);
auto const fromstart{glm::length(start - end) < 0.1};
auto past{0.0};
for (auto const &zone : track_sleepers::shared_zones(*next, 0))
{
if (fromstart && zone.first < 0.05)
past = zone.second;
else if (false == fromstart && zone.second > next->Segment->GetLength() - 0.05)
past = next->Segment->GetLength() - zone.first;
}
WriteLog(format("SELFTEST sharedzones %s path %d goes on into %s:%s; shared timbers reach %.2f m past the end", name.c_str(), path, next->name().c_str(), zones_text(*next, 0).c_str(), past));
}
return selftest_result::handled;
}
if (command == "sleepersbg")
{
// the sleepers worked out in the background as in the simulation, to see they come out the same, or back to as they're needed
int on{1};
words >> on;
track_sleepers::lay_in_background(on != 0);
for (auto *track : simulation::Paths.sequence())
if (track != nullptr)
track->m_sleepers3d.dirty = true;
track_sleepers::unsettle_around(*simulation::Paths.sequence().front());
WriteLog(format("SELFTEST sleepersbg %d", on));
return selftest_result::handled;
}
if (command == "beamsnear")
{
double x{0.0}, z{0.0}, radius{3.0};
words >> x >> z >> radius;
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || false == track->m_sleepers3d.active)
continue;
for (auto const &beam : track_sleepers::place(*track))
{
auto const middle{0.5 * (beam.from + beam.to)};
if (glm::length(glm::dvec2{middle.x - x, middle.z - z}) > radius)
continue;
WriteLog(format("SELFTEST beam %s: %.2f %.2f -> %.2f %.2f, %.2f m, along %.1f deg%s", track->name().c_str(), beam.from.x, beam.from.z, beam.to.x, beam.to.z, glm::length(beam.to - beam.from), glm::degrees(std::atan2(beam.along.x, beam.along.z)), beam.plain ? " plain" : ""));
}
}
return selftest_result::handled;
}
if (command == "sleepercheck")
{
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,
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count()));
for (auto const &example : result.examples)
WriteLog("SELFTEST sleepercheck " + example);
return selftest_result::handled;
}
if (command == "frames")
{
std::string name;
@@ -230,7 +415,7 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm
if (command == "mapstate")
{
auto const marked{scene::Layers.empty() ? std::vector<std::string>{} : scene::Layers.marked(scene::layer_handle{1}, "//$o")};
WriteLog(format("SELFTEST mapstate orthophoto %d top %d extent %.0f mark %s", m_orthophoto.enabled() ? 1 : 0, Global.EditorOrtho ? 1 : 0, Global.EditorOrthoExtent, marked.empty() ? "-" : marked.front().c_str()));
WriteLog(format("SELFTEST mapstate orthophoto %d top %d extent %.0f mark %s camera %.1f %.1f %.1f", m_orthophoto.enabled() ? 1 : 0, Global.EditorOrtho ? 1 : 0, Global.EditorOrthoExtent, marked.empty() ? "-" : marked.front().c_str(), Camera.Pos.x, Camera.Pos.y, Camera.Pos.z));
return selftest_result::handled;
}
if (command == "trainset")
@@ -1160,6 +1345,36 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm
WriteLog(format("SELFTEST vprofile zone %.3f %.3f %s", zone.from, zone.to, zone.name.c_str()));
return selftest_result::handled;
}
if (command == "vprofileapply")
{
// a plain grade line from the height of the start of the open route, applied to it
double grade{0.0};
words >> grade;
auto &state{m_profile};
if (state.samples.empty())
{
WriteLog("SELFTEST vprofileapply: no route open");
return selftest_result::handled;
}
auto const start{state.samples.front().position.y};
state.line.points = {{0.0, start, 0.0, false}, {state.route.length, start + grade / 1000.0 * state.route.length, 0.0, false}};
state.line.join_start = false;
state.line.join_end = false;
profile_check();
profile_apply();
WriteLog("SELFTEST vprofileapply: " + state.status + " " + state.error);
for (auto const &span : state.route.spans)
{
if (span.track->eType != tt_Switch || span.track->SwitchExtension == nullptr)
continue;
for (std::size_t i = 0; i < span.track->m_paths.size() && i < 2; ++i)
{
auto const &path{span.track->m_paths[i]};
WriteLog(format("SELFTEST switch %s path %zu%s: %zu pieces kept, %zu in use, ends %.3f %.3f", span.track->name().c_str(), i + 1, static_cast<int>(i) == span.path ? " (route)" : "", path.pieces.size(), span.track->SwitchExtension->Segments[i]->PieceCount(), path.points[segment_data::point::start].y, path.points[segment_data::point::end].y));
}
}
return selftest_result::handled;
}
if (command == "backwardcurve")
{
std::string name;
@@ -1359,6 +1574,7 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com
glm::dvec3 at{0.0};
glm::dvec3 toward{0.0};
words >> name >> at.x >> at.z >> toward.x >> toward.z >> label;
std::replace(label.begin(), label.end(), '_', ' ');
m_node = simulation::Paths.find(name);
ui()->set_node(m_node);
show_track_tab(track_tab::turnout);
@@ -1581,6 +1797,22 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com
WriteLog("SELFTEST conecheck " + name + ":" + text);
return selftest_result::handled;
}
if (command == "throw")
{
std::string name;
int state{0};
words >> name >> state;
auto *track{simulation::Paths.find(name)};
if (track != nullptr && track->eType == tt_Switch)
{
track->Switch(state, 0.f, 0.f);
// the blades go over at once, the editor doesn't run the animations of the switches
track->SwitchExtension->fOffset = track->SwitchExtension->fDesiredOffset;
track->RaAnimate();
}
WriteLog(format("SELFTEST throw %s %d%s", name.c_str(), state, track != nullptr && track->eType == tt_Switch ? "" : ": no such switch"));
return selftest_result::handled;
}
if (command == "switchdrive")
{
int index{-1};
@@ -2874,6 +3106,105 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
}
return selftest_result::handled;
}
if (command == "beddump")
{
// the trackbed triangles and the axes of the paths of the tracks near a point, in plan, for a look at where the trackbed has holes
glm::dvec3 point{0.0};
double radius{30.0};
std::string file;
words >> point.x >> point.z >> radius >> file;
std::ofstream output{file, std::ios_base::trunc};
auto triangles{0};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->m_paths.empty())
continue;
if (geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point) > radius)
continue;
auto const paths{track->eType == tt_Switch ? 2 : 1};
for (int path = 0; path < paths; ++path)
{
auto const &segment{track->eType == tt_Switch ? track->SwitchExtension->Segments[path] : track->Segment};
if (segment == nullptr)
continue;
output << "axis " << track->name() << ' ' << path;
for (auto at{0.0}; at <= segment->GetLength() + 1e-6; at += 0.25)
{
glm::dvec3 spot, across;
segment->plan_frame(std::min(at, segment->GetLength()), spot, across);
output << format(" %.4f %.4f", spot.x, spot.z);
}
output << "\n";
}
std::vector<gfx::geometry_handle> beds;
if (track->eType == tt_Switch && track->SwitchExtension != nullptr)
beds.push_back(track->SwitchExtension->Geometry3);
else
beds.insert(beds.end(), track->Geometry2.begin(), track->Geometry2.end());
for (auto const &bed : beds)
{
if (bed == null_handle)
continue;
auto const &vertices{GfxRenderer->Vertices(bed)};
for (std::size_t i = 2; i < vertices.size(); ++i)
{
glm::dvec3 const a{glm::dvec3{vertices[i - 2].position} + track->m_origin};
glm::dvec3 const b{glm::dvec3{vertices[i - 1].position} + track->m_origin};
glm::dvec3 const c{glm::dvec3{vertices[i].position} + track->m_origin};
output << format("tri %s %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f %.4f\n", track->name().c_str(), a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z);
++triangles;
}
}
}
WriteLog(format("SELFTEST beddump: %d triangles into %s", triangles, file.c_str()));
return selftest_result::handled;
}
if (command == "raildump")
{
// the rail geometry of the tracks near a point as drawn: s strips of the lofts, t triangles of the cut faces and the rest
glm::dvec3 point{0.0};
double radius{30.0};
std::string file;
words >> point.x >> point.z >> radius >> file;
std::ofstream output{file, std::ios_base::trunc};
auto chunks{0};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->m_paths.empty())
continue;
if (geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point) > radius)
continue;
output << format("track %s %.4f\n", track->name().c_str(), track->m_paths.front().points[segment_data::point::start].y);
auto const dump = [&](TTrack::geometryhandle_sequence const &Sequence, char const *List, char const Kind) {
for (std::size_t index = 0; index < Sequence.size(); ++index)
{
auto const &vertices{GfxRenderer->Vertices(Sequence[index])};
if (vertices.empty())
continue;
auto const primitive{GfxRenderer->Primitive(Sequence[index])};
auto const kind{primitive == GL_TRIANGLES ? 't' : primitive == GL_TRIANGLE_STRIP ? 's' : Kind};
output << format("chunk %s %s %zu %c %zu\n", track->name().c_str(), List, index, kind, vertices.size());
for (auto const &vertex : vertices)
{
glm::dvec3 const at{glm::dvec3{vertex.position} + track->m_origin};
output << format("%.4f %.4f %.4f\n", at.x, at.y, at.z);
}
++chunks;
}
};
dump(track->Geometry1, "rails1", 's');
dump(track->Geometry2, track->eType == tt_Switch ? "rails2" : "bed", 's');
dump(track->GeometryCut1, "cut1", 't');
dump(track->GeometryCut2, "cut2", 't');
for (std::size_t i = 0; i < track->GeometryTreads.size(); ++i)
{
dump(track->GeometryTreads[i].geometry, "tread", 's');
dump(track->GeometryTreads[i].cuts, "treadcut", 't');
}
}
WriteLog(format("SELFTEST raildump: %d chunks into %s", chunks, file.c_str()));
return selftest_result::handled;
}
if (command == "geomhash")
{
std::string name;
@@ -2974,6 +3305,507 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
}
return selftest_result::handled;
}
if (command == "headgap")
{
// the least gap between the heads of the rails of two tracks running side by side, by the gauge lines and the head of the profile
std::string first, second;
words >> first >> second;
auto *a{simulation::Paths.find(first)};
auto *b{simulation::Paths.find(second)};
if (a == nullptr || b == nullptr)
{
WriteLog("SELFTEST headgap: no such tracks");
return selftest_result::handled;
}
auto const head_a{a->rail_dimensions().head};
auto const head_b{b->rail_dimensions().head};
for (int pa = 0; pa < (a->eType == tt_Switch ? 2 : 1); ++pa)
for (auto const sa : {-1, 1})
for (int pb = 0; pb < (b->eType == tt_Switch ? 2 : 1); ++pb)
for (auto const sb : {-1, 1})
{
auto const length{a->eType == tt_Switch ? a->SwitchExtension->Segments[pa]->GetLength() : a->Segment->GetLength()};
auto const lengthb{b->eType == tt_Switch ? b->SwitchExtension->Segments[pb]->GetLength() : b->Segment->GetLength()};
auto least{1e9};
auto where{0.0};
for (auto at{0.0}; at <= length; at += 0.02)
{
double d, l;
track_rails::rail_offset(*b, pb, sb, track_rails::rail_point(*a, pa, sa, at, head_a), d, l);
if (d < 0.0 || d > lengthb || l < -0.3 || l > 0.3)
continue;
if (l - head_b < least)
{
least = l - head_b;
where = at;
}
}
if (least < 1e8)
WriteLog(format("SELFTEST headgap %s/%d%+d to %s/%d%+d: %.1f mm at %.2f m (heads %.1f %.1f mm, width %.4f)", first.c_str(), pa, sa, second.c_str(), pb, sb, least * 1000.0, where, head_a * 1000.0,
head_b * 1000.0, a->fTrackWidth));
}
return selftest_result::handled;
}
if (command == "checks")
{
// check rails of a track as they're drawn, and the rails of the tracks they run into
std::string name;
words >> name;
int count{0};
int faults{0};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->name() != name)
continue;
auto const layout{track->plan_rails(true)};
auto const size{track->rail_dimensions()};
std::vector<TTrack *> others;
for (auto *other : simulation::Paths.sequence())
if (other != nullptr && false == other->m_editorremoved && (other->iCategoryFlag & 15) == 1 && glm::length(other->location() - track->location()) < other->radius() + track->radius() + 1.0)
others.push_back(other);
for (int path = 0; path < (track->eType == tt_Switch ? 2 : 1); ++path)
for (int right = 0; right < 2; ++right)
{
auto const side{right ? 1 : -1};
for (auto const &check : layout.checks[path][right])
{
if (track_rails::replaced_by_wing(check, layout.wingends[path][right]))
continue;
auto const ownlength{track->eType == tt_Switch ? track->SwitchExtension->Segments[path]->GetLength() : track->Segment->GetLength()};
auto const from{std::max(check.from, 0.0)};
auto const to{std::min(check.to, ownlength)};
if (to - from <= 0.01)
continue;
++count;
std::string hits;
for (auto at{from}; at <= to; at += 0.05)
// the head of the check rail as drawn, 40 mm at the head of the rail of 73.5
for (auto const lateral : {-track_rails::check_flangeway, -track_rails::check_flangeway - 0.020 * size.head / 0.0735, -track_rails::check_flangeway - 0.040 * size.head / 0.0735})
{
auto const point{track_rails::rail_point(*track, path, side, at, lateral)};
for (auto *other : others)
for (int otherpath = 0; otherpath < (other->eType == tt_Switch ? 2 : 1); ++otherpath)
for (auto const otherside : {-1, 1})
{
if (other == track && otherpath == path && otherside == side)
continue;
auto const length{other->eType == tt_Switch ? other->SwitchExtension->Segments[otherpath]->GetLength() : other->Segment->GetLength()};
double d, l;
track_rails::rail_offset(*other, otherpath, otherside, point, d, l);
if (d < 0.0 || d > length || l < 0.0 || l > other->rail_dimensions().head)
continue;
auto const hit{format(" %s/%d%+d", other->name().c_str(), otherpath, otherside)};
if (hits.find(hit) == std::string::npos)
hits += hit + format(" at %.2f m", at);
}
}
if (false == hits.empty())
++faults;
WriteLog(format("SELFTEST check %s path %d side %+d: %.3f-%.3f m drawn of %.3f-%.3f m%s", name.c_str(), path, side, from, to, check.from, check.to, hits.empty() ? ", clear" : (", over" + hits + " FAULT").c_str()));
}
}
}
WriteLog(format("SELFTEST checks: %d check rails, %d faults", count, faults));
return selftest_result::handled;
}
if (command == "cuts")
{
// the cuts of the rails of a track, with the ends of the parts in the world
std::string name, detail;
words >> name >> detail;
auto *track{simulation::Paths.find(name)};
if (track == nullptr)
return selftest_result::handled;
auto const layout{track->plan_rails(true)};
auto const head{track->rail_dimensions().head};
// with knots: the lines of the parts too
auto const knots_of = [](knots::line const &Line) {
std::string result;
for (auto const &knot : Line)
result += format(" %.3f:%.4f", knot.x, knot.y);
return result;
};
for (int path = 0; path < (track->eType == tt_Switch ? 2 : 1); ++path)
for (int right = 0; right < 2; ++right)
for (auto const &cut : layout.plans[path][right].cuts)
{
auto const side{right ? 1 : -1};
auto const where = [&](double const At, knots::line const &Offsets) {
auto const point{track_rails::rail_point(*track, path, side, At, knots::sample(Offsets, At, 0.0) + 0.5 * head)};
return format("%.3f m (%.3f %.3f) offset %.3f", At, point.x, point.z, knots::sample(Offsets, At, 0.0));
};
WriteLog(format("SELFTEST cut %s path %d side %+d at %.3f: end %s%s, start %s%s, of %.3f m", name.c_str(), path, side, cut.at, where(cut.end, cut.endoffsets).c_str(), cut.endextents.empty() ? "" : " extents", where(cut.start, cut.startoffsets).c_str(), cut.startextents.empty() ? "" : " extents", track_rails::path_segment(*track, path).GetLength()));
if (detail == "knots")
for (auto const &[label, line] : std::initializer_list<std::pair<char const *, knots::line const *>>{
{"endoffsets", &cut.endoffsets}, {"endextents", &cut.endextents}, {"endinner", &cut.endinner}, {"endrolled", &cut.endrolled}, {"endworn", &cut.endworn},
{"startoffsets", &cut.startoffsets}, {"startextents", &cut.startextents}, {"startinner", &cut.startinner}, {"startrolled", &cut.startrolled}, {"startworn", &cut.startworn}})
if (false == line->empty())
WriteLog(format("SELFTEST %s%s", label, knots_of(*line).c_str()));
}
return selftest_result::handled;
}
if (command == "wings")
{
// the heads of the rails bent out at the cuts, the wing rails mostly, against the heads of the other rails as they're laid out
std::string name;
words >> name;
std::map<TTrack const *, track_rails::rail_layout> layouts;
auto const layout_of = [&](TTrack *Track) -> track_rails::rail_layout const & {
auto found{layouts.find(Track)};
if (found == layouts.end())
found = layouts.emplace(Track, Track->plan_rails(true)).first;
return found->second;
};
auto const length_of = [](TTrack const &Track, int const Path) { return Track.eType == tt_Switch ? Track.SwitchExtension->Segments[Path]->GetLength() : Track.Segment->GetLength(); };
// whether the head of a rail lies at a distance along it and out from its gauge line, the part of the nearest cut there
auto const occupied = [](track_rails::rail_plan const &Plan, double const Head, double const At, double const Lateral) {
if (Plan.cuts.empty())
return Lateral >= 0.0 && Lateral <= Head;
auto const *cut{&Plan.cuts.front()};
for (auto const &each : Plan.cuts)
if (std::abs(each.at - At) < std::abs(cut->at - At))
cut = &each;
auto const part = [&](knots::line const &Offsets, knots::line const &Extents, knots::line const &Inner) {
auto const offset{knots::sample(Offsets, At, 0.0)};
return Lateral >= offset + std::max(0.0, knots::sample(Inner, At, 0.0)) && Lateral <= offset + std::min(Head, knots::sample(Extents, At, Head));
};
return (At <= cut->end && part(cut->endoffsets, cut->endextents, cut->endinner)) || (At >= cut->start && part(cut->startoffsets, cut->startextents, cut->startinner));
};
int count{0};
int faults{0};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->m_paths.empty())
continue;
if (name != "all" && track->name() != name)
continue;
auto const &layout{layout_of(track)};
auto const head{track->rail_dimensions().head};
std::vector<TTrack *> others;
for (auto *other : simulation::Paths.sequence())
if (other != nullptr && false == other->m_editorremoved && (other->iCategoryFlag & 15) == 1 && false == other->m_paths.empty() && glm::length(other->location() - track->location()) < other->radius() + track->radius() + 1.0)
others.push_back(other);
for (int path = 0; path < (track->eType == tt_Switch ? 2 : 1); ++path)
for (int right = 0; right < 2; ++right)
{
auto const side{right ? 1 : -1};
auto const ownlength{length_of(*track, path)};
for (auto const &cut : layout.plans[path][right].cuts)
for (auto const coming : {true, false})
{
auto const &offsets{coming ? cut.endoffsets : cut.startoffsets};
if (offsets.size() < 2 || offsets.back().x - offsets.front().x < 0.05)
continue;
auto const from{std::max(0.0, coming ? offsets.front().x : std::max(offsets.front().x, cut.start))};
auto const to{std::min(ownlength, coming ? std::min(offsets.back().x, cut.end) : offsets.back().x)};
if (to - from < 0.05)
continue;
++count;
std::string hits;
for (auto at{from}; at <= to; at += 0.02)
{
auto const offset{knots::sample(offsets, at, 0.0)};
if (offset <= 0.002)
continue;
// only what is left of the head where the wing rail is planed
auto const lo{offset + std::max(0.0, knots::sample(coming ? cut.endinner : cut.startinner, at, 0.0))};
auto const hi{offset + std::min(head, knots::sample(coming ? cut.endextents : cut.startextents, at, head))};
if (hi - lo < 0.012)
continue;
for (auto const lateral : {lo + 0.005, 0.5 * (lo + hi), hi - 0.005})
{
auto const point{track_rails::rail_point(*track, path, side, at, lateral)};
for (auto *other : others)
for (int otherpath = 0; otherpath < (other->eType == tt_Switch ? 2 : 1); ++otherpath)
for (auto const otherside : {-1, 1})
{
if (other == track && otherpath == path && otherside == side)
continue;
double d, l;
track_rails::rail_offset(*other, otherpath, otherside, point, d, l);
if (d < 0.0 || d > length_of(*other, otherpath) || l < -0.3 || l > 0.3)
continue;
// a wing rail planed flush against another head from the outside covers its rounded shoulder
auto const theirs{other->rail_dimensions()};
if (false == occupied(layout_of(other).plans[otherpath][otherside > 0], theirs.head - theirs.shoulder, d, l))
continue;
auto const hit{format(" %s/%d%+d", other->name().c_str(), otherpath, otherside)};
if (hits.find(hit) == std::string::npos)
hits += hit + format(" at %.2f m (theirs %.2f m, %.0f mm in)", at, d, l * 1000.0);
}
}
}
if (false == hits.empty())
++faults;
if (false == hits.empty() || name != "all")
WriteLog(format("SELFTEST wing %s path %d side %+d cut at %.3f, %s part %.3f-%.3f m, out %.0f-%.0f mm%s", track->name().c_str(), path, side, cut.at, coming ? "coming" : "going", from, to, offsets.front().y * 1000.0,
offsets.back().y * 1000.0, hits.empty() ? ", clear" : (", over" + hits + " FAULT").c_str()));
}
}
}
WriteLog(format("SELFTEST wings: %d bent parts, %d faults", count, faults));
return selftest_result::handled;
}
if (command == "space")
{
// the rails as laid out, every 2 cm along their parts: a head in the flangeway of another rail where its wheels run (F), over the head of
// another rail past its shoulder (O), a line of a part planing on past its last knot (C), the head jumping in width (J), nothing left of the
// head inside a part (V)
std::string name;
double tolerance{0.5};
words >> name >> tolerance;
if (name.empty())
name = "all";
tolerance *= 0.001;
struct piece
{
double from{-1e9};
double to{1e9};
track_rails::rail_cut const *before{nullptr};
track_rails::rail_cut const *after{nullptr};
};
struct rail
{
TTrack *track{nullptr};
int path{0};
int side{0};
double length{0.0};
double laidfrom{0.0};
double heel{0.0};
track_rails::rail_size size;
track_rails::gauge_line const *line{nullptr};
std::vector<piece> pieces;
};
std::map<TTrack const *, track_rails::rail_layout> layouts;
std::map<TTrack const *, std::vector<rail>> rails;
auto const rails_of = [&](TTrack *Track) -> std::vector<rail> & {
auto found{rails.find(Track)};
if (found != rails.end())
return found->second;
auto &result{rails[Track]};
auto const &layout{layouts.emplace(Track, Track->plan_rails(true)).first->second};
auto const heel{Track->eType == tt_Switch ? std::max(track_rails::blade_length(*Track, 0), track_rails::blade_length(*Track, 1)) : 0.0};
for (int path = 0; path < track_rails::path_count(*Track); ++path)
for (int right = 0; right < 2; ++right)
{
rail each;
each.track = Track;
each.path = path;
each.side = (right ? 1 : -1);
each.length = track_rails::path_segment(*Track, path).GetLength();
each.laidfrom = (Track->eType == tt_Switch && each.side == (Track->SwitchExtension->RightSwitch == (path == 0) ? 1 : -1) ? track_rails::blade_length(*Track, path) : 0.0);
each.heel = heel;
each.size = Track->rail_dimensions();
each.line = &track_rails::rail_line(*Track, path, right, false);
std::vector<track_rails::rail_cut const *> cuts;
for (auto const &cut : layout.plans[path][right].cuts)
cuts.push_back(&cut);
std::stable_sort(cuts.begin(), cuts.end(), [](track_rails::rail_cut const *A, track_rails::rail_cut const *B) { return A->at < B->at; });
for (std::size_t j = 0; j <= cuts.size(); ++j)
{
piece part;
part.before = (j > 0 ? cuts[j - 1] : nullptr);
part.after = (j < cuts.size() ? cuts[j] : nullptr);
if (part.before != nullptr)
part.from = part.before->start;
if (part.after != nullptr)
part.to = part.after->end;
each.pieces.push_back(part);
}
result.push_back(each);
}
return result;
};
knots::line const none;
struct lines
{
knots::line const *offsets;
knots::line const *extents;
knots::line const *inner;
};
// the lines of the part from the nearer of its cuts
auto const lines_at = [&](piece const &Part, double const S) -> lines {
auto const before{Part.before != nullptr && (Part.after == nullptr || std::abs(S - Part.before->at) <= std::abs(S - Part.after->at))};
if (before)
return {&Part.before->startoffsets, &Part.before->startextents, &Part.before->startinner};
if (Part.after != nullptr)
return {&Part.after->endoffsets, &Part.after->endextents, &Part.after->endinner};
return {&none, &none, &none};
};
auto const head_of = [&](rail const &Rail, piece const &Part, double const S, double &Offset, double &Low, double &High) {
auto const l{lines_at(Part, S)};
Offset = knots::sample(*l.offsets, S, 0.0);
Low = Offset + std::max(0.0, knots::sample(*l.inner, S, 0.0));
High = Offset + std::min(Rail.size.head, knots::sample(*l.extents, S, Rail.size.head));
};
// whether the head of a rail covers a point out from its gauge line, past its rounded shoulder and Tolerance
auto const covers = [&](rail const &Rail, double const D, double const L) {
for (auto const &part : Rail.pieces)
{
if (D < part.from || D > part.to)
continue;
double offset, low, high;
head_of(Rail, part, D, offset, low, high);
if (L > low + tolerance && L < std::min(high, offset + Rail.size.head - Rail.size.shoulder) - tolerance)
return true;
}
return false;
};
// the parts near an obtuse crossing of either rail are designed to meet
auto const obtuse_near = [](piece const &Part, double const S) {
for (auto const *cut : {Part.before, Part.after})
if (cut != nullptr && cut->obtuse && std::abs(S - cut->at) < 0.3)
return true;
return false;
};
auto const throwable = [](piece const &Part) { return Part.before != nullptr && Part.before->throwable; };
int counts[5]{};
int parts{0};
char const kinds[]{"FOCJV"};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1 || track->m_paths.empty() || false == track_rails::cut_rails(*track) || track->rail_dimensions().grooved)
continue;
if (name != "all" && track->name() != name)
continue;
std::vector<TTrack *> others;
for (auto *other : simulation::Paths.sequence())
if (other != nullptr && false == other->m_editorremoved && (other->iCategoryFlag & 15) == 1 && false == other->m_paths.empty() && track_rails::cut_rails(*other) && false == other->rail_dimensions().grooved &&
glm::length(other->location() - track->location()) < other->radius() + track->radius() + 1.0)
others.push_back(other);
for (auto const &ours : rails_of(track))
for (auto const &part : ours.pieces)
{
auto const from{std::max({part.from, ours.laidfrom, 0.0})};
auto const to{std::min(part.to, ours.length)};
if (to - from < 0.04)
continue;
++parts;
bool found[5]{};
std::string where[5];
auto const fault = [&](int const Kind, std::string const &What) {
if (false == found[Kind])
{
found[Kind] = true;
where[Kind] = What;
} };
// past the last knots of its lines, the lines planing on as they end
for (auto s{from}; s <= to + 1e-9; s += 0.02)
{
auto const l{lines_at(part, s)};
for (auto const *line : {l.extents, l.inner})
{
if (line->empty())
continue;
auto const value{knots::sample(*line, s, 0.0)};
auto const planing{line == l.extents ? value < ours.size.head - 1e-4 : value > 1e-4};
// the lines have knots every 2 cm at most
if (planing && (s > line->back().x + 0.025 || s < line->front().x - 0.025))
fault(2, format("%s at %.2f m, past its knots %.2f-%.2f m", line == l.extents ? "extents" : "inner", s, line->front().x, line->back().x));
}
}
// along the part: nothing of the head left, the head jumping in width
auto start{from + 0.05};
auto end{to - 0.05};
if (part.before != nullptr && part.before->startkind == track_rails::rail_end::sharp)
for (auto s{from}; s < to; s += 0.02)
{
double offset, low, high;
head_of(ours, part, s, offset, low, high);
if (high - low >= 0.001)
{
start = std::max(start, s + 0.04);
break;
}
}
std::vector<double> stations, widths;
for (auto s{std::ceil(start / 0.02) * 0.02}; s <= end; s += 0.02)
{
double offset, low, high;
head_of(ours, part, s, offset, low, high);
auto const width{std::max(0.0, high - low)};
if (width < 0.001 && false == obtuse_near(part, s))
fault(4, format("at %.2f m, %.1f mm left", s, width * 1000.0));
stations.push_back(s);
widths.push_back(width);
}
// a step in width well past those around it: a head tapering off steeply at a wide angle aside
for (std::size_t i = 1; i < widths.size(); ++i)
{
auto const step{std::abs(widths[i] - widths[i - 1])};
auto const around{std::max(i > 1 ? std::abs(widths[i - 1] - widths[i - 2]) : 0.0, i + 1 < widths.size() ? std::abs(widths[i + 1] - widths[i]) : 0.0)};
if (step > 0.003 && step > 2.0 * around + 0.001 && false == obtuse_near(part, stations[i]))
fault(3, format("at %.2f m, %.1f to %.1f mm", stations[i], widths[i - 1] * 1000.0, widths[i] * 1000.0));
}
// against the rails around
for (auto *other : others)
for (auto const &theirs : rails_of(other))
{
if (theirs.track == ours.track && theirs.path == ours.path)
continue;
auto const &low{theirs.line->low};
auto const &high{theirs.line->high};
auto distance{-1e9};
for (auto s{std::ceil(from / 0.02) * 0.02}; s <= to; s += 0.02)
{
double offset, lo, hi;
head_of(ours, part, s, offset, lo, hi);
if (hi - lo < 2.0 * tolerance)
continue;
auto const centre{track_rails::rail_point(*ours.track, ours.path, ours.side, s, offset + 0.5 * ours.size.head)};
if (centre.x < low.x - 0.5 || centre.x > high.x + 0.5 || centre.z < low.y - 0.5 || centre.z > high.y + 0.5)
continue;
if (distance < -1e8)
distance = track_rails::nearest_distance(*theirs.line, centre);
auto const across{track_rails::lateral(*theirs.line, theirs.side, centre, distance)};
if (distance < 0.0 || distance > theirs.length || std::abs(across) > 0.3)
continue;
if (theirs.track == ours.track && (theirs.side == ours.side && distance < theirs.heel + 0.1 || throwable(part)))
continue;
// the same rail going on past a joint
{
auto d{distance};
if (std::abs(track_rails::lateral(*theirs.line, theirs.side, track_rails::rail_point(*ours.track, ours.path, ours.side, s, 0.0), d)) < 0.002)
continue;
}
// the head of ours keeps out of the flangeways, the top of it out from under other heads: their rounded shoulders lie under
// it, ours under theirs
auto const outer{hi - tolerance};
auto const crown{std::min(hi, offset + ours.size.head - ours.size.shoulder) - tolerance};
for (auto const lateral : {lo + tolerance, 0.5 * (lo + crown), crown, outer})
{
auto const top{lateral <= crown + 1e-9};
auto const point{track_rails::rail_point(*ours.track, ours.path, ours.side, s, lateral)};
auto d{distance};
auto const l{track_rails::lateral(*theirs.line, theirs.side, point, d)};
// rails meeting end on at joints aside
if (d < 0.05 || d > theirs.length - 0.05 || s < 0.05 || s > ours.length - 0.05)
continue;
if (l > -track_rails::flangeway + tolerance && l < -tolerance)
fault(0, format("at %.2f m, %.1f mm into the flangeway of %s/%d%+d at %.2f m", s, (-l) * 1000.0, theirs.track->name().c_str(), theirs.path, theirs.side, d));
if (top && false == obtuse_near(part, s) && covers(theirs, d, l))
{
auto inobtuse{false};
for (auto const &their : theirs.pieces)
if (d >= their.from && d <= their.to && obtuse_near(their, d))
inobtuse = true;
if (false == inobtuse)
fault(1, format("at %.2f m, over the head of %s/%d%+d at %.2f m, %.1f mm in", s, theirs.track->name().c_str(), theirs.path, theirs.side, d, l * 1000.0));
}
}
}
}
for (int kind = 0; kind < 5; ++kind)
if (found[kind])
{
++counts[kind];
WriteLog(format("SELFTEST space %c %s/%d%+d part %.3f-%.3f m: %s", kinds[kind], ours.track->name().c_str(), ours.path, ours.side, from, to, where[kind].c_str()));
}
}
}
WriteLog(format("SELFTEST space: %d parts, F %d, O %d, C %d, J %d, V %d", parts, counts[0], counts[1], counts[2], counts[3], counts[4]));
return selftest_result::handled;
}
if (command == "guards")
{
std::string name;
@@ -3008,7 +3840,7 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
std::string text;
auto const end = [&](glm::dvec2 const &Miter, bool const Flared, bool const Glued) {
if (Miter.y == 0.0)
return std::string{Flared ? "flared" : "square"};
return std::string{Flared ? "planed" : "square"};
if (false == drawn(Miter, 0.0))
return std::string{"past the end of the track"};
glm::dvec3 points[2];
@@ -3080,15 +3912,24 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
}
if (command == "throw")
{
// throw <path> [part] [name]: the switches set over to the path, by a part of the way as in the middle of the animation
int path{0};
double part{1.0};
std::string name;
words >> path;
if (false == static_cast<bool>(words >> part))
part = 1.0;
words >> name;
int count{0};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->SwitchExtension == nullptr)
continue;
if (false == name.empty() && track->name() != name)
continue;
auto const before{track->SwitchExtension->fOffset};
track->Switch(path);
track->SwitchExtension->fOffset = track->SwitchExtension->fDesiredOffset;
track->SwitchExtension->fOffset = before + static_cast<float>(part) * (track->SwitchExtension->fDesiredOffset - before);
track->RaAnimate();
++count;
}
@@ -3137,21 +3978,22 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
if (false == name.empty() && track->name() != name)
continue;
++tracks;
for (auto const &crossing : track->rail_crossings())
{
++found;
auto const middle{crossing.angle > track_rails::middle_angle && crossing.angle <= track_rails::wide_angle};
middles += middle ? 1 : 0;
auto const kind{crossing.angle > track_rails::wide_angle ? "wide" : crossing.facing < 0.0 ? (middle ? "middle acute" : "acute") : (middle ? "middle obtuse" : "obtuse")};
WriteLog(format("SELFTEST crossing %s path %d side %d at %.3f m: %s with %s, 1:%.2f, into %.3f, facing %.3f, nose %d", track->name().c_str(), crossing.path, crossing.side, crossing.at, kind,
crossing.track->name().c_str(), 1.0 / std::tan(crossing.angle), crossing.into, crossing.facing, crossing.nose ? 1 : 0));
}
auto const gauge = [](TTrack *Track, track_rails::rail_crossing const &Crossing) {
auto const &segment{Track->eType == tt_Switch ? *Track->SwitchExtension->Segments[Crossing.path] : *Track->Segment};
glm::dvec3 point, across;
segment.plan_frame(Crossing.at, point, across);
return point + across * (Crossing.side * 0.5 * std::abs(Track->fTrackWidth));
};
for (auto const &crossing : track->rail_crossings())
{
++found;
auto const middle{crossing.angle > track_rails::middle_angle && crossing.angle <= track_rails::wide_angle};
middles += middle ? 1 : 0;
auto const kind{crossing.angle > track_rails::wide_angle ? "wide" : crossing.facing < 0.0 ? (middle ? "middle acute" : "acute") : (middle ? "middle obtuse" : "obtuse")};
auto const where{gauge(track, crossing)};
WriteLog(format("SELFTEST crossing %s path %d side %d at %.3f m: %s with %s, 1:%.2f, into %.3f, facing %.3f, nose %d, plan %.2f %.2f", track->name().c_str(), crossing.path, crossing.side, crossing.at, kind,
crossing.track->name().c_str(), 1.0 / std::tan(crossing.angle), crossing.into, crossing.facing, crossing.nose ? 1 : 0, where.x, where.z));
}
for (auto const &crossing : track->rail_crossings(false))
{
if (crossing.angle > track_rails::wide_angle || crossing.facing >= 0.0)
@@ -3257,6 +4099,10 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
// switchbreak [name] the first piece of the diverging path moved off its neighbour, the switch committed
// switches logs the switches kept in the scenery and the speed of each diverging track
// deletetrack <name>, deletekind <kind> the path deleted as Del does it, or the first path over an element of the kind
// tracksel <name>... the tracks selected together, as Shift+clicks select them; none: nothing selected
// trackprop <key> [value] the value given to the selected tracks, as typed into the inspector; trackprops logs their values
// trackstyle copy [name] | groups <n>... | paste | panel the properties of a track copied, the groups chosen, pasted on the selected tracks
// tracksimilar <key> the tracks of the scenery with the value of the property of the selected ones selected
// btfcheck <terrain file> the .txtf converted to .btf, the editor lines the binary file keeps logged
// mouse <x> <y>, mouseat <x> <y> <z> the cursor moved to the point of the window, or to where the point of the scene is seen, as the mouse moves it
// glide <x> <y> <z> <steps> <frames> [shot] the cursor led to where the point is seen in steps, frames apart, a screenshot after each step with shot
@@ -3267,6 +4113,12 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm
// screenshot
// save
// log <text>
// perf <label> the frames drawn since the last perf and the time per frame logged, with the timings and counts of the renderer
// rebuildall the geometry of every path in the sections made already laid anew, the time it took logged,
// and the 3d sleepers of all the tracks laid anew, timed apart
// sleepercheck the 3d sleepers of all the tracks checked for beams over each other and rails without enough under them
// beams <track> <z from> <z to> the beams the track lays with their middles between the two z
// sharedzones <track> where the timbers shared with other tracks reach along the paths of the track and past its ends
// quit
void editor_mode::selftest_step()
{
@@ -3284,6 +4136,7 @@ void editor_mode::selftest_step()
test.lines.push_back(line);
WriteLog("SELFTEST: " + std::to_string(test.lines.size()) + " steps from " + file);
}
++selftest_frames;
if (test.wait > 0)
{
--test.wait;