/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" #include "application/editormode.h" #include "editor/editorSettings.hpp" #include "editor/editorGeometry.hpp" #include "application/application.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "simulation/simulation.h" #include "simulation/simulationtime.h" #include "scene/scenelayers.h" #include "utilities/Globals.h" #include "utilities/Logs.h" #include "world/Track.h" #include "world/TrackRails.h" #include "world/MemCell.h" #include "world/Event.h" #include "vehicle/DynObj.h" #include "model/AnimModel.h" #include "model/Model3d.h" #include "rendering/renderer.h" #include "world/Sweep.h" #include "input/keyboardinput.h" #include "launcher/textures_scanner.h" #include #include #include #include "editor/editorFormat.hpp" #include "editor/editorAlignmentStore.hpp" #include "editor/editorSwitchStore.hpp" #include "scene/sceneterrain.h" #include "editor/editorSpeed.hpp" #include #include #include #include #include #include #include "application/editormodeselftest.h" editor_mode::selftest_result editor_mode::selftest_switch(std::string const &command, std::istringstream &words) { if (command == "curvedswitch") { std::string name; std::string label; glm::dvec3 at{0.0}; glm::dvec3 toward{0.0}; words >> name >> at.x >> at.z >> toward.x >> toward.z >> label; std::replace(label.begin(), label.end(), '_', ' '); m_node = simulation::Paths.find(name); ui()->set_node(m_node); show_track_tab(track_tab::turnout); arm_switch(); auto &tool{m_switch}; for (int i = 0; i < static_cast(tool.templates.size()); ++i) if (false == label.empty() && tool.templates[i].label.find(label) != std::string::npos) { tool.armed = i; break; } m_cursor_override = at; auto const started{start_switch_placement()}; m_cursor_override.reset(); if (false == started) { WriteLog("SELFTEST curvedswitch: not started, " + tool.status); return selftest_result::handled; } tool.mouse = glm::dvec3{toward.x, tool.point.y, toward.z}; finish_switch_placement(); WriteLog("SELFTEST curvedswitch " + (tool.armed >= 0 ? tool.templates[tool.armed].label : std::string{}) + ": " + tool.status); return selftest_result::handled; } if (command == "designer") { std::string list; int index{0}; words >> list >> index; auto &designer{m_switch_designer}; designer.open = list != "close"; designer.from_catalogue = list != "own"; designer.selected = index; return selftest_result::handled; } if (command == "release") { m_switch.placing = false; lay_cancel(); m_cursor_override.reset(); return selftest_result::handled; } if (command == "hover") { glm::dvec3 at{0.0}; if (words >> at.x >> at.z) m_cursor_override = at; else m_cursor_override.reset(); return selftest_result::handled; } if (command == "layhold") { int layout{0}; words >> layout >> m_lay.radius; m_track_layout = static_cast(std::clamp(layout, 0, 3)); m_lay.active = true; lay_cancel(); std::vector points; for (glm::dvec3 point{0.0}; words >> point.x >> point.z;) points.push_back(point); for (std::size_t i = 0; i + 1 < points.size(); ++i) { auto point{points[i]}; point.y = 5000.0; std::vector found; auto const ground{ground_heights({point}, true, &found)}; point.y = found[0] != 0 ? ground[0] : m_lay.points.empty() ? 0.0 : m_lay.points.back().y; m_lay.mouse_snap = editor_track::find_free_end(nullptr, 1, point, 3.0, {}); m_lay.mouse = m_lay.mouse_snap.track != nullptr ? point : lay_bed(point); lay_click(); } if (false == points.empty()) m_cursor_override = points.back(); WriteLog("SELFTEST layhold: " + m_lay.status); return selftest_result::handled; } if (command == "switchkind") { // what the switch tool places, as chosen in its panel: single, slip, crossing or connection with the kind of it std::string word; int sub{0}; words >> word >> sub; auto const kind{word == "slip" ? switch_kind::slip : word == "crossing" ? switch_kind::crossing : word == "connection" ? switch_kind::connection : switch_kind::single}; m_switch.connection_kind = sub; choose_switch_kind(kind); WriteLog(format("SELFTEST switchkind %s: kind %d, connection %d, armed %d", word.c_str(), static_cast(current_switch_kind()), m_switch.connection, m_switch.armed)); return selftest_result::handled; } if (command == "connection" || command == "connectionplan" || command == "connectionhold") { std::string kindword; std::string label; std::string second; glm::dvec3 at{0.0}; glm::dvec3 toward{0.0}; words >> kindword >> at.x >> at.z >> toward.x >> toward.z >> label >> second; connection::kind kind{connection::kind::crossover}; if (kindword != "single" && false == connection::parse(kindword, kind)) { WriteLog("SELFTEST connection: unknown kind " + kindword); return selftest_result::handled; } auto &tool{m_switch}; for (std::string key; words >> key;) { if (key == "radius") words >> tool.connection_radius; else if (key == "gap") words >> tool.connection_gap; else if (key == "together") words >> tool.connection_together; else if (key == "stub") words >> tool.connection_stub; else if (key == "spacings") { std::string value; words >> value; std::snprintf(tool.connection_spacings, sizeof(tool.connection_spacings), "%s", value.c_str()); } } show_track_tab(track_tab::turnout); arm_switch(); auto const find = [&](std::string Label) { std::replace(Label.begin(), Label.end(), '_', ' '); for (int i = 0; i < static_cast(tool.templates.size()); ++i) if (false == Label.empty() && Label != "-" && false == tool.templates[i].double_slip && false == tool.templates[i].twofold() && tool.templates[i].label.find(Label) != std::string::npos) return i; return -1; }; auto const first{find(label)}; if (first < 0) { WriteLog("SELFTEST connection: no template " + label); return selftest_result::handled; } tool.armed = first; tool.connection_second = find(second); tool.connection = kindword == "single" ? -1 : static_cast(kind); auto const line{straight_near(at, {}, 3.0)}; TTrack *start{line.tracks.empty() ? nullptr : line.tracks.front()}; if (start == nullptr) { // along a curve: the track nearest to the press auto best{3.0}; for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) continue; auto const distance{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, at)), at)}; if (distance < best) { best = distance; start = track; } } } if (start == nullptr) { WriteLog("SELFTEST connection: no track at the start"); return selftest_result::handled; } m_node = start; 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 (tool.curved) { finish_switch_placement(); WriteLog("SELFTEST connection along the curve: " + tool.status); return selftest_result::handled; } 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; }