/* 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 namespace { // frames gone through the selftest, for the measurements of perf std::size_t selftest_frames{0}; std::string one_line(std::string Text) { std::replace(Text.begin(), Text.end(), '\n', ';'); return Text; } } // namespace editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &command, std::istringstream &words) { for (auto const group : {&editor_mode::selftest_scene, &editor_mode::selftest_sweep, &editor_mode::selftest_track, &editor_mode::selftest_switch, &editor_mode::selftest_rails, &editor_mode::selftest_join, &editor_mode::selftest_signal, &editor_mode::selftest_terrain, &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; in %.1f ms", result.beams, result.overlaps, result.gaps, result.bare, result.toolong, result.longest, result.widest, result.widestat.c_str(), result.offplane, result.panels, result.clipped, result.dropped, result.slowest, result.biggest, std::chrono::duration(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}; 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"] == "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 == "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) { ++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 // select // selectat the path nearest the point, within 3 m, as a click on it // blade [name] logs the blades of each switch cut from the rails: tip, 50 mm head, full head, heel, drop of the head, open flangeway, the gauge lines apart against x^2/2R, vertices in both positions // frog [name] logs where the inner rails of each switch cross: distance on both paths, 1:n, throat, nose, check rails // guards [name] logs each guard rail and what each of its ends and breaks is cut along; a fault when a cut is not along a rail or a glued end not on the guard rail of the other track // railtime lays out the rails of every track, as when its geometry is made, and logs how long it took // throw <0|1> sets every switch to the path and moves its blades there at once // crossings [name] logs where rails of tracks cross rails of other tracks and of what kind (acute, obtuse, middle acute or obtuse, wide), checks that each acute crossing has one partner and one nose // frogkey name [on|off] sets or clears the frogs key of a switch (one undo step); logs the key, whether it is written, the frog and the crossings // geomhash [name] logs the number of vertices and a hash of the drawn geometry of each rail track (rails, trackbed, cut faces), to compare builds; exact: the bits of every value, userdata and tread materials too // sweep =... model, params (separated by commas), lateral, height, side (origin, near, far, // centre), top (origin, bottom, top, edge), mode (bend, repeat), step, // length (original, all, or metres), from, to, widen (0, 1), face (0, 1), tilt (0, 1) // selectmodel the model instance nearest the point // bend =... the selected model bent along a track: track (index on the list), axis (auto, z, x), // turned (0, 1), along (start, origin, middle, end, point), side (origin, near, far, // middle, point), height (origin, bottom, top, edge, point), point=,,, // pick=,, (a point of the scene picked as the reference point), go (0, 1) // selectinclude the include placed nearest the point // addmodel the model instance nearest the point added to the selection, or taken out (Shift+LMB) // selection logs the selected models, with the models of their groups // moveselection the selection moved by the offset, as the gizmo moves it // turnselection the selection turned around the model with the gizmo // copy, paste , group, ungroup, drop, delete as the keys of the selection do them // bendset =... the bent model being edited: length, lateral, height, along, side, height // bendprobe logs the bent model being edited // undo, redo // key [ctrl] a key or right button event, as the window gives it // lineside the tab of the objects along the track in the track window // event queues the event // memcell logs the memory cell // bendpanel unfolds the bend section of the toolset // openscenerypopup the window of File / Open scenery, its list searched for zz_ // openscenery the editor starts again with the scenery // checkspans logs whether the text the scenery files hold at each node's place starts and ends it // spread logs the paths the spread from the track takes // newscenery the editor starts again with the wizard of a new scenery // mapstate logs the orthophoto, the top view and the //$o line of the scenery // trainset [km/h] the trainset tool puts the vehicle in the middle of the track // vehicles logs each vehicle: its head, facing, and what its couplers hold // terrainmode the terrain field of work, with its texture browser // terrainsculpt , terrainpaint , // terrainlevel [mode] as a stroke of the brush // terrainadd chunks made around the camera, count x count, as the button of the chunks panel does it // terrainprobe logs the height of the heightmap terrain and the materials of the chunk // terrainconvert converts every material of the terrain files of the scenery // terrainmodifier a straight modifier, as the vertical profile makes them // terraintouchup <0|1> the brushes touch up over the modifiers, or shape the ground under them // terrainrestore a stroke of the restore brush // terrainbake the modifier baked into the ground // terrainselect [add] the chunk selected with the spacing tool (add: Ctrl) // terrainresolution [paint 0|1] the selected chunks converted, as the button of the resolution panel does it // terrainfill ... the area outlined filled, as the fill tool does it // terrainturn the textures of the palette entry turned // terrainwater [size] material and repeat of a body of water; terrainwaterdefault // orthophoto [in scene 0|1] the imagery layer on, placed and fitted as given // join [radius] [variant] [transitions 0|1] the free ends at the points joined, as a drag from one to the other // layto from the free end at the first point towards the second, as a drag in the layout // layclicks ... clicks of the lay tool at the points, a free end within 3 m joined // joints ... the joints check over the scenery, the issues around the points logged, every issue with free ends without points // route the route design of the path nearest the point, a curve or the whole line // routevertex , routearc , routeshare , routekm // routeapply, routetake as the buttons of the route design do it // alignments logs the alignments kept in the scenery and the element under each path // digitizeall every line not digitized yet kept in the scenery, as the button of the route design does it // splitkind , nudgekind the first path over an element of the kind split in two, or moved // switchplace