diff --git a/application/editormode.h b/application/editormode.h index 863930f0..d9ff9643 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -36,6 +36,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include #include @@ -1804,6 +1805,20 @@ class editor_mode : public application_mode, private editor_track::observer bool quitting{false}; }; selftest m_selftest; + enum class selftest_result + { + unhandled, + handled, + pause + }; + selftest_result selftest_scene(std::string const &command, std::istringstream &words); + selftest_result selftest_sweep(std::string const &command, std::istringstream &words); + selftest_result selftest_track(std::string const &command, std::istringstream &words); + selftest_result selftest_switch(std::string const &command, std::istringstream &words); + selftest_result selftest_join(std::string const &command, std::istringstream &words); + selftest_result selftest_signal(std::string const &command, std::istringstream &words); + selftest_result selftest_terrain(std::string const &command, std::istringstream &words); + selftest_result selftest_dispatch(std::string const &command, std::istringstream &words); void selftest_step(); // a value typed during a gesture, taken instead of the one the cursor gives std::string m_typed; diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index ee138139..931e8e4b 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -44,6 +44,2150 @@ http://mozilla.org/MPL/2.0/. #include #include +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_join, &editor_mode::selftest_signal, &editor_mode::selftest_terrain}) + 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, 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 == "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 == "frames") + { + std::string name; + double from{0.0}, to{0.0}, 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 == "screenshot") + { + Application.queue_screenshot(); + test.wait = 3; + return selftest_result::pause; + } + if (command == "save") + { + save(); + 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()}, 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, action, 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}, 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, high, shift; + sweep->bounds(low, high, shift); + auto const &state{sweep->definition()}; + auto const marker{bend_marker(*sweep)}; + glm::dvec3 boxlow{std::numeric_limits::max()}, 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(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, 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 == "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, 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)); + 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, label; + glm::dvec3 at{0.0}, toward{0.0}; + words >> name >> at.x >> at.z >> toward.x >> toward.z >> label; + 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 == "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()}, 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 == "switchdrive") + { + int index{-1}, 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}, 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}, 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 == "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}, high{0.0}; + std::string reason; + auto const room{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 &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{paths.size() >= 2 && move_switch_paths(*track, paths, status)}; + 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 == "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}, 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}, 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 == "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}, 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 (issue.type == joint_issue::kind::open) + continue; + if (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, 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}, 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}, z{0.0}, radius{10.0}, 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}, halfwidth{3.5}; + words >> modifier.name >> a.position.x >> a.position.y >> b.position.x >> b.position.y >> formation >> halfwidth; + a.formation = b.formation = formation; + a.half_width = b.half_width = halfwidth; + 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, 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}, z{0.0}, 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}, 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}, z{0.0}, 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}, 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}, 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; +} + // the script is a text file, one action a line: // wait // camera from the first point, looking at the second @@ -141,2004 +2285,9 @@ void editor_mode::selftest_step() words >> test.wait; return; } - if (command == "camera") - { - glm::dvec3 from, 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; - } - else 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")); - } - else 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); - } - } - else if (command == "drag") - { - glm::dvec3 from{0.0}, 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"); - continue; - } - sweep_drag_at(to); - sweep_release(); - WriteLog("SELFTEST drag: " + m_sweep.status); - } - else 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"); - continue; - } - 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]))); - } - } - else 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"); - continue; - } - 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); - } - else if (command == "curvedswitch") - { - std::string name, label; - glm::dvec3 at{0.0}, toward{0.0}; - words >> name >> at.x >> at.z >> toward.x >> toward.z >> label; - 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); - continue; - } - 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); - } - else 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); - continue; - } - 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()}, 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); - } - else if (command == "frames") - { - std::string name; - double from{0.0}, to{0.0}, step{0.1}; - words >> name >> from >> to >> step; - auto *sweep{simulation::Sweeps.find(name)}; - if (sweep == nullptr) - continue; - auto const side{sweep->definition().lateral >= 0.0 ? 1 : 0}; - if (step <= 0.0) - continue; - 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))); - } - } - else if (command == "switchdrive") - { - int index{-1}, 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"})); - } - else if (command == "drivechoice") - { - int index{-1}, 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); - } - else 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())); - } - else 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); - } - else 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); - } - else 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); - continue; - } - 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)); - } - else 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)); - } - else 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); - } - } - else 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"); - continue; - } - 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); - } - } - else 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; - } - else 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)); - } - else if (command == "lay") - { - std::string name, side; - words >> name >> side; - auto *track{simulation::Paths.find(name)}; - if (track == nullptr || track->m_paths.empty()) - { - WriteLog("SELFTEST lay: no track " + name); - return; - } - 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); - } - else 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"); - } - else 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)); - } - } - else 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{"-"})); - } - } - else if (command == "newscenery") - { - WriteLog(restart_for_new_scenery() ? "SELFTEST newscenery: started" : "SELFTEST newscenery: failed"); + if (selftest_dispatch(command, words) == selftest_result::pause) return; - } - else 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())); - } - } - else if (command == "screenshot") - { - Application.queue_screenshot(); - test.wait = 3; - return; - } - else if (command == "save") - { - save(); - } - else 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)); - } - else 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"})); - } - else 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"})); - } - else 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); - } - else 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()); - } - else 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()); - } - else if (command == "copy") - copy_selection(); - else if (command == "paste") - { - glm::dvec3 point{0.0}; - words >> point.x >> point.y >> point.z; - paste_clipboard(point); - } - else if (command == "group") - group_selection(); - else if (command == "ungroup") - ungroup_selection(); - else if (command == "drop") - drop_selection_to_ground(); - else if (command == "delete") - delete_selected(); - else 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"})); - } - else 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))); - continue; - } - 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"); - continue; - } - 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"); - } - } - else if (command == "bendprobe") - { - auto *sweep{m_bend.edited}; - if (sweep == nullptr) - { - WriteLog("SELFTEST bendprobe: nothing bent"); - continue; - } - glm::dvec3 low, high, shift; - sweep->bounds(low, high, shift); - auto const &state{sweep->definition()}; - auto const marker{bend_marker(*sweep)}; - glm::dvec3 boxlow{std::numeric_limits::max()}, 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)); - } - else 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"); - continue; - } - 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()}, 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)); - } - else if (command == "key") - { - std::string key, action, 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))); - } - else if (command == "lineside") - { - std::string tab; - words >> tab; - show_track_tab(track_tab::lineside); - (tab == "vehicle" ? m_vehicle.expand : tab == "hekto" ? m_hekto.expand : tab == "fouling" ? m_fouling.expand : m_parallel.expand) = true; - } - else 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")); - } - else 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"})); - } - else if (command == "bendpanel") - { - ui()->expand_bend(); - } - else if (command == "newscenerypopup") - { - m_newscenery_asked = true; - } - else if (command == "openscenerypopup") - { - m_openscenery_asked = true; - std::snprintf(m_openscenery_search, sizeof(m_openscenery_search), "%s", "zz_"); - } - else if (command == "openscenery") - { - std::string name; - words >> name; - WriteLog(restart_editor(name) ? "SELFTEST openscenery: started " + name : "SELFTEST openscenery: failed"); - } - else 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"); - } - else 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); - } - } - else 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)); - continue; - } - 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); - } - else if (command == "routekm") - { - double km{0.0}; - words >> km; - m_route.origin = km * 1000.0; - } - else 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); - } - else 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); - } - } - else if (command == "vprofile") - { - std::string from, 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"); - continue; - } - 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)); - } - else 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); - continue; - } - 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); - } - else if (command == "switchplace" || command == "switchcurve" || command == "slipplace") - { - std::string label; - glm::dvec3 point{0.0}; - int direction{1}, 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"); - continue; - } - 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"); - continue; - } - 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); - } - else if (command == "deletetrack" || command == "deletekind") - { - std::string name; - words >> name; - 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); - continue; - } - m_node = chosen; - ui()->set_node(m_node); - auto const deleted{chosen->name()}; - delete_selected_track(); - WriteLog("SELFTEST " + command + ": " + deleted + " deleted"); - } - else 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); - } - else 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); - continue; - } - if (command == "switchslide") - { - double distance{0.0}; - words >> distance; - double low{0.0}, high{0.0}; - std::string reason; - auto const room{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())); - continue; - } - 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); - continue; - } - 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{paths.size() >= 2 && move_switch_paths(*track, paths, status)}; - 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())); - } - else 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())); - } - } - } - else 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)); - } - } - else 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); - continue; - } - 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"); - } - } - else if (command == "join" || command == "layto") - { - glm::dvec3 a{0.0}, 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"); - continue; - } - std::vector pieces; - std::string error; - double length{0.0}; - if (command == "join") - { - double radius{0.0}; - int variant{0}, 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"); - continue; - } - 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))); - } - else 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)); - } - else 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); - } - else 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}, 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)); - } - else 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 (issue.type == joint_issue::kind::open) - continue; - if (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())); - } - else if (command == "gizmo") - { - std::string action; - words >> action; - auto *track{selected_track()}; - if (track == nullptr) - { - WriteLog("SELFTEST gizmo: nothing selected"); - continue; - } - 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, 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)); - } - } - } - } - else if (command == "deletetrack") - { - delete_selected_track(); - WriteLog("SELFTEST deletetrack"); - } - else 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)); - } - else if (command == "undo") - { - undo_last(); - WriteLog("SELFTEST undo"); - } - else if (command == "redo") - { - redo_last(); - WriteLog("SELFTEST redo"); - } - else if (command == "checkspans") - { - WriteLog("SELFTEST spans: " + scene::Layers.check_sources()); - } - else if (command == "log") - { - std::string text; - std::getline(words, text); - WriteLog("SELFTEST" + text); - } - else 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"]); - continue; - } - 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"]); - continue; - } - 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"]); - continue; - } - 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"]); - continue; - } - 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"]); - continue; - } - 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())); - } - } - } - else if (command == "orthophoto") - { - auto config{m_orthophoto.settings()}; - int fit{1}, 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" : "")); - } - else if (command == "terrainmode") - { - m_terrain_open = true; - WriteLog("SELFTEST terrainmode"); - } - else if (command == "terrainsculpt" || command == "terrainpaint" || command == "terrainlevel") - { - double x{0.0}, z{0.0}, radius{10.0}, 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")); - } - else if (command == "terrainmodifier") - { - heightmap::modifier modifier; - heightmap::corridor_point a, b; - double formation{0.0}, halfwidth{3.5}; - words >> modifier.name >> a.position.x >> a.position.y >> b.position.x >> b.position.y >> formation >> halfwidth; - a.formation = b.formation = formation; - a.half_width = b.half_width = halfwidth; - modifier.points = {a, b}; - m_streamer.modifier(std::move(modifier)); - forget_ground(); - WriteLog(format("SELFTEST terrainmodifier: %zu modifiers", m_streamer.modifiers().size())); - } - else if (command == "terrainfill") - { - int layer{0}; - words >> layer >> m_paint_feather >> m_paint_rotation; - m_terrain_layer = layer; - m_paint_outline.clear(); - double x, 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); - } - else 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)); - } - else if (command == "terraintouchup") - { - int state{1}; - words >> state; - m_streamer.touch_up(state != 0); - WriteLog(format("SELFTEST terraintouchup %d", state)); - } - else if (command == "terrainrestore") - { - double x{0.0}, z{0.0}, 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())); - } - else 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())); - } - else if (command == "terrainselect") - { - int cx{0}, 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())); - } - else 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); - } - else 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); - } - else 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); - } - else if (command == "terrainprobe") - { - double x{0.0}, z{0.0}, 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}, 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())); - } - else if (command == "terrainconvert") - { - load_ground_files(); - for (auto const &source : m_ground_conversion.files) - { - glm::dvec3 low{1e30}, 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); - } - else if (command == "quit") + if (command == "quit") { if (false == test.quitting) {