From 79642a95c7cac195e74f1d842dd2eb52024ebc6f Mon Sep 17 00:00:00 2001 From: maj00r Date: Wed, 21 Oct 2026 17:57:05 +0200 Subject: [PATCH] feat(editor): a measuring tape in the track mode, M: the distance, the difference of height, the grade and the azimuth between two clicks M in any tab of the track mode turns the tape on: a click puts its start, the next one its end, at a free end, on the axis of the track under the cursor or else on the ground. The readout at the cursor gives the distance in plan and in space, the difference of height and the grade between them and the azimuth, while the second point follows the cursor and once it's put; a further click starts a new measure, M or Esc puts the tape away. Self-test command tape. --- application/editormode.cpp | 6 ++- application/editormode.h | 13 ++++- application/editormodeselftesttrack.cpp | 17 ++++++ application/editormodetrack.cpp | 9 ++++ application/editormodetrackux.cpp | 69 +++++++++++++++++++++++++ lang/editor_pl.po | 21 ++++++++ 6 files changed, 133 insertions(+), 2 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 4176d9e2..43ad3783 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1494,12 +1494,14 @@ bool editor_mode::update() } render_track_context(); update_build_tools(); + tape_update(); update_track_intent(); draw_track_hover(); draw_track_set(); draw_track_spread(); draw_build_overlay(); draw_track_intent(); + draw_tape(); m_speed.update(); m_joints.update(); draw_track_hints(); @@ -6447,7 +6449,9 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in // the first Esc drops what is going on in the track mode, the next one goes back to the selection if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK) { - if (m_lay.active && false == m_lay.points.empty()) + if (m_tape.active) + m_tape = {}; + else if (m_lay.active && false == m_lay.points.empty()) lay_cancel(); else if (m_track_style.picking) m_track_style.picking = false; diff --git a/application/editormode.h b/application/editormode.h index 20d334df..dd841175 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1580,7 +1580,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void draw_track_hover() const; struct track_intent { - enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling }; + enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move, switch_slide, pick_style, fouling, measure }; kind what{kind::none}; TTrack *track{nullptr}; int path{0}; @@ -1591,6 +1591,17 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::string action; }; track_intent m_intent; + // the measure between two points (M): on the ground, at a free end or on the axis of the track under the cursor + struct tape_state + { + bool active{false}; + std::vector points; + glm::dvec3 mouse{0.0}; + }; + tape_state m_tape; + void tape_update(); + std::string tape_readout() const; + void draw_tape() const; track_intent track_intent_at(int const Mods); bool track_gesture_on() const; void update_track_intent(); diff --git a/application/editormodeselftesttrack.cpp b/application/editormodeselftesttrack.cpp index 021669ae..845e9fd2 100644 --- a/application/editormodeselftesttrack.cpp +++ b/application/editormodeselftesttrack.cpp @@ -396,6 +396,23 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm WriteLog(format("SELFTEST vprofile zone %.3f %.3f %s", zone.from, zone.to, zone.name.c_str())); return selftest_result::handled; } + if (command == "tape") + { + // the measure between two points on the ground, as two clicks of the measuring tape take it + std::vector points; + for (glm::dvec3 point{0.0, 5000.0, 0.0}; words >> point.x >> point.z;) + points.push_back(point); + std::vector found; + auto const heights{ground_heights(points, true, &found)}; + for (std::size_t i = 0; i < points.size(); ++i) + points[i].y = found[i] != 0 ? heights[i] : 0.0; + m_tape = {true, points, points.empty() ? glm::dvec3{0.0} : points.back()}; + auto readout{tape_readout()}; + std::replace(readout.begin(), readout.end(), '\n', '|'); + WriteLog("SELFTEST tape: " + readout); + m_tape = {}; + return selftest_result::handled; + } if (command == "trackcolours") { int colouring{0}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 1da2fb0c..406924ec 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -501,6 +501,11 @@ void editor_mode::arm_switch() bool editor_mode::track_shortcut(int const Key) { + if (Key == GLFW_KEY_M) + { + m_tape = {false == m_tape.active, {}, m_tape.mouse}; + return true; + } auto const key{Key == GLFW_KEY_C ? GLFW_KEY_G : Key}; for (auto const &mode : track_modes()) { @@ -531,6 +536,8 @@ bool editor_mode::track_busy() const std::string editor_mode::track_mode_name() const { + if (m_tape.active) + return STR_C("Measure"); if (false == m_track_window_open) return STR_C("Select"); switch (m_track_tab) @@ -1750,6 +1757,8 @@ std::vector editor_mode::track_key_hints(bool const All) // values of the change in progress, shown next to the cursor std::string editor_mode::track_readout() const { + if (m_tape.active) + return tape_readout(); auto const name = [](TTrack const *Track) { return name_of(*Track); }; auto const azimuth = [](glm::dvec3 const &From, glm::dvec3 const &To) { auto const degrees{glm::degrees(std::atan2(To.x - From.x, To.z - From.z))}; diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 26919c39..0869faba 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -392,6 +392,11 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) }; auto *selected{selected_track()}; + if (m_tape.active) + { + intent.position = m_tape.mouse; + return make(kind::measure, m_tape.points.size() == 1 ? "Click: the end of the measure" : "Click: the start of the measure. M or Esc: done"); + } if (m_lay.active) { auto const &lay{m_lay}; @@ -637,11 +642,75 @@ void editor_mode::update_track_intent() } } +void editor_mode::tape_update() +{ + auto &tape{m_tape}; + if (false == tape.active || ImGui::GetIO().WantCaptureMouse) + return; + glm::vec2 const cursor{ImGui::GetIO().MousePos.x, ImGui::GetIO().MousePos.y}; + auto const ground{cursor_ground()}; + if (auto const end{snap_free_end(ground, cursor, nullptr, kRailCategory, {})}; end.track != nullptr) + tape.mouse = end.position; + else if (auto const under{track_under_cursor()}; under.track != nullptr) + tape.mouse = under.point; + else + tape.mouse = ground; +} + +std::string editor_mode::tape_readout() const +{ + auto const &tape{m_tape}; + if (tape.points.empty()) + return {}; + auto const &from{tape.points.front()}; + auto const &to{tape.points.size() >= 2 ? tape.points[1] : tape.mouse}; + auto const plan{geometry::plan_distance(from, to)}; + auto const rise{to.y - from.y}; + auto azimuth{glm::degrees(std::atan2(to.x - from.x, to.z - from.z))}; + if (azimuth < 0.0) + azimuth += 360.0; + auto text{format(STR_C("%.2f m in plan, %.2f m in space"), plan, glm::distance(from, to))}; + text += "\n" + format(STR_C("height %+.2f m"), rise) + (plan > 0.01 ? format(STR_C(" grade %+.1f per mille"), rise / plan * 1000.0) : std::string{}); + text += "\n" + format(STR_C("azimuth %.1f deg"), azimuth); + if (tape.points.size() >= 2) + text += "\n" + STR("Click: a new measure. M or Esc: done"); + return text; +} + +void editor_mode::draw_tape() const +{ + auto const &tape{m_tape}; + if (false == tape.active) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())}; + auto const colour{IM_COL32(255, 230, 60, 255)}; + std::vector points{tape.points}; + if (points.size() < 2) + points.push_back(tape.mouse); + for (std::size_t i = 0; i + 1 < points.size(); ++i) + projection.line(drawlist, points[i], points[i + 1], colour, 2.0f); + for (auto const &point : points) + { + ImVec2 screen; + if (projection.project(point, screen)) + { + drawlist->AddCircleFilled(screen, 4.0f, colour); + drawlist->AddCircle(screen, 7.0f, IM_COL32(0, 0, 0, 200), 12, 1.5f); + } + } +} + void editor_mode::track_press(track_intent const &Intent) { using kind = track_intent::kind; switch (Intent.what) { + case kind::measure: + if (m_tape.points.size() >= 2) + m_tape.points.clear(); + m_tape.points.push_back(m_tape.mouse); + return; case kind::lay_end: if (m_lay.points.empty() && start_extend(Intent.snap)) return; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 3280d254..ca1a37ee 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10129,3 +10129,24 @@ msgstr "Opisy torów w widoku z góry" msgid "Zoomed in from above, the curves get their radius and length and the switches their name and type, as on a plan; where they'd crowd, some are left out" msgstr "Po przybliżeniu z góry łuki dostają promień i długość, a rozjazdy nazwę i typ, jak na planie; gdzie byłoby ich za gęsto, część się chowa" + +msgid "%.2f m in plan, %.2f m in space" +msgstr "%.2f m w planie, %.2f m w przestrzeni" + +msgid "height %+.2f m" +msgstr "różnica wysokości %+.2f m" + +msgid "azimuth %.1f deg" +msgstr "azymut %.1f°" + +msgid "Click: a new measure. M or Esc: done" +msgstr "LPM: nowy pomiar. M albo Esc: koniec" + +msgid "Click: the end of the measure" +msgstr "LPM: koniec pomiaru" + +msgid "Click: the start of the measure. M or Esc: done" +msgstr "LPM: początek pomiaru. M albo Esc: koniec" + +msgid "Measure" +msgstr "Miarka"