From 1dc61eaf313f44f04b6195f403315c85a23799f7 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 15 Oct 2026 16:58:44 +0200 Subject: [PATCH] feat(editor): scripted mouse and keys for the self-tests and the launcher: mouse, mouseat, glide, button, wheel, press, release, tap, type and softcursor drive the window as the user does, EU07_LAUNCHER_SELFTEST runs a script in the launcher and the scenery wizard and hands an editor script on with editorscript; selectat selects the path nearest a point, switchlink no longer takes over a crossover left in the tool, joints without points logs every issue with free ends --- CMakeLists.txt | 1 + application/editormode.h | 2 + application/editormodeselftest.cpp | 79 ++++++++++- application/scriptedinput.cpp | 218 +++++++++++++++++++++++++++++ application/scriptedinput.h | 49 +++++++ launcher/launchermode.cpp | 1 + launcher/launchermode.h | 4 + 7 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 application/scriptedinput.cpp create mode 100644 application/scriptedinput.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3967638b..c8c4324c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -214,6 +214,7 @@ set(SOURCES "application/editormodesweep.cpp" "application/editormodeturntable.cpp" "application/editormodeselftest.cpp" +"application/scriptedinput.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" "application/editoruipanels.cpp" diff --git a/application/editormode.h b/application/editormode.h index 7162d83b..a3f3a873 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -29,6 +29,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorProfile.hpp" #include "editor/editorInfra.hpp" #include "editor/editorGauge.hpp" +#include "application/scriptedinput.h" #include "world/Sweep.h" #include @@ -1915,6 +1916,7 @@ class editor_mode : public application_mode, private editor_track::observer std::size_t next{0}; int wait{0}; bool quitting{false}; + scripted_input input; }; selftest m_selftest; enum class selftest_result diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 5f3729da..9215828a 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -69,6 +69,29 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm m_focus_active = false; return selftest_result::handled; } + if (command == "selectat") + { + glm::dvec3 point{0.0}; + words >> point.x >> point.z; + TTrack *nearest{nullptr}; + double distance{1e9}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) + continue; + auto const d{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point)}; + if (d < distance) + { + distance = d; + nearest = track; + } + } + m_node = distance < 3.0 ? nearest : nullptr; + m_track_point = {}; + ui()->set_node(m_node); + WriteLog(format("SELFTEST selectat %.1f %.1f: %s", point.x, point.z, m_node != nullptr ? m_node->name().c_str() : "nothing")); + return selftest_result::handled; + } if (command == "select") { std::string name; @@ -139,6 +162,48 @@ editor_mode::selftest_result editor_mode::selftest_scene(std::string const &comm } return selftest_result::handled; } + if (command == "mouseat") + { + glm::dvec3 at{0.0}; + words >> at.x >> at.y >> at.z; + ImVec2 projected; + if (false == screen_projection{}.project(at, projected)) + { + WriteLog("SELFTEST mouseat: behind the camera"); + return selftest_result::handled; + } + test.input.mouse_to(glm::dvec2{projected.x, projected.y}); + WriteLog(format("SELFTEST mouse %.0f %.0f", projected.x, projected.y)); + return selftest_result::handled; + } + if (command == "glide") + { + glm::dvec3 at{0.0}; + int steps{8}; + int every{2}; + std::string shots; + words >> at.x >> at.y >> at.z >> steps >> every >> shots; + ImVec2 projected; + if (false == screen_projection{}.project(at, projected)) + return selftest_result::handled; + glm::dvec2 const from{test.input.mouse().value_or(glm::dvec2{projected.x, projected.y})}; + glm::dvec2 const to{projected.x, projected.y}; + std::vector expanded; + for (int step = 1; step <= std::max(1, steps); ++step) + { + auto const t{static_cast(step) / std::max(1, steps)}; + auto const eased{t * t * (3.0 - 2.0 * t)}; + auto const point{glm::mix(from, to, eased)}; + expanded.push_back(format("mouse %.1f %.1f", point.x, point.y)); + expanded.push_back("wait " + std::to_string(std::max(1, every))); + if (shots == "shot") + expanded.push_back("screenshot"); + } + test.lines.insert(test.lines.begin() + static_cast(test.next), expanded.begin(), expanded.end()); + return selftest_result::handled; + } + if (test.input.dispatch(command, words)) + return selftest_result::handled; if (command == "screenshot") { Application.queue_screenshot(); @@ -1697,6 +1762,7 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com tool.templates = editor_track::standard_switch_templates(); tool.collected = true; tool.armed = -1; + tool.connection = -1; for (int i = 0; i < static_cast(tool.templates.size()); ++i) if (tool.templates[i].label.rfind(label, 0) == 0) { @@ -2174,9 +2240,9 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma int around{0}; for (auto const &issue : m_joints.issues) { - if (issue.type == joint_issue::kind::open) + if (false == points.empty() && 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; })) + if (false == points.empty() && std::none_of(points.begin(), points.end(), [&](glm::dvec3 const &Point) { return geometry::plan_distance(Point, issue.position) < 5.0; })) continue; ++around; WriteLog(format("SELFTEST joints: %s %.3f at %.1f %.1f, %s / %s", kinds[static_cast(issue.type)], issue.value, issue.position.x, issue.position.z, issue.track != nullptr ? issue.track->name().c_str() : "-", @@ -3095,6 +3161,7 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm // wait // camera from the first point, looking at the second // select +// selectat the path nearest the point, within 3 m, as a click on it // blade [name] logs the blades of each switch cut from the rails: tip, 50 mm head, full head, heel, drop of the head, open flangeway, the gauge lines apart against x^2/2R, vertices in both positions // frog [name] logs where the inner rails of each switch cross: distance on both paths, 1:n, throat, nose, check rails // guards [name] logs each guard rail and what each of its ends and breaks is cut along; a fault when a cut is not along a rail or a glued end not on the guard rail of the other track @@ -3152,7 +3219,7 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm // join [radius] [variant] [transitions 0|1] the free ends at the points joined, as a drag from one to the other // layto from the free end at the first point towards the second, as a drag in the layout // layclicks ... clicks of the lay tool at the points, a free end within 3 m joined -// joints ... the joints check over the scenery, the issues around the points logged +// joints ... the joints check over the scenery, the issues around the points logged, every issue with free ends without points // route the route design of the path nearest the point, a curve or the whole line // routevertex , routearc , routeshare , routekm // routeapply, routetake as the buttons of the route design do it @@ -3166,6 +3233,12 @@ editor_mode::selftest_result editor_mode::selftest_rails(std::string const &comm // switches logs the switches kept in the scenery and the speed of each diverging track // deletetrack , deletekind the path deleted as Del does it, or the first path over an element of the kind // btfcheck the .txtf converted to .btf, the editor lines the binary file keeps logged +// mouse , mouseat the cursor moved to the point of the window, or to where the point of the scene is seen, as the mouse moves it +// glide [shot] the cursor led to where the point is seen in steps, frames apart, a screenshot after each step with shot +// button , wheel the buttons and the wheel of the mouse, with the keys held +// press , release , tap a key held, let go, or both (ctrl, shift, alt, esc, enter, backspace, del, space, tab, a..z, 0..9, f1..f12) +// softcursor <0|1> the cursor drawn into the picture, for screenshots +// type the characters typed into the text field with the focus // screenshot // save // log diff --git a/application/scriptedinput.cpp b/application/scriptedinput.cpp new file mode 100644 index 00000000..b11ff8b4 --- /dev/null +++ b/application/scriptedinput.cpp @@ -0,0 +1,218 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/scriptedinput.h" + +#include "application/application.h" +#include "application/uilayer.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" +#include "editor/editorFormat.hpp" +#include "imgui/imgui.h" + +#include +#include +#include +#include + +namespace +{ + +int key_named(std::string const &Name) +{ + static std::map const names{{"ctrl", GLFW_KEY_LEFT_CONTROL}, {"shift", GLFW_KEY_LEFT_SHIFT}, {"alt", GLFW_KEY_LEFT_ALT}, {"esc", GLFW_KEY_ESCAPE}, {"enter", GLFW_KEY_ENTER}, {"backspace", GLFW_KEY_BACKSPACE}, + {"del", GLFW_KEY_DELETE}, {"space", GLFW_KEY_SPACE}, {"tab", GLFW_KEY_TAB}}; + if (auto const found{names.find(Name)}; found != names.end()) + return found->second; + if (Name.size() == 1 && std::isalpha(static_cast(Name[0]))) + return GLFW_KEY_A + (std::toupper(static_cast(Name[0])) - 'A'); + if (Name.size() == 1 && std::isdigit(static_cast(Name[0]))) + return GLFW_KEY_0 + (Name[0] - '0'); + if (Name.size() > 1 && (Name[0] == 'f' || Name[0] == 'F') && std::isdigit(static_cast(Name[1]))) + return GLFW_KEY_F1 + std::stoi(Name.substr(1)) - 1; + return GLFW_KEY_UNKNOWN; +} + +int modifier_of(int const Key) +{ + return Key == GLFW_KEY_LEFT_CONTROL ? GLFW_MOD_CONTROL : Key == GLFW_KEY_LEFT_SHIFT ? GLFW_MOD_SHIFT : Key == GLFW_KEY_LEFT_ALT ? GLFW_MOD_ALT : 0; +} + +} // namespace + +void scripted_input::modifiers() const +{ + auto &io{ImGui::GetIO()}; + io.AddKeyEvent(ImGuiMod_Ctrl, (m_mods & GLFW_MOD_CONTROL) != 0); + io.AddKeyEvent(ImGuiMod_Shift, (m_mods & GLFW_MOD_SHIFT) != 0); + io.AddKeyEvent(ImGuiMod_Alt, (m_mods & GLFW_MOD_ALT) != 0); +} + +void scripted_input::mouse_to(glm::dvec2 const &Screen) +{ + if (false == m_mouse.has_value()) + ui_layer::cursor_enter_callback(1); + m_mouse = Screen; + ui_layer::cursor_pos_callback(Screen.x, Screen.y); + Global.cursor_pos = glm::ivec2(Screen); + Application.on_cursor_pos(Screen.x, Screen.y); +} + +bool scripted_input::dispatch(std::string const &Command, std::istringstream &Words) +{ + if (Command == "mouse") + { + glm::dvec2 screen{0.0}; + Words >> screen.x >> screen.y; + mouse_to(screen); + WriteLog(format("SELFTEST mouse %.0f %.0f", screen.x, screen.y)); + return true; + } + if (Command == "button") + { + std::string which; + std::string action; + Words >> which >> action; + auto const button{which == "right" ? GLFW_MOUSE_BUTTON_RIGHT : which == "middle" ? GLFW_MOUSE_BUTTON_MIDDLE : GLFW_MOUSE_BUTTON_LEFT}; + Application.on_mouse_button(button, action == "release" ? GLFW_RELEASE : GLFW_PRESS, m_mods); + modifiers(); + return true; + } + if (Command == "wheel") + { + double offset{0.0}; + Words >> offset; + Application.on_scroll(0.0, offset); + modifiers(); + return true; + } + if (Command == "press" || Command == "release" || Command == "tap") + { + std::string name; + Words >> name; + auto const key{key_named(name)}; + if (key == GLFW_KEY_UNKNOWN) + { + WriteLog("SELFTEST " + Command + ": unknown key " + name); + return true; + } + if (Command != "release") + { + m_mods |= modifier_of(key); + Application.on_key(key, 0, GLFW_PRESS, m_mods); + modifiers(); + } + if (Command != "press") + { + m_mods &= ~modifier_of(key); + Application.on_key(key, 0, GLFW_RELEASE, m_mods); + modifiers(); + } + return true; + } + if (Command == "type") + { + std::string text; + std::getline(Words >> std::ws, text); + for (std::size_t index = 0; index < text.size();) + { + auto const lead{static_cast(text[index])}; + auto const extra{lead >= 0xF0 ? 3 : lead >= 0xE0 ? 2 : lead >= 0xC0 ? 1 : 0}; + unsigned int code{extra == 0 ? lead : lead & (0x3Fu >> extra)}; + for (int k = 1; k <= extra && index + k < text.size(); ++k) + code = (code << 6) | (static_cast(text[index + k]) & 0x3Fu); + Application.on_char(code); + index += extra + 1; + } + return true; + } + if (Command == "softcursor") + { + int on{1}; + Words >> on; + ImGui::GetIO().MouseDrawCursor = on != 0; + return true; + } + return false; +} + +void input_script::step() +{ + if (false == m_loaded) + { + m_loaded = true; + auto const *file{std::getenv(m_variable)}; + if (file == nullptr) + return; + std::ifstream stream(file); + std::string line; + while (std::getline(stream, line)) + if (false == line.empty() && line.front() != '#') + m_lines.push_back(line); + WriteLog("SELFTEST: " + std::to_string(m_lines.size()) + " steps from " + file); + } + if (m_wait > 0) + { + --m_wait; + return; + } + while (m_next < m_lines.size()) + { + std::istringstream words(m_lines[m_next++]); + std::string command; + words >> command; + if (command == "wait") + { + words >> m_wait; + return; + } + if (command == "screenshot") + { + Application.queue_screenshot(); + m_wait = 3; + return; + } + if (command == "log") + { + std::string text; + std::getline(words >> std::ws, text); + WriteLog("SELFTEST " + text); + continue; + } + if (command == "editorscript") + { + std::string file; + words >> file; +#ifdef _WIN32 + _putenv_s("EU07_EDITOR_SELFTEST", file.c_str()); +#else + setenv("EU07_EDITOR_SELFTEST", file.c_str(), 1); +#endif + WriteLog("SELFTEST editor script " + file); + continue; + } + if (command == "quit") + { + if (false == m_quitting) + { + WriteLog("SELFTEST done"); + m_quitting = true; + m_wait = 30; + --m_next; + return; + } + Application.queue_quit(true); + return; + } + if (false == m_input.dispatch(command, words)) + WriteLog("SELFTEST unknown command " + command); + } +} diff --git a/application/scriptedinput.h b/application/scriptedinput.h new file mode 100644 index 00000000..ee12a07a --- /dev/null +++ b/application/scriptedinput.h @@ -0,0 +1,49 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include + +#include + +class scripted_input +{ + public: + bool dispatch(std::string const &Command, std::istringstream &Words); + void mouse_to(glm::dvec2 const &Screen); + std::optional const &mouse() const + { + return m_mouse; + } + + private: + void modifiers() const; + std::optional m_mouse; + int m_mods{0}; +}; + +class input_script +{ + public: + explicit input_script(char const *Variable) : m_variable{Variable} {} + void step(); + + private: + char const *m_variable; + bool m_loaded{false}; + std::vector m_lines; + std::size_t m_next{0}; + int m_wait{0}; + bool m_quitting{false}; + scripted_input m_input; +}; diff --git a/launcher/launchermode.cpp b/launcher/launchermode.cpp index 5059fa37..80208e99 100644 --- a/launcher/launchermode.cpp +++ b/launcher/launchermode.cpp @@ -17,6 +17,7 @@ bool launcher_mode::init() bool launcher_mode::update() { + m_script.step(); return true; } diff --git a/launcher/launchermode.h b/launcher/launchermode.h index 0a9475e3..c7c22878 100644 --- a/launcher/launchermode.h +++ b/launcher/launchermode.h @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #pragma once #include "application/applicationmode.h" +#include "application/scriptedinput.h" class launcher_mode : public application_mode { @@ -33,4 +34,7 @@ public: void on_window_resize( int w, int h ) override; void on_event_poll() override { /* not handled by the launcher */ } bool is_command_processor() const override { return false; } + +private: + input_script m_script{"EU07_LAUNCHER_SELFTEST"}; };