From 4912199bd1c5ca4df182b9ae76e0cb8489a24316 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 18 Oct 2026 02:18:36 +0200 Subject: [PATCH] refactor(editor): the self-test commands in a file per group instead of one file of 4200 lines editormodeselftest.cpp keeps the dispatch, the list of the commands and the step; the groups go to editormodeselftest{scene,sleepers,sweep,track,switch,join,signal,terrain,rails}.cpp, the sleeper commands (beams, sharedzones, sleepersbg, beamsnear, sleepercheck) in a group of their own; the frame count and one_line shared through editormodeselftest.h. The output of geomhash, sleepercheck, wings, space, guards, crossings and switches is the same on the seven sceneries of the regression. --- CMakeLists.txt | 9 + application/editormode.h | 1 + application/editormodeselftest.cpp | 4006 +------------------- application/editormodeselftest.h | 33 + application/editormodeselftestjoin.cpp | 398 ++ application/editormodeselftestrails.cpp | 1067 ++++++ application/editormodeselftestscene.cpp | 567 +++ application/editormodeselftestsignal.cpp | 201 + application/editormodeselftestsleepers.cpp | 164 + application/editormodeselftestsweep.cpp | 429 +++ application/editormodeselftestswitch.cpp | 771 ++++ application/editormodeselftestterrain.cpp | 282 ++ application/editormodeselftesttrack.cpp | 558 +++ 13 files changed, 4483 insertions(+), 4003 deletions(-) create mode 100644 application/editormodeselftest.h create mode 100644 application/editormodeselftestjoin.cpp create mode 100644 application/editormodeselftestrails.cpp create mode 100644 application/editormodeselftestscene.cpp create mode 100644 application/editormodeselftestsignal.cpp create mode 100644 application/editormodeselftestsleepers.cpp create mode 100644 application/editormodeselftestsweep.cpp create mode 100644 application/editormodeselftestswitch.cpp create mode 100644 application/editormodeselftestterrain.cpp create mode 100644 application/editormodeselftesttrack.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2478f8ee..3546ff50 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -230,6 +230,15 @@ set(SOURCES "application/editormodesweep.cpp" "application/editormodeturntable.cpp" "application/editormodeselftest.cpp" +"application/editormodeselftestscene.cpp" +"application/editormodeselftestsleepers.cpp" +"application/editormodeselftestsweep.cpp" +"application/editormodeselftesttrack.cpp" +"application/editormodeselftestswitch.cpp" +"application/editormodeselftestjoin.cpp" +"application/editormodeselftestsignal.cpp" +"application/editormodeselftestterrain.cpp" +"application/editormodeselftestrails.cpp" "application/scriptedinput.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" diff --git a/application/editormode.h b/application/editormode.h index f696249d..475a5756 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -2052,6 +2052,7 @@ class editor_mode : public application_mode, private editor_track::observer pause }; selftest_result selftest_scene(std::string const &command, std::istringstream &words); + selftest_result selftest_sleepers(std::string const &command, std::istringstream &words); selftest_result selftest_sweep(std::string const &command, std::istringstream &words); selftest_result selftest_track(std::string const &command, std::istringstream &words); selftest_result selftest_switch(std::string const &command, std::istringstream &words); diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 878ffbd0..c8e8124b 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -47,4016 +47,16 @@ http://mozilla.org/MPL/2.0/. #include #include -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 +#include "application/editormodeselftest.h" 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, &editor_mode::selftest_props}) + for (auto const group : {&editor_mode::selftest_scene, &editor_mode::selftest_sleepers, &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; } -editor_mode::selftest_result editor_mode::selftest_scene(std::string const &command, std::istringstream &words) -{ - auto &test{m_selftest}; - if (command == "camera") - { - glm::dvec3 from; - glm::dvec3 to; - words >> from.x >> from.y >> from.z >> to.x >> to.y >> to.z; - auto const look{glm::normalize(to - from)}; - Camera.Pos = from; - Camera.Angle = glm::vec3(static_cast(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast(std::atan2(-look.x, -look.z)), 0.0f); - Global.pCamera = Camera; - 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}; - words >> point.x >> point.z; - TTrack *nearest{nullptr}; - double distance{1e9}; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) - continue; - auto const d{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point)}; - if (d < distance) - { - distance = d; - nearest = track; - } - } - m_node = distance < 3.0 ? nearest : nullptr; - m_track_point = {}; - ui()->set_node(m_node); - WriteLog(format("SELFTEST selectat %.1f %.1f: %s", point.x, point.z, m_node != nullptr ? m_node->name().c_str() : "nothing")); - return selftest_result::handled; - } - if (command == "select") - { - std::string name; - words >> name; - m_node = simulation::Paths.find(name); - m_track_point = {}; - ui()->set_node(m_node); - 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(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 §ion{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(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 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(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 §iontime : 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; 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(result.digest), - std::chrono::duration(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; - double from{0.0}; - double to{0.0}; - double step{0.1}; - words >> name >> from >> to >> step; - auto *sweep{simulation::Sweeps.find(name)}; - if (sweep == nullptr) - return selftest_result::handled; - auto const side{sweep->definition().lateral >= 0.0 ? 1 : 0}; - if (step <= 0.0) - return selftest_result::handled; - for (int i = 0; from + i * step <= to + 1e-9; ++i) - { - auto const station{from + i * step}; - auto const at{sweep->frame_at(station)}; - WriteLog(format("SELFTEST frame %s %.2f: %.3f %.3f %.3f widening %.4f cant %.4f setback %.4f", name.c_str(), station, at.position.x, at.position.y, at.position.z, at.widening[side], at.cant[side], sweep->setback(at))); - } - return selftest_result::handled; - } - if (command == "weather") - { - auto const &time{simulation::Time.data()}; - WriteLog(format("SELFTEST weather: %02d:%02d, fog end %.0f m, overcast %.2f", time.wHour, time.wMinute, Global.fFogEnd, Global.Overcast)); - return selftest_result::handled; - } - if (command == "projtest") - { - screen_projection const projection; - for (std::size_t i = 0; i < m_sweep.outline.size() && i < 45; i += 4) - { - ImVec2 screen; - auto const shown{projection.project(m_sweep.outline[i], screen)}; - auto const clip{projection.clip(m_sweep.outline[i])}; - WriteLog(format("SELFTEST proj %zu: %.1f %.1f %.1f -> %d %.0f %.0f w %.2f", i, m_sweep.outline[i].x, m_sweep.outline[i].y, m_sweep.outline[i].z, shown ? 1 : 0, screen.x, screen.y, clip.w)); - } - return selftest_result::handled; - } - if (command == "newscenery") - { - WriteLog(restart_for_new_scenery() ? "SELFTEST newscenery: started" : "SELFTEST newscenery: failed"); - return selftest_result::pause; - } - if (command == "vehicles") - { - for (auto *vehicle : simulation::Vehicles.sequence()) - { - if (vehicle == nullptr) - continue; - auto const head{vehicle->HeadPosition()}; - auto const front{vehicle->VectorFront()}; - auto const coupler = [&](int const End) { - auto const &coupler{vehicle->MoverParameters->Couplers[End]}; - return coupler.Connected != nullptr ? coupler.Connected->Name + "/" + std::to_string(coupler.CouplingFlag) : std::string{"-"}; - }; - WriteLog(format("SELFTEST vehicle %s head %.2f %.2f front %.2f %.2f dir %d coupler0 %s coupler1 %s", vehicle->name().c_str(), head.x, head.z, front.x, front.z, vehicle->iDirection, coupler(0).c_str(), coupler(1).c_str())); - } - return selftest_result::handled; - } - if (command == "mouseat") - { - glm::dvec3 at{0.0}; - words >> at.x >> at.y >> at.z; - ImVec2 projected; - if (false == screen_projection{}.project(at, projected)) - { - WriteLog("SELFTEST mouseat: behind the camera"); - return selftest_result::handled; - } - test.input.mouse_to(glm::dvec2{projected.x, projected.y}); - WriteLog(format("SELFTEST mouse %.0f %.0f", projected.x, projected.y)); - return selftest_result::handled; - } - if (command == "glide") - { - glm::dvec3 at{0.0}; - int steps{8}; - int every{2}; - std::string shots; - words >> at.x >> at.y >> at.z >> steps >> every >> shots; - ImVec2 projected; - if (false == screen_projection{}.project(at, projected)) - return selftest_result::handled; - glm::dvec2 const from{test.input.mouse().value_or(glm::dvec2{projected.x, projected.y})}; - glm::dvec2 const to{projected.x, projected.y}; - std::vector expanded; - for (int step = 1; step <= std::max(1, steps); ++step) - { - auto const t{static_cast(step) / std::max(1, steps)}; - auto const eased{t * t * (3.0 - 2.0 * t)}; - auto const point{glm::mix(from, to, eased)}; - expanded.push_back(format("mouse %.1f %.1f", point.x, point.y)); - expanded.push_back("wait " + std::to_string(std::max(1, every))); - if (shots == "shot") - expanded.push_back("screenshot"); - } - test.lines.insert(test.lines.begin() + static_cast(test.next), expanded.begin(), expanded.end()); - return selftest_result::handled; - } - if (test.input.dispatch(command, words)) - return selftest_result::handled; - if (command == "screenshot") - { - Application.queue_screenshot(); - test.wait = 3; - return selftest_result::pause; - } - if (command == "save") - { - save(); - return selftest_result::handled; - } - if (command == "ortho") - { - float extent{0.0f}; - words >> extent; - choose_work_area(work_area::tracks); - if (Global.EditorOrtho == extent < 0.0f) - toggle_ortho(); - if (extent > 0.0f) - Global.EditorOrthoExtent = std::clamp(extent, 5.0f, 50000.0f); - WriteLog(format("SELFTEST ortho extent %.0f", Global.EditorOrthoExtent)); - return selftest_result::handled; - } - if (command == "mapstate") - { - auto const marked{scene::Layers.empty() ? std::vector{} : scene::Layers.marked(scene::layer_handle{1}, "//$o")}; - 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") - { - std::string name; - std::string vehicle; - std::string skin; - int facing{1}; - words >> name >> vehicle >> skin >> facing >> m_vehicle.velocity; - auto *track{simulation::Paths.find(name)}; - if (track == nullptr) - return selftest_result::handled; - auto desc{std::make_shared()}; - desc->path = vehicle; - auto set{std::make_shared()}; - set->skin = skin; - m_vehicle.consist.clear(); - m_vehicle.vehicle = desc; - m_vehicle.skin = set; - vehicle_start(*track, geometry::bezier{track->m_paths.front()}.point(0.5)); - m_vehicle.facing = facing; - auto const placed{vehicle_place()}; - WriteLog(format("SELFTEST trainset %s placed %d pending %d: %s", name.c_str(), placed ? 1 : 0, static_cast(m_vehicle.trainsets.size()), m_vehicle.status.c_str())); - return selftest_result::handled; - } - if (command == "selectmodel") - { - glm::dvec3 point{0.0}; - words >> point.x >> point.z; - TAnimModel *nearest{nullptr}; - auto best{50.0}; - for (auto *model : simulation::Instances.sequence()) - { - if (model == nullptr) - continue; - auto const distance{glm::length(glm::dvec2{model->location().x - point.x, model->location().z - point.z})}; - if (distance < best) - { - best = distance; - nearest = model; - } - } - m_node = nearest; - select_include(0); - ui()->set_node(m_node); - WriteLog("SELFTEST selectmodel: " + (nearest != nullptr ? format("%s at %.2f %.2f %.2f", nearest->name().c_str(), nearest->location().x, nearest->location().y, nearest->location().z) : std::string{"none"})); - return selftest_result::handled; - } - if (command == "addmodel") - { - glm::dvec3 point{0.0}; - words >> point.x >> point.z; - TAnimModel *nearest{nullptr}; - auto best{50.0}; - for (auto *model : simulation::Instances.sequence()) - { - if (model == nullptr) - continue; - auto const distance{glm::length(glm::dvec2{model->location().x - point.x, model->location().z - point.z})}; - if (distance < best) - { - best = distance; - nearest = model; - } - } - select_node(nearest, true); - WriteLog("SELFTEST addmodel: " + (nearest != nullptr ? format("%s at %.2f %.2f %.2f", nearest->name().c_str(), nearest->location().x, nearest->location().y, nearest->location().z) : std::string{"none"})); - return selftest_result::handled; - } - if (command == "selection") - { - std::string text; - for (auto *node : selection_with_groups()) - text += format(" [%s %.2f %.2f %.2f group %d]", node->name().c_str(), node->location().x, node->location().y, node->location().z, static_cast(node->group())); - WriteLog("SELFTEST selection:" + text); - return selftest_result::handled; - } - if (command == "moveselection" && m_node != nullptr) - { - glm::dvec3 offset{0.0}; - words >> offset.x >> offset.y >> offset.z; - selection_snapshots(EditorSnapshot::Action::Move); - auto const before{m_node->location()}; - m_editor.translate(m_node, before + offset, true); - selection_follow(m_node->location() - before, glm::vec3(0.0f), m_node->location()); - return selftest_result::handled; - } - if (command == "turnselection" && m_node != nullptr) - { - float degrees{0.0f}; - words >> degrees; - selection_snapshots(EditorSnapshot::Action::Rotate); - glm::vec3 const turned{0.0f, degrees, 0.0f}; - m_editor.rotate(m_node, turned, 0.0f); - selection_follow(glm::dvec3(0.0), turned, m_node->location()); - return selftest_result::handled; - } - if (command == "copy") - { - copy_selection(); - return selftest_result::handled; - } - if (command == "paste") - { - glm::dvec3 point{0.0}; - words >> point.x >> point.y >> point.z; - paste_clipboard(point); - return selftest_result::handled; - } - if (command == "group") - { - group_selection(); - return selftest_result::handled; - } - if (command == "ungroup") - { - ungroup_selection(); - return selftest_result::handled; - } - if (command == "drop") - { - drop_selection_to_ground(); - return selftest_result::handled; - } - if (command == "delete") - { - delete_selected(); - return selftest_result::handled; - } - if (command == "selectinclude") - { - glm::dvec3 point{0.0}; - words >> point.x >> point.z; - scene::instance_handle nearest{0}; - auto best{50.0}; - m_node = nullptr; - for (scene::instance_handle instance = 1; instance <= scene::Layers.instance_count(); ++instance) - { - if (false == scene::Layers.removable(instance)) - continue; - select_include(instance); - bend_source source; - if (false == bend_source_of_selection(source)) - continue; - auto const at{glm::dvec3(source.transform[3])}; - auto const distance{glm::length(glm::dvec2{at.x - point.x, at.z - point.z})}; - if (distance < best) - { - best = distance; - nearest = instance; - } - } - select_include(nearest); - WriteLog("SELFTEST selectinclude: " + (nearest != 0 ? *scene::Layers.instance(nearest).file : std::string{"none"})); - return selftest_result::handled; - } - if (command == "modelbox") - { - auto *model{dynamic_cast(m_node)}; - bend_source source; - if (model == nullptr || false == bend_source_of_selection(source)) - { - WriteLog("SELFTEST modelbox: no model selected"); - return selftest_result::handled; - } - scene::node_data data; - sweep_node probe{data}; - sweep_node::state state; - state.model = source.file; - state.skin = source.skin; - state.scale = source.scale; - probe.define(state); - glm::dvec3 boxlow{std::numeric_limits::max()}; - glm::dvec3 boxhigh{-std::numeric_limits::max()}; - for (auto const &item : probe.items()) - { - auto const transform{source.transform * glm::scale(glm::dmat4(1.0), source.scale) * item.transform}; - std::vector pending{item.model->GetSMRoot()}; - while (false == pending.empty()) - { - auto *submodel{pending.back()}; - pending.pop_back(); - for (; submodel != nullptr; submodel = submodel->Next) - { - if (submodel->Child != nullptr) - pending.push_back(submodel->Child); - if (submodel->eType != GL_TRIANGLES || submodel->fSquareMinDist > 0.f || (submodel->m_geometry.handle.bank == 0 && submodel->m_geometry.handle.chunk == 0)) - continue; - glm::dmat4 local{1.0}; - for (auto *parent = submodel; parent != nullptr; parent = parent->Parent) - if ((parent->iFlags & 0xC000) != 0 && parent->GetMatrix() != nullptr) - local = glm::dmat4(glm::make_mat4(parent->GetMatrix()->readArray())) * local; - for (auto const &vertex : GfxRenderer->Vertices(submodel->m_geometry.handle)) - { - auto const world{glm::dvec3(transform * local * glm::dvec4(glm::dvec3(vertex.position), 1.0))}; - boxlow = glm::min(boxlow, world); - boxhigh = glm::max(boxhigh, world); - } - } - } - } - WriteLog(format("SELFTEST modelbox: %.2f %.2f %.2f - %.2f %.2f %.2f", boxlow.x, boxlow.y, boxlow.z, boxhigh.x, boxhigh.y, boxhigh.z)); - return selftest_result::handled; - } - if (command == "key") - { - std::string key; - std::string action; - std::string mods; - words >> key >> action >> mods; - if (key == "RMB") - m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT, action == "press" ? GLFW_PRESS : GLFW_RELEASE); - else - { - auto const code{key == "S" ? GLFW_KEY_S : key == "W" ? GLFW_KEY_W : key == "CTRL" ? GLFW_KEY_LEFT_CONTROL : 0}; - on_key(code, 0, action == "press" ? GLFW_PRESS : GLFW_RELEASE, mods == "ctrl" ? GLFW_MOD_CONTROL : 0); - } - WriteLog(format("SELFTEST key %s %s %s: camera %.2f %.2f %.2f, key S %d, right button %d", key.c_str(), action.c_str(), mods.c_str(), Camera.Pos.x, Camera.Pos.y, Camera.Pos.z, static_cast(input::keys[GLFW_KEY_S]), m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT))); - return selftest_result::handled; - } - if (command == "event") - { - std::string name; - words >> name; - auto *event{simulation::Events.FindEvent(name)}; - if (event != nullptr) - simulation::Events.AddToQuery(event, nullptr); - WriteLog("SELFTEST event " + name + (event != nullptr ? " queued" : ": no such event")); - return selftest_result::handled; - } - if (command == "memcell") - { - std::string name; - words >> name; - auto *cell{simulation::Memory.find(name)}; - WriteLog("SELFTEST memcell " + name + (cell != nullptr ? format(": %s %.1f %.1f", cell->Text().c_str(), cell->Value1(), cell->Value2()) : std::string{": none"})); - return selftest_result::handled; - } - if (command == "newscenerypopup") - { - m_newscenery_asked = true; - return selftest_result::handled; - } - if (command == "openscenerypopup") - { - m_openscenery_asked = true; - std::snprintf(m_openscenery_search, sizeof(m_openscenery_search), "%s", "zz_"); - return selftest_result::handled; - } - if (command == "openscenery") - { - std::string name; - words >> name; - WriteLog(restart_editor(name) ? "SELFTEST openscenery: started " + name : "SELFTEST openscenery: failed"); - return selftest_result::handled; - } - if (command == "undo") - { - undo_last(); - WriteLog("SELFTEST undo"); - return selftest_result::handled; - } - if (command == "redo") - { - redo_last(); - WriteLog("SELFTEST redo"); - return selftest_result::handled; - } - if (command == "log") - { - std::string text; - std::getline(words, text); - WriteLog("SELFTEST" + text); - return selftest_result::handled; - } - return selftest_result::unhandled; -} - -editor_mode::selftest_result editor_mode::selftest_sweep(std::string const &command, std::istringstream &words) -{ - if (command == "sweep") - { - std::map values; - std::string pair; - while (words >> pair) - { - auto const equals{pair.find('=')}; - if (equals != std::string::npos) - values[pair.substr(0, equals)] = pair.substr(equals + 1); - } - auto &tool{m_sweep}; - auto const get = [&](char const *Key, std::string const &Default) { auto const found{values.find(Key)}; return found != values.end() ? found->second : Default; }; - tool.edited = nullptr; - tool.curve_for = nullptr; - std::snprintf(tool.model, sizeof(tool.model), "%s", get("model", "").c_str()); - tool.parameters.clear(); - std::stringstream parameters(get("params", "")); - std::string parameter; - while (std::getline(parameters, parameter, ',')) - { - tool.parameters.emplace_back(); - std::snprintf(tool.parameters.back().data(), tool.parameters.back().size(), "%s", parameter.c_str()); - } - tool.parameters_for = tool.model; - auto &settings{tool.settings}; - settings.lateral = std::stod(get("lateral", "0")); - settings.height = std::stod(get("height", "0")); - auto const side{get("side", "origin")}; - settings.side_anchor = side == "near" ? 1 : side == "far" ? 2 : side == "centre" ? 3 : 0; - auto const top{get("top", "origin")}; - settings.height_anchor = top == "bottom" ? 1 : top == "top" ? 2 : top == "edge" ? 3 : 0; - settings.bend = get("mode", "bend") != "repeat"; - settings.step = std::stod(get("step", "0")); - settings.widen = get("widen", "0") == "1"; - settings.platform = get("platform", "0") == "1"; - settings.face = get("face", "1") == "1"; - settings.tilt = get("tilt", "0") == "1"; - settings.from = std::stod(get("from", "0")); - settings.to = std::stod(get("to", "-1")); - auto const length{get("length", "all")}; - tool.length_mode = length == "original" ? 0 : length == "all" ? 2 : 1; - if (tool.length_mode == 1) - tool.length = std::stod(length); - if (values.count("edge")) - EditorSettings.platform_edge(std::stod(values["edge"])); - tool.measured.clear(); - tool.open = true; - sweep_curve(); - sweep_measure(); - if (values.count("widen")) - settings.widen = get("widen", "0") == "1"; - if (values.count("platform")) - settings.platform = get("platform", "0") == "1"; - if (get("create", "1") == "1") - { - sweep_create(); - WriteLog("SELFTEST sweep: " + tool.status); - } - return selftest_result::handled; - } - if (command == "drag") - { - glm::dvec3 from{0.0}; - glm::dvec3 to{0.0}; - words >> from.x >> from.z >> to.x >> to.z; - m_sweep.open = true; - if (false == sweep_press_at(from, false)) - { - WriteLog("SELFTEST drag: the press wasn't taken"); - return selftest_result::handled; - } - sweep_drag_at(to); - sweep_release(); - WriteLog("SELFTEST drag: " + m_sweep.status); - return selftest_result::handled; - } - if (command == "fouling") - { - std::map values; - std::string pair; - while (words >> pair) - { - auto const equals{pair.find('=')}; - if (equals != std::string::npos) - values[pair.substr(0, equals)] = pair.substr(equals + 1); - } - auto &tool{m_fouling}; - if (values["show"] == "1") - { - show_track_tab(track_tab::lineside); - tool.expand = true; - } - tool.scope = std::stoi(values.count("scope") ? values["scope"] : "2"); - tool.height_mode = values["mode"] == "ground" ? 0 : 1; - tool.height = std::stod(values.count("height") ? values["height"] : "0"); - tool.dirty = true; - fouling_update(); - WriteLog("SELFTEST fouling: " + tool.status); - int shown{0}; - for (auto const &found : tool.found) - if (found.wrong && shown++ < 5) - WriteLog(format("SELFTEST fouling: %s %.2f m off, %+.3f m high", found.marker.file.c_str(), found.error, found.height_error)); - if (values["place"] == "one") - { - // the first fouling point without a marker alone, as a click on it in the view - for (int index = 0; index < static_cast(tool.points.size()); ++index) - if (tool.points[index].standing < 0) - { - fouling_place(index); - break; - } - tool.dirty = true; - fouling_update(); - WriteLog("SELFTEST fouling one: " + tool.status); - } - if (values["place"] == "1") - { - fouling_place(); - tool.dirty = true; - fouling_update(); - WriteLog("SELFTEST fouling after placing: " + tool.status); - } - if (values["fix"] == "1") - { - fouling_fix(); - tool.dirty = true; - fouling_update(); - WriteLog("SELFTEST fouling after the fix: " + tool.status); - } - return selftest_result::handled; - } - if (command == "hekto") - { - std::map values; - std::string pair; - while (words >> pair) - { - auto const equals{pair.find('=')}; - if (equals != std::string::npos) - values[pair.substr(0, equals)] = pair.substr(equals + 1); - } - auto *track{simulation::Paths.find(values["track"])}; - if (track == nullptr) - { - WriteLog("SELFTEST hekto: no such track"); - return selftest_result::handled; - } - hekto_start(*track, track->m_paths.front().points[segment_data::point::start]); - auto &tool{m_hekto}; - tool.dirty = true; - hekto_update(); - if (values.count("km")) - { - tool.km = std::stod(values["km"]); - tool.growth = values["growth"] == "-1" ? -1 : 1; - } - tool.side = values["side"] == "alt" ? 0 : values["side"] == "left" ? -1 : 1; - tool.dirty = true; - hekto_update(); - auto const missing{std::count_if(tool.posts.begin(), tool.posts.end(), [](hekto_post const &Post) { return Post.standing < 0; })}; - WriteLog(format("SELFTEST hekto: km %.3f growth %d, %zu posts along %.0f m, %d to place, %zu standing. %s", tool.km, tool.growth, tool.posts.size(), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, static_cast(missing), tool.found.size(), tool.status.c_str())); - for (std::size_t i = 0; i < tool.posts.size() && i < 4; ++i) - { - auto const &post{tool.posts[i]}; - double lateral; - glm::dvec3 const axis{editor_track::sampled_position(tool.samples, post.chainage)}; - auto const direction{editor_track::sampled_position(tool.samples, post.chainage + 1.0) - axis}; - glm::dvec2 const offset{post.position.x - axis.x, post.position.z - axis.z}; - lateral = glm::length(offset); - auto const right{(direction.x * offset.y - direction.z * offset.x) * tool.growth >= 0.0}; - WriteLog(format("SELFTEST hekto post %d.%d at %.1f m, %.2f m from the axis, %s of the growing kilometrage, at %.1f %.1f %.1f", post.hectometres / 10, post.hectometres % 10, post.chainage, lateral, right ? "right" : "left", post.position.x, post.position.y, - post.position.z)); - } - if (values["place"] == "1") - { - hekto_place(); - WriteLog("SELFTEST hekto: " + tool.status); - } - return selftest_result::handled; - } - if (command == "spread") - { - std::string name; - words >> name >> m_spread.stop >> m_spread.reach; - if (auto *track{simulation::Paths.find(name)}) - { - std::string names; - for (auto const *other : track_spread_from(*track)) - names += ' ' + other->name(); - WriteLog("SELFTEST spread " + name + ":" + names + " | prev " + (track->trPrev ? track->trPrev->name() : std::string{"-"}) + " next " + (track->trNext ? track->trNext->name() : std::string{"-"})); - } - return selftest_result::handled; - } - if (command == "turntable") - { - glm::dvec3 centre{0.0}; - double yaw{0.0}; - words >> centre.x >> centre.y >> centre.z >> yaw; - turntable_place(centre, yaw); - for (double angle; words >> angle;) - { - m_turntable.heading = m_turntable.table_yaw + angle; - m_turntable.exit_length = 10.0; - track_intent intent; - intent.what = track_intent::kind::turntable_exit; - turntable_press(intent); - } - WriteLog("SELFTEST turntable: " + (m_turntable.error.empty() ? m_turntable.status : m_turntable.error)); - return selftest_result::handled; - } - if (command == "bend" || command == "bendset") - { - std::map values; - std::string pair; - while (words >> pair) - { - auto const equals{pair.find('=')}; - if (equals != std::string::npos) - values[pair.substr(0, equals)] = pair.substr(equals + 1); - } - auto const vector = [](std::string Text) { - std::replace(Text.begin(), Text.end(), ',', ' '); - std::istringstream numbers(Text); - glm::dvec3 result{0.0}; - numbers >> result.x >> result.y >> result.z; - return result; - }; - auto const index = [](std::string const &Text, std::vector const &Names) { - return static_cast(std::find(Names.begin(), Names.end(), Text) - Names.begin()); - }; - auto &tool{m_bend}; - auto const along{values.count("along") ? index(values["along"], {"start", "origin", "middle", "end", "point"}) : tool.along_anchor}; - auto const side{values.count("side") ? index(values["side"], {"origin", "near", "far", "middle", "point"}) : tool.side_anchor}; - auto const height{values.count("height") ? index(values["height"], {"origin", "bottom", "top", "edge", "point"}) : tool.height_anchor}; - auto const point{values.count("point") ? vector(values["point"]) : tool.point}; - if (command == "bend") - { - tool.edited = nullptr; - tool.along_anchor = along; - tool.side_anchor = side; - tool.height_anchor = height; - tool.point = point; - tool.axis = values.count("axis") ? index(values["axis"], {"auto", "z", "x"}) : 0; - tool.turned = values["turned"] == "1"; - bend_source source; - if (false == bend_source_of_selection(source)) - { - std::string reason; - auto const editable{m_node != nullptr && scene::Layers.editable(m_node, &reason)}; - WriteLog(format("SELFTEST bend: nothing to bend (node %p template %d editable %d %s, instance %d)", static_cast(m_node), m_node != nullptr && m_node->from_template() ? 1 : 0, editable ? 1 : 0, reason.c_str(), static_cast(m_instance))); - return selftest_result::handled; - } - tool.tracks_for = nullptr; - bend_find_tracks(glm::dvec3(source.transform[3]), source.model != nullptr ? static_cast(source.model) : reinterpret_cast(static_cast(source.instance))); - tool.track = values.count("track") ? std::stoi(values["track"]) : 0; - if (values.count("pick")) - bend_pick(vector(values["pick"])); - if (tool.tracks.empty()) - { - WriteLog("SELFTEST bend: no track nearby"); - return selftest_result::handled; - } - sweep_node::state state; - std::vector tracks; - bend_definition(source, *tool.tracks[tool.track].first, state, tracks); - WriteLog(format("SELFTEST bend: track %s, %zu paths, anchor before %.3f %.3f %.3f, lateral %.3f height %.3f from %.3f to %.3f, axis %s flip %d; %s", tool.tracks[tool.track].first->name().c_str(), tracks.size(), - tool.marker.x, tool.marker.y, tool.marker.z, state.lateral, state.height, state.from, state.to, state.along_x ? "x" : "z", state.flip ? 1 : 0, tool.status.c_str())); - if (values["go"] == "1") - { - bend_selection(); - WriteLog("SELFTEST bend: " + tool.status); - } - } - else if (auto *sweep{tool.edited}) - { - auto state{sweep->definition()}; - tool.along_anchor = along; - if (side != tool.side_anchor || height != tool.height_anchor || point != tool.point) - { - tool.side_anchor = side; - tool.height_anchor = height; - tool.point = point; - bend_reanchor(state, side, height, point); - } - if (values.count("lateral")) - state.lateral = std::stod(values["lateral"]); - if (values.count("up")) - state.height = std::stod(values["up"]); - if (values.count("widen")) - state.widen = values["widen"] == "1"; - if (values.count("pick")) - { - bend_pick(vector(values["pick"])); - state = sweep->definition(); - } - if (values.count("length")) - { - bend_stretch(*sweep, bend_fixed(*sweep, state.point), std::stod(values["length"]), state.point, state); - } - bend_edit(state); - WriteLog("SELFTEST bendset done"); - } - return selftest_result::handled; - } - if (command == "bendprobe") - { - auto *sweep{m_bend.edited}; - if (sweep == nullptr) - { - WriteLog("SELFTEST bendprobe: nothing bent"); - return selftest_result::handled; - } - glm::dvec3 low; - glm::dvec3 high; - glm::dvec3 shift; - sweep->bounds(low, high, shift); - auto const &state{sweep->definition()}; - auto const marker{bend_marker(*sweep)}; - glm::dvec3 boxlow{std::numeric_limits::max()}; - glm::dvec3 boxhigh{-std::numeric_limits::max()}; - std::size_t vertices{0}; - for (auto const &shape : sweep->create_shapes()) - for (auto const &vertex : shape.data().vertices) - { - boxlow = glm::min(boxlow, vertex.position); - boxhigh = glm::max(boxhigh, vertex.position); - ++vertices; - } - WriteLog(format("SELFTEST bendprobe %s: stations %.3f - %.3f, along the reference line %.3f m, model %.3f m, lateral %.3f height %.3f anchors %d/%d point %.3f %.3f %.3f, marker %.3f %.3f %.3f, box %.2f %.2f %.2f - %.2f %.2f %.2f, %zu vertices, removed %d", - sweep->name().c_str(), sweep->start(), sweep->end(), sweep->distance_at(sweep->end()) - sweep->distance_at(sweep->start()), high.x - low.x, state.lateral, state.height, state.side_anchor, state.height_anchor, state.point.x, - state.point.y, state.point.z, marker.x, marker.y, marker.z, boxlow.x, boxlow.y, boxlow.z, boxhigh.x, boxhigh.y, boxhigh.z, vertices, sweep->m_editorremoved ? 1 : 0)); - return selftest_result::handled; - } - if (command == "lineside") - { - std::string tab; - words >> tab; - show_track_tab(tab == "parallel" ? track_tab::parallel : track_tab::lineside); - (tab == "vehicle" ? m_vehicle.expand : tab == "hekto" ? m_hekto.expand : tab == "fouling" ? m_fouling.expand : m_parallel.expand) = true; - return selftest_result::handled; - } - if (command == "bendpanel") - { - ui()->expand_bend(); - return selftest_result::handled; - } - if (command == "gauge") - { - std::string name; - words >> name; - if (auto *track{simulation::Paths.find(name)}) - { - scan_gauge(*track); - WriteLog(format("SELFTEST gauge %s: %d hits", name.c_str(), static_cast(m_gauge.line.hits.size()))); - for (auto const &hit : m_gauge.line.hits) - WriteLog(format("SELFTEST gauge hit %s depth %.3f at %.2f %.2f %.2f", hit.model.c_str(), hit.depth, hit.point.x, hit.point.y, hit.point.z)); - } - else - WriteLog("SELFTEST gauge " + name + ": no such track"); - return selftest_result::handled; - } - if (command == "ttincludes") - { - std::string name; - words >> name; - auto const *track{simulation::Paths.find(name)}; - WriteLog(format("SELFTEST ttincludes %s: track %s, includes %d", name.c_str(), track == nullptr ? "none" : track->m_editorremoved ? "removed" : "present", - track != nullptr ? static_cast(turntable_includes(*track).size()) : -1)); - return selftest_result::handled; - } - if (command == "checkspans") - { - WriteLog("SELFTEST spans: " + scene::Layers.check_sources()); - return selftest_result::handled; - } - return selftest_result::unhandled; -} - -editor_mode::selftest_result editor_mode::selftest_track(std::string const &command, std::istringstream &words) -{ - if (command == "probe") - { - // edge of the last laid model as it was made: its distance from the axis and the height of its top over the rail head - std::string name; - double step{20.0}; - words >> name >> step; - if (name == "-") - name.clear(); - auto *sweep{name.empty() ? m_sweep.edited : simulation::Sweeps.find(name)}; - if (sweep == nullptr) - { - WriteLog("SELFTEST probe: nothing laid"); - return selftest_result::handled; - } - auto const &definition{sweep->definition()}; - double const sign{definition.lateral >= 0.0 ? 1.0 : -1.0}; - struct bin - { - double edge{std::numeric_limits::max()}; - std::vector> points; // lateral, height over the axis - }; - std::map bins; - for (auto const &shape : sweep->create_shapes()) - for (auto const &vertex : shape.data().vertices) - { - double lateral; - auto const station{sweep->project(vertex.position, lateral)}; - if (lateral * sign <= 0.0) - continue; - auto const axis{sweep->frame_at(station)}; - auto &entry{bins[static_cast(std::floor(station / step))]}; - entry.edge = std::min(entry.edge, std::abs(lateral)); - entry.points.emplace_back(std::abs(lateral), vertex.position.y - axis.position.y); - } - WriteLog(format("SELFTEST probe %s: %zu stretches with the model, %.1f - %.1f m of %.1f m", sweep->name().c_str(), bins.size(), sweep->start(), sweep->end(), sweep->length())); - for (auto const &[index, entry] : bins) - { - auto const station{(index + 0.5) * step}; - if (station < sweep->start() || station > sweep->end()) - continue; - double top{-1000.0}; - for (auto const &point : entry.points) - if (point.first < entry.edge + 0.15) - top = std::max(top, point.second); - auto const at{sweep->frame_at(station)}; - auto const head{0.18 + sweep_node::rail_head(at.roll, definition.lateral)}; - WriteLog(format("SELFTEST probe %s at %.0f m: edge %.3f m from the axis, its top %.3f m over the rail head (cant %.1f deg, gauge widening %.3f m, cant part %.3f m)", sweep->name().c_str(), station, entry.edge, top - head, at.roll, - at.widening[definition.lateral >= 0.0 ? 1 : 0], std::max(0.0, at.cant[definition.lateral >= 0.0 ? 1 : 0]))); - } - return selftest_result::handled; - } - if (command == "profile") - { - std::string name; - double station{0.0}; - words >> name >> station; - auto *sweep{simulation::Sweeps.find(name)}; - if (sweep == nullptr) - { - WriteLog("SELFTEST profile: no such sweep"); - return selftest_result::handled; - } - auto const &definition{sweep->definition()}; - std::map> columns; - for (auto const &shape : sweep->create_shapes()) - for (auto const &vertex : shape.data().vertices) - { - double lateral; - auto const at{sweep->project(vertex.position, lateral)}; - if (std::abs(at - station) > 1.0) - continue; - auto const axis{sweep->frame_at(at)}; - auto const height{vertex.position.y - axis.position.y - 0.18 - sweep_node::rail_head(axis.roll, definition.lateral)}; - auto &column{columns.try_emplace(static_cast(std::floor(lateral / 0.1)), std::numeric_limits::max(), -std::numeric_limits::max()).first->second}; - column.first = std::min(column.first, height); - column.second = std::max(column.second, height); - } - std::string text; - for (auto const &[index, column] : columns) - text += format(" [%.1f %.2f..%.2f]", index * 0.1, column.first, column.second); - WriteLog(format("SELFTEST profile %s at %.0f m:", name.c_str(), station) + text); - return selftest_result::handled; - } - if (command == "tracks") - { - int tab{0}; - words >> tab; - show_track_tab(track_tab::lineside); - m_sweep.expand = tab == 0; - m_hekto.expand = tab == 1; - m_fouling.expand = tab == 2; - m_parallel.expand = tab == 3; - m_vehicle.expand = tab == 4; - return selftest_result::handled; - } - if (command == "curve") - { - sweep_curve(); - for (auto const &piece : m_sweep.curve) - { - auto const &a{piece.points[segment_data::point::start]}; - auto const &b{piece.points[segment_data::point::end]}; - WriteLog(format("SELFTEST curve piece %.1f %.1f -> %.1f %.1f", a.x, a.z, b.x, b.z)); - } - if (false == m_sweep.outline.empty()) - WriteLog(format("SELFTEST curve outline %zu points, %.1f %.1f -> %.1f %.1f, %.1f m", m_sweep.outline.size(), m_sweep.outline.front().x, m_sweep.outline.front().z, m_sweep.outline.back().x, m_sweep.outline.back().z, m_sweep.curve_length)); - return selftest_result::handled; - } - if (command == "lay") - { - std::string name; - std::string side; - words >> name >> side; - auto *track{simulation::Paths.find(name)}; - if (track == nullptr || track->m_paths.empty()) - { - WriteLog("SELFTEST lay: no track " + name); - return selftest_result::pause; - } - auto const &path{track->m_paths.front()}; - auto const atend{side == "end"}; - editor_track::snap_target start; - start.track = track; - start.point = {0, atend ? editor_track::point_kind::end : editor_track::point_kind::start}; - start.position = path.points[atend ? segment_data::point::end : segment_data::point::start]; - auto const &control{path.points[atend ? segment_data::point::control2 : segment_data::point::control1]}; - start.direction = glm::normalize(control != glm::dvec3{} ? control : path.points[atend ? segment_data::point::start : segment_data::point::end] - start.position); - m_lay.points = {start.position}; - m_lay.start = start; - for (glm::dvec3 point{0.0, start.position.y, 0.0}; words >> point.x >> point.z;) - m_lay.points.push_back(point); - lay_finish({}); - WriteLog("SELFTEST lay: " + m_lay.status); - return selftest_result::handled; - } - if (command == "analyse") - { - std::string name; - words >> name; - editor_track::curve curve; - if (auto *track{simulation::Paths.find(name)}; track != nullptr && editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve)) - { - std::string arcs; - for (auto const &arc : curve.arcs) - arcs += format(" R%.0f/%.1fdeg/after %.1fm", arc.radius, glm::degrees(arc.turn), arc.transition); - WriteLog(format("SELFTEST analyse %s: R %.0f, compound %d, in %.1f m, out %.1f m, turn %.1f deg,", name.c_str(), curve.radius, curve.compound ? 1 : 0, curve.transition_in, curve.transition_out, glm::degrees(curve.turn)) + arcs); - } - else - WriteLog("SELFTEST analyse " + name + ": no curve"); - return selftest_result::handled; - } - if (command == "route") - { - glm::dvec3 point{0.0}; - int single{1}; - words >> point.x >> point.z >> single; - TTrack *nearest{nullptr}; - double distance{1e9}; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) - continue; - auto const d{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point)}; - if (d < distance) - { - distance = d; - nearest = track; - } - } - m_track_tab = track_tab::route; - m_route.single = (single != 0); - auto const loaded{nearest != nullptr && route_load(*nearest)}; - WriteLog(format("SELFTEST route: %s from %s, %zu paths, %.3f m, %zu vertices, %d from the design, km %.3f, departed %d by %.3f m, valid %d", loaded ? "loaded" : "failed", nearest != nullptr ? nearest->name().c_str() : "-", - m_route.chain.tracks.size(), m_route.chain.length, m_route.design.vertices.size(), m_route.designed, m_route.origin / 1000.0, m_route.departed, m_route.departure, m_route.result.valid ? 1 : 0)); - WriteLog("SELFTEST route status: " + m_route.status + " " + m_route.error); - for (std::size_t i = 0; i < m_route.design.vertices.size(); ++i) - { - auto const &v{m_route.design.vertices[i]}; - auto text{format("SELFTEST route vertex %zu: at %.3f %.3f offset %.3f R %.3f L %.3f / %.3f cant %.1f compound %d share %.4f", i, v.position.x, v.position.y, v.offset, v.radius, v.transition_in, v.transition_out, v.cant, v.compound ? 1 : 0, v.share)}; - for (auto const &arc : v.arcs) - text += format(" [R %.3f L %.3f share %.4f]", arc.radius, arc.transition, arc.share); - WriteLog(text); - } - return selftest_result::handled; - } - if (command == "routevertex" || command == "routearc" || command == "routeshare") - { - int index{0}; - words >> index; - if (index < 0 || index >= static_cast(m_route.design.vertices.size())) - { - WriteLog("SELFTEST " + command + ": no vertex " + std::to_string(index)); - return selftest_result::handled; - } - auto &vertex{m_route.design.vertices[index]}; - if (command == "routevertex") - words >> vertex.radius >> vertex.transition_in >> vertex.transition_out >> vertex.cant; - else if (command == "routeshare") - words >> vertex.share; - else - { - alignment::compound_arc arc; - words >> arc.radius >> arc.transition >> arc.share; - vertex.compound = true; - vertex.arcs.push_back(arc); - } - route_update(); - std::string errors; - for (auto const &error : m_route.result.errors) - errors += " " + error; - WriteLog(format("SELFTEST %s: valid %d, %zu elements, extension %.3f / %.3f", command.c_str(), m_route.result.valid ? 1 : 0, m_route.result.elements.size(), m_route.result.start_extension, m_route.result.end_extension) + errors); - return selftest_result::handled; - } - if (command == "routekm") - { - double km{0.0}; - words >> km; - m_route.origin = km * 1000.0; - return selftest_result::handled; - } - if (command == "routeapply" || command == "routetake") - { - if (command == "routeapply") - route_apply(); - else - route_take_stored(m_route.departed); - WriteLog("SELFTEST " + command + ": " + m_route.status + " " + m_route.error); - return selftest_result::handled; - } - if (command == "digitizeall") - { - digitize_all(); - WriteLog(format("SELFTEST digitizeall: %zu alignments", alignment_store::records().size())); - return selftest_result::handled; - } - if (command == "alignments") - { - auto const &records{alignment_store::records()}; - for (std::size_t r = 0; r < records.size(); ++r) - { - auto const &entry{records[r]}; - WriteLog(format("SELFTEST alignment %zu in %s: km %.3f, %zu vertices, %zu elements, consistent %d, length %.3f", r, scene::Layers.layer(entry.layer).name.c_str(), entry.origin / 1000.0, entry.design.vertices.size(), entry.elements.size(), entry.consistent ? 1 : 0, entry.result.length)); - for (auto const &e : entry.elements) - WriteLog(format("SELFTEST element %s vertex %d km %.3f L %.3f R %.3f -> %.3f cant %.1f", e.kind.c_str(), e.vertex, (entry.origin + e.chainage) / 1000.0, e.length, e.radius_start, e.radius_end, e.cant)); - } - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) - continue; - auto const found{alignment_store::on_track(*track)}; - std::string text{"SELFTEST path " + track->name() + ": "}; - if (false == found.has_value()) - { - auto const closest{alignment_store::at(geometry::bezier{track->m_paths.front()}.point(0.5), 1000.0)}; - text += closest.has_value() ? format("off the alignments, %.3f m from alignment %d", closest->offset, closest->record) : std::string{"off the alignments"}; - } - else - { - auto const e{alignment_store::describe(records[found->record].result.elements[found->element])}; - text += format("alignment %d element %d %s R %.3f -> %.3f, offset %.4f", found->record, found->element, e.kind.c_str(), e.radius_start, e.radius_end, found->offset); - } - editor_track::curve curve; - if (editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve)) - text += format(" | curve R %.3f L %.3f / %.3f cant %.1f compound %d arcs %zu %s", curve.radius, curve.transition_in, curve.transition_out, curve.cant, curve.compound ? 1 : 0, curve.arcs.size(), curve.designed.has_value() ? "designed" : "recognized"); - WriteLog(text); - } - return selftest_result::handled; - } - if (command == "vprofile") - { - std::string from; - std::string to; - words >> from >> to; - auto *first{simulation::Paths.find(from)}; - auto *last{simulation::Paths.find(to)}; - if (first == nullptr || last == nullptr) - { - WriteLog("SELFTEST vprofile: no such paths"); - return selftest_result::handled; - } - m_profile.origin = 0.0; - profile_open(first, last); - WriteLog(format("SELFTEST vprofile: %zu spans, %.3f m, km of the start %.3f", m_profile.route.spans.size(), m_profile.route.length, m_profile.origin / 1000.0)); - for (auto const &zone : m_profile.context.switches) - 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(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; - words >> name; - auto *track{simulation::Paths.find(name)}; - editor_track::curve curve; - editor_track::chain chain; - std::string error; - if (track == nullptr || false == editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve) || false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) - { - WriteLog("SELFTEST backwardcurve: no curve at " + name); - return selftest_result::handled; - } - std::vector> run; - for (std::size_t i = chain.tracks.size(); i > 0; --i) - run.emplace_back(chain.tracks[i - 1], false == chain.forward[i - 1]); - editor_track::curve backward; - editor_track::analyse_curve(run, m_route.design.norms.gauge, backward); - auto text{format("SELFTEST backwardcurve: %zu paths, R %.3f L %.3f / %.3f cant %.1f compound %d %s", run.size(), backward.radius, backward.transition_in, backward.transition_out, backward.cant, backward.compound ? 1 : 0, backward.designed.has_value() ? "designed" : "recognized")}; - for (auto const &arc : backward.arcs) - text += format(" [R %.3f turn %.4f L %.3f]", arc.radius, arc.turn, arc.transition); - WriteLog(text); - return selftest_result::handled; - } - if (command == "deletetrack" || command == "deletekind") - { - std::string name; - words >> name; - if (command == "deletetrack" && name.empty()) - { - delete_selected_track(); - WriteLog("SELFTEST deletetrack"); - return selftest_result::handled; - } - TTrack *chosen{nullptr}; - if (command == "deletetrack") - chosen = simulation::Paths.find(name); - else - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) - continue; - auto const found{alignment_store::on_track(*track)}; - if (found.has_value() && alignment_store::describe(alignment_store::records()[found->record].result.elements[found->element]).kind == name) - { - chosen = track; - break; - } - } - if (chosen == nullptr || chosen->m_editorremoved) - { - WriteLog("SELFTEST " + command + ": no path " + name); - return selftest_result::handled; - } - m_node = chosen; - ui()->set_node(m_node); - auto const deleted{chosen->name()}; - delete_selected_track(); - WriteLog("SELFTEST " + command + ": " + deleted + " deleted"); - return selftest_result::handled; - } - if (command == "btfcheck") - { - std::string name; - words >> name; - auto const textfile{Global.asCurrentSceneryPath + name + ".txtf"}; - auto const binaryfile{Global.asCurrentSceneryPath + name + ".btf"}; - auto const written{scene::terrain_file::prepare(textfile, binaryfile) == scene::terrain_file::state::binary}; - auto const kept{scene::terrain_file::extra(binaryfile)}; - WriteLog(format("SELFTEST btfcheck: binary %d, %zu bytes of the editor lines kept", written ? 1 : 0, kept.size())); - std::istringstream lines{kept}; - std::string line; - while (std::getline(lines, line)) - WriteLog("SELFTEST btf kept: " + line); - return selftest_result::handled; - } - if (command == "pathsnear") - { - glm::dvec3 point{0.0}; - double radius{100.0}; - words >> point.x >> point.z >> radius; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) - continue; - for (std::size_t i = 0; i < track->m_paths.size(); ++i) - { - auto const &path{track->m_paths[i]}; - auto const &a{path.points[segment_data::point::start]}; - auto const &b{path.points[segment_data::point::end]}; - if (geometry::plan_distance(a, point) > radius && geometry::plan_distance(b, point) > radius) - continue; - auto const &c1{path.points[segment_data::point::control1]}; - auto const &c2{path.points[segment_data::point::control2]}; - WriteLog(format("SELFTEST path %s/%zu %s: %.3f %.3f -> %.3f %.3f, c1 %.3f %.3f, c2 %.3f %.3f, %.3f m", track->name().c_str(), i, track->eType == tt_Switch ? "switch" : "normal", a.x, a.z, b.x, b.z, c1.x, c1.z, c2.x, c2.z, geometry::bezier{path}.plan_length())); - } - } - return selftest_result::handled; - } - if (command == "splitkind" || command == "nudgekind") - { - std::string kind; - glm::dvec3 offset{0.0}; - words >> kind >> offset.x >> offset.z; - auto const &records{alignment_store::records()}; - TTrack *chosen{nullptr}; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) - continue; - auto const found{alignment_store::on_track(*track)}; - if (found.has_value() && alignment_store::describe(records[found->record].result.elements[found->element]).kind == kind) - { - chosen = track; - break; - } - } - if (chosen == nullptr) - { - WriteLog("SELFTEST " + command + ": no path over " + kind); - return selftest_result::handled; - } - if (command == "splitkind") - { - auto const before{editor_track::capture(*chosen)}; - auto *created{editor_track::split_path(*chosen, 0.5)}; - if (created != nullptr) - push_track_snapshot({{chosen, before}}, {created}); - WriteLog("SELFTEST splitkind: " + chosen->name() + " split, the second half " + (created != nullptr ? created->name() : std::string{"-"})); - } - else - { - std::vector> states{{chosen, editor_track::capture(*chosen)}}; - std::vector moved{chosen}; - auto const &path{chosen->m_paths.front()}; - for (auto const &end : {path.points[segment_data::point::start], path.points[segment_data::point::end]}) - for (auto *neighbour : {chosen->trPrev, chosen->trNext}) - { - if (neighbour == nullptr || neighbour->m_paths.empty() || std::find(moved.begin(), moved.end(), neighbour) != moved.end()) - continue; - auto const &other{neighbour->m_paths.front()}; - for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end}) - if (glm::distance(other.points[kind == editor_track::point_kind::start ? segment_data::point::start : segment_data::point::end], end) < 0.01) - { - states.emplace_back(neighbour, editor_track::capture(*neighbour)); - editor_track::move_point(*neighbour, {0, kind}, end + offset); - moved.push_back(neighbour); - break; - } - } - editor_track::translate(*chosen, offset); - editor_track::commit(moved); - push_track_snapshot(std::move(states), {}); - WriteLog("SELFTEST nudgekind: " + chosen->name() + " moved with " + std::to_string(moved.size() - 1) + " joined ends"); - } - return selftest_result::handled; - } - if (command == "digitize") - { - glm::dvec3 point{0.0}; - words >> point.x >> point.z; - TTrack *nearest{nullptr}; - double distance{1e9}; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) - continue; - auto const d{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point)}; - if (d < distance) - { - distance = d; - nearest = track; - } - } - auto const kept{nearest != nullptr && digitize_drawn({nearest}, false)}; - WriteLog(format("SELFTEST digitize %s: %d", nearest != nullptr ? nearest->name().c_str() : "-", kept ? 1 : 0)); - return selftest_result::handled; - } - if (command == "parallel") - { - words >> m_parallel.spacing >> m_parallel.side; - m_parallel.scope = 1; - m_parallel.built_for = nullptr; - parallel_build(); - WriteLog("SELFTEST parallel: " + m_parallel.status + " " + m_parallel.error); - return selftest_result::handled; - } - return selftest_result::unhandled; -} - -editor_mode::selftest_result editor_mode::selftest_switch(std::string const &command, std::istringstream &words) -{ - if (command == "curvedswitch") - { - std::string name; - std::string label; - 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); - arm_switch(); - auto &tool{m_switch}; - for (int i = 0; i < static_cast(tool.templates.size()); ++i) - if (false == label.empty() && tool.templates[i].label.find(label) != std::string::npos) - { - tool.armed = i; - break; - } - m_cursor_override = at; - auto const started{start_switch_placement()}; - m_cursor_override.reset(); - if (false == started) - { - WriteLog("SELFTEST curvedswitch: not started, " + tool.status); - return selftest_result::handled; - } - tool.mouse = glm::dvec3{toward.x, tool.point.y, toward.z}; - finish_switch_placement(); - WriteLog("SELFTEST curvedswitch " + (tool.armed >= 0 ? tool.templates[tool.armed].label : std::string{}) + ": " + tool.status); - return selftest_result::handled; - } - if (command == "designer") - { - std::string list; - int index{0}; - words >> list >> index; - auto &designer{m_switch_designer}; - designer.open = list != "close"; - designer.from_catalogue = list != "own"; - designer.selected = index; - return selftest_result::handled; - } - if (command == "release") - { - m_switch.placing = false; - lay_cancel(); - m_cursor_override.reset(); - return selftest_result::handled; - } - if (command == "hover") - { - glm::dvec3 at{0.0}; - if (words >> at.x >> at.z) - m_cursor_override = at; - else - m_cursor_override.reset(); - return selftest_result::handled; - } - if (command == "layhold") - { - int layout{0}; - words >> layout >> m_lay.radius; - m_track_layout = static_cast(std::clamp(layout, 0, 3)); - m_lay.active = true; - lay_cancel(); - std::vector points; - for (glm::dvec3 point{0.0}; words >> point.x >> point.z;) - points.push_back(point); - for (std::size_t i = 0; i + 1 < points.size(); ++i) - { - auto point{points[i]}; - point.y = 5000.0; - std::vector found; - auto const ground{ground_heights({point}, true, &found)}; - point.y = found[0] != 0 ? ground[0] : m_lay.points.empty() ? 0.0 : m_lay.points.back().y; - m_lay.mouse_snap = editor_track::find_free_end(nullptr, 1, point, 3.0, {}); - m_lay.mouse = m_lay.mouse_snap.track != nullptr ? point : lay_bed(point); - lay_click(); - } - if (false == points.empty()) - m_cursor_override = points.back(); - WriteLog("SELFTEST layhold: " + m_lay.status); - return selftest_result::handled; - } - if (command == "switchkind") - { - // what the switch tool places, as chosen in its panel: single, slip, crossing or connection with the kind of it - std::string word; - int sub{0}; - words >> word >> sub; - auto const kind{word == "slip" ? switch_kind::slip : word == "crossing" ? switch_kind::crossing : word == "connection" ? switch_kind::connection : switch_kind::single}; - m_switch.connection_kind = sub; - choose_switch_kind(kind); - WriteLog(format("SELFTEST switchkind %s: kind %d, connection %d, armed %d", word.c_str(), static_cast(current_switch_kind()), m_switch.connection, m_switch.armed)); - return selftest_result::handled; - } - if (command == "connection" || command == "connectionplan" || command == "connectionhold") - { - std::string kindword; - std::string label; - std::string second; - glm::dvec3 at{0.0}; - glm::dvec3 toward{0.0}; - words >> kindword >> at.x >> at.z >> toward.x >> toward.z >> label >> second; - connection::kind kind{connection::kind::crossover}; - if (kindword != "single" && false == connection::parse(kindword, kind)) - { - WriteLog("SELFTEST connection: unknown kind " + kindword); - return selftest_result::handled; - } - auto &tool{m_switch}; - for (std::string key; words >> key;) - { - if (key == "radius") - words >> tool.connection_radius; - else if (key == "gap") - words >> tool.connection_gap; - else if (key == "together") - words >> tool.connection_together; - else if (key == "stub") - words >> tool.connection_stub; - else if (key == "spacings") - { - std::string value; - words >> value; - std::snprintf(tool.connection_spacings, sizeof(tool.connection_spacings), "%s", value.c_str()); - } - } - show_track_tab(track_tab::turnout); - arm_switch(); - auto const find = [&](std::string Label) { - std::replace(Label.begin(), Label.end(), '_', ' '); - for (int i = 0; i < static_cast(tool.templates.size()); ++i) - if (false == Label.empty() && Label != "-" && false == tool.templates[i].double_slip && false == tool.templates[i].twofold() && tool.templates[i].label.find(Label) != std::string::npos) - return i; - return -1; - }; - auto const first{find(label)}; - if (first < 0) - { - WriteLog("SELFTEST connection: no template " + label); - return selftest_result::handled; - } - tool.armed = first; - tool.connection_second = find(second); - tool.connection = kindword == "single" ? -1 : static_cast(kind); - auto const line{straight_near(at, {}, 3.0)}; - if (line.tracks.empty()) - { - WriteLog("SELFTEST connection: no straight at the start"); - return selftest_result::handled; - } - m_node = line.tracks.front(); - ui()->set_node(m_node); - m_cursor_override = at; - auto const started{start_switch_placement()}; - m_cursor_override.reset(); - if (false == started) - { - WriteLog("SELFTEST connection: not started, " + tool.status); - return selftest_result::handled; - } - tool.mouse = glm::dvec3{toward.x, tool.point.y, toward.z}; - if (command == "connectionhold") - { - m_cursor_override = toward; - WriteLog("SELFTEST connectionhold: " + tool.status); - return selftest_result::handled; - } - connection::request request; - connection::result plan; - auto const valid{plan_connection(request, plan)}; - WriteLog("SELFTEST connection " + kindword + ": " + (valid ? connection_readout(plan) : "invalid, " + plan.error)); - for (auto const &entry : plan.switches) - { - auto const &start{entry.paths[0].points[segment_data::point::start]}; - auto const &end{entry.paths[1].points[segment_data::point::end]}; - WriteLog(format("SELFTEST connection switch on %d at %.3f, forward %d side %d%s: %.3f %.3f %.3f -> branch end %.3f %.3f %.3f", entry.line, entry.along, entry.forward, entry.side, entry.second ? " (second)" : "", start.x, start.y, start.z, end.x, end.y, - end.z)); - } - for (auto const &insert : plan.inserts) - { - auto const &start{insert.points[segment_data::point::start]}; - auto const &end{insert.points[segment_data::point::end]}; - WriteLog(format("SELFTEST connection insert %.3f %.3f %.3f -> %.3f %.3f %.3f, %.3f m, R %.1f", start.x, start.y, start.z, end.x, end.y, end.z, geometry::bezier{insert}.plan_length(), std::abs(insert.radius))); - } - if (command == "connectionplan" || false == valid) - { - tool.placing = false; - return selftest_result::handled; - } - insert_connection(); - WriteLog("SELFTEST connection built: " + tool.status); - for (auto const &entry : switch_store::records()) - if (entry.kind == "connection") - { - std::string parts; - for (auto const &part : entry.parts) - parts += ' ' + part; - WriteLog(format("SELFTEST connection record %s %s: spacing %.3f, insert %.3f, parts%s", entry.type.c_str(), entry.name.c_str(), entry.a, entry.b, parts.c_str())); - } - return selftest_result::handled; - } - if (command == "conecheck") - { - std::string name; - words >> name; - auto *track{simulation::Paths.find(name)}; - if (track == nullptr || track->m_paths.size() < 2) - { - WriteLog("SELFTEST conecheck: no switch " + name); - return selftest_result::handled; - } - geometry::bezier const main{track->m_paths[0]}; - geometry::bezier const other{track->m_paths[1]}; - std::string text; - for (int i = 1; i <= 8; ++i) - { - auto const t{i * 0.125}; - auto const point{other.point(t)}; - double best{std::numeric_limits::max()}; - double along{0.0}; - for (int k = 0; k <= 2000; ++k) - if (auto const distance{geometry::plan_distance(main.point(k / 2000.0), point)}; distance < best) - { - best = distance; - along = k / 2000.0; - } - auto const base{main.point(along)}; - auto const direction{glm::normalize(geometry::plan_of(main.first(along)))}; - auto const lateral{(point.x - base.x) * -direction.y + (point.z - base.z) * direction.x}; - auto const &path{track->m_paths[0]}; - auto const roll{glm::radians(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * along)}; - auto const tangent{main.first(along)}; - auto const beyond{(point.x - base.x) * direction.x + (point.z - base.z) * direction.y}; - auto const cone{base.y + beyond * tangent.y / glm::length(geometry::plan_of(tangent)) - lateral * std::tan(roll)}; - text += format(" [t %.3f off %.3f dy %.4f]", t, lateral, point.y - cone); - } - 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}; - int side{0}; - words >> index >> side; - scan_switch_drives(); - m_switch.drive = index; - m_switch.drive_side = side; - WriteLog("SELFTEST switchdrive: " + (index >= 0 && index < static_cast(m_switch.drives.size()) ? m_switch.drives[index].name : std::string{"none"})); - return selftest_result::handled; - } - if (command == "drivechoice") - { - int index{-1}; - int side{0}; - words >> index >> side; - scan_switch_drives(); - m_switch.drive = index; - m_switch.drive_side = side; - m_switch.status.clear(); - auto *track{selected_track()}; - if (track != nullptr && track->eType == tt_Switch && false == track->m_editorremoved) - apply_switch_drive_choice(*track); - WriteLog("SELFTEST drivechoice: " + (track != nullptr ? track->name() : std::string{"no switch"}) + ", " + m_switch.status); - return selftest_result::handled; - } - if (command == "drives") - { - std::string name; - words >> name; - scan_switch_drives(); - std::string files; - for (auto const &standing : standing_templates([](std::string const &File) { return File.find("napedy/") != std::string::npos; })) - if (false == standing.values.empty() && standing.values[0] == name) - { - files += " " + standing.file; - for (std::size_t i = 1; i < standing.values.size(); ++i) - files += (i == 1 ? " (" : " ") + standing.values[i] + (i + 1 == standing.values.size() ? ")" : ""); - } - for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) - if (scene::Layers.tracked(handle) && scene::Layers.instance(handle).file != nullptr && scene::Layers.instance(handle).file->find("napedy") != std::string::npos) - files += format(" [%d %s removed %d dead %d]", static_cast(handle), scene::Layers.instance(handle).file->c_str(), scene::Layers.instance(handle).removed ? 1 : 0, scene::Layers.instance(handle).dead ? 1 : 0); - WriteLog("SELFTEST drives " + name + ": chosen " + (m_switch.drive >= 0 ? m_switch.drives[m_switch.drive].name : std::string{"none"}) + ", side " + std::to_string(m_switch.drive_side) + ", standing" + files + ", steps " + std::to_string(m_history.size())); - return selftest_result::handled; - } - if (command == "driveflip") - { - std::string name; - words >> name; - auto *track{simulation::Paths.find(name)}; - auto const done{track != nullptr && flip_switch_drive(*track)}; - WriteLog("SELFTEST driveflip " + name + ": " + (done ? "done " : "failed ") + m_switch.status); - return selftest_result::handled; - } - if (command == "driveput" || command == "drivefamily") - { - std::string name; - int family{-1}; - words >> name >> family; - auto *track{simulation::Paths.find(name)}; - auto const done{track != nullptr && (command == "driveput" ? place_switch_drive(*track) : replace_switch_drive(*track, family, false))}; - WriteLog("SELFTEST " + command + " " + name + ": " + (done ? "done " : "failed ") + m_switch.status); - return selftest_result::handled; - } - if (command == "drivetilt") - { - std::string name; - words >> name; - auto *model{simulation::Instances.find(name)}; - auto *track{simulation::Paths.find(name.substr(0, name.find('_')))}; - if (model == nullptr || track == nullptr) - { - WriteLog("SELFTEST drivetilt: no " + name); - return selftest_result::handled; - } - auto const &path{track->m_paths[0]}; - auto const transform{glm::dmat3(model->rotation_scale())}; - auto const across{transform * glm::dvec3{1.0, 0.0, 0.0}}; - auto const forward{transform * glm::dvec3{0.0, 0.0, 1.0}}; - auto const direction{glm::normalize(glm::dvec2{forward.x, forward.z})}; - auto const lateral{across.x * -direction.y + across.z * direction.x}; - auto const cone{-lateral * std::tan(glm::radians(static_cast(path.rolls[0])))}; - auto const control{path.points[segment_data::point::control1]}; - auto const grade{control.y / std::hypot(control.x, control.z)}; - WriteLog(format("SELFTEST drivetilt %s: angles %.3f %.3f %.3f, +x rises %.4f (cone %.4f), +z rises %.4f (grade %.4f), origin %.3f %.3f %.3f, points %.3f", name.c_str(), model->Angles().x, model->Angles().y, model->Angles().z, across.y, cone, forward.y, grade, - model->location().x, model->location().y, model->location().z, path.points[segment_data::point::start].y)); - return selftest_result::handled; - } - if (command == "switchplace" || command == "switchcurve" || command == "slipplace") - { - std::string label; - glm::dvec3 point{0.0}; - int direction{1}; - int side{1}; - words >> label >> point.x >> point.z >> direction >> side; - std::replace(label.begin(), label.end(), '_', ' '); - auto &tool{m_switch}; - tool.templates = editor_track::standard_switch_templates(); - tool.collected = true; - tool.armed = -1; - for (int i = 0; i < static_cast(tool.templates.size()); ++i) - if (tool.templates[i].label.rfind(label, 0) == 0) - { - tool.armed = i; - break; - } - TTrack *nearest{nullptr}; - double distance{1e9}; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) - continue; - auto const d{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point)}; - if (d < distance) - { - distance = d; - nearest = track; - } - } - if (tool.armed < 0 || nearest == nullptr) - { - WriteLog("SELFTEST " + command + ": no template " + label + " or no path"); - return selftest_result::handled; - } - tool.status.clear(); - if (command == "switchcurve") - { - editor_track::curve curve; - editor_track::chain chain; - std::string error; - if (false == editor_track::find_curve(*nearest, m_straights.tolerance, m_route.design.norms.gauge, curve) || false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) - { - WriteLog("SELFTEST switchcurve: no curve"); - return selftest_result::handled; - } - tool.frame = sample_chain(chain); - tool.frame_tracks = chain.tracks; - tool.curved = true; - tool.along = nearest_station(tool.frame, point); - insert_curved_switch(direction, side); - } - else - { - auto const line{editor_track::find_straight(*nearest, m_straights.tolerance)}; - if (command == "slipplace") - { - WriteLog(format("SELFTEST slipplace: straight from %s, %zu paths, start %.3f %.3f, %.3f m", nearest->name().c_str(), line.tracks.size(), line.start.x, line.start.z, line.length)); - insert_double_slip(line, point, tool.templates[tool.armed]); - } - else - insert_switch(line, std::clamp(line.along(point), 0.0, line.length), direction, side); - } - WriteLog("SELFTEST " + command + ": " + tool.status + " " + m_straights.status); - return selftest_result::handled; - } - if (command == "switchlink" || command == "branchlink") - { - std::string label; - glm::dvec3 point{0.0}; - glm::dvec3 target{0.0}; - if (command == "switchlink") - words >> label; - words >> point.x >> point.z >> target.x >> target.z; - std::replace(label.begin(), label.end(), '_', ' '); - auto const log_link = [&](editor_track::branch_link const &Link, std::string const &Reason) { - WriteLog(format("SELFTEST %s plan: valid %d, R %.1f m, before %.3f m, after %.3f m, trim %.3f m, shortfall %.3f m, %zu pieces%s", command.c_str(), Link.valid ? 1 : 0, Link.radius, Link.before, Link.after, Link.trim, - Link.shortfall, Link.pieces.size(), Reason.empty() ? "" : (", " + Reason).c_str())); - for (auto const &piece : Link.pieces) - { - auto const &a{piece.points[segment_data::point::start]}; - auto const &b{piece.points[segment_data::point::end]}; - WriteLog(format("SELFTEST %s piece: %.3f %.3f %.3f -> %.3f %.3f %.3f, L %.3f m, R %.1f m", command.c_str(), a.x, a.y, a.z, b.x, b.y, b.z, geometry::bezier{piece}.plan_length(), static_cast(piece.radius))); - } - }; - if (command == "switchlink") - { - show_track_tab(track_tab::turnout); - auto &tool{m_switch}; - tool.templates = editor_track::standard_switch_templates(); - tool.collected = true; - tool.armed = -1; - tool.connection = -1; - for (int i = 0; i < static_cast(tool.templates.size()); ++i) - if (tool.templates[i].label.rfind(label, 0) == 0) - { - tool.armed = i; - break; - } - TTrack *nearest{nullptr}; - double distance{1e9}; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) - continue; - auto const d{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point)}; - if (d < distance) - { - distance = d; - nearest = track; - } - } - if (tool.armed < 0 || nearest == nullptr) - { - WriteLog("SELFTEST switchlink: no template " + label + " or no path"); - return selftest_result::handled; - } - m_node = nearest; - ui()->set_node(m_node); - m_cursor_override = point; - if (false == start_switch_placement()) - { - m_cursor_override.reset(); - WriteLog("SELFTEST switchlink: not started, " + tool.status); - return selftest_result::handled; - } - tool.mouse = glm::dvec3{target.x, tool.point.y, target.z}; - switch_link plan; - plan_switch_link(plan); - log_link(plan.link, plan.reason); - finish_switch_placement(); - m_cursor_override.reset(); - WriteLog("SELFTEST switchlink: " + tool.status); - return selftest_result::handled; - } - TTrack *own{nullptr}; - for (auto *track : simulation::Paths.sequence()) - if (track != nullptr && false == track->m_editorremoved && track->eType == tt_Switch && track->m_paths.size() == 2 && geometry::plan_distance(track->m_paths[1].points[segment_data::point::end], point) < 3.0) - own = track; - if (own == nullptr) - { - WriteLog("SELFTEST branchlink: no switch branch end there"); - return selftest_result::handled; - } - editor_track::snap_target end; - end.track = own; - end.point = {1, editor_track::point_kind::end}; - end.position = own->m_paths[1].points[segment_data::point::end]; - if (false == start_extend(end)) - { - WriteLog("SELFTEST branchlink: the branch end isn't free"); - return selftest_result::handled; - } - m_extend.pressed = {-10000.f, -10000.f}; - m_extend.mouse = glm::dvec3{target.x, m_extend.point.y, target.z}; - m_extend.snap = editor_track::find_free_end(own, own->iCategoryFlag & 15, m_extend.mouse, 3.0, {own}); - editor_track::branch_link link; - std::string reason; - plan_branch_link(link, reason); - log_link(link, reason); - finish_extend(); - WriteLog(format("SELFTEST branchlink: switch now %.3f %.3f -> %.3f %.3f", own->m_paths[0].points[segment_data::point::start].x, own->m_paths[0].points[segment_data::point::start].z, own->m_paths[0].points[segment_data::point::end].x, - own->m_paths[0].points[segment_data::point::end].z)); - return selftest_result::handled; - } - if (command == "switchslide" || command == "switchreplace") - { - std::string name; - words >> name; - auto *track{simulation::Paths.find(name)}; - if (track == nullptr || track->eType != tt_Switch || track->m_paths.size() < 2) - { - WriteLog("SELFTEST " + command + ": no switch " + name); - return selftest_result::handled; - } - if (command == "switchslide") - { - double distance{0.0}; - words >> distance; - double low{0.0}; - double high{0.0}; - std::string reason; - auto const set{double_of(*track)}; - double length{0.0}; - auto const room{set.has_value() ? double_room(*set, low, high, length, reason) : switch_room(*track, low, high, reason)}; - auto const done{slide_switch(*track, distance)}; - WriteLog(format("SELFTEST switchslide %s %.3f: room %d %.3f .. %.3f, done %d, %s", name.c_str(), distance, room ? 1 : 0, low, high, done ? 1 : 0, m_switch.status.c_str())); - return selftest_result::handled; - } - std::string label; - int side{1}; - words >> label >> side; - std::replace(label.begin(), label.end(), '_', ' '); - auto const templates{editor_track::standard_switch_templates()}; - auto const found{std::find_if(templates.begin(), templates.end(), [&](editor_track::switch_template const &Template) { return Template.label.rfind(label, 0) == 0; })}; - if (found == templates.end()) - { - WriteLog("SELFTEST switchreplace: no template " + label); - return selftest_result::handled; - } - auto const set{double_of(*track)}; - if (set.has_value()) - track = set->first; - auto const &main{track->m_paths[0]}; - auto const origin{main.points[segment_data::point::start]}; - auto const chord{geometry::plan_of(main.points[segment_data::point::end] - origin)}; - auto const length{glm::length(chord)}; - auto paths{editor_track::place_switch(*found, origin, chord / length, side, (main.points[segment_data::point::end].y - origin.y) / length)}; - std::string status; - auto const done{set.has_value() ? found->twofold() && paths.size() >= 5 && move_double_paths(*set, paths, status) : paths.size() >= 2 && move_switch_paths(*track, paths, status)}; - if (done && set.has_value()) - { - record_double(*set->first, *set->second, *found); - switch_store::follow(); - records_to_history(); - straight_refresh(); - } - else if (done) - { - record_switch(*track, *found); - records_to_history(); - straight_refresh(); - } - WriteLog(format("SELFTEST switchreplace %s %s: done %d, %s", name.c_str(), found->label.c_str(), done ? 1 : 0, status.c_str())); - return selftest_result::handled; - } - if (command == "switchpieces" || command == "switchbreak") - { - double radius{0.0}; - double ratio{0.0}; - std::string name; - if (command == "switchpieces") - words >> radius >> ratio; - words >> name; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->m_paths.size() < 2 || track->SwitchExtension == nullptr) - continue; - if (false == name.empty() ? track->name() != name : track->m_paths[1].pieces.empty()) - continue; - if (command == "switchbreak") - { - auto &pieces{track->m_paths[1].pieces}; - if (pieces.size() < 2) - continue; - pieces[0].points[segment_data::point::end].x += 0.3; - editor_track::commit({track}); - WriteLog(format("SELFTEST switchbreak %s: path 2 %.3f m, %zu pieces in use, %zu kept", track->name().c_str(), track->SwitchExtension->Segments[1]->GetLength(), track->SwitchExtension->Segments[1]->PieceCount(), pieces.size())); - return selftest_result::handled; - } - auto const &segment{*track->SwitchExtension->Segments[1]}; - auto const &main{track->m_paths[0]}; - auto const &branch{track->m_paths[1]}; - auto const origin{segment.FastGetPoint(0.0)}; - auto const plan{glm::normalize(geometry::plan_of(main.points[segment_data::point::end] - main.points[segment_data::point::start]))}; - glm::dvec3 const along{plan.x, 0.0, plan.y}; - auto const reach{branch.points[segment_data::point::end] - branch.points[segment_data::point::start]}; - auto const hand{along.x * reach.z - along.z * reach.x > 0.0 ? 1.0 : -1.0}; - auto const normal{glm::dvec3{-along.z, 0.0, along.x} * hand}; - auto const centre{origin + normal * radius}; - auto const angle{std::atan(1.0 / ratio)}; - auto const arc{radius * angle}; - auto const heading0{-std::atan2(along.x, along.z)}; - glm::dvec3 position; - glm::vec3 rotation; - double worst{0.0}; - for (int i = 0; i <= 10; ++i) - { - segment.RaPositionGet(arc * i / 10.0, position, rotation); - worst = std::max(worst, std::abs(geometry::plan_distance(position, centre) - radius)); - } - segment.RaPositionGet(arc, position, rotation); - auto const expected{centre - normal * radius * std::cos(angle) + along * radius * std::sin(angle)}; - auto const turn{std::abs(std::remainder(rotation.z - heading0, glm::two_pi()))}; - double low{1e9}; - double high{-1e9}; - for (int i = 0; i <= 10; ++i) - { - segment.RaPositionGet(arc + 0.05 + (segment.GetLength() - arc - 0.1) * i / 10.0, position, rotation); - low = std::min(low, static_cast(rotation.z)); - high = std::max(high, static_cast(rotation.z)); - } - segment.RaPositionGet(arc, position, rotation); - WriteLog(format("SELFTEST switchpieces %s: path 2 %.3f m in %zu pieces (%zu kept), arc %.3f m, arc end off %.4f m, turn there %.5f (angle %.5f), axis off the arc %.4f m, straight heading spread %.6f, end %.3f %.3f", track->name().c_str(), - segment.GetLength(), segment.PieceCount(), branch.pieces.size(), arc, geometry::plan_distance(position, expected), turn, angle, worst, high - low, segment.FastGetPoint(1.0).x, segment.FastGetPoint(1.0).z)); - } - return selftest_result::handled; - } - if (command == "switchlinks") - { - auto const name_of = [](TTrack const *Track) { return Track == nullptr ? std::string{"-"} : Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->SwitchExtension == nullptr) - continue; - auto const &extension{*track->SwitchExtension}; - WriteLog(format("SELFTEST switchlinks %s: prev %s / %s, next %s / %s, lengths %.3f / %.3f m", track->name().c_str(), name_of(extension.pPrevs[0]).c_str(), name_of(extension.pPrevs[1]).c_str(), name_of(extension.pNexts[0]).c_str(), name_of(extension.pNexts[1]).c_str(), - extension.Segments[0]->GetLength(), extension.Segments[1]->GetLength())); - } - return selftest_result::handled; - } - if (command == "switchtemplates") - { - for (auto const &entry : editor_track::standard_switch_templates()) - { - if (entry.double_slip) - continue; - auto const paths{editor_track::place_switch(entry, glm::dvec3{0.0}, glm::dvec2{0.0, 1.0}, 1, 0.0)}; - double gap{0.0}; - double kink{0.0}; - std::size_t pieces{0}; - for (auto const &path : paths) - { - pieces += path.pieces.size(); - for (std::size_t i = 0; i + 1 < path.pieces.size(); ++i) - { - auto const &one{path.pieces[i]}; - auto const &two{path.pieces[i + 1]}; - gap = std::max(gap, glm::distance(one.points[segment_data::point::end], two.points[segment_data::point::start])); - geometry::bezier const a{one}; - geometry::bezier const b{two}; - auto const ta{glm::normalize(geometry::plan_of(a.first(1.0)))}; - auto const tb{glm::normalize(geometry::plan_of(b.first(0.0)))}; - kink = std::max(kink, std::abs(geometry::signed_angle(ta, tb))); - } - if (false == path.pieces.empty()) - gap = std::max({gap, glm::distance(path.pieces.front().points[segment_data::point::start], path.points[segment_data::point::start]), glm::distance(path.pieces.back().points[segment_data::point::end], path.points[segment_data::point::end])}); - } - std::string ends; - for (auto const &path : paths) - ends += format(" (%.3f %.3f)", path.points[segment_data::point::end].x, path.points[segment_data::point::end].z); - std::string starts; - for (std::size_t i = 1; i < paths.size() && i <= 3; i += 2) - { - geometry::bezier const first{paths[i].pieces.empty() ? paths[i] : paths[i].pieces.front()}; - auto const tangent{glm::normalize(geometry::plan_of(first.first(0.0)))}; - starts += format(" %.4f", glm::degrees(std::abs(geometry::signed_angle(glm::dvec2{0.0, 1.0}, tangent)))); - } - WriteLog(format("SELFTEST switchtemplate %s: %s%s, L %.3f 1:%.3f R %.1f a %.3f b %.3f stagger %.3f, %zu paths, %zu pieces, gap %.6f m, kink %.6f deg, start deg%s, ends%s", entry.label.c_str(), entry.type.c_str(), entry.own ? " own" : "", entry.length, entry.ratio, entry.radius, entry.a, entry.b, entry.stagger, paths.size(), pieces, gap, - glm::degrees(kink), starts.c_str(), ends.c_str())); - } - return selftest_result::handled; - } - if (command == "switches") - { - for (auto const &entry : switch_store::records()) - { - std::string parts; - for (auto const &part : entry.parts) - parts += part + " "; - WriteLog(format("SELFTEST switch record %s %s: %s, R %.3f 1:%.3f hand %c a %.3f b %.3f main R %.3f, branch R %.3f, km %s, number %s, %s, parts %s", entry.kind.c_str(), entry.name.c_str(), switch_store::label(entry).c_str(), entry.radius, entry.ratio, entry.hand, entry.a, - entry.b, entry.main_radius, switch_store::branch_radius(entry), entry.km.has_value() ? format("%.3f", *entry.km / 1000.0).c_str() : "-", entry.number.c_str(), entry.source.c_str(), parts.c_str())); - } - speed_check::options options; - options.norms = m_route.design.norms; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->m_paths.size() < 2) - continue; - auto const known{switch_store::of(*track)}; - geometry::bezier const branch{track->m_paths[1]}; - auto const first{branch.first(0.3)}; - auto const second{branch.second(0.3)}; - auto const planar{std::abs(first.x * second.z - first.z * second.x) / std::pow(first.x * first.x + first.z * first.z, 1.5)}; - auto const verdict{speed_check::check(*track, 1, options)}; - WriteLog(format("SELFTEST switch %s: %s, branch R at 0.3 %.3f, path R %.3f, branch speed %.1f km/h at R %.1f", track->name().c_str(), known.has_value() ? switch_store::label(*known).c_str() : "no record", planar > 1e-9 ? 1.0 / planar : 0.0, - track->m_paths[1].radius, verdict.safe, verdict.radius)); - } - return selftest_result::handled; - } - return selftest_result::unhandled; -} - -editor_mode::selftest_result editor_mode::selftest_join(std::string const &command, std::istringstream &words) -{ - if (command == "join" || command == "layto") - { - glm::dvec3 a{0.0}; - glm::dvec3 b{0.0}; - words >> a.x >> a.z >> b.x >> b.z; - auto const from{editor_track::find_free_end(nullptr, 1, a, 3.0, {})}; - extend_tool tool; - if (from.track == nullptr || false == free_end(*from.track, from.point.path, from.point.kind == editor_track::point_kind::end, tool)) - { - WriteLog("SELFTEST " + command + ": no free end at the first point"); - return selftest_result::handled; - } - std::vector pieces; - std::string error; - double length{0.0}; - if (command == "join") - { - double radius{0.0}; - int variant{0}; - int transitions{0}; - words >> radius >> variant >> transitions; - auto const to{editor_track::find_free_end(nullptr, 1, b, 3.0, {from.track})}; - if (to.track == nullptr) - { - WriteLog("SELFTEST join: no free end at the second point"); - return selftest_result::handled; - } - m_join_radius = radius; - m_join_variant = variant; - m_lay.transitions = transitions != 0; - pieces = join_pieces(tool.track, from.point, tool.point, tool.direction, to, length, error); - } - else - { - int layout{0}; - words >> layout >> m_lay.radius; - m_track_layout = static_cast(std::clamp(layout, 0, 3)); - m_extend = tool; - m_extend.active = true; - m_extend.mouse = {b.x, tool.point.y, b.z}; - m_extend.snap = {}; - pieces = extend_pieces(); - m_extend.active = false; - } - std::vector created; - for (auto const &piece : pieces) - created.push_back(editor_track::create_path(*tool.track, piece)); - editor_track::commit(created); - digitize_drawn(created); - if (false == created.empty()) - push_track_snapshot({}, created); - char const *const kinds[]{"none", "straight", "arc", "curve", "reverse curve", "free curve"}; - auto const &join{m_join_last}; - WriteLog(format("SELFTEST %s: %s, %zu paths, R %.1f m, cant %.0f mm, transitions %.1f m, V %.0f km/h, variants %d%s", command.c_str(), kinds[static_cast(join.kind)], pieces.size(), join.radius, join.cant, - join.transition, join.speed, join.variants, error.empty() ? "" : (", " + error).c_str())); - auto heading{tool.direction}; - glm::dvec3 previous{tool.point}; - for (auto const *track : created) - { - auto const &path{track->m_paths.front()}; - geometry::bezier const curve{path}; - auto const begin{glm::normalize(geometry::plan_of(curve.first(0.0)))}; - auto const finish{glm::normalize(geometry::plan_of(curve.first(1.0)))}; - WriteLog(format("SELFTEST %s path %s: L %.2f m, R %.1f m, rolls %.2f %.2f, kink %.4f deg, step %.4f m", command.c_str(), track->name().c_str(), curve.plan_length(), static_cast(path.radius), - static_cast(path.rolls[0]), static_cast(path.rolls[1]), glm::degrees(std::abs(geometry::signed_angle(heading, begin))), glm::distance(previous, path.points[segment_data::point::start]))); - heading = finish; - previous = path.points[segment_data::point::end]; - } - if (false == created.empty()) - WriteLog(format("SELFTEST %s end: %.3f m from the target", command.c_str(), geometry::plan_distance(previous, b))); - return selftest_result::handled; - } - if (command == "tracktab") - { - std::string key; - words >> key; - for (auto const &mode : track_modes()) - if (key == mode.key) - show_track_tab(mode.tab); - WriteLog(format("SELFTEST tracktab %s: %s", key.c_str(), track_mode_name().c_str())); - return selftest_result::handled; - } - if (command == "laychain") - { - int layout{0}; - words >> layout >> m_lay.radius; - m_track_layout = static_cast(std::clamp(layout, 0, 3)); - m_lay.active = true; - lay_cancel(); - auto const report = [&](std::string const &Step) { - auto const *start{m_lay.start.track}; - WriteLog(format("SELFTEST laychain %s: chain %zu, points %zu, start %s%s at %.2f %.2f, history %zu, status %s", Step.c_str(), m_lay.chain.size(), m_lay.points.size(), start != nullptr ? start->name().c_str() : "-", start != nullptr && start->m_editorremoved ? " (removed)" : "", - m_lay.points.empty() ? 0.0 : m_lay.points.front().x, m_lay.points.empty() ? 0.0 : m_lay.points.front().z, m_history.size(), m_lay.status.c_str())); - }; - for (std::string word; words >> word;) - { - if (word == "b") - lay_back(); - else if (word == "u") - undo_last(); - else if (word == "r") - redo_last(); - else - { - glm::dvec3 point{std::strtod(word.c_str(), nullptr), 5000.0, 0.0}; - words >> point.z; - std::vector found; - auto const ground{ground_heights({point}, true, &found)}; - point.y = found[0] != 0 ? ground[0] : m_lay.points.empty() ? 0.0 : m_lay.points.back().y; - m_lay.mouse_snap = editor_track::find_free_end(nullptr, 1, point, 3.0, {}); - m_lay.mouse = m_lay.mouse_snap.track != nullptr ? point : lay_bed(point); - lay_click(); - word += format(" %.1f", point.z); - } - report(word); - } - lay_cancel(); - return selftest_result::handled; - } - if (command == "layclicks") - { - int layout{0}; - words >> layout >> m_lay.radius; - m_track_layout = static_cast(std::clamp(layout, 0, 3)); - auto const active{m_lay.active}; - m_lay.active = true; - lay_cancel(); - auto const before{m_history.size()}; - for (glm::dvec3 point{0.0}; words >> point.x >> point.z;) - { - point.y = 5000.0; - std::vector found; - auto const ground{ground_heights({point}, true, &found)}; - point.y = found[0] != 0 ? ground[0] : m_lay.points.empty() ? 0.0 : m_lay.points.back().y; - m_lay.mouse_snap = editor_track::find_free_end(nullptr, 1, point, 3.0, {}); - m_lay.mouse = m_lay.mouse_snap.track != nullptr ? point : lay_bed(point); - WriteLog(format("SELFTEST layclicks ground %.3f, track %.3f", point.y, m_lay.mouse.y)); - lay_click(); - WriteLog(format("SELFTEST layclicks %.1f %.1f: %s", point.x, point.z, m_lay.status.c_str())); - } - lay_cancel(); - m_lay.active = active; - glm::dvec2 heading{0.0}; - glm::dvec3 previous{0.0}; - double slope{0.0}; - bool first{true}; - double hanging{0.0}; - double buried{0.0}; - int paths{0}; - for (auto step{before}; step < m_history.size(); ++step) - for (auto const *track : m_history[step].created) - { - auto const &path{track->m_paths.front()}; - geometry::bezier const curve{path}; - auto const begin{glm::normalize(geometry::plan_of(curve.first(0.0)))}; - auto const &a{path.points[segment_data::point::start]}; - auto const &b{path.points[segment_data::point::end]}; - auto const rise = [](glm::dvec3 const &Tangent) { return Tangent.y / std::max(1e-9, glm::length(geometry::plan_of(Tangent))); }; - WriteLog(format("SELFTEST layclicks path %s: %.2f %.2f -> %.2f %.2f, L %.2f m, R %.1f m, kink %.4f deg, step %.4f m, y %.3f -> %.3f, grade %.2f -> %.2f permille, grade break %.3f permille", track->name().c_str(), a.x, a.z, b.x, b.z, curve.plan_length(), static_cast(path.radius), - first ? 0.0 : glm::degrees(std::abs(geometry::signed_angle(heading, begin))), first ? 0.0 : glm::distance(previous, a), a.y, b.y, 1000.0 * rise(curve.first(0.0)), 1000.0 * rise(curve.first(1.0)), - first ? 0.0 : 1000.0 * std::abs(rise(curve.first(0.0)) - slope))); - slope = rise(curve.first(1.0)); - heading = glm::normalize(geometry::plan_of(curve.first(1.0))); - previous = b; - first = false; - auto const count{std::max(1, static_cast(std::ceil(curve.plan_length() / 2.0)))}; - for (int k = 0; k <= count; ++k) - { - auto const t{curve.parameter(curve.plan_length() * k / count)}; - auto const target{bed_heights({curve.point(t) + glm::dvec3{0.0, 50.0, 0.0}}, {geometry::plan_of(curve.first(t))}, track).front()}; - if (std::isnan(target)) - continue; - hanging = std::max(hanging, curve.point(t).y - target); - buried = std::max(buried, target - curve.point(t).y); - } - ++paths; - } - WriteLog(format("SELFTEST layclicks %d paths, ballast foot over the ground up to %.3f m, under it up to %.3f m", paths, hanging, buried)); - return selftest_result::handled; - } - if (command == "joints") - { - std::vector points; - for (glm::dvec3 point{0.0}; words >> point.x >> point.z;) - points.push_back(point); - auto const scope{m_joints.scope}; - m_joints.scope = 1; - joints_start(); - while (m_joints.next < m_joints.queue.size()) - joints_step(); - m_joints.scope = scope; - char const *const kinds[]{"gap", "step", "kink", "cant jump", "grade break", "free end"}; - int around{0}; - for (auto const &issue : m_joints.issues) - { - if (false == points.empty() && issue.type == joint_issue::kind::open) - continue; - if (false == points.empty() && std::none_of(points.begin(), points.end(), [&](glm::dvec3 const &Point) { return geometry::plan_distance(Point, issue.position) < 5.0; })) - continue; - ++around; - WriteLog(format("SELFTEST joints: %s %.3f at %.1f %.1f, %s / %s", kinds[static_cast(issue.type)], issue.value, issue.position.x, issue.position.z, issue.track != nullptr ? issue.track->name().c_str() : "-", - issue.other != nullptr ? issue.other->name().c_str() : "-")); - } - WriteLog(format("SELFTEST joints: %d issues around the points, %zu in all", around, m_joints.issues.size())); - return selftest_result::handled; - } - if (command == "gizmo") - { - std::string action; - words >> action; - auto *track{selected_track()}; - if (track == nullptr) - { - WriteLog("SELFTEST gizmo: nothing selected"); - return selftest_result::handled; - } - if (action == "set") - { - m_track_set.clear(); - for (std::string name; words >> name;) - if (auto *each{simulation::Paths.find(name)}; each != nullptr) - m_track_set.push_back(each); - m_track_set_pivot = track_set_centre(); - WriteLog(format("SELFTEST gizmo set: %zu tracks, centre %.3f %.3f", m_track_set.size(), m_track_set_pivot.x, m_track_set_pivot.z)); - } - else if (action == "setturn") - { - double degrees{0.0}; - words >> degrees; - if (track_set_begin(true)) - { - track_set_turn(glm::radians(degrees)); - track_set_end(); - } - } - else if (action == "op") - { - std::string operation; - words >> operation; - choose_work_area(work_area::tracks); - m_gizmo_op = operation == "rotate" ? gizmo_operation::rotate : operation == "scale" ? gizmo_operation::scale : gizmo_operation::translate; - int path{-1}; - std::string kind; - words >> path >> kind; - m_track_point = {}; - if (path >= 0) - m_track_point = {path, kind == "c1" ? editor_track::point_kind::control1 : kind == "c2" ? editor_track::point_kind::control2 : kind == "end" ? editor_track::point_kind::end : editor_track::point_kind::start}; - } - else if (action == "begin") - { - std::string operation; - std::string kind; - int path{-1}; - words >> operation >> path >> kind; - m_track_point = {}; - if (path >= 0) - m_track_point = {path, kind == "c1" ? editor_track::point_kind::control1 : kind == "c2" ? editor_track::point_kind::control2 : kind == "end" ? editor_track::point_kind::end : editor_track::point_kind::start}; - auto const wanted{operation == "rotate" ? gizmo_operation::rotate : operation == "scale" ? gizmo_operation::scale : gizmo_operation::translate}; - auto const previous{m_gizmo_op}; - m_gizmo_op = wanted; - auto const chosen{track_gizmo_operation(*track, m_track_point.valid())}; - m_gizmo_op = previous; - track_gizmo_begin(*track, m_track_point.valid(), chosen); - WriteLog(format("SELFTEST gizmo begin %s: %s, %zu tracks dragged, %zu joints, %zu partners%s", operation.c_str(), chosen == gizmo_operation::rotate ? "rotate" : chosen == gizmo_operation::scale ? "scale" : "translate", m_track_drag.size(), m_track_joints.size(), - m_track_gizmo_drag.partners.size(), m_track_gizmo_drag.fixed != glm::dvec3{} ? ", fixed direction" : "")); - } - else if (action == "turn") - { - double degrees{0.0}; - std::string axis{"y"}; - words >> degrees >> axis; - m_track_gizmo_drag.ring = axis == "x" ? 0 : axis == "z" ? 2 : 1; - track_gizmo_turn(*track, m_track_gizmo_drag.ring, glm::radians(degrees)); - track_gizmo_switches(*track); - WriteLog(format("SELFTEST gizmo turn %s: %s", axis.c_str(), track_readout().c_str())); - } - else if (action == "length") - { - double length{0.0}; - words >> length; - track_gizmo_length(*track, length); - track_gizmo_switches(*track); - WriteLog(format("SELFTEST gizmo length %.3f of %.3f, %zu pieces", m_track_gizmo_drag.stretch, m_track_gizmo_drag.length, m_track_gizmo_drag.pieces)); - } - else if (action == "control") - { - glm::dvec3 control{0.0}; - words >> control.x >> control.y >> control.z; - track_gizmo_control(*track, control); - track_gizmo_switches(*track); - } - else if (action == "offset") - { - glm::dvec3 offset{0.0}; - words >> offset.x >> offset.y >> offset.z; - if (m_track_point.valid()) - { - track_gizmo_point(*track, editor_track::point_position(*track, m_track_point) + offset); - } - else - { - editor_track::translate(*track, offset); - editor_track::follow(*track, m_track_joints); - } - track_gizmo_switches(*track); - m_track_dirty = true; - } - else if (action == "end") - track_gizmo_end(*track); - else if (action == "show") - { - std::vector shown{track}; - for (auto *neighbour : editor_track::neighbours(*track)) - shown.push_back(neighbour); - WriteLog(format("SELFTEST gizmo %s prev %s next %s", track->name().c_str(), track->trPrev != nullptr ? track->trPrev->name().c_str() : "-", track->trNext != nullptr ? track->trNext->name().c_str() : "-")); - for (auto *each : shown) - for (int i = 0; i < static_cast(each->m_paths.size()); ++i) - { - auto const &path{each->m_paths[i]}; - auto const &a{path.points[segment_data::point::start]}; - auto const &b{path.points[segment_data::point::end]}; - auto const &c1{path.points[segment_data::point::control1]}; - auto const &c2{path.points[segment_data::point::control2]}; - WriteLog(format("SELFTEST gizmo path %s[%d] %.3f %.3f %.3f -> %.3f %.3f %.3f, c1 %.3f %.3f %.3f, c2 %.3f %.3f %.3f, L %.3f, R %.1f, rolls %.2f %.2f", each->name().c_str(), i, a.x, a.y, a.z, b.x, b.y, b.z, c1.x, c1.y, c1.z, c2.x, c2.y, c2.z, - geometry::bezier{path}.plan_length(), path.radius, path.rolls[0], path.rolls[1])); - } - for (int i = 0; i < static_cast(track->m_paths.size()); ++i) - for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end}) - { - geometry::bezier const own{track->m_paths[i]}; - bool const start{kind == editor_track::point_kind::start}; - auto const out{start ? -own.first(0.0) : own.first(1.0)}; - for (auto const &partner : editor_track::partners(*track, {i, kind})) - { - geometry::bezier const theirs{partner.first->m_paths[partner.second.path]}; - auto const in{partner.second.kind == editor_track::point_kind::start ? theirs.first(0.0) : -theirs.first(1.0)}; - auto const angle{glm::degrees(std::acos(std::clamp(glm::dot(glm::normalize(out), glm::normalize(in)), -1.0, 1.0)))}; - auto const gap{glm::distance(editor_track::point_position(*track, {i, kind}), editor_track::point_position(*partner.first, partner.second))}; - WriteLog(format("SELFTEST gizmo joint %s[%d].%s - %s[%d]: kink %.4f deg, gap %.4f m", track->name().c_str(), i, start ? "start" : "end", partner.first->name().c_str(), partner.second.path, angle, gap)); - } - } - } - return selftest_result::handled; - } - return selftest_result::unhandled; -} - -editor_mode::selftest_result editor_mode::selftest_signal(std::string const &command, std::istringstream &words) -{ - if (command == "signal" || command == "signalmove" || command == "signaldelete" || command == "signalrename" || command == "signalprobe") - { - std::map values; - std::string pair; - while (words >> pair) - { - auto const equals{pair.find('=')}; - if (equals != std::string::npos) - values[pair.substr(0, equals)] = pair.substr(equals + 1); - } - show_track_tab(track_tab::signals); - signal_refresh(true); - auto &tool{m_signal}; - auto const standing_named = [&](std::string const &Name) -> signal_standing const * { - auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.name == Name; })}; - return found != tool.standing.end() ? &*found : nullptr; - }; - auto const spot_on = [&](signal_template const &Template, int const Facing, int const Side, signal_spot &Spot) { - auto *track{simulation::Paths.find(values["track"])}; - if (track == nullptr) - return false; - auto const path{values.count("path") ? std::stoi(values["path"]) : 0}; - auto const at{values.count("at") ? std::stod(values["at"]) : 0.5}; - geometry::bezier const curve{track->m_paths[std::clamp(path, 0, static_cast(track->m_paths.size()) - 1)]}; - return signal_spot_at(*track, path, curve.point(at), curve.point(at), Facing, Side, Template, Spot); - }; - if (command == "signal") - { - auto const *item{signal_template_of(values["file"])}; - if (item == nullptr) - { - WriteLog("SELFTEST signal: no signal template " + values["file"]); - return selftest_result::handled; - } - for (int k = 0; k < static_cast(tool.templates.size()); ++k) - if (&tool.templates[k] == item) - { - static char const *const kinds[] = {"main", "block", "warning", "shunt", "stop", "substitute", "repeater"}; - for (int i = 0; i < 7; ++i) - if (item->kind == kinds[i]) - { - tool.kind = i; - tool.chosen[i] = k; - } - } - if (values.count("name")) - std::snprintf(tool.name, sizeof(tool.name), "%s", values["name"].c_str()); - if (values.count("plate")) - std::snprintf(tool.plate, sizeof(tool.plate), "%s", values["plate"].c_str()); - auto const side{values["side"] == "left" ? -1 : 1}; - auto const facing{values["facing"] == "along" ? 1 : values["facing"] == "against" ? -1 : 0}; - signal_spot spot; - if (false == spot_on(*signal_armed(), facing, side, spot)) - { - WriteLog("SELFTEST signal: no such track " + values["track"]); - return selftest_result::handled; - } - auto const placed{signal_place(spot, *signal_armed())}; - WriteLog(format("SELFTEST signal %s: %s", placed ? "placed" : "failed", tool.status.c_str())); - } - else if (command == "signalmove") - { - auto const *standing{standing_named(values["name"])}; - auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr}; - if (item == nullptr) - { - WriteLog("SELFTEST signalmove: no signal " + values["name"]); - return selftest_result::handled; - } - auto const yaw{glm::radians(standing->include.yaw + 180.0)}; - glm::dvec2 const travel{std::sin(yaw), std::cos(yaw)}; - signal_spot probe; - if (false == spot_on(*item, 1, 1, probe)) - { - WriteLog("SELFTEST signalmove: no such track " + values["track"]); - return selftest_result::handled; - } - auto const facing{glm::dot(probe.travel, travel) >= 0.0 ? 1 : -1}; - auto const sideoftravel{glm::dot(geometry::plan_of(standing->include.location) - geometry::plan_of(standing->spot.axis), glm::dvec2{-travel.y, travel.x}) >= 0.0 ? 1 : -1}; - signal_select(standing->include.instance); - tool.moving = true; - tool.moved_valid = spot_on(*item, facing, sideoftravel * facing, tool.moved); - tool.pressed = {-1000.f, -1000.f}; - signal_release(); - WriteLog("SELFTEST signalmove: " + tool.status); - } - else if (command == "signaldelete") - { - auto const *standing{standing_named(values["name"])}; - auto const deleted{standing != nullptr && signal_delete(standing->include.instance)}; - WriteLog(std::string{"SELFTEST signaldelete "} + values["name"] + (deleted ? ": removed with its event" : ": not removed by the signal tool")); - } - else if (command == "signalrename") - { - auto const *standing{standing_named(values["name"])}; - if (standing == nullptr) - { - WriteLog("SELFTEST signalrename: no signal " + values["name"]); - return selftest_result::handled; - } - select_include(standing->include.instance); - m_include.values[0] = values["to"]; - apply_include(); - m_include_gesture = false; - WriteLog("SELFTEST signalrename " + values["name"] + " -> " + values["to"]); - } - else - { - signal_refresh(true); - WriteLog(format("SELFTEST signalprobe: %zu signals", tool.standing.size())); - for (auto const &standing : tool.standing) - { - std::string where{"not read"}; - if (standing.track != nullptr) - { - auto const &curve{standing.track->m_paths.front()}; - geometry::bezier const bez{curve}; - auto best{std::numeric_limits::max()}; - double parameter{0.0}; - for (int k = 0; k <= 1024; ++k) - { - auto const d{geometry::plan_distance(bez.point(k / 1024.0), standing.include.location)}; - if (d < best) - { - best = d; - parameter = k / 1024.0; - } - } - auto const axis{bez.point(parameter)}; - auto tangent{bez.first(parameter)}; - glm::dvec2 travel{tangent.x, tangent.z}; - travel = glm::normalize(travel) * (standing.list == 1 ? -1.0 : 1.0); - glm::dvec2 const offset{standing.include.location.x - axis.x, standing.include.location.z - axis.z}; - auto const cross{travel.x * offset.y - travel.y * offset.x}; - auto heading{glm::degrees(std::atan2(travel.x, travel.y))}; - auto turn{std::fmod(standing.include.yaw - heading + 720.0, 360.0)}; - auto const &list{editor_track::events(*standing.track, standing.list)}; - auto const bound{std::any_of(list.begin(), list.end(), [&](auto const &Event) { return Event.second != nullptr && ToLower(Event.first) == ToLower(standing.read); })}; - where = std::string{bound ? "bound, " : "unbound, "} + format("%s of %s, %.3f m from the axis, %s of the trains, %.3f m over the axis, model turned %.1f deg from the travel", editor_track::event_keyword(standing.list), standing.track->name().c_str(), glm::length(offset), cross > 0.0 ? "right" : "left", - standing.include.location.y - axis.y, turn); - } - WriteLog(format("SELFTEST signalprobe %s %s at %.3f %.3f %.3f yaw %.2f read %s: %s", standing.name.c_str(), standing.include.file.c_str(), standing.include.location.x, standing.include.location.y, standing.include.location.z, standing.include.yaw, - standing.read.empty() ? "-" : standing.read.c_str(), where.c_str())); - } - } - return selftest_result::handled; - } - return selftest_result::unhandled; -} - -editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &command, std::istringstream &words) -{ - if (command == "orthophoto") - { - auto config{m_orthophoto.settings()}; - int fit{1}; - int inscene{0}; - words >> config.north >> config.east >> config.radius >> fit >> inscene; - config.drape = fit != 0; - config.in_scene = inscene != 0; - m_orthophoto.settings(config); - m_orthophoto.enabled(true); - WriteLog(format("SELFTEST orthophoto at %.0f %.0f, radius %d, %s%s", config.north, config.east, config.radius, config.drape ? "fitted" : "flat", config.in_scene ? ", in the scene" : "")); - return selftest_result::handled; - } - if (command == "terrainmode") - { - m_terrain_open = true; - WriteLog("SELFTEST terrainmode"); - return selftest_result::handled; - } - if (command == "terrainsculpt" || command == "terrainpaint" || command == "terrainlevel") - { - double x{0.0}; - double z{0.0}; - double radius{10.0}; - double value{1.0}; - int mode{0}; - words >> x >> z >> radius >> value >> mode; - auto const changed{command == "terrainsculpt" ? m_streamer.sculpt(x, z, radius, value) - : command == "terrainlevel" ? m_streamer.level(x, z, radius, value, 1.0, mode) - : m_streamer.paint(x, z, radius, 1.0, static_cast(value))}; - forget_ground(); - WriteLog(format("SELFTEST %s %.1f %.1f: %s", command.c_str(), x, z, changed ? "changed" : "nothing changed")); - return selftest_result::handled; - } - if (command == "terrainmodifier") - { - heightmap::modifier modifier; - heightmap::corridor_point a, b; - double formation{0.0}; - double halfwidth{3.5}; - words >> modifier.name >> a.position.x >> a.position.y >> b.position.x >> b.position.y >> formation >> halfwidth; - b.formation = formation; - a.formation = b.formation; - b.half_width = halfwidth; - a.half_width = b.half_width; - modifier.points = {a, b}; - m_streamer.modifier(std::move(modifier)); - forget_ground(); - WriteLog(format("SELFTEST terrainmodifier: %zu modifiers", m_streamer.modifiers().size())); - return selftest_result::handled; - } - if (command == "terrainfill") - { - int layer{0}; - words >> layer >> m_paint_feather >> m_paint_rotation; - m_terrain_layer = layer; - m_paint_outline.clear(); - double x; - double z; - while (words >> x >> z) - m_paint_outline.emplace_back(x, 0.0, z); - fill_terrain_area(); - std::string palette; - for (auto const &entry : m_streamer.manifest().layers) - palette += format(" [%s %.1f m %.0f deg]", entry.material.c_str(), entry.size, entry.rotation); - WriteLog("SELFTEST terrainfill: " + m_terrain_status + ", palette" + palette); - return selftest_result::handled; - } - if (command == "terrainturn") - { - int layer{0}; - float degrees{0.f}; - words >> layer >> degrees; - m_streamer.layer_rotation(static_cast(layer), degrees); - WriteLog(format("SELFTEST terrainturn %d: %.1f deg", layer, m_streamer.manifest().layers.at(layer).rotation)); - return selftest_result::handled; - } - if (command == "terraintouchup") - { - int state{1}; - words >> state; - m_streamer.touch_up(state != 0); - WriteLog(format("SELFTEST terraintouchup %d", state)); - return selftest_result::handled; - } - if (command == "terrainrestore") - { - double x{0.0}; - double z{0.0}; - double radius{10.0}; - words >> x >> z >> radius; - auto const changed{m_streamer.restore_stitching(x, z, radius, 1.0)}; - forget_ground(); - WriteLog(format("SELFTEST terrainrestore %.1f %.1f: %s, touch-ups left %zu", x, z, changed ? "changed" : "nothing changed", m_streamer.touched_up())); - return selftest_result::handled; - } - if (command == "terrainbake") - { - std::string name; - words >> name; - auto const baked{m_streamer.bake_modifier(name)}; - forget_ground(); - WriteLog(format("SELFTEST terrainbake %s: %s, %zu modifiers left", name.c_str(), baked ? "baked" : "not found", m_streamer.modifiers().size())); - return selftest_result::handled; - } - if (command == "terrainselect") - { - int cx{0}; - int cz{0}; - std::string add; - words >> cx >> cz >> add; - if (add != "add") - m_terrain_selection.clear(); - if (m_streamer.exists({cx, cz})) - m_terrain_selection.insert({cx, cz}); - WriteLog(format("SELFTEST terrainselect: %zu selected", m_terrain_selection.size())); - return selftest_result::handled; - } - if (command == "terrainresolution") - { - float spacing{1.f}; - int paint{1}; - words >> spacing >> paint; - m_terrain_convert_spacing = heightmap::valid_spacing(spacing); - m_terrain_convert_paint = paint != 0; - convert_selected_chunks(); - WriteLog("SELFTEST terrainresolution: " + m_terrain_status); - return selftest_result::handled; - } - if (command == "terrainwater" || command == "terrainwaterdefault") - { - if (command == "terrainwaterdefault") - { - std::string material; - words >> material; - m_streamer.water_material(material); - } - else - { - std::size_t index{0}; - std::string material; - float size{0.f}; - words >> index >> material >> size; - auto water{m_streamer.water()}; - if (index < water.size()) - { - water[index].material = material == "-" ? std::string{} : material; - water[index].size = size; - m_streamer.water(std::move(water)); - } - } - std::string list; - for (auto const &body : m_streamer.water()) - list += format(" [%s: %s, %.1f m]", body.name.c_str(), body.material.empty() ? "default" : body.material.c_str(), body.size); - WriteLog("SELFTEST " + command + ": default " + m_streamer.manifest().water_material + list); - return selftest_result::handled; - } - if (command == "terrainadd") - { - int count{1}; - float spacing{m_terrain_spacing}; - words >> count >> spacing; - m_terrain_spacing = heightmap::valid_spacing(spacing); - add_chunks_around(std::clamp(count, 1, 8)); - WriteLog("SELFTEST terrainadd: " + m_terrain_status); - return selftest_result::handled; - } - if (command == "terrainprobe") - { - double x{0.0}; - double z{0.0}; - double height{0.0}; - words >> x >> z; - auto const *chunk{m_streamer.terrain_at(x, z)}; - std::string materials; - if (chunk != nullptr) - for (std::size_t slot = 0; slot < std::max(1, chunk->data().layer_count()); ++slot) - { - auto const layer{chunk->data().layers[slot]}; - auto const &palette{m_streamer.manifest().layers}; - materials += (layer < palette.size() ? palette[layer].material : std::string{"?"}) + " "; - } - auto const found{m_streamer.height_at(x, z, height)}; - std::string grid; - if (chunk != nullptr) - { - auto const &data{chunk->data()}; - double base{0.0}; - double adjust{0.0}; - if (false == data.base.empty()) - base = data.height_in(data.base, x, z); - if (false == data.adjust.empty()) - adjust = data.height_in(data.adjust, x, z); - grid = format(", spacing %.1f, paint %d, base %s %.3f, touch-up %.3f", data.spacing(), data.paint, data.base.empty() ? "none" : "", base, adjust); - if (data.paint > 0) - { - // weights at the paint sample nearest the point - auto const corner{data.corner()}; - auto const ix{static_cast(std::lround((x - corner.x) / heightmap::chunk_size * data.paint))}; - auto const iz{static_cast(std::lround((z - corner.y) / heightmap::chunk_size * data.paint))}; - grid += ", weights"; - for (std::size_t slot = 0; slot < data.layer_count(); ++slot) - grid += format(" %d", data.weight(slot, std::clamp(ix, 0, data.paint), std::clamp(iz, 0, data.paint))); - } - } - WriteLog(format("SELFTEST terrainprobe %.1f %.1f: %s %.3f, %zu chunks, materials %s%s", x, z, found ? "height" : "no terrain", height, m_streamer.resident(), materials.c_str(), grid.c_str())); - return selftest_result::handled; - } - if (command == "terrainconvert") - { - load_ground_files(); - for (auto const &source : m_ground_conversion.files) - { - glm::dvec3 low{1e30}; - glm::dvec3 high{-1e30}; - for (auto index = source.first; index < source.first + source.count; ++index) - for (auto const &vertex : m_ground_conversion.triangles[index]) - { - low = glm::min(low, vertex); - high = glm::max(high, vertex); - } - WriteLog("SELFTEST terrain file " + source.file + (source.text ? " (text): " : " (binary): ") + source.message + - format(", from %.1f %.1f %.1f to %.1f %.1f %.1f", low.x, low.y, low.z, high.x, high.y, high.z)); - } - m_ground_conversion.status = convert_ground(); - WriteLog("SELFTEST terrainconvert: " + m_ground_conversion.status); - return selftest_result::handled; - } - return selftest_result::unhandled; -} - -editor_mode::selftest_result editor_mode::selftest_rails(std::string const &command, std::istringstream &words) -{ - if (command == "blade") - { - std::string name; - words >> name; - 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 vertices = [&]() { - return std::make_pair(track->Geometry1.empty() ? std::size_t{0} : GfxRenderer->Vertices(track->Geometry1[0]).size(), track->Geometry2.empty() ? std::size_t{0} : GfxRenderer->Vertices(track->Geometry2[0]).size()); - }; - auto const set = [&](int const Path) { - track->Switch(Path); - track->SwitchExtension->fOffset = track->SwitchExtension->fDesiredOffset; - track->RaAnimate(); - }; - set(0); - auto const straight{vertices()}; - set(1); - auto const diverging{vertices()}; - set(0); - auto const frog{track->frog()}; - auto const radius{std::abs(track->m_paths.size() > 1 ? track->m_paths[1].radius : 0.f)}; - for (int path = 0; path < 2; ++path) - { - auto const &shape{track->blade(path)}; - auto const &segment{*track->SwitchExtension->Segments[path]}; - auto const old{segment.GetLength() * track_rails::blade_steps(*track, path) / segment.RaSegCount()}; - auto const throat{frog.valid ? frog.at[path] - track_rails::flangeway / std::tan(frog.angle) : 0.0}; - auto const at = [&](double const Width) { - for (std::size_t i = 1; i < shape.gauge.size(); ++i) - if (shape.gauge[i].y >= Width) - return glm::mix(shape.gauge[i - 1].x, shape.gauge[i].x, (Width - shape.gauge[i - 1].y) / std::max(shape.gauge[i].y - shape.gauge[i - 1].y, 1e-9)); - return -1.0; - }; - auto const gauge = [&](double const Distance) { - for (std::size_t i = 1; i < shape.gauge.size(); ++i) - if (shape.gauge[i].x >= Distance) - return glm::mix(shape.gauge[i - 1].y, shape.gauge[i].y, (Distance - shape.gauge[i - 1].x) / std::max(shape.gauge[i].x - shape.gauge[i - 1].x, 1e-9)); - return shape.gauge.empty() ? 0.0 : shape.gauge.back().y; - }; - std::string samples; - auto worst{0.0}; - for (auto const distance : {1.0, 2.0, 4.0, 6.0, 8.0, 10.0}) - { - auto const analytic{radius > 0.f ? distance * distance / (2.0 * radius) : 0.0}; - samples += format(" %.0f:%.4f/%.4f", distance, gauge(distance), analytic); - if (shape.valid && distance <= shape.heel) - worst = std::max(worst, std::abs(gauge(distance) - analytic)); - } - std::string faults; - if (track_rails::cut_rails(*track) && false == shape.valid) - faults += " FAULT blade not cut from the rails"; - if (shape.valid && shape.opening < track_rails::blade_clearance - 1e-4) - faults += " FAULT flangeway"; - if (shape.valid && frog.valid && shape.heel >= throat) - faults += " FAULT heel past the throat"; - if (straight != diverging) - faults += " FAULT vertex count changes"; - WriteLog(format("SELFTEST blade %s path %d: %s, R %.0f, tip %.3f, 50mm %.3f, full head %.3f, heel %.3f (old %.3f), drop %.4f, opening %.4f at %.3f, throat %.3f, d(x)/x^2/2R%s, worst %.4f, vertices %zu/%zu %zu/%zu%s", track->name().c_str(), path, shape.valid ? (shape.rigid ? "geometric rigid" : "geometric") : "old", radius, shape.tip, at(track_rails::blade_lowered + track_rails::blade_gap), shape.full, shape.heel, old, shape.drop, shape.opening, shape.openingat, - throat, samples.c_str(), worst, straight.first, diverging.first, straight.second, diverging.second, faults.c_str())); - } - } - return selftest_result::handled; - } - if (command == "frog") - { - std::string name; - words >> name; - 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 frog{track->frog()}; - if (false == frog.valid) - { - WriteLog(format("SELFTEST frog %s: no frog, paths %.3f and %.3f m, blades %.3f m", track->name().c_str(), track->SwitchExtension->Segments[0]->GetLength(), track->SwitchExtension->Segments[1]->GetLength(), frog.blades[0])); - continue; - } - auto const n{1.0 / std::tan(frog.angle)}; - glm::dvec3 point, across; - track->SwitchExtension->Segments[0]->plan_frame(frog.at[0], point, across); - auto const gauge{point + across * (frog.side[0] * 0.5 * std::abs(track->fTrackWidth))}; - auto const slope{std::tan(frog.angle)}; - auto const check{0.5 * track_rails::check_length(slope)}; - WriteLog(format("SELFTEST frog %s: crossing at %.3f / %.3f m (%.3f %.3f %.3f), 1:%.2f, side %d, blades %.3f / %.3f m, throat %.3f, nose %.3f, wing end %.3f, check rails %.3f-%.3f / %.3f-%.3f m", track->name().c_str(), - frog.at[0], frog.at[1], gauge.x, gauge.y, gauge.z, n, frog.side[0], frog.blades[0], frog.blades[1], frog.at[0] - track_rails::flangeway / slope, frog.at[0] + track_rails::nose_tip / slope, frog.at[0] + track_rails::wing_reach(slope), - frog.at[0] - 0.25 - check, frog.at[0] - 0.25 + check, frog.at[1] - 0.25 - check, frog.at[1] - 0.25 + check)); - } - 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 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; - words >> name; - auto const mix = [](std::uint64_t &Hash, float const Value) { - Hash = (Hash ^ static_cast(static_cast(std::llround(Value * 1e4)))) * 1099511628211ull; - }; - auto const exact = [](std::uint64_t &Hash, float const Value) { - std::uint32_t bits; - std::memcpy(&bits, &Value, sizeof(bits)); - Hash = (Hash ^ bits) * 1099511628211ull; - }; - auto const named = [](std::uint64_t &Hash, material_handle const Material) { - for (auto const letter : (Material != null_handle ? GfxRenderer->Material(Material)->GetName() : std::string{}) + '|') - Hash = (Hash ^ static_cast(letter)) * 1099511628211ull; - }; - std::uint64_t total{1469598103934665603ull}; - std::uint64_t exacttotal{1469598103934665603ull}; - std::size_t vertices{0}; - int tracks{0}; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1) - continue; - if (false == name.empty() && track->name() != name) - continue; - std::uint64_t hash{1469598103934665603ull}; - std::uint64_t exacthash{1469598103934665603ull}; - std::size_t count{0}; - int chunks{0}; - std::vector sequences{&track->Geometry1, &track->Geometry2, &track->GeometryCut1, &track->GeometryCut2}; - for (auto const &tread : track->GeometryTreads) - { - sequences.push_back(&tread.geometry); - sequences.push_back(&tread.cuts); - named(exacthash, tread.material); - named(exacthash, tread.matte); - } - for (auto const *sequence : sequences) - { - 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)) - { - for (int i = 0; i < 3; ++i) - { - mix(hash, vertex.position[i]); - mix(hash, vertex.normal[i]); - exact(exacthash, vertex.position[i]); - exact(exacthash, vertex.normal[i]); - } - mix(hash, vertex.texture.x); - mix(hash, vertex.texture.y); - exact(exacthash, vertex.texture.x); - exact(exacthash, vertex.texture.y); - ++count; - } - for (auto const &userdata : GfxRenderer->UserData(geometry)) - { - for (int i = 0; i < 4; ++i) - exact(exacthash, userdata.data[i]); - } - exact(exacthash, -2.f); - } - mix(hash, -1.f); - exact(exacthash, -1.f); - } - WriteLog(format("SELFTEST geom %s: chunks %d, vertices %zu, hash %016llx, exact %016llx", track->name().c_str(), chunks, count, static_cast(hash), static_cast(exacthash))); - total = (total ^ hash) * 1099511628211ull; - exacttotal = (exacttotal ^ exacthash) * 1099511628211ull; - vertices += count; - ++tracks; - } - WriteLog(format("SELFTEST geom total: tracks %d, vertices %zu, hash %016llx, exact %016llx", tracks, vertices, static_cast(total), static_cast(exacttotal))); - return selftest_result::handled; - } - if (command == "frogkey") - { - std::string name, value; - words >> name >> value; - auto *track{simulation::Paths.find(name)}; - if (track == nullptr || track->eType != tt_Switch || track->SwitchExtension == nullptr) - WriteLog(format("SELFTEST frogkey %s: no such switch", name.c_str())); - else - { - if (value == "on" || value == "off") - { - auto const before{editor_track::capture(*track)}; - track->SwitchExtension->m_frogs = (value == "on"); - push_track_snapshot({{track, before}}); - editor_track::commit_parameters(*track); - } - std::ostringstream text; - track->export_as_text(text); - WriteLog(format("SELFTEST frogkey %s: key %d, written %d, frog %d, crossings %zu, profile '%s'", name.c_str(), track->SwitchExtension->m_frogs ? 1 : 0, text.str().find(" frogs ") != std::string::npos ? 1 : 0, - track->frog().valid ? 1 : 0, track->rail_crossings().size(), track->m_profile1.first.c_str())); - } - 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 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>{ - {"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 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 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 pieces; - }; - std::map layouts; - std::map> rails; - auto const rails_of = [&](TTrack *Track) -> std::vector & { - 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 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 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 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; - words >> name; - int count{0}; - int faults{0}; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1) - continue; - if (false == name.empty() && track->name() != name) - continue; - auto const layout{track->plan_rails(true)}; - auto const size{track->rail_dimensions()}; - auto const guardoffset{-(track_rails::flangeway + size.head)}; - double const laterals[2]{size.low + guardoffset, track_rails::flangeway + size.head + size.low - 0.002 + guardoffset}; - for (int path = 0; path < 2; ++path) - { - for (int right = 0; right < 2; ++right) - { - auto const side{right ? 1 : -1}; - auto const length{track->eType == tt_Switch ? track->SwitchExtension->Segments[path]->GetLength() : track->Segment->GetLength()}; - auto const drawn = [&](glm::dvec2 const &Miter, double const Square) { - auto const at{Miter.y == 0.0 ? Square : Miter.x + Miter.y * 0.5 * (laterals[0] + laterals[1])}; - return at > 0.0 && at < length; - }; - for (auto const &guard : layout.guards[path][right]) - { - if (false == drawn(guard.frommiter, guard.from) && false == drawn(guard.tomiter, guard.to) && (guard.to <= 0.0 || guard.from >= length)) - continue; - ++count; - 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 ? "planed" : "square"}; - if (false == drawn(Miter, 0.0)) - return std::string{"past the end of the track"}; - glm::dvec3 points[2]; - for (int i = 0; i < 2; ++i) - points[i] = track_rails::rail_point(*track, path, side, Miter.x + Miter.y * laterals[i], laterals[i]); - auto best{1e9}; - double lateral{0.0}, along{0.0}; - TTrack *against{nullptr}; - int againstpath{0}, againstside{0}; - for (auto *other : layout.crossed) - for (int otherpath = 0; otherpath < (other->eType == tt_Switch ? 2 : 1); ++otherpath) - for (auto const otherside : {-1, 1}) - { - double d[2], l[2]; - for (int i = 0; i < 2; ++i) - track_rails::rail_offset(*other, otherpath, otherside, points[i], d[i], l[i]); - if (std::abs(l[0]) > 0.5 || std::abs(l[1]) > 0.5 || std::abs(l[0] - l[1]) >= best) - continue; - best = std::abs(l[0] - l[1]); - lateral = 0.5 * (l[0] + l[1]); - along = 0.5 * (d[0] + d[1]); - against = other; - againstpath = otherpath; - againstside = otherside; - } - if (against == nullptr) - { - ++faults; - return std::string{"cut along nothing FAULT"}; - } - auto result{format("along %s/%d%+d at %.3f m, %.1f mm off its gauge line, skew %.1f mm", against->name().c_str(), againstpath, againstside, along, lateral * 1000.0, best * 1000.0)}; - auto fault{best > 0.002}; - if (Glued) - { - auto const theirsize{against->rail_dimensions()}; - auto const theiroffset{-(track_rails::flangeway + theirsize.head)}; - double const theirlaterals[2]{theirsize.low + theiroffset, track_rails::flangeway + theirsize.head + theirsize.low - 0.002 + theiroffset}; - auto const reach = [&](glm::dvec2 const &Miter, double const Square, bool const Latest) { - if (Miter.y == 0.0) - return Square; - auto const a{Miter.x + Miter.y * theirlaterals[0]}; - auto const b{Miter.x + Miter.y * theirlaterals[1]}; - return Latest ? std::max(a, b) : std::min(a, b); - }; - auto covered{false}; - for (auto const &theirs : against->plan_rails(true).guards[againstpath][againstside > 0]) - covered |= (along > reach(theirs.frommiter, theirs.from, false) - 0.002 && along < reach(theirs.tomiter, theirs.to, true) + 0.002); - result += covered ? ", on its guard rail" : ", no guard rail there"; - fault |= (false == covered || std::abs(lateral + track_rails::flangeway) > 0.002); - } - if (fault) - { - ++faults; - result += " FAULT"; - } - return result; - }; - text += "from " + end(guard.frommiter, guard.flarefrom, false == guard.flarefrom) + "; to " + end(guard.tomiter, guard.flareto, false == guard.flareto); - for (auto const &cut : guard.plan.cuts) - if (cut.end > 0.0 && cut.start < length) - text += format("; break %.3f-%.3f: ", cut.end, cut.start) + end(cut.endmiter, false, false) + " / " + end(cut.startmiter, false, false); - WriteLog(format("SELFTEST guard %s path %d side %+d %.3f-%.3f m: %s", track->name().c_str(), path, side, guard.from, guard.to, text.c_str())); - } - } - } - } - WriteLog(format("SELFTEST guards: %d guard rails, %d faults", count, faults)); - return selftest_result::handled; - } - if (command == "throw") - { - // throw [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(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 = before + static_cast(part) * (track->SwitchExtension->fDesiredOffset - before); - track->RaAnimate(); - ++count; - } - WriteLog(format("SELFTEST switches: %d switches set to path %d", count, path)); - return selftest_result::handled; - } - if (command == "railtime") - { - track_rails::forget(); - auto const started{std::chrono::steady_clock::now()}; - int tracks{0}; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1) - continue; - track->rail_crossings(false); - ++tracks; - } - auto const crossed{std::chrono::steady_clock::now()}; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1) - continue; - track->plan_rails(true); - } - auto const finished{std::chrono::steady_clock::now()}; - auto const crossings{std::chrono::duration(crossed - started).count()}; - auto const elapsed{std::chrono::duration(finished - started).count()}; - WriteLog(format("SELFTEST railtime: the rails of %d tracks laid out in %.1f ms, of which their crossings %.1f ms", tracks, elapsed, crossings)); - return selftest_result::handled; - } - if (command == "crossings") - { - std::string name; - words >> name; - auto const started{std::chrono::steady_clock::now()}; - int tracks{0}; - int found{0}; - int acutes{0}; - int middles{0}; - int mismatched{0}; - for (auto *track : simulation::Paths.sequence()) - { - if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1) - continue; - if (false == name.empty() && track->name() != name) - continue; - ++tracks; - 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) - continue; - ++acutes; - auto const here{gauge(track, crossing)}; - auto partners{0}; - auto noses{crossing.nose ? 1 : 0}; - for (auto const &other : crossing.track->rail_crossings(false)) - { - if (other.track != track || other.angle > track_rails::wide_angle || other.facing >= 0.0) - continue; - auto const there{gauge(crossing.track, other)}; - if (glm::length(glm::dvec2{there.x - here.x, there.z - here.z}) > 0.05) - continue; - ++partners; - noses += other.nose ? 1 : 0; - } - if (partners != 1 || noses != 1) - ++mismatched; - WriteLog(format("SELFTEST acute %s %s path %d side %d at %.3f m with %s (%.2f %.2f %.2f): partners %d, noses %d", partners != 1 || noses != 1 ? "mismatch" : "ok", track->name().c_str(), crossing.path, crossing.side, crossing.at, crossing.track->name().c_str(), here.x, here.y, here.z, partners, noses)); - } - } - auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - started).count()}; - WriteLog(format("SELFTEST crossings: %d rail crossings on %d tracks, %d of them middle, %d acute, %d mismatched, %.1f ms", found, tracks, middles, acutes, mismatched, elapsed)); - return selftest_result::handled; - } - return selftest_result::unhandled; -} - // the script is a text file, one action a line: // wait // camera from the first point, looking at the second @@ -4169,7 +169,7 @@ void editor_mode::selftest_step() test.lines.push_back(line); WriteLog("SELFTEST: " + std::to_string(test.lines.size()) + " steps from " + file); } - ++selftest_frames; + ++editor_selftest::frames(); if (test.wait > 0) { --test.wait; diff --git a/application/editormodeselftest.h b/application/editormodeselftest.h new file mode 100644 index 00000000..cd3c70be --- /dev/null +++ b/application/editormodeselftest.h @@ -0,0 +1,33 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include + +// what the groups of the self-test commands share, each group in a file of its own +namespace editor_selftest +{ + +// frames gone through the selftest, for the measurements of perf +inline std::size_t &frames() +{ + static std::size_t count{0}; + return count; +} + +inline std::string one_line(std::string Text) +{ + std::replace(Text.begin(), Text.end(), '\n', ';'); + return Text; +} + +} // namespace editor_selftest diff --git a/application/editormodeselftestjoin.cpp b/application/editormodeselftestjoin.cpp new file mode 100644 index 00000000..71564abf --- /dev/null +++ b/application/editormodeselftestjoin.cpp @@ -0,0 +1,398 @@ +/* +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 "application/editormode.h" +#include "editor/editorSettings.hpp" +#include "editor/editorGeometry.hpp" +#include "application/application.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" + +#include "simulation/simulation.h" +#include "simulation/simulationtime.h" +#include "scene/scenelayers.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Track.h" +#include "world/TrackRails.h" +#include "world/MemCell.h" +#include "world/Event.h" +#include "vehicle/DynObj.h" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "world/Sweep.h" +#include "input/keyboardinput.h" +#include "launcher/textures_scanner.h" +#include +#include +#include +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "scene/sceneterrain.h" +#include "editor/editorSpeed.hpp" + +#include +#include +#include +#include +#include +#include +#include "application/editormodeselftest.h" + +editor_mode::selftest_result editor_mode::selftest_join(std::string const &command, std::istringstream &words) +{ + if (command == "join" || command == "layto") + { + glm::dvec3 a{0.0}; + glm::dvec3 b{0.0}; + words >> a.x >> a.z >> b.x >> b.z; + auto const from{editor_track::find_free_end(nullptr, 1, a, 3.0, {})}; + extend_tool tool; + if (from.track == nullptr || false == free_end(*from.track, from.point.path, from.point.kind == editor_track::point_kind::end, tool)) + { + WriteLog("SELFTEST " + command + ": no free end at the first point"); + return selftest_result::handled; + } + std::vector pieces; + std::string error; + double length{0.0}; + if (command == "join") + { + double radius{0.0}; + int variant{0}; + int transitions{0}; + words >> radius >> variant >> transitions; + auto const to{editor_track::find_free_end(nullptr, 1, b, 3.0, {from.track})}; + if (to.track == nullptr) + { + WriteLog("SELFTEST join: no free end at the second point"); + return selftest_result::handled; + } + m_join_radius = radius; + m_join_variant = variant; + m_lay.transitions = transitions != 0; + pieces = join_pieces(tool.track, from.point, tool.point, tool.direction, to, length, error); + } + else + { + int layout{0}; + words >> layout >> m_lay.radius; + m_track_layout = static_cast(std::clamp(layout, 0, 3)); + m_extend = tool; + m_extend.active = true; + m_extend.mouse = {b.x, tool.point.y, b.z}; + m_extend.snap = {}; + pieces = extend_pieces(); + m_extend.active = false; + } + std::vector created; + for (auto const &piece : pieces) + created.push_back(editor_track::create_path(*tool.track, piece)); + editor_track::commit(created); + digitize_drawn(created); + if (false == created.empty()) + push_track_snapshot({}, created); + char const *const kinds[]{"none", "straight", "arc", "curve", "reverse curve", "free curve"}; + auto const &join{m_join_last}; + WriteLog(format("SELFTEST %s: %s, %zu paths, R %.1f m, cant %.0f mm, transitions %.1f m, V %.0f km/h, variants %d%s", command.c_str(), kinds[static_cast(join.kind)], pieces.size(), join.radius, join.cant, + join.transition, join.speed, join.variants, error.empty() ? "" : (", " + error).c_str())); + auto heading{tool.direction}; + glm::dvec3 previous{tool.point}; + for (auto const *track : created) + { + auto const &path{track->m_paths.front()}; + geometry::bezier const curve{path}; + auto const begin{glm::normalize(geometry::plan_of(curve.first(0.0)))}; + auto const finish{glm::normalize(geometry::plan_of(curve.first(1.0)))}; + WriteLog(format("SELFTEST %s path %s: L %.2f m, R %.1f m, rolls %.2f %.2f, kink %.4f deg, step %.4f m", command.c_str(), track->name().c_str(), curve.plan_length(), static_cast(path.radius), + static_cast(path.rolls[0]), static_cast(path.rolls[1]), glm::degrees(std::abs(geometry::signed_angle(heading, begin))), glm::distance(previous, path.points[segment_data::point::start]))); + heading = finish; + previous = path.points[segment_data::point::end]; + } + if (false == created.empty()) + WriteLog(format("SELFTEST %s end: %.3f m from the target", command.c_str(), geometry::plan_distance(previous, b))); + return selftest_result::handled; + } + if (command == "tracktab") + { + std::string key; + words >> key; + for (auto const &mode : track_modes()) + if (key == mode.key) + show_track_tab(mode.tab); + WriteLog(format("SELFTEST tracktab %s: %s", key.c_str(), track_mode_name().c_str())); + return selftest_result::handled; + } + if (command == "laychain") + { + int layout{0}; + words >> layout >> m_lay.radius; + m_track_layout = static_cast(std::clamp(layout, 0, 3)); + m_lay.active = true; + lay_cancel(); + auto const report = [&](std::string const &Step) { + auto const *start{m_lay.start.track}; + WriteLog(format("SELFTEST laychain %s: chain %zu, points %zu, start %s%s at %.2f %.2f, history %zu, status %s", Step.c_str(), m_lay.chain.size(), m_lay.points.size(), start != nullptr ? start->name().c_str() : "-", start != nullptr && start->m_editorremoved ? " (removed)" : "", + m_lay.points.empty() ? 0.0 : m_lay.points.front().x, m_lay.points.empty() ? 0.0 : m_lay.points.front().z, m_history.size(), m_lay.status.c_str())); + }; + for (std::string word; words >> word;) + { + if (word == "b") + lay_back(); + else if (word == "u") + undo_last(); + else if (word == "r") + redo_last(); + else + { + glm::dvec3 point{std::strtod(word.c_str(), nullptr), 5000.0, 0.0}; + words >> point.z; + std::vector found; + auto const ground{ground_heights({point}, true, &found)}; + point.y = found[0] != 0 ? ground[0] : m_lay.points.empty() ? 0.0 : m_lay.points.back().y; + m_lay.mouse_snap = editor_track::find_free_end(nullptr, 1, point, 3.0, {}); + m_lay.mouse = m_lay.mouse_snap.track != nullptr ? point : lay_bed(point); + lay_click(); + word += format(" %.1f", point.z); + } + report(word); + } + lay_cancel(); + return selftest_result::handled; + } + if (command == "layclicks") + { + int layout{0}; + words >> layout >> m_lay.radius; + m_track_layout = static_cast(std::clamp(layout, 0, 3)); + auto const active{m_lay.active}; + m_lay.active = true; + lay_cancel(); + auto const before{m_history.size()}; + for (glm::dvec3 point{0.0}; words >> point.x >> point.z;) + { + point.y = 5000.0; + std::vector found; + auto const ground{ground_heights({point}, true, &found)}; + point.y = found[0] != 0 ? ground[0] : m_lay.points.empty() ? 0.0 : m_lay.points.back().y; + m_lay.mouse_snap = editor_track::find_free_end(nullptr, 1, point, 3.0, {}); + m_lay.mouse = m_lay.mouse_snap.track != nullptr ? point : lay_bed(point); + WriteLog(format("SELFTEST layclicks ground %.3f, track %.3f", point.y, m_lay.mouse.y)); + lay_click(); + WriteLog(format("SELFTEST layclicks %.1f %.1f: %s", point.x, point.z, m_lay.status.c_str())); + } + lay_cancel(); + m_lay.active = active; + glm::dvec2 heading{0.0}; + glm::dvec3 previous{0.0}; + double slope{0.0}; + bool first{true}; + double hanging{0.0}; + double buried{0.0}; + int paths{0}; + for (auto step{before}; step < m_history.size(); ++step) + for (auto const *track : m_history[step].created) + { + auto const &path{track->m_paths.front()}; + geometry::bezier const curve{path}; + auto const begin{glm::normalize(geometry::plan_of(curve.first(0.0)))}; + auto const &a{path.points[segment_data::point::start]}; + auto const &b{path.points[segment_data::point::end]}; + auto const rise = [](glm::dvec3 const &Tangent) { return Tangent.y / std::max(1e-9, glm::length(geometry::plan_of(Tangent))); }; + WriteLog(format("SELFTEST layclicks path %s: %.2f %.2f -> %.2f %.2f, L %.2f m, R %.1f m, kink %.4f deg, step %.4f m, y %.3f -> %.3f, grade %.2f -> %.2f permille, grade break %.3f permille", track->name().c_str(), a.x, a.z, b.x, b.z, curve.plan_length(), static_cast(path.radius), + first ? 0.0 : glm::degrees(std::abs(geometry::signed_angle(heading, begin))), first ? 0.0 : glm::distance(previous, a), a.y, b.y, 1000.0 * rise(curve.first(0.0)), 1000.0 * rise(curve.first(1.0)), + first ? 0.0 : 1000.0 * std::abs(rise(curve.first(0.0)) - slope))); + slope = rise(curve.first(1.0)); + heading = glm::normalize(geometry::plan_of(curve.first(1.0))); + previous = b; + first = false; + auto const count{std::max(1, static_cast(std::ceil(curve.plan_length() / 2.0)))}; + for (int k = 0; k <= count; ++k) + { + auto const t{curve.parameter(curve.plan_length() * k / count)}; + auto const target{bed_heights({curve.point(t) + glm::dvec3{0.0, 50.0, 0.0}}, {geometry::plan_of(curve.first(t))}, track).front()}; + if (std::isnan(target)) + continue; + hanging = std::max(hanging, curve.point(t).y - target); + buried = std::max(buried, target - curve.point(t).y); + } + ++paths; + } + WriteLog(format("SELFTEST layclicks %d paths, ballast foot over the ground up to %.3f m, under it up to %.3f m", paths, hanging, buried)); + return selftest_result::handled; + } + if (command == "joints") + { + std::vector points; + for (glm::dvec3 point{0.0}; words >> point.x >> point.z;) + points.push_back(point); + auto const scope{m_joints.scope}; + m_joints.scope = 1; + joints_start(); + while (m_joints.next < m_joints.queue.size()) + joints_step(); + m_joints.scope = scope; + char const *const kinds[]{"gap", "step", "kink", "cant jump", "grade break", "free end"}; + int around{0}; + for (auto const &issue : m_joints.issues) + { + if (false == points.empty() && issue.type == joint_issue::kind::open) + continue; + if (false == points.empty() && std::none_of(points.begin(), points.end(), [&](glm::dvec3 const &Point) { return geometry::plan_distance(Point, issue.position) < 5.0; })) + continue; + ++around; + WriteLog(format("SELFTEST joints: %s %.3f at %.1f %.1f, %s / %s", kinds[static_cast(issue.type)], issue.value, issue.position.x, issue.position.z, issue.track != nullptr ? issue.track->name().c_str() : "-", + issue.other != nullptr ? issue.other->name().c_str() : "-")); + } + WriteLog(format("SELFTEST joints: %d issues around the points, %zu in all", around, m_joints.issues.size())); + return selftest_result::handled; + } + if (command == "gizmo") + { + std::string action; + words >> action; + auto *track{selected_track()}; + if (track == nullptr) + { + WriteLog("SELFTEST gizmo: nothing selected"); + return selftest_result::handled; + } + if (action == "set") + { + m_track_set.clear(); + for (std::string name; words >> name;) + if (auto *each{simulation::Paths.find(name)}; each != nullptr) + m_track_set.push_back(each); + m_track_set_pivot = track_set_centre(); + WriteLog(format("SELFTEST gizmo set: %zu tracks, centre %.3f %.3f", m_track_set.size(), m_track_set_pivot.x, m_track_set_pivot.z)); + } + else if (action == "setturn") + { + double degrees{0.0}; + words >> degrees; + if (track_set_begin(true)) + { + track_set_turn(glm::radians(degrees)); + track_set_end(); + } + } + else if (action == "op") + { + std::string operation; + words >> operation; + choose_work_area(work_area::tracks); + m_gizmo_op = operation == "rotate" ? gizmo_operation::rotate : operation == "scale" ? gizmo_operation::scale : gizmo_operation::translate; + int path{-1}; + std::string kind; + words >> path >> kind; + m_track_point = {}; + if (path >= 0) + m_track_point = {path, kind == "c1" ? editor_track::point_kind::control1 : kind == "c2" ? editor_track::point_kind::control2 : kind == "end" ? editor_track::point_kind::end : editor_track::point_kind::start}; + } + else if (action == "begin") + { + std::string operation; + std::string kind; + int path{-1}; + words >> operation >> path >> kind; + m_track_point = {}; + if (path >= 0) + m_track_point = {path, kind == "c1" ? editor_track::point_kind::control1 : kind == "c2" ? editor_track::point_kind::control2 : kind == "end" ? editor_track::point_kind::end : editor_track::point_kind::start}; + auto const wanted{operation == "rotate" ? gizmo_operation::rotate : operation == "scale" ? gizmo_operation::scale : gizmo_operation::translate}; + auto const previous{m_gizmo_op}; + m_gizmo_op = wanted; + auto const chosen{track_gizmo_operation(*track, m_track_point.valid())}; + m_gizmo_op = previous; + track_gizmo_begin(*track, m_track_point.valid(), chosen); + WriteLog(format("SELFTEST gizmo begin %s: %s, %zu tracks dragged, %zu joints, %zu partners%s", operation.c_str(), chosen == gizmo_operation::rotate ? "rotate" : chosen == gizmo_operation::scale ? "scale" : "translate", m_track_drag.size(), m_track_joints.size(), + m_track_gizmo_drag.partners.size(), m_track_gizmo_drag.fixed != glm::dvec3{} ? ", fixed direction" : "")); + } + else if (action == "turn") + { + double degrees{0.0}; + std::string axis{"y"}; + words >> degrees >> axis; + m_track_gizmo_drag.ring = axis == "x" ? 0 : axis == "z" ? 2 : 1; + track_gizmo_turn(*track, m_track_gizmo_drag.ring, glm::radians(degrees)); + track_gizmo_switches(*track); + WriteLog(format("SELFTEST gizmo turn %s: %s", axis.c_str(), track_readout().c_str())); + } + else if (action == "length") + { + double length{0.0}; + words >> length; + track_gizmo_length(*track, length); + track_gizmo_switches(*track); + WriteLog(format("SELFTEST gizmo length %.3f of %.3f, %zu pieces", m_track_gizmo_drag.stretch, m_track_gizmo_drag.length, m_track_gizmo_drag.pieces)); + } + else if (action == "control") + { + glm::dvec3 control{0.0}; + words >> control.x >> control.y >> control.z; + track_gizmo_control(*track, control); + track_gizmo_switches(*track); + } + else if (action == "offset") + { + glm::dvec3 offset{0.0}; + words >> offset.x >> offset.y >> offset.z; + if (m_track_point.valid()) + { + track_gizmo_point(*track, editor_track::point_position(*track, m_track_point) + offset); + } + else + { + editor_track::translate(*track, offset); + editor_track::follow(*track, m_track_joints); + } + track_gizmo_switches(*track); + m_track_dirty = true; + } + else if (action == "end") + track_gizmo_end(*track); + else if (action == "show") + { + std::vector shown{track}; + for (auto *neighbour : editor_track::neighbours(*track)) + shown.push_back(neighbour); + WriteLog(format("SELFTEST gizmo %s prev %s next %s", track->name().c_str(), track->trPrev != nullptr ? track->trPrev->name().c_str() : "-", track->trNext != nullptr ? track->trNext->name().c_str() : "-")); + for (auto *each : shown) + for (int i = 0; i < static_cast(each->m_paths.size()); ++i) + { + auto const &path{each->m_paths[i]}; + auto const &a{path.points[segment_data::point::start]}; + auto const &b{path.points[segment_data::point::end]}; + auto const &c1{path.points[segment_data::point::control1]}; + auto const &c2{path.points[segment_data::point::control2]}; + WriteLog(format("SELFTEST gizmo path %s[%d] %.3f %.3f %.3f -> %.3f %.3f %.3f, c1 %.3f %.3f %.3f, c2 %.3f %.3f %.3f, L %.3f, R %.1f, rolls %.2f %.2f", each->name().c_str(), i, a.x, a.y, a.z, b.x, b.y, b.z, c1.x, c1.y, c1.z, c2.x, c2.y, c2.z, + geometry::bezier{path}.plan_length(), path.radius, path.rolls[0], path.rolls[1])); + } + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end}) + { + geometry::bezier const own{track->m_paths[i]}; + bool const start{kind == editor_track::point_kind::start}; + auto const out{start ? -own.first(0.0) : own.first(1.0)}; + for (auto const &partner : editor_track::partners(*track, {i, kind})) + { + geometry::bezier const theirs{partner.first->m_paths[partner.second.path]}; + auto const in{partner.second.kind == editor_track::point_kind::start ? theirs.first(0.0) : -theirs.first(1.0)}; + auto const angle{glm::degrees(std::acos(std::clamp(glm::dot(glm::normalize(out), glm::normalize(in)), -1.0, 1.0)))}; + auto const gap{glm::distance(editor_track::point_position(*track, {i, kind}), editor_track::point_position(*partner.first, partner.second))}; + WriteLog(format("SELFTEST gizmo joint %s[%d].%s - %s[%d]: kink %.4f deg, gap %.4f m", track->name().c_str(), i, start ? "start" : "end", partner.first->name().c_str(), partner.second.path, angle, gap)); + } + } + } + return selftest_result::handled; + } + return selftest_result::unhandled; +} diff --git a/application/editormodeselftestrails.cpp b/application/editormodeselftestrails.cpp new file mode 100644 index 00000000..86d69280 --- /dev/null +++ b/application/editormodeselftestrails.cpp @@ -0,0 +1,1067 @@ +/* +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 "application/editormode.h" +#include "editor/editorSettings.hpp" +#include "editor/editorGeometry.hpp" +#include "application/application.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" + +#include "simulation/simulation.h" +#include "simulation/simulationtime.h" +#include "scene/scenelayers.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Track.h" +#include "world/TrackRails.h" +#include "world/MemCell.h" +#include "world/Event.h" +#include "vehicle/DynObj.h" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "world/Sweep.h" +#include "input/keyboardinput.h" +#include "launcher/textures_scanner.h" +#include +#include +#include +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "scene/sceneterrain.h" +#include "editor/editorSpeed.hpp" + +#include +#include +#include +#include +#include +#include +#include "application/editormodeselftest.h" + +editor_mode::selftest_result editor_mode::selftest_rails(std::string const &command, std::istringstream &words) +{ + if (command == "blade") + { + std::string name; + words >> name; + 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 vertices = [&]() { + return std::make_pair(track->Geometry1.empty() ? std::size_t{0} : GfxRenderer->Vertices(track->Geometry1[0]).size(), track->Geometry2.empty() ? std::size_t{0} : GfxRenderer->Vertices(track->Geometry2[0]).size()); + }; + auto const set = [&](int const Path) { + track->Switch(Path); + track->SwitchExtension->fOffset = track->SwitchExtension->fDesiredOffset; + track->RaAnimate(); + }; + set(0); + auto const straight{vertices()}; + set(1); + auto const diverging{vertices()}; + set(0); + auto const frog{track->frog()}; + auto const radius{std::abs(track->m_paths.size() > 1 ? track->m_paths[1].radius : 0.f)}; + for (int path = 0; path < 2; ++path) + { + auto const &shape{track->blade(path)}; + auto const &segment{*track->SwitchExtension->Segments[path]}; + auto const old{segment.GetLength() * track_rails::blade_steps(*track, path) / segment.RaSegCount()}; + auto const throat{frog.valid ? frog.at[path] - track_rails::flangeway / std::tan(frog.angle) : 0.0}; + auto const at = [&](double const Width) { + for (std::size_t i = 1; i < shape.gauge.size(); ++i) + if (shape.gauge[i].y >= Width) + return glm::mix(shape.gauge[i - 1].x, shape.gauge[i].x, (Width - shape.gauge[i - 1].y) / std::max(shape.gauge[i].y - shape.gauge[i - 1].y, 1e-9)); + return -1.0; + }; + auto const gauge = [&](double const Distance) { + for (std::size_t i = 1; i < shape.gauge.size(); ++i) + if (shape.gauge[i].x >= Distance) + return glm::mix(shape.gauge[i - 1].y, shape.gauge[i].y, (Distance - shape.gauge[i - 1].x) / std::max(shape.gauge[i].x - shape.gauge[i - 1].x, 1e-9)); + return shape.gauge.empty() ? 0.0 : shape.gauge.back().y; + }; + std::string samples; + auto worst{0.0}; + for (auto const distance : {1.0, 2.0, 4.0, 6.0, 8.0, 10.0}) + { + auto const analytic{radius > 0.f ? distance * distance / (2.0 * radius) : 0.0}; + samples += format(" %.0f:%.4f/%.4f", distance, gauge(distance), analytic); + if (shape.valid && distance <= shape.heel) + worst = std::max(worst, std::abs(gauge(distance) - analytic)); + } + std::string faults; + if (track_rails::cut_rails(*track) && false == shape.valid) + faults += " FAULT blade not cut from the rails"; + if (shape.valid && shape.opening < track_rails::blade_clearance - 1e-4) + faults += " FAULT flangeway"; + if (shape.valid && frog.valid && shape.heel >= throat) + faults += " FAULT heel past the throat"; + if (straight != diverging) + faults += " FAULT vertex count changes"; + WriteLog(format("SELFTEST blade %s path %d: %s, R %.0f, tip %.3f, 50mm %.3f, full head %.3f, heel %.3f (old %.3f), drop %.4f, opening %.4f at %.3f, throat %.3f, d(x)/x^2/2R%s, worst %.4f, vertices %zu/%zu %zu/%zu%s", track->name().c_str(), path, shape.valid ? (shape.rigid ? "geometric rigid" : "geometric") : "old", radius, shape.tip, at(track_rails::blade_lowered + track_rails::blade_gap), shape.full, shape.heel, old, shape.drop, shape.opening, shape.openingat, + throat, samples.c_str(), worst, straight.first, diverging.first, straight.second, diverging.second, faults.c_str())); + } + } + return selftest_result::handled; + } + if (command == "frog") + { + std::string name; + words >> name; + 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 frog{track->frog()}; + if (false == frog.valid) + { + WriteLog(format("SELFTEST frog %s: no frog, paths %.3f and %.3f m, blades %.3f m", track->name().c_str(), track->SwitchExtension->Segments[0]->GetLength(), track->SwitchExtension->Segments[1]->GetLength(), frog.blades[0])); + continue; + } + auto const n{1.0 / std::tan(frog.angle)}; + glm::dvec3 point, across; + track->SwitchExtension->Segments[0]->plan_frame(frog.at[0], point, across); + auto const gauge{point + across * (frog.side[0] * 0.5 * std::abs(track->fTrackWidth))}; + auto const slope{std::tan(frog.angle)}; + auto const check{0.5 * track_rails::check_length(slope)}; + WriteLog(format("SELFTEST frog %s: crossing at %.3f / %.3f m (%.3f %.3f %.3f), 1:%.2f, side %d, blades %.3f / %.3f m, throat %.3f, nose %.3f, wing end %.3f, check rails %.3f-%.3f / %.3f-%.3f m", track->name().c_str(), + frog.at[0], frog.at[1], gauge.x, gauge.y, gauge.z, n, frog.side[0], frog.blades[0], frog.blades[1], frog.at[0] - track_rails::flangeway / slope, frog.at[0] + track_rails::nose_tip / slope, frog.at[0] + track_rails::wing_reach(slope), + frog.at[0] - 0.25 - check, frog.at[0] - 0.25 + check, frog.at[1] - 0.25 - check, frog.at[1] - 0.25 + check)); + } + 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 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; + words >> name; + auto const mix = [](std::uint64_t &Hash, float const Value) { + Hash = (Hash ^ static_cast(static_cast(std::llround(Value * 1e4)))) * 1099511628211ull; + }; + auto const exact = [](std::uint64_t &Hash, float const Value) { + std::uint32_t bits; + std::memcpy(&bits, &Value, sizeof(bits)); + Hash = (Hash ^ bits) * 1099511628211ull; + }; + auto const named = [](std::uint64_t &Hash, material_handle const Material) { + for (auto const letter : (Material != null_handle ? GfxRenderer->Material(Material)->GetName() : std::string{}) + '|') + Hash = (Hash ^ static_cast(letter)) * 1099511628211ull; + }; + std::uint64_t total{1469598103934665603ull}; + std::uint64_t exacttotal{1469598103934665603ull}; + std::size_t vertices{0}; + int tracks{0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1) + continue; + if (false == name.empty() && track->name() != name) + continue; + std::uint64_t hash{1469598103934665603ull}; + std::uint64_t exacthash{1469598103934665603ull}; + std::size_t count{0}; + int chunks{0}; + std::vector sequences{&track->Geometry1, &track->Geometry2, &track->GeometryCut1, &track->GeometryCut2}; + for (auto const &tread : track->GeometryTreads) + { + sequences.push_back(&tread.geometry); + sequences.push_back(&tread.cuts); + named(exacthash, tread.material); + named(exacthash, tread.matte); + } + for (auto const *sequence : sequences) + { + 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)) + { + for (int i = 0; i < 3; ++i) + { + mix(hash, vertex.position[i]); + mix(hash, vertex.normal[i]); + exact(exacthash, vertex.position[i]); + exact(exacthash, vertex.normal[i]); + } + mix(hash, vertex.texture.x); + mix(hash, vertex.texture.y); + exact(exacthash, vertex.texture.x); + exact(exacthash, vertex.texture.y); + ++count; + } + for (auto const &userdata : GfxRenderer->UserData(geometry)) + { + for (int i = 0; i < 4; ++i) + exact(exacthash, userdata.data[i]); + } + exact(exacthash, -2.f); + } + mix(hash, -1.f); + exact(exacthash, -1.f); + } + WriteLog(format("SELFTEST geom %s: chunks %d, vertices %zu, hash %016llx, exact %016llx", track->name().c_str(), chunks, count, static_cast(hash), static_cast(exacthash))); + total = (total ^ hash) * 1099511628211ull; + exacttotal = (exacttotal ^ exacthash) * 1099511628211ull; + vertices += count; + ++tracks; + } + WriteLog(format("SELFTEST geom total: tracks %d, vertices %zu, hash %016llx, exact %016llx", tracks, vertices, static_cast(total), static_cast(exacttotal))); + return selftest_result::handled; + } + if (command == "frogkey") + { + std::string name, value; + words >> name >> value; + auto *track{simulation::Paths.find(name)}; + if (track == nullptr || track->eType != tt_Switch || track->SwitchExtension == nullptr) + WriteLog(format("SELFTEST frogkey %s: no such switch", name.c_str())); + else + { + if (value == "on" || value == "off") + { + auto const before{editor_track::capture(*track)}; + track->SwitchExtension->m_frogs = (value == "on"); + push_track_snapshot({{track, before}}); + editor_track::commit_parameters(*track); + } + std::ostringstream text; + track->export_as_text(text); + WriteLog(format("SELFTEST frogkey %s: key %d, written %d, frog %d, crossings %zu, profile '%s'", name.c_str(), track->SwitchExtension->m_frogs ? 1 : 0, text.str().find(" frogs ") != std::string::npos ? 1 : 0, + track->frog().valid ? 1 : 0, track->rail_crossings().size(), track->m_profile1.first.c_str())); + } + 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 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>{ + {"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 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 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 pieces; + }; + std::map layouts; + std::map> rails; + auto const rails_of = [&](TTrack *Track) -> std::vector & { + 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 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 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 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; + words >> name; + int count{0}; + int faults{0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1) + continue; + if (false == name.empty() && track->name() != name) + continue; + auto const layout{track->plan_rails(true)}; + auto const size{track->rail_dimensions()}; + auto const guardoffset{-(track_rails::flangeway + size.head)}; + double const laterals[2]{size.low + guardoffset, track_rails::flangeway + size.head + size.low - 0.002 + guardoffset}; + for (int path = 0; path < 2; ++path) + { + for (int right = 0; right < 2; ++right) + { + auto const side{right ? 1 : -1}; + auto const length{track->eType == tt_Switch ? track->SwitchExtension->Segments[path]->GetLength() : track->Segment->GetLength()}; + auto const drawn = [&](glm::dvec2 const &Miter, double const Square) { + auto const at{Miter.y == 0.0 ? Square : Miter.x + Miter.y * 0.5 * (laterals[0] + laterals[1])}; + return at > 0.0 && at < length; + }; + for (auto const &guard : layout.guards[path][right]) + { + if (false == drawn(guard.frommiter, guard.from) && false == drawn(guard.tomiter, guard.to) && (guard.to <= 0.0 || guard.from >= length)) + continue; + ++count; + 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 ? "planed" : "square"}; + if (false == drawn(Miter, 0.0)) + return std::string{"past the end of the track"}; + glm::dvec3 points[2]; + for (int i = 0; i < 2; ++i) + points[i] = track_rails::rail_point(*track, path, side, Miter.x + Miter.y * laterals[i], laterals[i]); + auto best{1e9}; + double lateral{0.0}, along{0.0}; + TTrack *against{nullptr}; + int againstpath{0}, againstside{0}; + for (auto *other : layout.crossed) + for (int otherpath = 0; otherpath < (other->eType == tt_Switch ? 2 : 1); ++otherpath) + for (auto const otherside : {-1, 1}) + { + double d[2], l[2]; + for (int i = 0; i < 2; ++i) + track_rails::rail_offset(*other, otherpath, otherside, points[i], d[i], l[i]); + if (std::abs(l[0]) > 0.5 || std::abs(l[1]) > 0.5 || std::abs(l[0] - l[1]) >= best) + continue; + best = std::abs(l[0] - l[1]); + lateral = 0.5 * (l[0] + l[1]); + along = 0.5 * (d[0] + d[1]); + against = other; + againstpath = otherpath; + againstside = otherside; + } + if (against == nullptr) + { + ++faults; + return std::string{"cut along nothing FAULT"}; + } + auto result{format("along %s/%d%+d at %.3f m, %.1f mm off its gauge line, skew %.1f mm", against->name().c_str(), againstpath, againstside, along, lateral * 1000.0, best * 1000.0)}; + auto fault{best > 0.002}; + if (Glued) + { + auto const theirsize{against->rail_dimensions()}; + auto const theiroffset{-(track_rails::flangeway + theirsize.head)}; + double const theirlaterals[2]{theirsize.low + theiroffset, track_rails::flangeway + theirsize.head + theirsize.low - 0.002 + theiroffset}; + auto const reach = [&](glm::dvec2 const &Miter, double const Square, bool const Latest) { + if (Miter.y == 0.0) + return Square; + auto const a{Miter.x + Miter.y * theirlaterals[0]}; + auto const b{Miter.x + Miter.y * theirlaterals[1]}; + return Latest ? std::max(a, b) : std::min(a, b); + }; + auto covered{false}; + for (auto const &theirs : against->plan_rails(true).guards[againstpath][againstside > 0]) + covered |= (along > reach(theirs.frommiter, theirs.from, false) - 0.002 && along < reach(theirs.tomiter, theirs.to, true) + 0.002); + result += covered ? ", on its guard rail" : ", no guard rail there"; + fault |= (false == covered || std::abs(lateral + track_rails::flangeway) > 0.002); + } + if (fault) + { + ++faults; + result += " FAULT"; + } + return result; + }; + text += "from " + end(guard.frommiter, guard.flarefrom, false == guard.flarefrom) + "; to " + end(guard.tomiter, guard.flareto, false == guard.flareto); + for (auto const &cut : guard.plan.cuts) + if (cut.end > 0.0 && cut.start < length) + text += format("; break %.3f-%.3f: ", cut.end, cut.start) + end(cut.endmiter, false, false) + " / " + end(cut.startmiter, false, false); + WriteLog(format("SELFTEST guard %s path %d side %+d %.3f-%.3f m: %s", track->name().c_str(), path, side, guard.from, guard.to, text.c_str())); + } + } + } + } + WriteLog(format("SELFTEST guards: %d guard rails, %d faults", count, faults)); + return selftest_result::handled; + } + if (command == "throw") + { + // throw [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(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 = before + static_cast(part) * (track->SwitchExtension->fDesiredOffset - before); + track->RaAnimate(); + ++count; + } + WriteLog(format("SELFTEST switches: %d switches set to path %d", count, path)); + return selftest_result::handled; + } + if (command == "railtime") + { + track_rails::forget(); + auto const started{std::chrono::steady_clock::now()}; + int tracks{0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1) + continue; + track->rail_crossings(false); + ++tracks; + } + auto const crossed{std::chrono::steady_clock::now()}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1) + continue; + track->plan_rails(true); + } + auto const finished{std::chrono::steady_clock::now()}; + auto const crossings{std::chrono::duration(crossed - started).count()}; + auto const elapsed{std::chrono::duration(finished - started).count()}; + WriteLog(format("SELFTEST railtime: the rails of %d tracks laid out in %.1f ms, of which their crossings %.1f ms", tracks, elapsed, crossings)); + return selftest_result::handled; + } + if (command == "crossings") + { + std::string name; + words >> name; + auto const started{std::chrono::steady_clock::now()}; + int tracks{0}; + int found{0}; + int acutes{0}; + int middles{0}; + int mismatched{0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1) + continue; + if (false == name.empty() && track->name() != name) + continue; + ++tracks; + 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) + continue; + ++acutes; + auto const here{gauge(track, crossing)}; + auto partners{0}; + auto noses{crossing.nose ? 1 : 0}; + for (auto const &other : crossing.track->rail_crossings(false)) + { + if (other.track != track || other.angle > track_rails::wide_angle || other.facing >= 0.0) + continue; + auto const there{gauge(crossing.track, other)}; + if (glm::length(glm::dvec2{there.x - here.x, there.z - here.z}) > 0.05) + continue; + ++partners; + noses += other.nose ? 1 : 0; + } + if (partners != 1 || noses != 1) + ++mismatched; + WriteLog(format("SELFTEST acute %s %s path %d side %d at %.3f m with %s (%.2f %.2f %.2f): partners %d, noses %d", partners != 1 || noses != 1 ? "mismatch" : "ok", track->name().c_str(), crossing.path, crossing.side, crossing.at, crossing.track->name().c_str(), here.x, here.y, here.z, partners, noses)); + } + } + auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - started).count()}; + WriteLog(format("SELFTEST crossings: %d rail crossings on %d tracks, %d of them middle, %d acute, %d mismatched, %.1f ms", found, tracks, middles, acutes, mismatched, elapsed)); + return selftest_result::handled; + } + return selftest_result::unhandled; +} diff --git a/application/editormodeselftestscene.cpp b/application/editormodeselftestscene.cpp new file mode 100644 index 00000000..4a8cc66e --- /dev/null +++ b/application/editormodeselftestscene.cpp @@ -0,0 +1,567 @@ +/* +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 "application/editormode.h" +#include "editor/editorSettings.hpp" +#include "editor/editorGeometry.hpp" +#include "application/application.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" + +#include "simulation/simulation.h" +#include "simulation/simulationtime.h" +#include "scene/scenelayers.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Track.h" +#include "world/TrackRails.h" +#include "world/MemCell.h" +#include "world/Event.h" +#include "vehicle/DynObj.h" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "world/Sweep.h" +#include "input/keyboardinput.h" +#include "launcher/textures_scanner.h" +#include +#include +#include +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "scene/sceneterrain.h" +#include "editor/editorSpeed.hpp" + +#include +#include +#include +#include +#include +#include +#include "application/editormodeselftest.h" + +editor_mode::selftest_result editor_mode::selftest_scene(std::string const &command, std::istringstream &words) +{ + auto &test{m_selftest}; + if (command == "camera") + { + glm::dvec3 from; + glm::dvec3 to; + words >> from.x >> from.y >> from.z >> to.x >> to.y >> to.z; + auto const look{glm::normalize(to - from)}; + Camera.Pos = from; + Camera.Angle = glm::vec3(static_cast(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast(std::atan2(-look.x, -look.z)), 0.0f); + Global.pCamera = Camera; + 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}; + words >> point.x >> point.z; + TTrack *nearest{nullptr}; + double distance{1e9}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) + continue; + auto const d{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point)}; + if (d < distance) + { + distance = d; + nearest = track; + } + } + m_node = distance < 3.0 ? nearest : nullptr; + m_track_point = {}; + ui()->set_node(m_node); + WriteLog(format("SELFTEST selectat %.1f %.1f: %s", point.x, point.z, m_node != nullptr ? m_node->name().c_str() : "nothing")); + return selftest_result::handled; + } + if (command == "select") + { + std::string name; + words >> name; + m_node = simulation::Paths.find(name); + m_track_point = {}; + ui()->set_node(m_node); + 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{editor_selftest::frames() - lastframe}; + auto const elapsed{std::chrono::duration(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, editor_selftest::one_line(GfxRenderer->info_times()).c_str(), editor_selftest::one_line(GfxRenderer->info_stats()).c_str())); + last = now; + lastframe = editor_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 §ion{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(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 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(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 §iontime : 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 == "frames") + { + std::string name; + double from{0.0}; + double to{0.0}; + double step{0.1}; + words >> name >> from >> to >> step; + auto *sweep{simulation::Sweeps.find(name)}; + if (sweep == nullptr) + return selftest_result::handled; + auto const side{sweep->definition().lateral >= 0.0 ? 1 : 0}; + if (step <= 0.0) + return selftest_result::handled; + for (int i = 0; from + i * step <= to + 1e-9; ++i) + { + auto const station{from + i * step}; + auto const at{sweep->frame_at(station)}; + WriteLog(format("SELFTEST frame %s %.2f: %.3f %.3f %.3f widening %.4f cant %.4f setback %.4f", name.c_str(), station, at.position.x, at.position.y, at.position.z, at.widening[side], at.cant[side], sweep->setback(at))); + } + return selftest_result::handled; + } + if (command == "weather") + { + auto const &time{simulation::Time.data()}; + WriteLog(format("SELFTEST weather: %02d:%02d, fog end %.0f m, overcast %.2f", time.wHour, time.wMinute, Global.fFogEnd, Global.Overcast)); + return selftest_result::handled; + } + if (command == "projtest") + { + screen_projection const projection; + for (std::size_t i = 0; i < m_sweep.outline.size() && i < 45; i += 4) + { + ImVec2 screen; + auto const shown{projection.project(m_sweep.outline[i], screen)}; + auto const clip{projection.clip(m_sweep.outline[i])}; + WriteLog(format("SELFTEST proj %zu: %.1f %.1f %.1f -> %d %.0f %.0f w %.2f", i, m_sweep.outline[i].x, m_sweep.outline[i].y, m_sweep.outline[i].z, shown ? 1 : 0, screen.x, screen.y, clip.w)); + } + return selftest_result::handled; + } + if (command == "newscenery") + { + WriteLog(restart_for_new_scenery() ? "SELFTEST newscenery: started" : "SELFTEST newscenery: failed"); + return selftest_result::pause; + } + if (command == "vehicles") + { + for (auto *vehicle : simulation::Vehicles.sequence()) + { + if (vehicle == nullptr) + continue; + auto const head{vehicle->HeadPosition()}; + auto const front{vehicle->VectorFront()}; + auto const coupler = [&](int const End) { + auto const &coupler{vehicle->MoverParameters->Couplers[End]}; + return coupler.Connected != nullptr ? coupler.Connected->Name + "/" + std::to_string(coupler.CouplingFlag) : std::string{"-"}; + }; + WriteLog(format("SELFTEST vehicle %s head %.2f %.2f front %.2f %.2f dir %d coupler0 %s coupler1 %s", vehicle->name().c_str(), head.x, head.z, front.x, front.z, vehicle->iDirection, coupler(0).c_str(), coupler(1).c_str())); + } + return selftest_result::handled; + } + if (command == "mouseat") + { + glm::dvec3 at{0.0}; + words >> at.x >> at.y >> at.z; + ImVec2 projected; + if (false == screen_projection{}.project(at, projected)) + { + WriteLog("SELFTEST mouseat: behind the camera"); + return selftest_result::handled; + } + test.input.mouse_to(glm::dvec2{projected.x, projected.y}); + WriteLog(format("SELFTEST mouse %.0f %.0f", projected.x, projected.y)); + return selftest_result::handled; + } + if (command == "glide") + { + glm::dvec3 at{0.0}; + int steps{8}; + int every{2}; + std::string shots; + words >> at.x >> at.y >> at.z >> steps >> every >> shots; + ImVec2 projected; + if (false == screen_projection{}.project(at, projected)) + return selftest_result::handled; + glm::dvec2 const from{test.input.mouse().value_or(glm::dvec2{projected.x, projected.y})}; + glm::dvec2 const to{projected.x, projected.y}; + std::vector expanded; + for (int step = 1; step <= std::max(1, steps); ++step) + { + auto const t{static_cast(step) / std::max(1, steps)}; + auto const eased{t * t * (3.0 - 2.0 * t)}; + auto const point{glm::mix(from, to, eased)}; + expanded.push_back(format("mouse %.1f %.1f", point.x, point.y)); + expanded.push_back("wait " + std::to_string(std::max(1, every))); + if (shots == "shot") + expanded.push_back("screenshot"); + } + test.lines.insert(test.lines.begin() + static_cast(test.next), expanded.begin(), expanded.end()); + return selftest_result::handled; + } + if (test.input.dispatch(command, words)) + return selftest_result::handled; + if (command == "screenshot") + { + Application.queue_screenshot(); + test.wait = 3; + return selftest_result::pause; + } + if (command == "save") + { + save(); + return selftest_result::handled; + } + if (command == "ortho") + { + float extent{0.0f}; + words >> extent; + choose_work_area(work_area::tracks); + if (Global.EditorOrtho == extent < 0.0f) + toggle_ortho(); + if (extent > 0.0f) + Global.EditorOrthoExtent = std::clamp(extent, 5.0f, 50000.0f); + WriteLog(format("SELFTEST ortho extent %.0f", Global.EditorOrthoExtent)); + return selftest_result::handled; + } + if (command == "mapstate") + { + auto const marked{scene::Layers.empty() ? std::vector{} : scene::Layers.marked(scene::layer_handle{1}, "//$o")}; + 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") + { + std::string name; + std::string vehicle; + std::string skin; + int facing{1}; + words >> name >> vehicle >> skin >> facing >> m_vehicle.velocity; + auto *track{simulation::Paths.find(name)}; + if (track == nullptr) + return selftest_result::handled; + auto desc{std::make_shared()}; + desc->path = vehicle; + auto set{std::make_shared()}; + set->skin = skin; + m_vehicle.consist.clear(); + m_vehicle.vehicle = desc; + m_vehicle.skin = set; + vehicle_start(*track, geometry::bezier{track->m_paths.front()}.point(0.5)); + m_vehicle.facing = facing; + auto const placed{vehicle_place()}; + WriteLog(format("SELFTEST trainset %s placed %d pending %d: %s", name.c_str(), placed ? 1 : 0, static_cast(m_vehicle.trainsets.size()), m_vehicle.status.c_str())); + return selftest_result::handled; + } + if (command == "selectmodel") + { + glm::dvec3 point{0.0}; + words >> point.x >> point.z; + TAnimModel *nearest{nullptr}; + auto best{50.0}; + for (auto *model : simulation::Instances.sequence()) + { + if (model == nullptr) + continue; + auto const distance{glm::length(glm::dvec2{model->location().x - point.x, model->location().z - point.z})}; + if (distance < best) + { + best = distance; + nearest = model; + } + } + m_node = nearest; + select_include(0); + ui()->set_node(m_node); + WriteLog("SELFTEST selectmodel: " + (nearest != nullptr ? format("%s at %.2f %.2f %.2f", nearest->name().c_str(), nearest->location().x, nearest->location().y, nearest->location().z) : std::string{"none"})); + return selftest_result::handled; + } + if (command == "addmodel") + { + glm::dvec3 point{0.0}; + words >> point.x >> point.z; + TAnimModel *nearest{nullptr}; + auto best{50.0}; + for (auto *model : simulation::Instances.sequence()) + { + if (model == nullptr) + continue; + auto const distance{glm::length(glm::dvec2{model->location().x - point.x, model->location().z - point.z})}; + if (distance < best) + { + best = distance; + nearest = model; + } + } + select_node(nearest, true); + WriteLog("SELFTEST addmodel: " + (nearest != nullptr ? format("%s at %.2f %.2f %.2f", nearest->name().c_str(), nearest->location().x, nearest->location().y, nearest->location().z) : std::string{"none"})); + return selftest_result::handled; + } + if (command == "selection") + { + std::string text; + for (auto *node : selection_with_groups()) + text += format(" [%s %.2f %.2f %.2f group %d]", node->name().c_str(), node->location().x, node->location().y, node->location().z, static_cast(node->group())); + WriteLog("SELFTEST selection:" + text); + return selftest_result::handled; + } + if (command == "moveselection" && m_node != nullptr) + { + glm::dvec3 offset{0.0}; + words >> offset.x >> offset.y >> offset.z; + selection_snapshots(EditorSnapshot::Action::Move); + auto const before{m_node->location()}; + m_editor.translate(m_node, before + offset, true); + selection_follow(m_node->location() - before, glm::vec3(0.0f), m_node->location()); + return selftest_result::handled; + } + if (command == "turnselection" && m_node != nullptr) + { + float degrees{0.0f}; + words >> degrees; + selection_snapshots(EditorSnapshot::Action::Rotate); + glm::vec3 const turned{0.0f, degrees, 0.0f}; + m_editor.rotate(m_node, turned, 0.0f); + selection_follow(glm::dvec3(0.0), turned, m_node->location()); + return selftest_result::handled; + } + if (command == "copy") + { + copy_selection(); + return selftest_result::handled; + } + if (command == "paste") + { + glm::dvec3 point{0.0}; + words >> point.x >> point.y >> point.z; + paste_clipboard(point); + return selftest_result::handled; + } + if (command == "group") + { + group_selection(); + return selftest_result::handled; + } + if (command == "ungroup") + { + ungroup_selection(); + return selftest_result::handled; + } + if (command == "drop") + { + drop_selection_to_ground(); + return selftest_result::handled; + } + if (command == "delete") + { + delete_selected(); + return selftest_result::handled; + } + if (command == "selectinclude") + { + glm::dvec3 point{0.0}; + words >> point.x >> point.z; + scene::instance_handle nearest{0}; + auto best{50.0}; + m_node = nullptr; + for (scene::instance_handle instance = 1; instance <= scene::Layers.instance_count(); ++instance) + { + if (false == scene::Layers.removable(instance)) + continue; + select_include(instance); + bend_source source; + if (false == bend_source_of_selection(source)) + continue; + auto const at{glm::dvec3(source.transform[3])}; + auto const distance{glm::length(glm::dvec2{at.x - point.x, at.z - point.z})}; + if (distance < best) + { + best = distance; + nearest = instance; + } + } + select_include(nearest); + WriteLog("SELFTEST selectinclude: " + (nearest != 0 ? *scene::Layers.instance(nearest).file : std::string{"none"})); + return selftest_result::handled; + } + if (command == "modelbox") + { + auto *model{dynamic_cast(m_node)}; + bend_source source; + if (model == nullptr || false == bend_source_of_selection(source)) + { + WriteLog("SELFTEST modelbox: no model selected"); + return selftest_result::handled; + } + scene::node_data data; + sweep_node probe{data}; + sweep_node::state state; + state.model = source.file; + state.skin = source.skin; + state.scale = source.scale; + probe.define(state); + glm::dvec3 boxlow{std::numeric_limits::max()}; + glm::dvec3 boxhigh{-std::numeric_limits::max()}; + for (auto const &item : probe.items()) + { + auto const transform{source.transform * glm::scale(glm::dmat4(1.0), source.scale) * item.transform}; + std::vector pending{item.model->GetSMRoot()}; + while (false == pending.empty()) + { + auto *submodel{pending.back()}; + pending.pop_back(); + for (; submodel != nullptr; submodel = submodel->Next) + { + if (submodel->Child != nullptr) + pending.push_back(submodel->Child); + if (submodel->eType != GL_TRIANGLES || submodel->fSquareMinDist > 0.f || (submodel->m_geometry.handle.bank == 0 && submodel->m_geometry.handle.chunk == 0)) + continue; + glm::dmat4 local{1.0}; + for (auto *parent = submodel; parent != nullptr; parent = parent->Parent) + if ((parent->iFlags & 0xC000) != 0 && parent->GetMatrix() != nullptr) + local = glm::dmat4(glm::make_mat4(parent->GetMatrix()->readArray())) * local; + for (auto const &vertex : GfxRenderer->Vertices(submodel->m_geometry.handle)) + { + auto const world{glm::dvec3(transform * local * glm::dvec4(glm::dvec3(vertex.position), 1.0))}; + boxlow = glm::min(boxlow, world); + boxhigh = glm::max(boxhigh, world); + } + } + } + } + WriteLog(format("SELFTEST modelbox: %.2f %.2f %.2f - %.2f %.2f %.2f", boxlow.x, boxlow.y, boxlow.z, boxhigh.x, boxhigh.y, boxhigh.z)); + return selftest_result::handled; + } + if (command == "key") + { + std::string key; + std::string action; + std::string mods; + words >> key >> action >> mods; + if (key == "RMB") + m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT, action == "press" ? GLFW_PRESS : GLFW_RELEASE); + else + { + auto const code{key == "S" ? GLFW_KEY_S : key == "W" ? GLFW_KEY_W : key == "CTRL" ? GLFW_KEY_LEFT_CONTROL : 0}; + on_key(code, 0, action == "press" ? GLFW_PRESS : GLFW_RELEASE, mods == "ctrl" ? GLFW_MOD_CONTROL : 0); + } + WriteLog(format("SELFTEST key %s %s %s: camera %.2f %.2f %.2f, key S %d, right button %d", key.c_str(), action.c_str(), mods.c_str(), Camera.Pos.x, Camera.Pos.y, Camera.Pos.z, static_cast(input::keys[GLFW_KEY_S]), m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT))); + return selftest_result::handled; + } + if (command == "event") + { + std::string name; + words >> name; + auto *event{simulation::Events.FindEvent(name)}; + if (event != nullptr) + simulation::Events.AddToQuery(event, nullptr); + WriteLog("SELFTEST event " + name + (event != nullptr ? " queued" : ": no such event")); + return selftest_result::handled; + } + if (command == "memcell") + { + std::string name; + words >> name; + auto *cell{simulation::Memory.find(name)}; + WriteLog("SELFTEST memcell " + name + (cell != nullptr ? format(": %s %.1f %.1f", cell->Text().c_str(), cell->Value1(), cell->Value2()) : std::string{": none"})); + return selftest_result::handled; + } + if (command == "newscenerypopup") + { + m_newscenery_asked = true; + return selftest_result::handled; + } + if (command == "openscenerypopup") + { + m_openscenery_asked = true; + std::snprintf(m_openscenery_search, sizeof(m_openscenery_search), "%s", "zz_"); + return selftest_result::handled; + } + if (command == "openscenery") + { + std::string name; + words >> name; + WriteLog(restart_editor(name) ? "SELFTEST openscenery: started " + name : "SELFTEST openscenery: failed"); + return selftest_result::handled; + } + if (command == "undo") + { + undo_last(); + WriteLog("SELFTEST undo"); + return selftest_result::handled; + } + if (command == "redo") + { + redo_last(); + WriteLog("SELFTEST redo"); + return selftest_result::handled; + } + if (command == "log") + { + std::string text; + std::getline(words, text); + WriteLog("SELFTEST" + text); + return selftest_result::handled; + } + return selftest_result::unhandled; +} diff --git a/application/editormodeselftestsignal.cpp b/application/editormodeselftestsignal.cpp new file mode 100644 index 00000000..7d706ac9 --- /dev/null +++ b/application/editormodeselftestsignal.cpp @@ -0,0 +1,201 @@ +/* +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 "application/editormode.h" +#include "editor/editorSettings.hpp" +#include "editor/editorGeometry.hpp" +#include "application/application.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" + +#include "simulation/simulation.h" +#include "simulation/simulationtime.h" +#include "scene/scenelayers.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Track.h" +#include "world/TrackRails.h" +#include "world/MemCell.h" +#include "world/Event.h" +#include "vehicle/DynObj.h" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "world/Sweep.h" +#include "input/keyboardinput.h" +#include "launcher/textures_scanner.h" +#include +#include +#include +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "scene/sceneterrain.h" +#include "editor/editorSpeed.hpp" + +#include +#include +#include +#include +#include +#include +#include "application/editormodeselftest.h" + +editor_mode::selftest_result editor_mode::selftest_signal(std::string const &command, std::istringstream &words) +{ + if (command == "signal" || command == "signalmove" || command == "signaldelete" || command == "signalrename" || command == "signalprobe") + { + std::map values; + std::string pair; + while (words >> pair) + { + auto const equals{pair.find('=')}; + if (equals != std::string::npos) + values[pair.substr(0, equals)] = pair.substr(equals + 1); + } + show_track_tab(track_tab::signals); + signal_refresh(true); + auto &tool{m_signal}; + auto const standing_named = [&](std::string const &Name) -> signal_standing const * { + auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.name == Name; })}; + return found != tool.standing.end() ? &*found : nullptr; + }; + auto const spot_on = [&](signal_template const &Template, int const Facing, int const Side, signal_spot &Spot) { + auto *track{simulation::Paths.find(values["track"])}; + if (track == nullptr) + return false; + auto const path{values.count("path") ? std::stoi(values["path"]) : 0}; + auto const at{values.count("at") ? std::stod(values["at"]) : 0.5}; + geometry::bezier const curve{track->m_paths[std::clamp(path, 0, static_cast(track->m_paths.size()) - 1)]}; + return signal_spot_at(*track, path, curve.point(at), curve.point(at), Facing, Side, Template, Spot); + }; + if (command == "signal") + { + auto const *item{signal_template_of(values["file"])}; + if (item == nullptr) + { + WriteLog("SELFTEST signal: no signal template " + values["file"]); + return selftest_result::handled; + } + for (int k = 0; k < static_cast(tool.templates.size()); ++k) + if (&tool.templates[k] == item) + { + static char const *const kinds[] = {"main", "block", "warning", "shunt", "stop", "substitute", "repeater"}; + for (int i = 0; i < 7; ++i) + if (item->kind == kinds[i]) + { + tool.kind = i; + tool.chosen[i] = k; + } + } + if (values.count("name")) + std::snprintf(tool.name, sizeof(tool.name), "%s", values["name"].c_str()); + if (values.count("plate")) + std::snprintf(tool.plate, sizeof(tool.plate), "%s", values["plate"].c_str()); + auto const side{values["side"] == "left" ? -1 : 1}; + auto const facing{values["facing"] == "along" ? 1 : values["facing"] == "against" ? -1 : 0}; + signal_spot spot; + if (false == spot_on(*signal_armed(), facing, side, spot)) + { + WriteLog("SELFTEST signal: no such track " + values["track"]); + return selftest_result::handled; + } + auto const placed{signal_place(spot, *signal_armed())}; + WriteLog(format("SELFTEST signal %s: %s", placed ? "placed" : "failed", tool.status.c_str())); + } + else if (command == "signalmove") + { + auto const *standing{standing_named(values["name"])}; + auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr}; + if (item == nullptr) + { + WriteLog("SELFTEST signalmove: no signal " + values["name"]); + return selftest_result::handled; + } + auto const yaw{glm::radians(standing->include.yaw + 180.0)}; + glm::dvec2 const travel{std::sin(yaw), std::cos(yaw)}; + signal_spot probe; + if (false == spot_on(*item, 1, 1, probe)) + { + WriteLog("SELFTEST signalmove: no such track " + values["track"]); + return selftest_result::handled; + } + auto const facing{glm::dot(probe.travel, travel) >= 0.0 ? 1 : -1}; + auto const sideoftravel{glm::dot(geometry::plan_of(standing->include.location) - geometry::plan_of(standing->spot.axis), glm::dvec2{-travel.y, travel.x}) >= 0.0 ? 1 : -1}; + signal_select(standing->include.instance); + tool.moving = true; + tool.moved_valid = spot_on(*item, facing, sideoftravel * facing, tool.moved); + tool.pressed = {-1000.f, -1000.f}; + signal_release(); + WriteLog("SELFTEST signalmove: " + tool.status); + } + else if (command == "signaldelete") + { + auto const *standing{standing_named(values["name"])}; + auto const deleted{standing != nullptr && signal_delete(standing->include.instance)}; + WriteLog(std::string{"SELFTEST signaldelete "} + values["name"] + (deleted ? ": removed with its event" : ": not removed by the signal tool")); + } + else if (command == "signalrename") + { + auto const *standing{standing_named(values["name"])}; + if (standing == nullptr) + { + WriteLog("SELFTEST signalrename: no signal " + values["name"]); + return selftest_result::handled; + } + select_include(standing->include.instance); + m_include.values[0] = values["to"]; + apply_include(); + m_include_gesture = false; + WriteLog("SELFTEST signalrename " + values["name"] + " -> " + values["to"]); + } + else + { + signal_refresh(true); + WriteLog(format("SELFTEST signalprobe: %zu signals", tool.standing.size())); + for (auto const &standing : tool.standing) + { + std::string where{"not read"}; + if (standing.track != nullptr) + { + auto const &curve{standing.track->m_paths.front()}; + geometry::bezier const bez{curve}; + auto best{std::numeric_limits::max()}; + double parameter{0.0}; + for (int k = 0; k <= 1024; ++k) + { + auto const d{geometry::plan_distance(bez.point(k / 1024.0), standing.include.location)}; + if (d < best) + { + best = d; + parameter = k / 1024.0; + } + } + auto const axis{bez.point(parameter)}; + auto tangent{bez.first(parameter)}; + glm::dvec2 travel{tangent.x, tangent.z}; + travel = glm::normalize(travel) * (standing.list == 1 ? -1.0 : 1.0); + glm::dvec2 const offset{standing.include.location.x - axis.x, standing.include.location.z - axis.z}; + auto const cross{travel.x * offset.y - travel.y * offset.x}; + auto heading{glm::degrees(std::atan2(travel.x, travel.y))}; + auto turn{std::fmod(standing.include.yaw - heading + 720.0, 360.0)}; + auto const &list{editor_track::events(*standing.track, standing.list)}; + auto const bound{std::any_of(list.begin(), list.end(), [&](auto const &Event) { return Event.second != nullptr && ToLower(Event.first) == ToLower(standing.read); })}; + where = std::string{bound ? "bound, " : "unbound, "} + format("%s of %s, %.3f m from the axis, %s of the trains, %.3f m over the axis, model turned %.1f deg from the travel", editor_track::event_keyword(standing.list), standing.track->name().c_str(), glm::length(offset), cross > 0.0 ? "right" : "left", + standing.include.location.y - axis.y, turn); + } + WriteLog(format("SELFTEST signalprobe %s %s at %.3f %.3f %.3f yaw %.2f read %s: %s", standing.name.c_str(), standing.include.file.c_str(), standing.include.location.x, standing.include.location.y, standing.include.location.z, standing.include.yaw, + standing.read.empty() ? "-" : standing.read.c_str(), where.c_str())); + } + } + return selftest_result::handled; + } + return selftest_result::unhandled; +} diff --git a/application/editormodeselftestsleepers.cpp b/application/editormodeselftestsleepers.cpp new file mode 100644 index 00000000..15cf6d92 --- /dev/null +++ b/application/editormodeselftestsleepers.cpp @@ -0,0 +1,164 @@ +/* +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 "application/editormode.h" +#include "editor/editorSettings.hpp" +#include "editor/editorGeometry.hpp" +#include "application/application.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" + +#include "simulation/simulation.h" +#include "simulation/simulationtime.h" +#include "scene/scenelayers.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Track.h" +#include "world/TrackRails.h" +#include "world/MemCell.h" +#include "world/Event.h" +#include "vehicle/DynObj.h" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "world/Sweep.h" +#include "input/keyboardinput.h" +#include "launcher/textures_scanner.h" +#include +#include +#include +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "scene/sceneterrain.h" +#include "editor/editorSpeed.hpp" + +#include +#include +#include +#include +#include +#include +#include "application/editormodeselftest.h" + +#include "world/TrackSleepers.h" + +editor_mode::selftest_result editor_mode::selftest_sleepers(std::string const &command, std::istringstream &words) +{ + 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; 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(result.digest), + std::chrono::duration(std::chrono::steady_clock::now() - start).count())); + for (auto const &example : result.examples) + WriteLog("SELFTEST sleepercheck " + example); + return selftest_result::handled; + } + return selftest_result::unhandled; +} diff --git a/application/editormodeselftestsweep.cpp b/application/editormodeselftestsweep.cpp new file mode 100644 index 00000000..03ba0e21 --- /dev/null +++ b/application/editormodeselftestsweep.cpp @@ -0,0 +1,429 @@ +/* +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 "application/editormode.h" +#include "editor/editorSettings.hpp" +#include "editor/editorGeometry.hpp" +#include "application/application.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" + +#include "simulation/simulation.h" +#include "simulation/simulationtime.h" +#include "scene/scenelayers.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Track.h" +#include "world/TrackRails.h" +#include "world/MemCell.h" +#include "world/Event.h" +#include "vehicle/DynObj.h" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "world/Sweep.h" +#include "input/keyboardinput.h" +#include "launcher/textures_scanner.h" +#include +#include +#include +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "scene/sceneterrain.h" +#include "editor/editorSpeed.hpp" + +#include +#include +#include +#include +#include +#include +#include "application/editormodeselftest.h" + +editor_mode::selftest_result editor_mode::selftest_sweep(std::string const &command, std::istringstream &words) +{ + if (command == "sweep") + { + std::map values; + std::string pair; + while (words >> pair) + { + auto const equals{pair.find('=')}; + if (equals != std::string::npos) + values[pair.substr(0, equals)] = pair.substr(equals + 1); + } + auto &tool{m_sweep}; + auto const get = [&](char const *Key, std::string const &Default) { auto const found{values.find(Key)}; return found != values.end() ? found->second : Default; }; + tool.edited = nullptr; + tool.curve_for = nullptr; + std::snprintf(tool.model, sizeof(tool.model), "%s", get("model", "").c_str()); + tool.parameters.clear(); + std::stringstream parameters(get("params", "")); + std::string parameter; + while (std::getline(parameters, parameter, ',')) + { + tool.parameters.emplace_back(); + std::snprintf(tool.parameters.back().data(), tool.parameters.back().size(), "%s", parameter.c_str()); + } + tool.parameters_for = tool.model; + auto &settings{tool.settings}; + settings.lateral = std::stod(get("lateral", "0")); + settings.height = std::stod(get("height", "0")); + auto const side{get("side", "origin")}; + settings.side_anchor = side == "near" ? 1 : side == "far" ? 2 : side == "centre" ? 3 : 0; + auto const top{get("top", "origin")}; + settings.height_anchor = top == "bottom" ? 1 : top == "top" ? 2 : top == "edge" ? 3 : 0; + settings.bend = get("mode", "bend") != "repeat"; + settings.step = std::stod(get("step", "0")); + settings.widen = get("widen", "0") == "1"; + settings.platform = get("platform", "0") == "1"; + settings.face = get("face", "1") == "1"; + settings.tilt = get("tilt", "0") == "1"; + settings.from = std::stod(get("from", "0")); + settings.to = std::stod(get("to", "-1")); + auto const length{get("length", "all")}; + tool.length_mode = length == "original" ? 0 : length == "all" ? 2 : 1; + if (tool.length_mode == 1) + tool.length = std::stod(length); + if (values.count("edge")) + EditorSettings.platform_edge(std::stod(values["edge"])); + tool.measured.clear(); + tool.open = true; + sweep_curve(); + sweep_measure(); + if (values.count("widen")) + settings.widen = get("widen", "0") == "1"; + if (values.count("platform")) + settings.platform = get("platform", "0") == "1"; + if (get("create", "1") == "1") + { + sweep_create(); + WriteLog("SELFTEST sweep: " + tool.status); + } + return selftest_result::handled; + } + if (command == "drag") + { + glm::dvec3 from{0.0}; + glm::dvec3 to{0.0}; + words >> from.x >> from.z >> to.x >> to.z; + m_sweep.open = true; + if (false == sweep_press_at(from, false)) + { + WriteLog("SELFTEST drag: the press wasn't taken"); + return selftest_result::handled; + } + sweep_drag_at(to); + sweep_release(); + WriteLog("SELFTEST drag: " + m_sweep.status); + return selftest_result::handled; + } + if (command == "fouling") + { + std::map values; + std::string pair; + while (words >> pair) + { + auto const equals{pair.find('=')}; + if (equals != std::string::npos) + values[pair.substr(0, equals)] = pair.substr(equals + 1); + } + auto &tool{m_fouling}; + if (values["show"] == "1") + { + show_track_tab(track_tab::lineside); + tool.expand = true; + } + tool.scope = std::stoi(values.count("scope") ? values["scope"] : "2"); + tool.height_mode = values["mode"] == "ground" ? 0 : 1; + tool.height = std::stod(values.count("height") ? values["height"] : "0"); + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling: " + tool.status); + int shown{0}; + for (auto const &found : tool.found) + if (found.wrong && shown++ < 5) + WriteLog(format("SELFTEST fouling: %s %.2f m off, %+.3f m high", found.marker.file.c_str(), found.error, found.height_error)); + if (values["place"] == "one") + { + // the first fouling point without a marker alone, as a click on it in the view + for (int index = 0; index < static_cast(tool.points.size()); ++index) + if (tool.points[index].standing < 0) + { + fouling_place(index); + break; + } + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling one: " + tool.status); + } + if (values["place"] == "1") + { + fouling_place(); + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling after placing: " + tool.status); + } + if (values["fix"] == "1") + { + fouling_fix(); + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling after the fix: " + tool.status); + } + return selftest_result::handled; + } + if (command == "hekto") + { + std::map values; + std::string pair; + while (words >> pair) + { + auto const equals{pair.find('=')}; + if (equals != std::string::npos) + values[pair.substr(0, equals)] = pair.substr(equals + 1); + } + auto *track{simulation::Paths.find(values["track"])}; + if (track == nullptr) + { + WriteLog("SELFTEST hekto: no such track"); + return selftest_result::handled; + } + hekto_start(*track, track->m_paths.front().points[segment_data::point::start]); + auto &tool{m_hekto}; + tool.dirty = true; + hekto_update(); + if (values.count("km")) + { + tool.km = std::stod(values["km"]); + tool.growth = values["growth"] == "-1" ? -1 : 1; + } + tool.side = values["side"] == "alt" ? 0 : values["side"] == "left" ? -1 : 1; + tool.dirty = true; + hekto_update(); + auto const missing{std::count_if(tool.posts.begin(), tool.posts.end(), [](hekto_post const &Post) { return Post.standing < 0; })}; + WriteLog(format("SELFTEST hekto: km %.3f growth %d, %zu posts along %.0f m, %d to place, %zu standing. %s", tool.km, tool.growth, tool.posts.size(), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, static_cast(missing), tool.found.size(), tool.status.c_str())); + for (std::size_t i = 0; i < tool.posts.size() && i < 4; ++i) + { + auto const &post{tool.posts[i]}; + double lateral; + glm::dvec3 const axis{editor_track::sampled_position(tool.samples, post.chainage)}; + auto const direction{editor_track::sampled_position(tool.samples, post.chainage + 1.0) - axis}; + glm::dvec2 const offset{post.position.x - axis.x, post.position.z - axis.z}; + lateral = glm::length(offset); + auto const right{(direction.x * offset.y - direction.z * offset.x) * tool.growth >= 0.0}; + WriteLog(format("SELFTEST hekto post %d.%d at %.1f m, %.2f m from the axis, %s of the growing kilometrage, at %.1f %.1f %.1f", post.hectometres / 10, post.hectometres % 10, post.chainage, lateral, right ? "right" : "left", post.position.x, post.position.y, + post.position.z)); + } + if (values["place"] == "1") + { + hekto_place(); + WriteLog("SELFTEST hekto: " + tool.status); + } + return selftest_result::handled; + } + if (command == "spread") + { + std::string name; + words >> name >> m_spread.stop >> m_spread.reach; + if (auto *track{simulation::Paths.find(name)}) + { + std::string names; + for (auto const *other : track_spread_from(*track)) + names += ' ' + other->name(); + WriteLog("SELFTEST spread " + name + ":" + names + " | prev " + (track->trPrev ? track->trPrev->name() : std::string{"-"}) + " next " + (track->trNext ? track->trNext->name() : std::string{"-"})); + } + return selftest_result::handled; + } + if (command == "turntable") + { + glm::dvec3 centre{0.0}; + double yaw{0.0}; + words >> centre.x >> centre.y >> centre.z >> yaw; + turntable_place(centre, yaw); + for (double angle; words >> angle;) + { + m_turntable.heading = m_turntable.table_yaw + angle; + m_turntable.exit_length = 10.0; + track_intent intent; + intent.what = track_intent::kind::turntable_exit; + turntable_press(intent); + } + WriteLog("SELFTEST turntable: " + (m_turntable.error.empty() ? m_turntable.status : m_turntable.error)); + return selftest_result::handled; + } + if (command == "bend" || command == "bendset") + { + std::map values; + std::string pair; + while (words >> pair) + { + auto const equals{pair.find('=')}; + if (equals != std::string::npos) + values[pair.substr(0, equals)] = pair.substr(equals + 1); + } + auto const vector = [](std::string Text) { + std::replace(Text.begin(), Text.end(), ',', ' '); + std::istringstream numbers(Text); + glm::dvec3 result{0.0}; + numbers >> result.x >> result.y >> result.z; + return result; + }; + auto const index = [](std::string const &Text, std::vector const &Names) { + return static_cast(std::find(Names.begin(), Names.end(), Text) - Names.begin()); + }; + auto &tool{m_bend}; + auto const along{values.count("along") ? index(values["along"], {"start", "origin", "middle", "end", "point"}) : tool.along_anchor}; + auto const side{values.count("side") ? index(values["side"], {"origin", "near", "far", "middle", "point"}) : tool.side_anchor}; + auto const height{values.count("height") ? index(values["height"], {"origin", "bottom", "top", "edge", "point"}) : tool.height_anchor}; + auto const point{values.count("point") ? vector(values["point"]) : tool.point}; + if (command == "bend") + { + tool.edited = nullptr; + tool.along_anchor = along; + tool.side_anchor = side; + tool.height_anchor = height; + tool.point = point; + tool.axis = values.count("axis") ? index(values["axis"], {"auto", "z", "x"}) : 0; + tool.turned = values["turned"] == "1"; + bend_source source; + if (false == bend_source_of_selection(source)) + { + std::string reason; + auto const editable{m_node != nullptr && scene::Layers.editable(m_node, &reason)}; + WriteLog(format("SELFTEST bend: nothing to bend (node %p template %d editable %d %s, instance %d)", static_cast(m_node), m_node != nullptr && m_node->from_template() ? 1 : 0, editable ? 1 : 0, reason.c_str(), static_cast(m_instance))); + return selftest_result::handled; + } + tool.tracks_for = nullptr; + bend_find_tracks(glm::dvec3(source.transform[3]), source.model != nullptr ? static_cast(source.model) : reinterpret_cast(static_cast(source.instance))); + tool.track = values.count("track") ? std::stoi(values["track"]) : 0; + if (values.count("pick")) + bend_pick(vector(values["pick"])); + if (tool.tracks.empty()) + { + WriteLog("SELFTEST bend: no track nearby"); + return selftest_result::handled; + } + sweep_node::state state; + std::vector tracks; + bend_definition(source, *tool.tracks[tool.track].first, state, tracks); + WriteLog(format("SELFTEST bend: track %s, %zu paths, anchor before %.3f %.3f %.3f, lateral %.3f height %.3f from %.3f to %.3f, axis %s flip %d; %s", tool.tracks[tool.track].first->name().c_str(), tracks.size(), + tool.marker.x, tool.marker.y, tool.marker.z, state.lateral, state.height, state.from, state.to, state.along_x ? "x" : "z", state.flip ? 1 : 0, tool.status.c_str())); + if (values["go"] == "1") + { + bend_selection(); + WriteLog("SELFTEST bend: " + tool.status); + } + } + else if (auto *sweep{tool.edited}) + { + auto state{sweep->definition()}; + tool.along_anchor = along; + if (side != tool.side_anchor || height != tool.height_anchor || point != tool.point) + { + tool.side_anchor = side; + tool.height_anchor = height; + tool.point = point; + bend_reanchor(state, side, height, point); + } + if (values.count("lateral")) + state.lateral = std::stod(values["lateral"]); + if (values.count("up")) + state.height = std::stod(values["up"]); + if (values.count("widen")) + state.widen = values["widen"] == "1"; + if (values.count("pick")) + { + bend_pick(vector(values["pick"])); + state = sweep->definition(); + } + if (values.count("length")) + { + bend_stretch(*sweep, bend_fixed(*sweep, state.point), std::stod(values["length"]), state.point, state); + } + bend_edit(state); + WriteLog("SELFTEST bendset done"); + } + return selftest_result::handled; + } + if (command == "bendprobe") + { + auto *sweep{m_bend.edited}; + if (sweep == nullptr) + { + WriteLog("SELFTEST bendprobe: nothing bent"); + return selftest_result::handled; + } + glm::dvec3 low; + glm::dvec3 high; + glm::dvec3 shift; + sweep->bounds(low, high, shift); + auto const &state{sweep->definition()}; + auto const marker{bend_marker(*sweep)}; + glm::dvec3 boxlow{std::numeric_limits::max()}; + glm::dvec3 boxhigh{-std::numeric_limits::max()}; + std::size_t vertices{0}; + for (auto const &shape : sweep->create_shapes()) + for (auto const &vertex : shape.data().vertices) + { + boxlow = glm::min(boxlow, vertex.position); + boxhigh = glm::max(boxhigh, vertex.position); + ++vertices; + } + WriteLog(format("SELFTEST bendprobe %s: stations %.3f - %.3f, along the reference line %.3f m, model %.3f m, lateral %.3f height %.3f anchors %d/%d point %.3f %.3f %.3f, marker %.3f %.3f %.3f, box %.2f %.2f %.2f - %.2f %.2f %.2f, %zu vertices, removed %d", + sweep->name().c_str(), sweep->start(), sweep->end(), sweep->distance_at(sweep->end()) - sweep->distance_at(sweep->start()), high.x - low.x, state.lateral, state.height, state.side_anchor, state.height_anchor, state.point.x, + state.point.y, state.point.z, marker.x, marker.y, marker.z, boxlow.x, boxlow.y, boxlow.z, boxhigh.x, boxhigh.y, boxhigh.z, vertices, sweep->m_editorremoved ? 1 : 0)); + return selftest_result::handled; + } + if (command == "lineside") + { + std::string tab; + words >> tab; + show_track_tab(tab == "parallel" ? track_tab::parallel : track_tab::lineside); + (tab == "vehicle" ? m_vehicle.expand : tab == "hekto" ? m_hekto.expand : tab == "fouling" ? m_fouling.expand : m_parallel.expand) = true; + return selftest_result::handled; + } + if (command == "bendpanel") + { + ui()->expand_bend(); + return selftest_result::handled; + } + if (command == "gauge") + { + std::string name; + words >> name; + if (auto *track{simulation::Paths.find(name)}) + { + scan_gauge(*track); + WriteLog(format("SELFTEST gauge %s: %d hits", name.c_str(), static_cast(m_gauge.line.hits.size()))); + for (auto const &hit : m_gauge.line.hits) + WriteLog(format("SELFTEST gauge hit %s depth %.3f at %.2f %.2f %.2f", hit.model.c_str(), hit.depth, hit.point.x, hit.point.y, hit.point.z)); + } + else + WriteLog("SELFTEST gauge " + name + ": no such track"); + return selftest_result::handled; + } + if (command == "ttincludes") + { + std::string name; + words >> name; + auto const *track{simulation::Paths.find(name)}; + WriteLog(format("SELFTEST ttincludes %s: track %s, includes %d", name.c_str(), track == nullptr ? "none" : track->m_editorremoved ? "removed" : "present", + track != nullptr ? static_cast(turntable_includes(*track).size()) : -1)); + return selftest_result::handled; + } + if (command == "checkspans") + { + WriteLog("SELFTEST spans: " + scene::Layers.check_sources()); + return selftest_result::handled; + } + return selftest_result::unhandled; +} diff --git a/application/editormodeselftestswitch.cpp b/application/editormodeselftestswitch.cpp new file mode 100644 index 00000000..74b7785d --- /dev/null +++ b/application/editormodeselftestswitch.cpp @@ -0,0 +1,771 @@ +/* +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 "application/editormode.h" +#include "editor/editorSettings.hpp" +#include "editor/editorGeometry.hpp" +#include "application/application.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" + +#include "simulation/simulation.h" +#include "simulation/simulationtime.h" +#include "scene/scenelayers.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Track.h" +#include "world/TrackRails.h" +#include "world/MemCell.h" +#include "world/Event.h" +#include "vehicle/DynObj.h" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "world/Sweep.h" +#include "input/keyboardinput.h" +#include "launcher/textures_scanner.h" +#include +#include +#include +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "scene/sceneterrain.h" +#include "editor/editorSpeed.hpp" + +#include +#include +#include +#include +#include +#include +#include "application/editormodeselftest.h" + +editor_mode::selftest_result editor_mode::selftest_switch(std::string const &command, std::istringstream &words) +{ + if (command == "curvedswitch") + { + std::string name; + std::string label; + 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); + arm_switch(); + auto &tool{m_switch}; + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + if (false == label.empty() && tool.templates[i].label.find(label) != std::string::npos) + { + tool.armed = i; + break; + } + m_cursor_override = at; + auto const started{start_switch_placement()}; + m_cursor_override.reset(); + if (false == started) + { + WriteLog("SELFTEST curvedswitch: not started, " + tool.status); + return selftest_result::handled; + } + tool.mouse = glm::dvec3{toward.x, tool.point.y, toward.z}; + finish_switch_placement(); + WriteLog("SELFTEST curvedswitch " + (tool.armed >= 0 ? tool.templates[tool.armed].label : std::string{}) + ": " + tool.status); + return selftest_result::handled; + } + if (command == "designer") + { + std::string list; + int index{0}; + words >> list >> index; + auto &designer{m_switch_designer}; + designer.open = list != "close"; + designer.from_catalogue = list != "own"; + designer.selected = index; + return selftest_result::handled; + } + if (command == "release") + { + m_switch.placing = false; + lay_cancel(); + m_cursor_override.reset(); + return selftest_result::handled; + } + if (command == "hover") + { + glm::dvec3 at{0.0}; + if (words >> at.x >> at.z) + m_cursor_override = at; + else + m_cursor_override.reset(); + return selftest_result::handled; + } + if (command == "layhold") + { + int layout{0}; + words >> layout >> m_lay.radius; + m_track_layout = static_cast(std::clamp(layout, 0, 3)); + m_lay.active = true; + lay_cancel(); + std::vector points; + for (glm::dvec3 point{0.0}; words >> point.x >> point.z;) + points.push_back(point); + for (std::size_t i = 0; i + 1 < points.size(); ++i) + { + auto point{points[i]}; + point.y = 5000.0; + std::vector found; + auto const ground{ground_heights({point}, true, &found)}; + point.y = found[0] != 0 ? ground[0] : m_lay.points.empty() ? 0.0 : m_lay.points.back().y; + m_lay.mouse_snap = editor_track::find_free_end(nullptr, 1, point, 3.0, {}); + m_lay.mouse = m_lay.mouse_snap.track != nullptr ? point : lay_bed(point); + lay_click(); + } + if (false == points.empty()) + m_cursor_override = points.back(); + WriteLog("SELFTEST layhold: " + m_lay.status); + return selftest_result::handled; + } + if (command == "switchkind") + { + // what the switch tool places, as chosen in its panel: single, slip, crossing or connection with the kind of it + std::string word; + int sub{0}; + words >> word >> sub; + auto const kind{word == "slip" ? switch_kind::slip : word == "crossing" ? switch_kind::crossing : word == "connection" ? switch_kind::connection : switch_kind::single}; + m_switch.connection_kind = sub; + choose_switch_kind(kind); + WriteLog(format("SELFTEST switchkind %s: kind %d, connection %d, armed %d", word.c_str(), static_cast(current_switch_kind()), m_switch.connection, m_switch.armed)); + return selftest_result::handled; + } + if (command == "connection" || command == "connectionplan" || command == "connectionhold") + { + std::string kindword; + std::string label; + std::string second; + glm::dvec3 at{0.0}; + glm::dvec3 toward{0.0}; + words >> kindword >> at.x >> at.z >> toward.x >> toward.z >> label >> second; + connection::kind kind{connection::kind::crossover}; + if (kindword != "single" && false == connection::parse(kindword, kind)) + { + WriteLog("SELFTEST connection: unknown kind " + kindword); + return selftest_result::handled; + } + auto &tool{m_switch}; + for (std::string key; words >> key;) + { + if (key == "radius") + words >> tool.connection_radius; + else if (key == "gap") + words >> tool.connection_gap; + else if (key == "together") + words >> tool.connection_together; + else if (key == "stub") + words >> tool.connection_stub; + else if (key == "spacings") + { + std::string value; + words >> value; + std::snprintf(tool.connection_spacings, sizeof(tool.connection_spacings), "%s", value.c_str()); + } + } + show_track_tab(track_tab::turnout); + arm_switch(); + auto const find = [&](std::string Label) { + std::replace(Label.begin(), Label.end(), '_', ' '); + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + if (false == Label.empty() && Label != "-" && false == tool.templates[i].double_slip && false == tool.templates[i].twofold() && tool.templates[i].label.find(Label) != std::string::npos) + return i; + return -1; + }; + auto const first{find(label)}; + if (first < 0) + { + WriteLog("SELFTEST connection: no template " + label); + return selftest_result::handled; + } + tool.armed = first; + tool.connection_second = find(second); + tool.connection = kindword == "single" ? -1 : static_cast(kind); + auto const line{straight_near(at, {}, 3.0)}; + if (line.tracks.empty()) + { + WriteLog("SELFTEST connection: no straight at the start"); + return selftest_result::handled; + } + m_node = line.tracks.front(); + ui()->set_node(m_node); + m_cursor_override = at; + auto const started{start_switch_placement()}; + m_cursor_override.reset(); + if (false == started) + { + WriteLog("SELFTEST connection: not started, " + tool.status); + return selftest_result::handled; + } + tool.mouse = glm::dvec3{toward.x, tool.point.y, toward.z}; + if (command == "connectionhold") + { + m_cursor_override = toward; + WriteLog("SELFTEST connectionhold: " + tool.status); + return selftest_result::handled; + } + connection::request request; + connection::result plan; + auto const valid{plan_connection(request, plan)}; + WriteLog("SELFTEST connection " + kindword + ": " + (valid ? connection_readout(plan) : "invalid, " + plan.error)); + for (auto const &entry : plan.switches) + { + auto const &start{entry.paths[0].points[segment_data::point::start]}; + auto const &end{entry.paths[1].points[segment_data::point::end]}; + WriteLog(format("SELFTEST connection switch on %d at %.3f, forward %d side %d%s: %.3f %.3f %.3f -> branch end %.3f %.3f %.3f", entry.line, entry.along, entry.forward, entry.side, entry.second ? " (second)" : "", start.x, start.y, start.z, end.x, end.y, + end.z)); + } + for (auto const &insert : plan.inserts) + { + auto const &start{insert.points[segment_data::point::start]}; + auto const &end{insert.points[segment_data::point::end]}; + WriteLog(format("SELFTEST connection insert %.3f %.3f %.3f -> %.3f %.3f %.3f, %.3f m, R %.1f", start.x, start.y, start.z, end.x, end.y, end.z, geometry::bezier{insert}.plan_length(), std::abs(insert.radius))); + } + if (command == "connectionplan" || false == valid) + { + tool.placing = false; + return selftest_result::handled; + } + insert_connection(); + WriteLog("SELFTEST connection built: " + tool.status); + for (auto const &entry : switch_store::records()) + if (entry.kind == "connection") + { + std::string parts; + for (auto const &part : entry.parts) + parts += ' ' + part; + WriteLog(format("SELFTEST connection record %s %s: spacing %.3f, insert %.3f, parts%s", entry.type.c_str(), entry.name.c_str(), entry.a, entry.b, parts.c_str())); + } + return selftest_result::handled; + } + if (command == "conecheck") + { + std::string name; + words >> name; + auto *track{simulation::Paths.find(name)}; + if (track == nullptr || track->m_paths.size() < 2) + { + WriteLog("SELFTEST conecheck: no switch " + name); + return selftest_result::handled; + } + geometry::bezier const main{track->m_paths[0]}; + geometry::bezier const other{track->m_paths[1]}; + std::string text; + for (int i = 1; i <= 8; ++i) + { + auto const t{i * 0.125}; + auto const point{other.point(t)}; + double best{std::numeric_limits::max()}; + double along{0.0}; + for (int k = 0; k <= 2000; ++k) + if (auto const distance{geometry::plan_distance(main.point(k / 2000.0), point)}; distance < best) + { + best = distance; + along = k / 2000.0; + } + auto const base{main.point(along)}; + auto const direction{glm::normalize(geometry::plan_of(main.first(along)))}; + auto const lateral{(point.x - base.x) * -direction.y + (point.z - base.z) * direction.x}; + auto const &path{track->m_paths[0]}; + auto const roll{glm::radians(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * along)}; + auto const tangent{main.first(along)}; + auto const beyond{(point.x - base.x) * direction.x + (point.z - base.z) * direction.y}; + auto const cone{base.y + beyond * tangent.y / glm::length(geometry::plan_of(tangent)) - lateral * std::tan(roll)}; + text += format(" [t %.3f off %.3f dy %.4f]", t, lateral, point.y - cone); + } + 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}; + int side{0}; + words >> index >> side; + scan_switch_drives(); + m_switch.drive = index; + m_switch.drive_side = side; + WriteLog("SELFTEST switchdrive: " + (index >= 0 && index < static_cast(m_switch.drives.size()) ? m_switch.drives[index].name : std::string{"none"})); + return selftest_result::handled; + } + if (command == "drivechoice") + { + int index{-1}; + int side{0}; + words >> index >> side; + scan_switch_drives(); + m_switch.drive = index; + m_switch.drive_side = side; + m_switch.status.clear(); + auto *track{selected_track()}; + if (track != nullptr && track->eType == tt_Switch && false == track->m_editorremoved) + apply_switch_drive_choice(*track); + WriteLog("SELFTEST drivechoice: " + (track != nullptr ? track->name() : std::string{"no switch"}) + ", " + m_switch.status); + return selftest_result::handled; + } + if (command == "drives") + { + std::string name; + words >> name; + scan_switch_drives(); + std::string files; + for (auto const &standing : standing_templates([](std::string const &File) { return File.find("napedy/") != std::string::npos; })) + if (false == standing.values.empty() && standing.values[0] == name) + { + files += " " + standing.file; + for (std::size_t i = 1; i < standing.values.size(); ++i) + files += (i == 1 ? " (" : " ") + standing.values[i] + (i + 1 == standing.values.size() ? ")" : ""); + } + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + if (scene::Layers.tracked(handle) && scene::Layers.instance(handle).file != nullptr && scene::Layers.instance(handle).file->find("napedy") != std::string::npos) + files += format(" [%d %s removed %d dead %d]", static_cast(handle), scene::Layers.instance(handle).file->c_str(), scene::Layers.instance(handle).removed ? 1 : 0, scene::Layers.instance(handle).dead ? 1 : 0); + WriteLog("SELFTEST drives " + name + ": chosen " + (m_switch.drive >= 0 ? m_switch.drives[m_switch.drive].name : std::string{"none"}) + ", side " + std::to_string(m_switch.drive_side) + ", standing" + files + ", steps " + std::to_string(m_history.size())); + return selftest_result::handled; + } + if (command == "driveflip") + { + std::string name; + words >> name; + auto *track{simulation::Paths.find(name)}; + auto const done{track != nullptr && flip_switch_drive(*track)}; + WriteLog("SELFTEST driveflip " + name + ": " + (done ? "done " : "failed ") + m_switch.status); + return selftest_result::handled; + } + if (command == "driveput" || command == "drivefamily") + { + std::string name; + int family{-1}; + words >> name >> family; + auto *track{simulation::Paths.find(name)}; + auto const done{track != nullptr && (command == "driveput" ? place_switch_drive(*track) : replace_switch_drive(*track, family, false))}; + WriteLog("SELFTEST " + command + " " + name + ": " + (done ? "done " : "failed ") + m_switch.status); + return selftest_result::handled; + } + if (command == "drivetilt") + { + std::string name; + words >> name; + auto *model{simulation::Instances.find(name)}; + auto *track{simulation::Paths.find(name.substr(0, name.find('_')))}; + if (model == nullptr || track == nullptr) + { + WriteLog("SELFTEST drivetilt: no " + name); + return selftest_result::handled; + } + auto const &path{track->m_paths[0]}; + auto const transform{glm::dmat3(model->rotation_scale())}; + auto const across{transform * glm::dvec3{1.0, 0.0, 0.0}}; + auto const forward{transform * glm::dvec3{0.0, 0.0, 1.0}}; + auto const direction{glm::normalize(glm::dvec2{forward.x, forward.z})}; + auto const lateral{across.x * -direction.y + across.z * direction.x}; + auto const cone{-lateral * std::tan(glm::radians(static_cast(path.rolls[0])))}; + auto const control{path.points[segment_data::point::control1]}; + auto const grade{control.y / std::hypot(control.x, control.z)}; + WriteLog(format("SELFTEST drivetilt %s: angles %.3f %.3f %.3f, +x rises %.4f (cone %.4f), +z rises %.4f (grade %.4f), origin %.3f %.3f %.3f, points %.3f", name.c_str(), model->Angles().x, model->Angles().y, model->Angles().z, across.y, cone, forward.y, grade, + model->location().x, model->location().y, model->location().z, path.points[segment_data::point::start].y)); + return selftest_result::handled; + } + if (command == "switchplace" || command == "switchcurve" || command == "slipplace") + { + std::string label; + glm::dvec3 point{0.0}; + int direction{1}; + int side{1}; + words >> label >> point.x >> point.z >> direction >> side; + std::replace(label.begin(), label.end(), '_', ' '); + auto &tool{m_switch}; + tool.templates = editor_track::standard_switch_templates(); + tool.collected = true; + tool.armed = -1; + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + if (tool.templates[i].label.rfind(label, 0) == 0) + { + tool.armed = i; + break; + } + TTrack *nearest{nullptr}; + double distance{1e9}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const d{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point)}; + if (d < distance) + { + distance = d; + nearest = track; + } + } + if (tool.armed < 0 || nearest == nullptr) + { + WriteLog("SELFTEST " + command + ": no template " + label + " or no path"); + return selftest_result::handled; + } + tool.status.clear(); + if (command == "switchcurve") + { + editor_track::curve curve; + editor_track::chain chain; + std::string error; + if (false == editor_track::find_curve(*nearest, m_straights.tolerance, m_route.design.norms.gauge, curve) || false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) + { + WriteLog("SELFTEST switchcurve: no curve"); + return selftest_result::handled; + } + tool.frame = sample_chain(chain); + tool.frame_tracks = chain.tracks; + tool.curved = true; + tool.along = nearest_station(tool.frame, point); + insert_curved_switch(direction, side); + } + else + { + auto const line{editor_track::find_straight(*nearest, m_straights.tolerance)}; + if (command == "slipplace") + { + WriteLog(format("SELFTEST slipplace: straight from %s, %zu paths, start %.3f %.3f, %.3f m", nearest->name().c_str(), line.tracks.size(), line.start.x, line.start.z, line.length)); + insert_double_slip(line, point, tool.templates[tool.armed]); + } + else + insert_switch(line, std::clamp(line.along(point), 0.0, line.length), direction, side); + } + WriteLog("SELFTEST " + command + ": " + tool.status + " " + m_straights.status); + return selftest_result::handled; + } + if (command == "switchlink" || command == "branchlink") + { + std::string label; + glm::dvec3 point{0.0}; + glm::dvec3 target{0.0}; + if (command == "switchlink") + words >> label; + words >> point.x >> point.z >> target.x >> target.z; + std::replace(label.begin(), label.end(), '_', ' '); + auto const log_link = [&](editor_track::branch_link const &Link, std::string const &Reason) { + WriteLog(format("SELFTEST %s plan: valid %d, R %.1f m, before %.3f m, after %.3f m, trim %.3f m, shortfall %.3f m, %zu pieces%s", command.c_str(), Link.valid ? 1 : 0, Link.radius, Link.before, Link.after, Link.trim, + Link.shortfall, Link.pieces.size(), Reason.empty() ? "" : (", " + Reason).c_str())); + for (auto const &piece : Link.pieces) + { + auto const &a{piece.points[segment_data::point::start]}; + auto const &b{piece.points[segment_data::point::end]}; + WriteLog(format("SELFTEST %s piece: %.3f %.3f %.3f -> %.3f %.3f %.3f, L %.3f m, R %.1f m", command.c_str(), a.x, a.y, a.z, b.x, b.y, b.z, geometry::bezier{piece}.plan_length(), static_cast(piece.radius))); + } + }; + if (command == "switchlink") + { + show_track_tab(track_tab::turnout); + auto &tool{m_switch}; + tool.templates = editor_track::standard_switch_templates(); + tool.collected = true; + tool.armed = -1; + tool.connection = -1; + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + if (tool.templates[i].label.rfind(label, 0) == 0) + { + tool.armed = i; + break; + } + TTrack *nearest{nullptr}; + double distance{1e9}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const d{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point)}; + if (d < distance) + { + distance = d; + nearest = track; + } + } + if (tool.armed < 0 || nearest == nullptr) + { + WriteLog("SELFTEST switchlink: no template " + label + " or no path"); + return selftest_result::handled; + } + m_node = nearest; + ui()->set_node(m_node); + m_cursor_override = point; + if (false == start_switch_placement()) + { + m_cursor_override.reset(); + WriteLog("SELFTEST switchlink: not started, " + tool.status); + return selftest_result::handled; + } + tool.mouse = glm::dvec3{target.x, tool.point.y, target.z}; + switch_link plan; + plan_switch_link(plan); + log_link(plan.link, plan.reason); + finish_switch_placement(); + m_cursor_override.reset(); + WriteLog("SELFTEST switchlink: " + tool.status); + return selftest_result::handled; + } + TTrack *own{nullptr}; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && track->eType == tt_Switch && track->m_paths.size() == 2 && geometry::plan_distance(track->m_paths[1].points[segment_data::point::end], point) < 3.0) + own = track; + if (own == nullptr) + { + WriteLog("SELFTEST branchlink: no switch branch end there"); + return selftest_result::handled; + } + editor_track::snap_target end; + end.track = own; + end.point = {1, editor_track::point_kind::end}; + end.position = own->m_paths[1].points[segment_data::point::end]; + if (false == start_extend(end)) + { + WriteLog("SELFTEST branchlink: the branch end isn't free"); + return selftest_result::handled; + } + m_extend.pressed = {-10000.f, -10000.f}; + m_extend.mouse = glm::dvec3{target.x, m_extend.point.y, target.z}; + m_extend.snap = editor_track::find_free_end(own, own->iCategoryFlag & 15, m_extend.mouse, 3.0, {own}); + editor_track::branch_link link; + std::string reason; + plan_branch_link(link, reason); + log_link(link, reason); + finish_extend(); + WriteLog(format("SELFTEST branchlink: switch now %.3f %.3f -> %.3f %.3f", own->m_paths[0].points[segment_data::point::start].x, own->m_paths[0].points[segment_data::point::start].z, own->m_paths[0].points[segment_data::point::end].x, + own->m_paths[0].points[segment_data::point::end].z)); + return selftest_result::handled; + } + if (command == "switchslide" || command == "switchreplace") + { + std::string name; + words >> name; + auto *track{simulation::Paths.find(name)}; + if (track == nullptr || track->eType != tt_Switch || track->m_paths.size() < 2) + { + WriteLog("SELFTEST " + command + ": no switch " + name); + return selftest_result::handled; + } + if (command == "switchslide") + { + double distance{0.0}; + words >> distance; + double low{0.0}; + double high{0.0}; + std::string reason; + auto const set{double_of(*track)}; + double length{0.0}; + auto const room{set.has_value() ? double_room(*set, low, high, length, reason) : switch_room(*track, low, high, reason)}; + auto const done{slide_switch(*track, distance)}; + WriteLog(format("SELFTEST switchslide %s %.3f: room %d %.3f .. %.3f, done %d, %s", name.c_str(), distance, room ? 1 : 0, low, high, done ? 1 : 0, m_switch.status.c_str())); + return selftest_result::handled; + } + std::string label; + int side{1}; + words >> label >> side; + std::replace(label.begin(), label.end(), '_', ' '); + auto const templates{editor_track::standard_switch_templates()}; + auto const found{std::find_if(templates.begin(), templates.end(), [&](editor_track::switch_template const &Template) { return Template.label.rfind(label, 0) == 0; })}; + if (found == templates.end()) + { + WriteLog("SELFTEST switchreplace: no template " + label); + return selftest_result::handled; + } + auto const set{double_of(*track)}; + if (set.has_value()) + track = set->first; + auto const &main{track->m_paths[0]}; + auto const origin{main.points[segment_data::point::start]}; + auto const chord{geometry::plan_of(main.points[segment_data::point::end] - origin)}; + auto const length{glm::length(chord)}; + auto paths{editor_track::place_switch(*found, origin, chord / length, side, (main.points[segment_data::point::end].y - origin.y) / length)}; + std::string status; + auto const done{set.has_value() ? found->twofold() && paths.size() >= 5 && move_double_paths(*set, paths, status) : paths.size() >= 2 && move_switch_paths(*track, paths, status)}; + if (done && set.has_value()) + { + record_double(*set->first, *set->second, *found); + switch_store::follow(); + records_to_history(); + straight_refresh(); + } + else if (done) + { + record_switch(*track, *found); + records_to_history(); + straight_refresh(); + } + WriteLog(format("SELFTEST switchreplace %s %s: done %d, %s", name.c_str(), found->label.c_str(), done ? 1 : 0, status.c_str())); + return selftest_result::handled; + } + if (command == "switchpieces" || command == "switchbreak") + { + double radius{0.0}; + double ratio{0.0}; + std::string name; + if (command == "switchpieces") + words >> radius >> ratio; + words >> name; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->m_paths.size() < 2 || track->SwitchExtension == nullptr) + continue; + if (false == name.empty() ? track->name() != name : track->m_paths[1].pieces.empty()) + continue; + if (command == "switchbreak") + { + auto &pieces{track->m_paths[1].pieces}; + if (pieces.size() < 2) + continue; + pieces[0].points[segment_data::point::end].x += 0.3; + editor_track::commit({track}); + WriteLog(format("SELFTEST switchbreak %s: path 2 %.3f m, %zu pieces in use, %zu kept", track->name().c_str(), track->SwitchExtension->Segments[1]->GetLength(), track->SwitchExtension->Segments[1]->PieceCount(), pieces.size())); + return selftest_result::handled; + } + auto const &segment{*track->SwitchExtension->Segments[1]}; + auto const &main{track->m_paths[0]}; + auto const &branch{track->m_paths[1]}; + auto const origin{segment.FastGetPoint(0.0)}; + auto const plan{glm::normalize(geometry::plan_of(main.points[segment_data::point::end] - main.points[segment_data::point::start]))}; + glm::dvec3 const along{plan.x, 0.0, plan.y}; + auto const reach{branch.points[segment_data::point::end] - branch.points[segment_data::point::start]}; + auto const hand{along.x * reach.z - along.z * reach.x > 0.0 ? 1.0 : -1.0}; + auto const normal{glm::dvec3{-along.z, 0.0, along.x} * hand}; + auto const centre{origin + normal * radius}; + auto const angle{std::atan(1.0 / ratio)}; + auto const arc{radius * angle}; + auto const heading0{-std::atan2(along.x, along.z)}; + glm::dvec3 position; + glm::vec3 rotation; + double worst{0.0}; + for (int i = 0; i <= 10; ++i) + { + segment.RaPositionGet(arc * i / 10.0, position, rotation); + worst = std::max(worst, std::abs(geometry::plan_distance(position, centre) - radius)); + } + segment.RaPositionGet(arc, position, rotation); + auto const expected{centre - normal * radius * std::cos(angle) + along * radius * std::sin(angle)}; + auto const turn{std::abs(std::remainder(rotation.z - heading0, glm::two_pi()))}; + double low{1e9}; + double high{-1e9}; + for (int i = 0; i <= 10; ++i) + { + segment.RaPositionGet(arc + 0.05 + (segment.GetLength() - arc - 0.1) * i / 10.0, position, rotation); + low = std::min(low, static_cast(rotation.z)); + high = std::max(high, static_cast(rotation.z)); + } + segment.RaPositionGet(arc, position, rotation); + WriteLog(format("SELFTEST switchpieces %s: path 2 %.3f m in %zu pieces (%zu kept), arc %.3f m, arc end off %.4f m, turn there %.5f (angle %.5f), axis off the arc %.4f m, straight heading spread %.6f, end %.3f %.3f", track->name().c_str(), + segment.GetLength(), segment.PieceCount(), branch.pieces.size(), arc, geometry::plan_distance(position, expected), turn, angle, worst, high - low, segment.FastGetPoint(1.0).x, segment.FastGetPoint(1.0).z)); + } + return selftest_result::handled; + } + if (command == "switchlinks") + { + auto const name_of = [](TTrack const *Track) { return Track == nullptr ? std::string{"-"} : Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->SwitchExtension == nullptr) + continue; + auto const &extension{*track->SwitchExtension}; + WriteLog(format("SELFTEST switchlinks %s: prev %s / %s, next %s / %s, lengths %.3f / %.3f m", track->name().c_str(), name_of(extension.pPrevs[0]).c_str(), name_of(extension.pPrevs[1]).c_str(), name_of(extension.pNexts[0]).c_str(), name_of(extension.pNexts[1]).c_str(), + extension.Segments[0]->GetLength(), extension.Segments[1]->GetLength())); + } + return selftest_result::handled; + } + if (command == "switchtemplates") + { + for (auto const &entry : editor_track::standard_switch_templates()) + { + if (entry.double_slip) + continue; + auto const paths{editor_track::place_switch(entry, glm::dvec3{0.0}, glm::dvec2{0.0, 1.0}, 1, 0.0)}; + double gap{0.0}; + double kink{0.0}; + std::size_t pieces{0}; + for (auto const &path : paths) + { + pieces += path.pieces.size(); + for (std::size_t i = 0; i + 1 < path.pieces.size(); ++i) + { + auto const &one{path.pieces[i]}; + auto const &two{path.pieces[i + 1]}; + gap = std::max(gap, glm::distance(one.points[segment_data::point::end], two.points[segment_data::point::start])); + geometry::bezier const a{one}; + geometry::bezier const b{two}; + auto const ta{glm::normalize(geometry::plan_of(a.first(1.0)))}; + auto const tb{glm::normalize(geometry::plan_of(b.first(0.0)))}; + kink = std::max(kink, std::abs(geometry::signed_angle(ta, tb))); + } + if (false == path.pieces.empty()) + gap = std::max({gap, glm::distance(path.pieces.front().points[segment_data::point::start], path.points[segment_data::point::start]), glm::distance(path.pieces.back().points[segment_data::point::end], path.points[segment_data::point::end])}); + } + std::string ends; + for (auto const &path : paths) + ends += format(" (%.3f %.3f)", path.points[segment_data::point::end].x, path.points[segment_data::point::end].z); + std::string starts; + for (std::size_t i = 1; i < paths.size() && i <= 3; i += 2) + { + geometry::bezier const first{paths[i].pieces.empty() ? paths[i] : paths[i].pieces.front()}; + auto const tangent{glm::normalize(geometry::plan_of(first.first(0.0)))}; + starts += format(" %.4f", glm::degrees(std::abs(geometry::signed_angle(glm::dvec2{0.0, 1.0}, tangent)))); + } + WriteLog(format("SELFTEST switchtemplate %s: %s%s, L %.3f 1:%.3f R %.1f a %.3f b %.3f stagger %.3f, %zu paths, %zu pieces, gap %.6f m, kink %.6f deg, start deg%s, ends%s", entry.label.c_str(), entry.type.c_str(), entry.own ? " own" : "", entry.length, entry.ratio, entry.radius, entry.a, entry.b, entry.stagger, paths.size(), pieces, gap, + glm::degrees(kink), starts.c_str(), ends.c_str())); + } + return selftest_result::handled; + } + if (command == "switches") + { + for (auto const &entry : switch_store::records()) + { + std::string parts; + for (auto const &part : entry.parts) + parts += part + " "; + WriteLog(format("SELFTEST switch record %s %s: %s, R %.3f 1:%.3f hand %c a %.3f b %.3f main R %.3f, branch R %.3f, km %s, number %s, %s, parts %s", entry.kind.c_str(), entry.name.c_str(), switch_store::label(entry).c_str(), entry.radius, entry.ratio, entry.hand, entry.a, + entry.b, entry.main_radius, switch_store::branch_radius(entry), entry.km.has_value() ? format("%.3f", *entry.km / 1000.0).c_str() : "-", entry.number.c_str(), entry.source.c_str(), parts.c_str())); + } + speed_check::options options; + options.norms = m_route.design.norms; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->m_paths.size() < 2) + continue; + auto const known{switch_store::of(*track)}; + geometry::bezier const branch{track->m_paths[1]}; + auto const first{branch.first(0.3)}; + auto const second{branch.second(0.3)}; + auto const planar{std::abs(first.x * second.z - first.z * second.x) / std::pow(first.x * first.x + first.z * first.z, 1.5)}; + auto const verdict{speed_check::check(*track, 1, options)}; + WriteLog(format("SELFTEST switch %s: %s, branch R at 0.3 %.3f, path R %.3f, branch speed %.1f km/h at R %.1f", track->name().c_str(), known.has_value() ? switch_store::label(*known).c_str() : "no record", planar > 1e-9 ? 1.0 / planar : 0.0, + track->m_paths[1].radius, verdict.safe, verdict.radius)); + } + return selftest_result::handled; + } + return selftest_result::unhandled; +} diff --git a/application/editormodeselftestterrain.cpp b/application/editormodeselftestterrain.cpp new file mode 100644 index 00000000..9dd22cf2 --- /dev/null +++ b/application/editormodeselftestterrain.cpp @@ -0,0 +1,282 @@ +/* +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 "application/editormode.h" +#include "editor/editorSettings.hpp" +#include "editor/editorGeometry.hpp" +#include "application/application.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" + +#include "simulation/simulation.h" +#include "simulation/simulationtime.h" +#include "scene/scenelayers.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Track.h" +#include "world/TrackRails.h" +#include "world/MemCell.h" +#include "world/Event.h" +#include "vehicle/DynObj.h" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "world/Sweep.h" +#include "input/keyboardinput.h" +#include "launcher/textures_scanner.h" +#include +#include +#include +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "scene/sceneterrain.h" +#include "editor/editorSpeed.hpp" + +#include +#include +#include +#include +#include +#include +#include "application/editormodeselftest.h" + +editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &command, std::istringstream &words) +{ + if (command == "orthophoto") + { + auto config{m_orthophoto.settings()}; + int fit{1}; + int inscene{0}; + words >> config.north >> config.east >> config.radius >> fit >> inscene; + config.drape = fit != 0; + config.in_scene = inscene != 0; + m_orthophoto.settings(config); + m_orthophoto.enabled(true); + WriteLog(format("SELFTEST orthophoto at %.0f %.0f, radius %d, %s%s", config.north, config.east, config.radius, config.drape ? "fitted" : "flat", config.in_scene ? ", in the scene" : "")); + return selftest_result::handled; + } + if (command == "terrainmode") + { + m_terrain_open = true; + WriteLog("SELFTEST terrainmode"); + return selftest_result::handled; + } + if (command == "terrainsculpt" || command == "terrainpaint" || command == "terrainlevel") + { + double x{0.0}; + double z{0.0}; + double radius{10.0}; + double value{1.0}; + int mode{0}; + words >> x >> z >> radius >> value >> mode; + auto const changed{command == "terrainsculpt" ? m_streamer.sculpt(x, z, radius, value) + : command == "terrainlevel" ? m_streamer.level(x, z, radius, value, 1.0, mode) + : m_streamer.paint(x, z, radius, 1.0, static_cast(value))}; + forget_ground(); + WriteLog(format("SELFTEST %s %.1f %.1f: %s", command.c_str(), x, z, changed ? "changed" : "nothing changed")); + return selftest_result::handled; + } + if (command == "terrainmodifier") + { + heightmap::modifier modifier; + heightmap::corridor_point a, b; + double formation{0.0}; + double halfwidth{3.5}; + words >> modifier.name >> a.position.x >> a.position.y >> b.position.x >> b.position.y >> formation >> halfwidth; + b.formation = formation; + a.formation = b.formation; + b.half_width = halfwidth; + a.half_width = b.half_width; + modifier.points = {a, b}; + m_streamer.modifier(std::move(modifier)); + forget_ground(); + WriteLog(format("SELFTEST terrainmodifier: %zu modifiers", m_streamer.modifiers().size())); + return selftest_result::handled; + } + if (command == "terrainfill") + { + int layer{0}; + words >> layer >> m_paint_feather >> m_paint_rotation; + m_terrain_layer = layer; + m_paint_outline.clear(); + double x; + double z; + while (words >> x >> z) + m_paint_outline.emplace_back(x, 0.0, z); + fill_terrain_area(); + std::string palette; + for (auto const &entry : m_streamer.manifest().layers) + palette += format(" [%s %.1f m %.0f deg]", entry.material.c_str(), entry.size, entry.rotation); + WriteLog("SELFTEST terrainfill: " + m_terrain_status + ", palette" + palette); + return selftest_result::handled; + } + if (command == "terrainturn") + { + int layer{0}; + float degrees{0.f}; + words >> layer >> degrees; + m_streamer.layer_rotation(static_cast(layer), degrees); + WriteLog(format("SELFTEST terrainturn %d: %.1f deg", layer, m_streamer.manifest().layers.at(layer).rotation)); + return selftest_result::handled; + } + if (command == "terraintouchup") + { + int state{1}; + words >> state; + m_streamer.touch_up(state != 0); + WriteLog(format("SELFTEST terraintouchup %d", state)); + return selftest_result::handled; + } + if (command == "terrainrestore") + { + double x{0.0}; + double z{0.0}; + double radius{10.0}; + words >> x >> z >> radius; + auto const changed{m_streamer.restore_stitching(x, z, radius, 1.0)}; + forget_ground(); + WriteLog(format("SELFTEST terrainrestore %.1f %.1f: %s, touch-ups left %zu", x, z, changed ? "changed" : "nothing changed", m_streamer.touched_up())); + return selftest_result::handled; + } + if (command == "terrainbake") + { + std::string name; + words >> name; + auto const baked{m_streamer.bake_modifier(name)}; + forget_ground(); + WriteLog(format("SELFTEST terrainbake %s: %s, %zu modifiers left", name.c_str(), baked ? "baked" : "not found", m_streamer.modifiers().size())); + return selftest_result::handled; + } + if (command == "terrainselect") + { + int cx{0}; + int cz{0}; + std::string add; + words >> cx >> cz >> add; + if (add != "add") + m_terrain_selection.clear(); + if (m_streamer.exists({cx, cz})) + m_terrain_selection.insert({cx, cz}); + WriteLog(format("SELFTEST terrainselect: %zu selected", m_terrain_selection.size())); + return selftest_result::handled; + } + if (command == "terrainresolution") + { + float spacing{1.f}; + int paint{1}; + words >> spacing >> paint; + m_terrain_convert_spacing = heightmap::valid_spacing(spacing); + m_terrain_convert_paint = paint != 0; + convert_selected_chunks(); + WriteLog("SELFTEST terrainresolution: " + m_terrain_status); + return selftest_result::handled; + } + if (command == "terrainwater" || command == "terrainwaterdefault") + { + if (command == "terrainwaterdefault") + { + std::string material; + words >> material; + m_streamer.water_material(material); + } + else + { + std::size_t index{0}; + std::string material; + float size{0.f}; + words >> index >> material >> size; + auto water{m_streamer.water()}; + if (index < water.size()) + { + water[index].material = material == "-" ? std::string{} : material; + water[index].size = size; + m_streamer.water(std::move(water)); + } + } + std::string list; + for (auto const &body : m_streamer.water()) + list += format(" [%s: %s, %.1f m]", body.name.c_str(), body.material.empty() ? "default" : body.material.c_str(), body.size); + WriteLog("SELFTEST " + command + ": default " + m_streamer.manifest().water_material + list); + return selftest_result::handled; + } + if (command == "terrainadd") + { + int count{1}; + float spacing{m_terrain_spacing}; + words >> count >> spacing; + m_terrain_spacing = heightmap::valid_spacing(spacing); + add_chunks_around(std::clamp(count, 1, 8)); + WriteLog("SELFTEST terrainadd: " + m_terrain_status); + return selftest_result::handled; + } + if (command == "terrainprobe") + { + double x{0.0}; + double z{0.0}; + double height{0.0}; + words >> x >> z; + auto const *chunk{m_streamer.terrain_at(x, z)}; + std::string materials; + if (chunk != nullptr) + for (std::size_t slot = 0; slot < std::max(1, chunk->data().layer_count()); ++slot) + { + auto const layer{chunk->data().layers[slot]}; + auto const &palette{m_streamer.manifest().layers}; + materials += (layer < palette.size() ? palette[layer].material : std::string{"?"}) + " "; + } + auto const found{m_streamer.height_at(x, z, height)}; + std::string grid; + if (chunk != nullptr) + { + auto const &data{chunk->data()}; + double base{0.0}; + double adjust{0.0}; + if (false == data.base.empty()) + base = data.height_in(data.base, x, z); + if (false == data.adjust.empty()) + adjust = data.height_in(data.adjust, x, z); + grid = format(", spacing %.1f, paint %d, base %s %.3f, touch-up %.3f", data.spacing(), data.paint, data.base.empty() ? "none" : "", base, adjust); + if (data.paint > 0) + { + // weights at the paint sample nearest the point + auto const corner{data.corner()}; + auto const ix{static_cast(std::lround((x - corner.x) / heightmap::chunk_size * data.paint))}; + auto const iz{static_cast(std::lround((z - corner.y) / heightmap::chunk_size * data.paint))}; + grid += ", weights"; + for (std::size_t slot = 0; slot < data.layer_count(); ++slot) + grid += format(" %d", data.weight(slot, std::clamp(ix, 0, data.paint), std::clamp(iz, 0, data.paint))); + } + } + WriteLog(format("SELFTEST terrainprobe %.1f %.1f: %s %.3f, %zu chunks, materials %s%s", x, z, found ? "height" : "no terrain", height, m_streamer.resident(), materials.c_str(), grid.c_str())); + return selftest_result::handled; + } + if (command == "terrainconvert") + { + load_ground_files(); + for (auto const &source : m_ground_conversion.files) + { + glm::dvec3 low{1e30}; + glm::dvec3 high{-1e30}; + for (auto index = source.first; index < source.first + source.count; ++index) + for (auto const &vertex : m_ground_conversion.triangles[index]) + { + low = glm::min(low, vertex); + high = glm::max(high, vertex); + } + WriteLog("SELFTEST terrain file " + source.file + (source.text ? " (text): " : " (binary): ") + source.message + + format(", from %.1f %.1f %.1f to %.1f %.1f %.1f", low.x, low.y, low.z, high.x, high.y, high.z)); + } + m_ground_conversion.status = convert_ground(); + WriteLog("SELFTEST terrainconvert: " + m_ground_conversion.status); + return selftest_result::handled; + } + return selftest_result::unhandled; +} diff --git a/application/editormodeselftesttrack.cpp b/application/editormodeselftesttrack.cpp new file mode 100644 index 00000000..087b88f4 --- /dev/null +++ b/application/editormodeselftesttrack.cpp @@ -0,0 +1,558 @@ +/* +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 "application/editormode.h" +#include "editor/editorSettings.hpp" +#include "editor/editorGeometry.hpp" +#include "application/application.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" + +#include "simulation/simulation.h" +#include "simulation/simulationtime.h" +#include "scene/scenelayers.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Track.h" +#include "world/TrackRails.h" +#include "world/MemCell.h" +#include "world/Event.h" +#include "vehicle/DynObj.h" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "world/Sweep.h" +#include "input/keyboardinput.h" +#include "launcher/textures_scanner.h" +#include +#include +#include +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "scene/sceneterrain.h" +#include "editor/editorSpeed.hpp" + +#include +#include +#include +#include +#include +#include +#include "application/editormodeselftest.h" + +editor_mode::selftest_result editor_mode::selftest_track(std::string const &command, std::istringstream &words) +{ + if (command == "probe") + { + // edge of the last laid model as it was made: its distance from the axis and the height of its top over the rail head + std::string name; + double step{20.0}; + words >> name >> step; + if (name == "-") + name.clear(); + auto *sweep{name.empty() ? m_sweep.edited : simulation::Sweeps.find(name)}; + if (sweep == nullptr) + { + WriteLog("SELFTEST probe: nothing laid"); + return selftest_result::handled; + } + auto const &definition{sweep->definition()}; + double const sign{definition.lateral >= 0.0 ? 1.0 : -1.0}; + struct bin + { + double edge{std::numeric_limits::max()}; + std::vector> points; // lateral, height over the axis + }; + std::map bins; + for (auto const &shape : sweep->create_shapes()) + for (auto const &vertex : shape.data().vertices) + { + double lateral; + auto const station{sweep->project(vertex.position, lateral)}; + if (lateral * sign <= 0.0) + continue; + auto const axis{sweep->frame_at(station)}; + auto &entry{bins[static_cast(std::floor(station / step))]}; + entry.edge = std::min(entry.edge, std::abs(lateral)); + entry.points.emplace_back(std::abs(lateral), vertex.position.y - axis.position.y); + } + WriteLog(format("SELFTEST probe %s: %zu stretches with the model, %.1f - %.1f m of %.1f m", sweep->name().c_str(), bins.size(), sweep->start(), sweep->end(), sweep->length())); + for (auto const &[index, entry] : bins) + { + auto const station{(index + 0.5) * step}; + if (station < sweep->start() || station > sweep->end()) + continue; + double top{-1000.0}; + for (auto const &point : entry.points) + if (point.first < entry.edge + 0.15) + top = std::max(top, point.second); + auto const at{sweep->frame_at(station)}; + auto const head{0.18 + sweep_node::rail_head(at.roll, definition.lateral)}; + WriteLog(format("SELFTEST probe %s at %.0f m: edge %.3f m from the axis, its top %.3f m over the rail head (cant %.1f deg, gauge widening %.3f m, cant part %.3f m)", sweep->name().c_str(), station, entry.edge, top - head, at.roll, + at.widening[definition.lateral >= 0.0 ? 1 : 0], std::max(0.0, at.cant[definition.lateral >= 0.0 ? 1 : 0]))); + } + return selftest_result::handled; + } + if (command == "profile") + { + std::string name; + double station{0.0}; + words >> name >> station; + auto *sweep{simulation::Sweeps.find(name)}; + if (sweep == nullptr) + { + WriteLog("SELFTEST profile: no such sweep"); + return selftest_result::handled; + } + auto const &definition{sweep->definition()}; + std::map> columns; + for (auto const &shape : sweep->create_shapes()) + for (auto const &vertex : shape.data().vertices) + { + double lateral; + auto const at{sweep->project(vertex.position, lateral)}; + if (std::abs(at - station) > 1.0) + continue; + auto const axis{sweep->frame_at(at)}; + auto const height{vertex.position.y - axis.position.y - 0.18 - sweep_node::rail_head(axis.roll, definition.lateral)}; + auto &column{columns.try_emplace(static_cast(std::floor(lateral / 0.1)), std::numeric_limits::max(), -std::numeric_limits::max()).first->second}; + column.first = std::min(column.first, height); + column.second = std::max(column.second, height); + } + std::string text; + for (auto const &[index, column] : columns) + text += format(" [%.1f %.2f..%.2f]", index * 0.1, column.first, column.second); + WriteLog(format("SELFTEST profile %s at %.0f m:", name.c_str(), station) + text); + return selftest_result::handled; + } + if (command == "tracks") + { + int tab{0}; + words >> tab; + show_track_tab(track_tab::lineside); + m_sweep.expand = tab == 0; + m_hekto.expand = tab == 1; + m_fouling.expand = tab == 2; + m_parallel.expand = tab == 3; + m_vehicle.expand = tab == 4; + return selftest_result::handled; + } + if (command == "curve") + { + sweep_curve(); + for (auto const &piece : m_sweep.curve) + { + auto const &a{piece.points[segment_data::point::start]}; + auto const &b{piece.points[segment_data::point::end]}; + WriteLog(format("SELFTEST curve piece %.1f %.1f -> %.1f %.1f", a.x, a.z, b.x, b.z)); + } + if (false == m_sweep.outline.empty()) + WriteLog(format("SELFTEST curve outline %zu points, %.1f %.1f -> %.1f %.1f, %.1f m", m_sweep.outline.size(), m_sweep.outline.front().x, m_sweep.outline.front().z, m_sweep.outline.back().x, m_sweep.outline.back().z, m_sweep.curve_length)); + return selftest_result::handled; + } + if (command == "lay") + { + std::string name; + std::string side; + words >> name >> side; + auto *track{simulation::Paths.find(name)}; + if (track == nullptr || track->m_paths.empty()) + { + WriteLog("SELFTEST lay: no track " + name); + return selftest_result::pause; + } + auto const &path{track->m_paths.front()}; + auto const atend{side == "end"}; + editor_track::snap_target start; + start.track = track; + start.point = {0, atend ? editor_track::point_kind::end : editor_track::point_kind::start}; + start.position = path.points[atend ? segment_data::point::end : segment_data::point::start]; + auto const &control{path.points[atend ? segment_data::point::control2 : segment_data::point::control1]}; + start.direction = glm::normalize(control != glm::dvec3{} ? control : path.points[atend ? segment_data::point::start : segment_data::point::end] - start.position); + m_lay.points = {start.position}; + m_lay.start = start; + for (glm::dvec3 point{0.0, start.position.y, 0.0}; words >> point.x >> point.z;) + m_lay.points.push_back(point); + lay_finish({}); + WriteLog("SELFTEST lay: " + m_lay.status); + return selftest_result::handled; + } + if (command == "analyse") + { + std::string name; + words >> name; + editor_track::curve curve; + if (auto *track{simulation::Paths.find(name)}; track != nullptr && editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve)) + { + std::string arcs; + for (auto const &arc : curve.arcs) + arcs += format(" R%.0f/%.1fdeg/after %.1fm", arc.radius, glm::degrees(arc.turn), arc.transition); + WriteLog(format("SELFTEST analyse %s: R %.0f, compound %d, in %.1f m, out %.1f m, turn %.1f deg,", name.c_str(), curve.radius, curve.compound ? 1 : 0, curve.transition_in, curve.transition_out, glm::degrees(curve.turn)) + arcs); + } + else + WriteLog("SELFTEST analyse " + name + ": no curve"); + return selftest_result::handled; + } + if (command == "route") + { + glm::dvec3 point{0.0}; + int single{1}; + words >> point.x >> point.z >> single; + TTrack *nearest{nullptr}; + double distance{1e9}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const d{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point)}; + if (d < distance) + { + distance = d; + nearest = track; + } + } + m_track_tab = track_tab::route; + m_route.single = (single != 0); + auto const loaded{nearest != nullptr && route_load(*nearest)}; + WriteLog(format("SELFTEST route: %s from %s, %zu paths, %.3f m, %zu vertices, %d from the design, km %.3f, departed %d by %.3f m, valid %d", loaded ? "loaded" : "failed", nearest != nullptr ? nearest->name().c_str() : "-", + m_route.chain.tracks.size(), m_route.chain.length, m_route.design.vertices.size(), m_route.designed, m_route.origin / 1000.0, m_route.departed, m_route.departure, m_route.result.valid ? 1 : 0)); + WriteLog("SELFTEST route status: " + m_route.status + " " + m_route.error); + for (std::size_t i = 0; i < m_route.design.vertices.size(); ++i) + { + auto const &v{m_route.design.vertices[i]}; + auto text{format("SELFTEST route vertex %zu: at %.3f %.3f offset %.3f R %.3f L %.3f / %.3f cant %.1f compound %d share %.4f", i, v.position.x, v.position.y, v.offset, v.radius, v.transition_in, v.transition_out, v.cant, v.compound ? 1 : 0, v.share)}; + for (auto const &arc : v.arcs) + text += format(" [R %.3f L %.3f share %.4f]", arc.radius, arc.transition, arc.share); + WriteLog(text); + } + return selftest_result::handled; + } + if (command == "routevertex" || command == "routearc" || command == "routeshare") + { + int index{0}; + words >> index; + if (index < 0 || index >= static_cast(m_route.design.vertices.size())) + { + WriteLog("SELFTEST " + command + ": no vertex " + std::to_string(index)); + return selftest_result::handled; + } + auto &vertex{m_route.design.vertices[index]}; + if (command == "routevertex") + words >> vertex.radius >> vertex.transition_in >> vertex.transition_out >> vertex.cant; + else if (command == "routeshare") + words >> vertex.share; + else + { + alignment::compound_arc arc; + words >> arc.radius >> arc.transition >> arc.share; + vertex.compound = true; + vertex.arcs.push_back(arc); + } + route_update(); + std::string errors; + for (auto const &error : m_route.result.errors) + errors += " " + error; + WriteLog(format("SELFTEST %s: valid %d, %zu elements, extension %.3f / %.3f", command.c_str(), m_route.result.valid ? 1 : 0, m_route.result.elements.size(), m_route.result.start_extension, m_route.result.end_extension) + errors); + return selftest_result::handled; + } + if (command == "routekm") + { + double km{0.0}; + words >> km; + m_route.origin = km * 1000.0; + return selftest_result::handled; + } + if (command == "routeapply" || command == "routetake") + { + if (command == "routeapply") + route_apply(); + else + route_take_stored(m_route.departed); + WriteLog("SELFTEST " + command + ": " + m_route.status + " " + m_route.error); + return selftest_result::handled; + } + if (command == "digitizeall") + { + digitize_all(); + WriteLog(format("SELFTEST digitizeall: %zu alignments", alignment_store::records().size())); + return selftest_result::handled; + } + if (command == "alignments") + { + auto const &records{alignment_store::records()}; + for (std::size_t r = 0; r < records.size(); ++r) + { + auto const &entry{records[r]}; + WriteLog(format("SELFTEST alignment %zu in %s: km %.3f, %zu vertices, %zu elements, consistent %d, length %.3f", r, scene::Layers.layer(entry.layer).name.c_str(), entry.origin / 1000.0, entry.design.vertices.size(), entry.elements.size(), entry.consistent ? 1 : 0, entry.result.length)); + for (auto const &e : entry.elements) + WriteLog(format("SELFTEST element %s vertex %d km %.3f L %.3f R %.3f -> %.3f cant %.1f", e.kind.c_str(), e.vertex, (entry.origin + e.chainage) / 1000.0, e.length, e.radius_start, e.radius_end, e.cant)); + } + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const found{alignment_store::on_track(*track)}; + std::string text{"SELFTEST path " + track->name() + ": "}; + if (false == found.has_value()) + { + auto const closest{alignment_store::at(geometry::bezier{track->m_paths.front()}.point(0.5), 1000.0)}; + text += closest.has_value() ? format("off the alignments, %.3f m from alignment %d", closest->offset, closest->record) : std::string{"off the alignments"}; + } + else + { + auto const e{alignment_store::describe(records[found->record].result.elements[found->element])}; + text += format("alignment %d element %d %s R %.3f -> %.3f, offset %.4f", found->record, found->element, e.kind.c_str(), e.radius_start, e.radius_end, found->offset); + } + editor_track::curve curve; + if (editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve)) + text += format(" | curve R %.3f L %.3f / %.3f cant %.1f compound %d arcs %zu %s", curve.radius, curve.transition_in, curve.transition_out, curve.cant, curve.compound ? 1 : 0, curve.arcs.size(), curve.designed.has_value() ? "designed" : "recognized"); + WriteLog(text); + } + return selftest_result::handled; + } + if (command == "vprofile") + { + std::string from; + std::string to; + words >> from >> to; + auto *first{simulation::Paths.find(from)}; + auto *last{simulation::Paths.find(to)}; + if (first == nullptr || last == nullptr) + { + WriteLog("SELFTEST vprofile: no such paths"); + return selftest_result::handled; + } + m_profile.origin = 0.0; + profile_open(first, last); + WriteLog(format("SELFTEST vprofile: %zu spans, %.3f m, km of the start %.3f", m_profile.route.spans.size(), m_profile.route.length, m_profile.origin / 1000.0)); + for (auto const &zone : m_profile.context.switches) + 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(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; + words >> name; + auto *track{simulation::Paths.find(name)}; + editor_track::curve curve; + editor_track::chain chain; + std::string error; + if (track == nullptr || false == editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve) || false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) + { + WriteLog("SELFTEST backwardcurve: no curve at " + name); + return selftest_result::handled; + } + std::vector> run; + for (std::size_t i = chain.tracks.size(); i > 0; --i) + run.emplace_back(chain.tracks[i - 1], false == chain.forward[i - 1]); + editor_track::curve backward; + editor_track::analyse_curve(run, m_route.design.norms.gauge, backward); + auto text{format("SELFTEST backwardcurve: %zu paths, R %.3f L %.3f / %.3f cant %.1f compound %d %s", run.size(), backward.radius, backward.transition_in, backward.transition_out, backward.cant, backward.compound ? 1 : 0, backward.designed.has_value() ? "designed" : "recognized")}; + for (auto const &arc : backward.arcs) + text += format(" [R %.3f turn %.4f L %.3f]", arc.radius, arc.turn, arc.transition); + WriteLog(text); + return selftest_result::handled; + } + if (command == "deletetrack" || command == "deletekind") + { + std::string name; + words >> name; + if (command == "deletetrack" && name.empty()) + { + delete_selected_track(); + WriteLog("SELFTEST deletetrack"); + return selftest_result::handled; + } + TTrack *chosen{nullptr}; + if (command == "deletetrack") + chosen = simulation::Paths.find(name); + else + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const found{alignment_store::on_track(*track)}; + if (found.has_value() && alignment_store::describe(alignment_store::records()[found->record].result.elements[found->element]).kind == name) + { + chosen = track; + break; + } + } + if (chosen == nullptr || chosen->m_editorremoved) + { + WriteLog("SELFTEST " + command + ": no path " + name); + return selftest_result::handled; + } + m_node = chosen; + ui()->set_node(m_node); + auto const deleted{chosen->name()}; + delete_selected_track(); + WriteLog("SELFTEST " + command + ": " + deleted + " deleted"); + return selftest_result::handled; + } + if (command == "btfcheck") + { + std::string name; + words >> name; + auto const textfile{Global.asCurrentSceneryPath + name + ".txtf"}; + auto const binaryfile{Global.asCurrentSceneryPath + name + ".btf"}; + auto const written{scene::terrain_file::prepare(textfile, binaryfile) == scene::terrain_file::state::binary}; + auto const kept{scene::terrain_file::extra(binaryfile)}; + WriteLog(format("SELFTEST btfcheck: binary %d, %zu bytes of the editor lines kept", written ? 1 : 0, kept.size())); + std::istringstream lines{kept}; + std::string line; + while (std::getline(lines, line)) + WriteLog("SELFTEST btf kept: " + line); + return selftest_result::handled; + } + if (command == "pathsnear") + { + glm::dvec3 point{0.0}; + double radius{100.0}; + words >> point.x >> point.z >> radius; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) + continue; + for (std::size_t i = 0; i < track->m_paths.size(); ++i) + { + auto const &path{track->m_paths[i]}; + auto const &a{path.points[segment_data::point::start]}; + auto const &b{path.points[segment_data::point::end]}; + if (geometry::plan_distance(a, point) > radius && geometry::plan_distance(b, point) > radius) + continue; + auto const &c1{path.points[segment_data::point::control1]}; + auto const &c2{path.points[segment_data::point::control2]}; + WriteLog(format("SELFTEST path %s/%zu %s: %.3f %.3f -> %.3f %.3f, c1 %.3f %.3f, c2 %.3f %.3f, %.3f m", track->name().c_str(), i, track->eType == tt_Switch ? "switch" : "normal", a.x, a.z, b.x, b.z, c1.x, c1.z, c2.x, c2.z, geometry::bezier{path}.plan_length())); + } + } + return selftest_result::handled; + } + if (command == "splitkind" || command == "nudgekind") + { + std::string kind; + glm::dvec3 offset{0.0}; + words >> kind >> offset.x >> offset.z; + auto const &records{alignment_store::records()}; + TTrack *chosen{nullptr}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const found{alignment_store::on_track(*track)}; + if (found.has_value() && alignment_store::describe(records[found->record].result.elements[found->element]).kind == kind) + { + chosen = track; + break; + } + } + if (chosen == nullptr) + { + WriteLog("SELFTEST " + command + ": no path over " + kind); + return selftest_result::handled; + } + if (command == "splitkind") + { + auto const before{editor_track::capture(*chosen)}; + auto *created{editor_track::split_path(*chosen, 0.5)}; + if (created != nullptr) + push_track_snapshot({{chosen, before}}, {created}); + WriteLog("SELFTEST splitkind: " + chosen->name() + " split, the second half " + (created != nullptr ? created->name() : std::string{"-"})); + } + else + { + std::vector> states{{chosen, editor_track::capture(*chosen)}}; + std::vector moved{chosen}; + auto const &path{chosen->m_paths.front()}; + for (auto const &end : {path.points[segment_data::point::start], path.points[segment_data::point::end]}) + for (auto *neighbour : {chosen->trPrev, chosen->trNext}) + { + if (neighbour == nullptr || neighbour->m_paths.empty() || std::find(moved.begin(), moved.end(), neighbour) != moved.end()) + continue; + auto const &other{neighbour->m_paths.front()}; + for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end}) + if (glm::distance(other.points[kind == editor_track::point_kind::start ? segment_data::point::start : segment_data::point::end], end) < 0.01) + { + states.emplace_back(neighbour, editor_track::capture(*neighbour)); + editor_track::move_point(*neighbour, {0, kind}, end + offset); + moved.push_back(neighbour); + break; + } + } + editor_track::translate(*chosen, offset); + editor_track::commit(moved); + push_track_snapshot(std::move(states), {}); + WriteLog("SELFTEST nudgekind: " + chosen->name() + " moved with " + std::to_string(moved.size() - 1) + " joined ends"); + } + return selftest_result::handled; + } + if (command == "digitize") + { + glm::dvec3 point{0.0}; + words >> point.x >> point.z; + TTrack *nearest{nullptr}; + double distance{1e9}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const d{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point)}; + if (d < distance) + { + distance = d; + nearest = track; + } + } + auto const kept{nearest != nullptr && digitize_drawn({nearest}, false)}; + WriteLog(format("SELFTEST digitize %s: %d", nearest != nullptr ? nearest->name().c_str() : "-", kept ? 1 : 0)); + return selftest_result::handled; + } + if (command == "parallel") + { + words >> m_parallel.spacing >> m_parallel.side; + m_parallel.scope = 1; + m_parallel.built_for = nullptr; + parallel_build(); + WriteLog("SELFTEST parallel: " + m_parallel.status + " " + m_parallel.error); + return selftest_result::handled; + } + return selftest_result::unhandled; +}