From 907068c69c57baae4dfcd4912b1770dd01802c22 Mon Sep 17 00:00:00 2001 From: maj00r Date: Mon, 19 Oct 2026 18:48:54 +0200 Subject: [PATCH] feat(editor): Alt while laying turns the straight to 1:9, 1:6.6... of a straight track Besides the steps of 15 degrees, Alt snaps the laid straight to the ratios of the switches (1:4.8 to 1:26.5) of the straight track the straight crosses, or of one within 30 m of where it starts; the label at the cursor says which ratio to which track. The first click with Alt within 2 m of a straight track puts the start on its axis, so a branch or a crossing at 1:9 starts exactly on the track. --- application/editormode.cpp | 4 +- application/editormode.h | 5 +- application/editormodelay.cpp | 84 +++++++++++++++++++++++++++-- application/editormodetrack.cpp | 6 +-- application/editormodetrackshared.h | 3 ++ lang/editor_pl.po | 21 +++++--- 6 files changed, 106 insertions(+), 17 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index ba91f84e..a6d3772c 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -5079,7 +5079,7 @@ void editor_mode::render_first_steps() "curves", {{"Ctrl + wheel", "The radius of the curve, while laying on from an end"}, {"Shift + wheel", "The shape: arc then straight, straight then arc, arc, straight"}, - {"Alt", "Held: the direction turns in steps of 15 degrees"}, + {"Alt", "Held: the direction turns in steps of 15 degrees, or to 1:9, 1:6.6... to a straight track it starts by or crosses"}, {"0 1 2 3", "Type the length of the next piece, Enter lays it"}, {"wheel", "Alone it zooms, as everywhere"}}, {{first_step::wheel, "Change the radius with Ctrl+wheel"}, {first_step::shape, "Change the shape with Shift+wheel"}}, @@ -5425,7 +5425,7 @@ 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"}, + {"Alt held while laying", "The direction in steps of 15 degrees, or 1:9, 1:6.6... to a straight track; the first click with Alt goes onto the axis of the track"}, {"N", "Parallel track: LMB on a track, then the side and the spacing"}, {"Shift+LMB on a track in L", "The Parallel tool (N) with it"}, {"Enter / Backspace", "Laying track: lay up to the cursor / take the last piece back"}, diff --git a/application/editormode.h b/application/editormode.h index 02bd2afa..42040efa 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1343,6 +1343,7 @@ class editor_mode : public application_mode, private editor_track::observer editor_track::path_style style; std::string status; double status_time{-100.0}; + std::string skew; // what Alt turned the straight to struct step { editor_track::snap_target start; @@ -1354,7 +1355,9 @@ class editor_mode : public application_mode, private editor_track::observer bool m_norms_free{true}; alignment::limits laying_norms() const; bool render_norms_choice(); - glm::dvec3 lay_angle_snap(glm::dvec3 const &Mouse) const; + glm::dvec3 lay_angle_snap(glm::dvec3 const &Mouse, std::string &Label) const; + // the straight track Alt turns the laid straight to: the nearest one the straight from From to To crosses, or one along From + TTrack const *skew_reference(glm::dvec3 const &From, glm::dvec3 const &To, double const Reach) const; void lay_after_history(bool const Undo); void lay_back(); void render_lay_ui(); diff --git a/application/editormodelay.cpp b/application/editormodelay.cpp index b6aa4a3d..40155f80 100644 --- a/application/editormodelay.cpp +++ b/application/editormodelay.cpp @@ -80,8 +80,9 @@ 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)); + lay.skew.clear(); if (pointing && ImGui::GetIO().KeyAlt && m_typed.empty() && false == lay.points.empty()) - lay.mouse = lay_angle_snap(lay.mouse); + lay.mouse = lay_angle_snap(lay.mouse, lay.skew); 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); @@ -926,7 +927,16 @@ void editor_mode::lay_click() { tool.chain.clear(); tool.start = snap; - tool.points.push_back(snap.track != nullptr ? snap.position : tool.mouse); + auto point{snap.track != nullptr ? snap.position : tool.mouse}; + if (auto const *axis{snap.track == nullptr && ImGui::GetIO().KeyAlt ? skew_reference(point, point, kSkewSeat) : nullptr}) + { + auto const &path{axis->m_paths.front()}; + auto const &from{path.points[segment_data::point::start]}; + auto const &to{path.points[segment_data::point::end]}; + auto const along{std::clamp(glm::dot(plan_of(point - from), plan_of(to - from)) / glm::dot(plan_of(to - from), plan_of(to - from)), 0.0, 1.0)}; + point = from + (to - from) * along; + } + tool.points.push_back(point); return; } auto const &point{tool.mouse}; @@ -1093,20 +1103,84 @@ void editor_mode::lay_back() tool.start = {}; } -glm::dvec3 editor_mode::lay_angle_snap(glm::dvec3 const &Mouse) const +glm::dvec3 editor_mode::lay_angle_snap(glm::dvec3 const &Mouse, std::string &Label) const { + Label.clear(); 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}}; + glm::dvec2 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}; + auto angle{std::round(signed_angle(reference, offset / length) / step) * step}; + if (auto const *axis{skew_reference(last, Mouse, kSkewReach)}) + { + auto const &path{axis->m_paths.front()}; + reference = glm::normalize(plan_of(path.points[segment_data::point::end] - path.points[segment_data::point::start])); + auto const relative{signed_angle(reference, offset / length)}; + angle = std::round(relative / step) * step; + double ratio{0.0}; + for (auto const n : kSkewRatios) + for (auto const base : {0.0, glm::pi()}) + for (auto const side : {-1.0, 1.0}) + { + auto const candidate{base + side * std::atan(1.0 / n)}; + if (std::abs(std::remainder(relative - candidate, 2.0 * glm::pi())) < std::abs(std::remainder(relative - angle, 2.0 * glm::pi()))) + { + angle = candidate; + ratio = n; + } + } + auto const name{axis->name().empty() ? std::string{STR_C("(no name)")} : axis->name()}; + auto const degrees{glm::degrees(std::abs(std::remainder(angle, glm::pi())))}; + Label = ratio > 0.0 ? format(STR_C("1:%g to %s"), ratio, name.c_str()) : degrees < 0.1 ? format(STR_C("parallel to %s"), name.c_str()) : format(STR_C("%.0f deg to %s"), degrees, name.c_str()); + } + auto const heading{turned(reference, angle) * length}; return {last.x + heading.x, Mouse.y, last.z + heading.y}; } +TTrack const *editor_mode::skew_reference(glm::dvec3 const &From, glm::dvec3 const &To, double const Reach) const +{ + auto const from{plan_of(From)}; + auto const leg{plan_of(To) - from}; + auto const length{glm::length(leg)}; + TTrack const *nearest{nullptr}; + auto best{std::numeric_limits::max()}; + for (auto *section : simulation::Region->sections((From + To) * 0.5, static_cast(std::min(0.5 * length + Reach, 2000.0)))) + for (auto const &cell : section->m_cells) + for (auto *track : cell.m_directories.paths) + { + if (track == nullptr || track->eType != tt_Normal || track->m_editorremoved || (track->iCategoryFlag & 15) != kRailCategory || track->m_paths.size() != 1 || + false == editor_track::is_straight(*track, m_straights.tolerance)) + continue; + auto const &path{track->m_paths.front()}; + auto const a{plan_of(path.points[segment_data::point::start])}; + auto const axis{plan_of(path.points[segment_data::point::end]) - a}; + auto const span{glm::length(axis)}; + if (span < 1.0) + continue; + auto const along{std::clamp(glm::dot(from - a, axis) / span, 0.0, span)}; + auto distance{glm::distance(from, a + axis / span * along)}; + if (distance > Reach) + distance = std::numeric_limits::max(); + if (auto const determinant{cross(leg, axis)}; length > 1.0 && std::abs(determinant) > 1e-9) + { + auto const t{cross(a - from, axis) / determinant}; + auto const u{cross(a - from, leg) / determinant}; + if (t >= 0.0 && t <= 1.0 && u >= 0.0 && u <= 1.0) + distance = std::min(distance, t * length); + } + if (distance < best) + { + best = distance; + nearest = track; + } + } + return nearest; +} + editor_mode::track_hover editor_mode::split_spot() const { auto const *selected{selected_track()}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 1d3f7607..a41f544c 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1521,14 +1521,14 @@ std::vector editor_mode::track_key_hints(bool const All) if (m_lay.active) { if (m_lay.points.empty()) - hints = {{"LMB", "start, on the ground or at a free end"}, {"Drag from a free end", "lays the shape to the cursor"}, {"Shift+click a straight", "a parallel track (N)"}, {"Alt", "the direction in steps of 15 degrees"}, {"Esc", "stop laying"}}; + hints = {{"LMB", "start, on the ground or at a free end"}, {"Drag from a free end", "lays the shape to the cursor"}, {"Shift+click a straight", "a parallel track (N)"}, {"Alt", "the direction in steps of 15 degrees, or 1:9, 1:6.6... to a straight track"}, {"Esc", "stop laying"}}; else { bool const chained{m_lay.start.track != nullptr}; hints = {{"LMB", chained ? "lay the shape up to here" : "lay the straight up to here"}, {"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", "another shape"}}); - 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"}}); + hints.insert(hints.end(), {{"Alt", "the direction in steps of 15 degrees, or 1:9, 1:6.6... to a straight track"}, {"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()) @@ -1738,7 +1738,7 @@ std::string editor_mode::track_readout() const 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 += "\n" + format(STR_C("azimuth %.0f deg"), azimuth(lay.points.back(), lay.mouse)); + text += "\n" + (lay.skew.empty() ? format(STR_C("azimuth %.0f deg"), azimuth(lay.points.back(), lay.mouse)) : lay.skew); if (false == lay.status.empty() && ImGui::GetTime() - lay.status_time < 4.0) text += "\n" + lay.status; return text; diff --git a/application/editormodetrackshared.h b/application/editormodetrackshared.h index c58d605a..91ad98ca 100644 --- a/application/editormodetrackshared.h +++ b/application/editormodetrackshared.h @@ -104,6 +104,9 @@ double const kLaySnapRadius{5.0}; float const kSnapPixels{16.0f}; double const kSwitchReach{6.0}; // a press farther from the selected track selects another one instead of placing the switch double const kSlipReach{12.0}; // m; a slip goes to the crossing nearest the cursor, if it's this close +double const kSkewSeat{2.0}; // m; the first point laid with Alt this close to a straight track goes onto its axis +double const kSkewReach{30.0}; // m; Alt while laying turns the straight to a straight track this close to its start, or one it crosses +double const kSkewRatios[]{4.8, 6.6, 7.5, 9.0, 12.0, 14.0, 18.5, 26.5}; double const kLineReach{2000.0}; // how far each way the Curve tab follows the line double const kDigitizeFit{0.5}; double const kDrawnReach{100000.0}; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 00afe098..d17701c1 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -4590,14 +4590,14 @@ msgstr "K: podziel tutaj" msgid "Shift+click a straight" msgstr "Shift+LPM na prostej" -msgid "the direction in steps of 15 degrees" -msgstr "kierunek co 15 stopni" +msgid "the direction in steps of 15 degrees, or 1:9, 1:6.6... to a straight track" +msgstr "kierunek co 15 stopni albo 1:9, 1:6,6… do prostego toru" 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 "The direction in steps of 15 degrees, or 1:9, 1:6.6... to a straight track; the first click with Alt goes onto the axis of the track" +msgstr "Kierunek co 15 stopni albo 1:9, 1:6,6… do prostego toru; pierwszy klik z Altem trafia w oś toru" msgid "Shift+LMB on a track in L" msgstr "Shift+LPM na torze przy układaniu (L)" @@ -5376,8 +5376,8 @@ msgstr "Promień łuku, gdy układasz dalej od końca toru" msgid "The shape: arc then straight, straight then arc, arc, straight" msgstr "Kształt: łuk i prosta, prosta i łuk, sam łuk, prosta" -msgid "Held: the direction turns in steps of 15 degrees" -msgstr "Trzymany: kierunek zmienia się co 15 stopni" +msgid "Held: the direction turns in steps of 15 degrees, or to 1:9, 1:6.6... to a straight track it starts by or crosses" +msgstr "Trzymany: kierunek zmienia się co 15 stopni albo na 1:9, 1:6,6… do prostego toru, przy którym się zaczyna albo który przecina" msgid "Type the length of the next piece, Enter lays it" msgstr "Wpisz długość następnego kawałka, Enter go układa" @@ -9934,3 +9934,12 @@ msgstr "Klik: ten rozjazd krzyżowy zostanie zastąpiony wybranym" msgid "Click: it goes here" msgstr "Klik: stanie tutaj" + +msgid "1:%g to %s" +msgstr "1:%g do %s" + +msgid "parallel to %s" +msgstr "równolegle do %s" + +msgid "%.0f deg to %s" +msgstr "%.0f° do %s"