From 7fea51ad066cb7c20e2b314746755678204950a1 Mon Sep 17 00:00:00 2001 From: maj00r Date: Mon, 12 Oct 2026 01:35:39 +0200 Subject: [PATCH] feat(editor): norms of laying, free by default: free lays plain arcs with no transition curves and no cant and keeps quiet about the norms, PKP PLK gives the transitions and the cant for the design speed with the warnings, chosen at the top of the Lay panel; digits in Tracks only type values, never change the edit mode; in Lay a track under the cursor says the click starts on the ground beside it, Shift+click chooses it for a parallel track; Alt while laying turns the direction in steps of 15 degrees, with the azimuth by the cursor; K splits only within 20 px of the selected track, where a marker shows it; the environment, overhead line, transition curve, view and include labels translated, one name for laying track in Polish --- application/editormode.cpp | 20 ++++---- application/editormode.h | 8 +++- application/editormodetrack.cpp | 74 +++++++++++++++++++++++++---- application/editormodetrackux.cpp | 13 +++++- editor/editorAlignment.cpp | 8 ++-- editor/editorAlignment.hpp | 1 + lang/editor_pl.po | 77 ++++++++++++++++++++++++++++++- 7 files changed, 175 insertions(+), 26 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 56dabcb8..eb4ca8e3 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -3516,7 +3516,7 @@ void editor_mode::render_view_menu() toggle_ortho(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("The view straight down without perspective, as on a map; again for the camera as it was")); - ImGui::MenuItem("Show lanes", nullptr, &m_roadtool.lanes); + ImGui::MenuItem(STR_C("Show lanes"), nullptr, &m_roadtool.lanes); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", "Draws the lanes of the roads with the ways the vehicles take through the junctions"); if (ImGui::MenuItem(STR_C("Structure gauge"), nullptr, m_gauge.open)) @@ -4705,6 +4705,8 @@ void editor_mode::render_shortcuts_window() {"Ctrl+wheel", "Laying: radius"}, {"Shift+wheel", "Laying: arc then straight, straight then arc, arc, straight; at a free end: another way to join"}, {"0-9 while laying", "Type the length (or the radius) of the next piece, Enter lays it"}, + {"Alt held while laying", "The direction in steps of 15 degrees"}, + {"Shift+LMB on a track in L", "Choose it for a parallel track"}, {"Enter / Backspace", "Laying track: lay up to the cursor / take the last piece back"}, {"Del", "Delete the selected tracks"}}}, {"Roads", @@ -4742,7 +4744,7 @@ void editor_mode::render_shortcuts_window() void editor_mode::render_gizmo_options() { - if (ImGui::Button(Global.EditorOrtho ? "3D view (O)" : "Top view, orthographic (O)")) + if (ImGui::Button(Global.EditorOrtho ? STR_C("3D view (O)") : STR_C("Top view, orthographic (O)"))) toggle_ortho(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Global.EditorOrtho ? STR_C("Wheel zooms. Hold the wheel and drag to pan.") : STR_C("Top-down orthographic view")); @@ -4785,7 +4787,7 @@ void editor_mode::render_gizmo_options() { auto const located = editor_includes::parameter_with_role(m_include.info, "pos.x") != 0 || editor_includes::parameter_with_role(m_include.info, "pos.y") != 0 || editor_includes::parameter_with_role(m_include.info, "pos.z") != 0; - ImGui::TextDisabled("%s", false == located ? "Include selected, no position parameters described" : + ImGui::TextDisabled("%s", false == located ? STR_C("Include selected, no position parameters described") : false == m_include.placement ? STR_C("Include selected, no gizmo under rotate or scale") : STR_C("Include selected, axes as its template allows")); } @@ -5399,11 +5401,11 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in case GLFW_KEY_F4: choose_work_area(work_area::roads); break; case GLFW_KEY_F5: choose_work_area(work_area::terrain); break; // the edit modes in the order of the toolbar; in the track mode the digits typed as a value were taken already - case GLFW_KEY_1: choose_edit_mode(nodebank_panel::MODIFY); break; - case GLFW_KEY_2: choose_edit_mode(nodebank_panel::ADD); break; - case GLFW_KEY_3: choose_edit_mode(nodebank_panel::BRUSH); break; - case GLFW_KEY_4: choose_edit_mode(nodebank_panel::FILL); break; - case GLFW_KEY_5: choose_edit_mode(nodebank_panel::COPY); break; + case GLFW_KEY_1: handled = ui()->mode() != nodebank_panel::TRACK; if (handled) choose_edit_mode(nodebank_panel::MODIFY); break; + case GLFW_KEY_2: handled = ui()->mode() != nodebank_panel::TRACK; if (handled) choose_edit_mode(nodebank_panel::ADD); break; + case GLFW_KEY_3: handled = ui()->mode() != nodebank_panel::TRACK; if (handled) choose_edit_mode(nodebank_panel::BRUSH); break; + case GLFW_KEY_4: handled = ui()->mode() != nodebank_panel::TRACK; if (handled) choose_edit_mode(nodebank_panel::FILL); break; + case GLFW_KEY_5: handled = ui()->mode() != nodebank_panel::TRACK; if (handled) choose_edit_mode(nodebank_panel::COPY); break; default: handled = false; break; } if (handled) @@ -5552,7 +5554,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in } if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && selected_track() != nullptr) { - auto const under{track_under_cursor(selected_track())}; + auto const under{split_spot()}; if (under.track != nullptr) split_track_at(*under.track, under.point); } diff --git a/application/editormode.h b/application/editormode.h index d33480e6..2bc552f8 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1092,7 +1092,7 @@ class editor_mode : public application_mode, private editor_track::observer std::string preview_error; double preview_length{0.0}; double radius{500.0}; - bool transitions{true}; + bool transitions{false}; double piece_length{100.0}; bool copy_selected{false}; editor_track::path_style style; @@ -1106,6 +1106,9 @@ class editor_mode : public application_mode, private editor_track::observer std::vector chain; }; lay_tool m_lay; + bool m_norms_free{true}; + alignment::limits laying_norms() const; + glm::dvec3 lay_angle_snap(glm::dvec3 const &Mouse) const; void lay_after_history(bool const Undo); void lay_back(); void render_lay_ui(); @@ -1250,7 +1253,8 @@ class editor_mode : public application_mode, private editor_track::observer double since{0.0}; }; track_hover m_hover; - track_hover track_under_cursor(TTrack const *Only = nullptr) const; + track_hover track_under_cursor(TTrack const *Only = nullptr, float const Pixels = 0.0f) const; + track_hover split_spot() const; void update_track_hover(); void draw_track_hover() const; struct track_intent diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 16e4956d..412b0516 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1111,9 +1111,9 @@ void editor_mode::render_path_parameters(TTrack &Track) editor_track::velocity(*track, velocity); item_tooltip("Speed limit of the path for the AI drivers and the timetable, -1: no limit of its own"); finish_edit(rebuild::none); - char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"}; + char const *environments[] = {STR_C("flat"), STR_C("mountains"), STR_C("canyon"), STR_C("tunnel"), STR_C("bridge"), STR_C("bank")}; int environment = std::clamp(static_cast(track->eEnvironment), 0, 5); - if (ImGui::Combo("Environment", &environment, environments, IM_ARRAYSIZE(environments))) + if (ImGui::Combo(STR_C("Environment"), &environment, environments, IM_ARRAYSIZE(environments))) { track->eEnvironment = static_cast(environment); apply_now(rebuild::none); @@ -1267,7 +1267,8 @@ void editor_mode::render_path_circuits(TTrack &Track) int overheadmode{track->fOverhead < 0.f ? 0 : track->fOverhead == 0.f ? 1 : 2}; ImGui::PushItemWidth(220.0f); - if (ImGui::Combo("Overhead line", &overheadmode, "Normal\0No current, coasting\0Pantographs lowered, speed limit\0")) + char const *overheadmodes[] = {STR_C("Normal"), STR_C("No current, coasting"), STR_C("Pantographs lowered, speed limit")}; + if (ImGui::Combo(STR_C("Overhead line"), &overheadmode, overheadmodes, IM_ARRAYSIZE(overheadmodes))) { editor_track::overhead(*track, overheadmode == 0 ? -1.f : overheadmode == 1 ? 0.f : std::max(1.f, track->fOverhead > 0.f ? track->fOverhead : 40.f)); changed(before, false); @@ -2373,8 +2374,8 @@ bool editor_mode::render_route_parameters() item_tooltip("Speed the cant, the transitions and the recommended radius are designed for"); design.speed = std::max(1.0, design.speed); int shape = static_cast(design.shape); - char const *shapes[] = {"Cubic parabola", "Clothoid"}; - if (ImGui::Combo("Transition curve", &shape, shapes, IM_ARRAYSIZE(shapes))) + char const *shapes[] = {STR_C("Cubic parabola"), STR_C("Clothoid")}; + if (ImGui::Combo(STR_C("Transition curve"), &shape, shapes, IM_ARRAYSIZE(shapes))) { design.shape = static_cast(shape); changed = true; @@ -3652,14 +3653,14 @@ std::vector editor_mode::track_key_hints(bool const All) if (m_lay.active) { if (m_lay.points.empty()) - hints = {{"LMB", "start of the new track, on the ground or at a free end"}, {"Drag from a free end", "the shape chosen towards the cursor, to a free end: joins it"}, {"Parallel", "first select a straight, then a second track beside it in the panel"}, {"Esc", "stop laying"}}; + hints = {{"LMB", "start of the new track, on the ground or at a free end"}, {"Drag from a free end", "the shape chosen towards the cursor, to a free end: joins it"}, {"Shift+click a straight", "a parallel track beside it, in the panel"}, {"Alt", "the direction in steps of 15 degrees"}, {"Esc", "stop laying"}}; else { bool const chained{m_lay.start.track != nullptr}; hints = {{"LMB", chained ? "lay the shape up to here and go on from its end" : "lay the straight up to here and go on from its end"}, {"LMB on a free end", "join and lay"}}; if (chained || m_lay.mouse_snap.track != nullptr) hints.insert(hints.end(), {{"Ctrl+wheel", "radius"}, {"Shift+wheel", "arc / straight, at a free end: another shape"}}); - hints.insert(hints.end(), {{"Enter", "lay up to the cursor"}, {"Backspace", m_lay.chain.empty() ? "take back the point" : "take back the last piece"}, {"Esc", "cancel"}}); + hints.insert(hints.end(), {{"Alt", "the direction in steps of 15 degrees"}, {"Enter", "lay up to the cursor"}, {"Backspace", m_lay.chain.empty() ? "take back the point" : "take back the last piece"}, {"Esc", "cancel"}}); } } else if (false == m_straights.detour.empty()) @@ -3844,6 +3845,8 @@ std::string editor_mode::track_readout() const text += "\n" + lay.preview_error; if (lay.points.size() == 1 && (lay.start.track != nullptr || lay.mouse_snap.track != nullptr)) text += "\n" + track_shaping(lay.mouse_snap.track != nullptr && lay.start.track != nullptr); + if (ImGui::GetIO().KeyAlt && false == lay.points.empty()) + text += format("\naz %.0f deg", azimuth(lay.points.back(), lay.mouse)); if (false == lay.status.empty() && ImGui::GetTime() - lay.status_time < 4.0) text += "\n" + lay.status; return text; @@ -4074,6 +4077,8 @@ void editor_mode::update_build_tools() double const height{false == lay.points.empty() ? lay.points.back().y : lay.start.track != nullptr ? lay.start.position.y : ground.y}; if (pointing) lay.mouse = typed_lay(cursor_terrain(height)); + if (pointing && ImGui::GetIO().KeyAlt && m_typed.empty() && false == lay.points.empty()) + lay.mouse = lay_angle_snap(lay.mouse); lay.mouse_snap = m_typed.empty() ? snap_free_end(lay.mouse, cursor, nullptr, kRailCategory, {}) : editor_track::find_free_end(nullptr, kRailCategory, lay.mouse, kLaySnapRadius, {}); if (lay.mouse_snap.track == nullptr) lay.mouse = lay_bed(lay.mouse); @@ -4463,7 +4468,7 @@ std::vector editor_mode::join_pieces(TTrack const *From, editor_tr request.speed = design_speed(From, End.track); request.kink = m_joints.angle * 0.5; request.shape = m_route.design.shape; - request.norms = m_route.design.norms; + request.norms = laying_norms(); auto result{alignment::join(request)}; if (m_lay.transitions && result.kind != alignment::join_kind::straight && result.kind != alignment::join_kind::arc && glm::length(request.end.direction) > 1e-9) { @@ -4784,7 +4789,7 @@ alignment::result editor_mode::lay_shape(std::vector const &Points, { auto const &tool{m_lay}; design = {}; - design.norms = m_route.design.norms; + design.norms = laying_norms(); design.shape = m_route.design.shape; design.speed = design_speed(tool.start.track, nullptr); design.start = Points.front(); @@ -5008,9 +5013,50 @@ void editor_mode::lay_back() tool.start = {}; } +glm::dvec3 editor_mode::lay_angle_snap(glm::dvec3 const &Mouse) const +{ + auto const &last{m_lay.points.back()}; + glm::dvec2 const offset{Mouse.x - last.x, Mouse.z - last.z}; + auto const length{glm::length(offset)}; + if (length < 1.0) + return Mouse; + auto const leaving{m_lay.start.track != nullptr ? -plan_of(m_lay.start.direction) : glm::dvec2{0.0}}; + glm::dvec2 const reference{glm::length(leaving) > 1e-6 ? glm::normalize(leaving) : glm::dvec2{0.0, 1.0}}; + auto const step{glm::radians(15.0)}; + auto const heading{turned(reference, std::round(signed_angle(reference, offset / length) / step) * step) * length}; + return {last.x + heading.x, Mouse.y, last.z + heading.y}; +} + +editor_mode::track_hover editor_mode::split_spot() const +{ + auto const *selected{selected_track()}; + if (selected == nullptr || ui()->mode() != nodebank_panel::TRACK || ImGui::GetIO().WantCaptureMouse) + return {}; + return track_under_cursor(selected, 20.0f); +} + +alignment::limits editor_mode::laying_norms() const +{ + auto norms{m_route.design.norms}; + norms.checked = false == m_norms_free; + return norms; +} + void editor_mode::render_lay_ui() { auto &tool{m_lay}; + { + int norms{m_norms_free ? 0 : 1}; + char const *names[]{STR_C("free"), STR_C("PKP PLK")}; + ImGui::PushItemWidth(120.0f); + if (ImGui::Combo(STR_C("Norms"), &norms, names, IM_ARRAYSIZE(names))) + { + m_norms_free = norms == 0; + tool.transitions = false == m_norms_free; + } + ImGui::PopItemWidth(); + item_tooltip("Free: plain arcs with no transition curves and no cant, no warnings about the norms, the track goes where it's clicked.\nPKP PLK: transition curves and cant for the design speed, warnings when the radius, the cant or the transitions don't meet the norms"); + } if (auto *track{selected_track()}; track != nullptr) { std::string reason; @@ -5424,6 +5470,16 @@ void editor_mode::draw_build_overlay() const projection.line(drawlist, origin + glm::dvec3{i * cell, 0.0, j * cell}, origin + glm::dvec3{(i + 1) * cell, 0.0, j * cell}, colour, 1.0f); } } + if (m_track_tab == track_tab::path && false == m_lay.active && false == m_track_gizmo_using) + if (auto const spot{split_spot()}; spot.track != nullptr) + { + ImVec2 at; + if (projection.project(spot.point, at)) + { + drawlist->AddCircle(at, 6.0f, overlay_color::highlight, 12, 1.5f); + drawlist->AddText({at.x + 9.0f, at.y - 18.0f}, overlay_color::highlight, STR_C("K: split here")); + } + } auto const &display{ImGui::GetIO().DisplaySize}; for (auto const *track : simulation::Paths.sequence()) { diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 11ae4ac3..ec508fc9 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -92,13 +92,13 @@ double segment_distance2(glm::vec2 const &Point, glm::vec2 const &A, glm::vec2 c } // namespace -editor_mode::track_hover editor_mode::track_under_cursor(TTrack const *Only) const +editor_mode::track_hover editor_mode::track_under_cursor(TTrack const *Only, float const Pixels) const { track_hover result; auto const &io{ImGui::GetIO()}; screen_projection const projection; glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; - double best{Only != nullptr ? std::numeric_limits::max() : kHoverPixels * kHoverPixels}; + double best{Pixels > 0.0f ? Pixels * Pixels : Only != nullptr ? std::numeric_limits::max() : kHoverPixels * kHoverPixels}; for (auto *track : simulation::Paths.sequence()) { if (track == nullptr || false == rail(*track) || track->m_editorremoved || (Only != nullptr && track != Only)) @@ -216,6 +216,15 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) return make(kind::lay_end, lay.points.empty() ? "Click: start from this free end, drag: lead a track out of it" : "Click: join this end and lay"); } intent.position = lay.mouse; + if (lay.points.empty()) + if (auto const under{track_under_cursor()}; under.track != nullptr) + { + intent.track = under.track; + intent.path = under.path; + if (shift) + return make(kind::select, "Click: choose this track, the parallel track goes beside it"); + return make(kind::lay, "Click: start on the ground beside this track. Shift+click: a parallel track to it, T: a switch into it"); + } return make(kind::lay, lay.points.empty() ? "Click: start of the new track" : "Click: lay the track to here"); } if (m_track_tab == track_tab::turntable) diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index c702f17a..59ba13c5 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -656,9 +656,9 @@ result fit_between(design const &Design) report.unbalanced = unbalanced_acceleration(Design.speed, sharpest, std::max(0.0, vertex.cant), Design.norms); report.recommended = recommend(Design.speed, sharpest, Design.norms); r.curves.push_back(report); - if (sharpest < report.recommended.radius_min - 1e-6) + if (Design.norms.checked && sharpest < report.recommended.radius_min - 1e-6) r.warnings.emplace_back(format("Vertex %zu: R %.0f m is below the minimum %.0f m for %.0f km/h", k + 1, sharpest, report.recommended.radius_min, Design.speed)); - if (report.unbalanced > Design.norms.unbalanced + 1e-6) + if (Design.norms.checked && report.unbalanced > Design.norms.unbalanced + 1e-6) r.warnings.emplace_back(format("Vertex %zu: unbalanced acceleration %.2f m/s2 exceeds %.2f m/s2", k + 1, report.unbalanced, Design.norms.unbalanced)); continue; } @@ -756,6 +756,8 @@ result fit_between(design const &Design) r.curves.push_back(report); auto const &norms{Design.norms}; + if (false == norms.checked) + continue; if (radius < report.recommended.radius_min - 1e-6) r.warnings.emplace_back(format("Vertex %zu: R %.0f m is below the minimum %.0f m for %.0f km/h", k + 1, radius, report.recommended.radius_min, Design.speed)); if (curve.cant > norms.cant_max + 1e-6) @@ -782,7 +784,7 @@ result fit_between(design const &Design) push_straight(cursor, lastdirection, tail); r.length = chainage; - if (Design.norms.tangent_min_time > 0.0) + if (Design.norms.checked && Design.norms.tangent_min_time > 0.0) { auto const minimum{Design.norms.tangent_min_time * Design.speed / 3.6}; for (std::size_t i = 1; i + 1 < r.elements.size(); ++i) diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index 08c03b22..474465c6 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -37,6 +37,7 @@ struct limits double tangent_min_time{0.0}; double arc_min{20.0}; double arc_min_factor{0.4}; + bool checked{true}; }; struct compound_arc diff --git a/lang/editor_pl.po b/lang/editor_pl.po index b9689cce..00b87ee8 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -15,7 +15,7 @@ msgid "Select Esc" msgstr "Wybór Esc" msgid "Lay track L" -msgstr "Układaj tor L" +msgstr "Układanie toru L" msgid "Switch T" msgstr "Rozjazd T" @@ -5559,3 +5559,78 @@ msgstr "0-9 w trakcie układania" msgid "Type the length (or the radius) of the next piece, Enter lays it" msgstr "Wpisz długość (lub promień) następnego odcinka, Enter go kładzie" + +msgid "canyon" +msgstr "jar" + +msgid "tunnel" +msgstr "tunel" + +msgid "bridge" +msgstr "most" + +msgid "bank" +msgstr "nasyp" + +msgid "Normal" +msgstr "Normalna" + +msgid "No current, coasting" +msgstr "Bez napięcia, jazda z rozpędu" + +msgid "Pantographs lowered, speed limit" +msgstr "Opuszczone pantografy, ograniczenie prędkości" + +msgid "Overhead line" +msgstr "Sieć trakcyjna" + +msgid "Transition curve" +msgstr "Krzywa przejściowa" + +msgid "Include selected, no position parameters described" +msgstr "Zaznaczony include, bez opisanych parametrów położenia" + +msgid "3D view (O)" +msgstr "Widok 3D (O)" + +msgid "free" +msgstr "swobodne" + +msgid "PKP PLK" +msgstr "PKP PLK" + +msgid "Norms" +msgstr "Normy" + +msgid "Free: plain arcs with no transition curves and no cant, no warnings about the norms, the track goes where it's clicked.\nPKP PLK: transition curves and cant for the design speed, warnings when the radius, the cant or the transitions don't meet the norms" +msgstr "Swobodne: zwykłe łuki bez krzywych przejściowych i przechyłki, bez ostrzeżeń o normach, tor idzie tam, gdzie klikniesz.\nPKP PLK: krzywe przejściowe i przechyłka dla prędkości projektowej, ostrzeżenia, gdy promień, przechyłka lub krzywe przejściowe nie spełniają norm" + +msgid "Click: choose this track, the parallel track goes beside it" +msgstr "Klik: wybierz ten tor, obok niego pójdzie tor równoległy" + +msgid "Click: start on the ground beside this track. Shift+click: a parallel track to it, T: a switch into it" +msgstr "Klik: początek na terenie obok tego toru. Shift+klik: tor równoległy do niego, T: rozjazd w nim" + +msgid "K: split here" +msgstr "K: podziel tutaj" + +msgid "Shift+click a straight" +msgstr "Shift+klik na prostej" + +msgid "a parallel track beside it, in the panel" +msgstr "tor równoległy obok niej, w panelu" + +msgid "the direction in steps of 15 degrees" +msgstr "kierunek co 15 stopni" + +msgid "Alt held while laying" +msgstr "Wciśnięty Alt w trakcie układania" + +msgid "The direction in steps of 15 degrees" +msgstr "Kierunek co 15 stopni" + +msgid "Shift+LMB on a track in L" +msgstr "Shift+LPM na torze w L" + +msgid "Choose it for a parallel track" +msgstr "Wybiera go pod tor równoległy"