diff --git a/application/editormode.cpp b/application/editormode.cpp index 98dfd14b..1ad54751 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1361,9 +1361,9 @@ bool editor_mode::update() signal_move_update(); draw_signal_overlay(); } + draw_parallel_preview(); if (m_track_tab == track_tab::lineside) { - draw_parallel_preview(); draw_hekto_overlay(); draw_fouling_overlay(); draw_vehicle_marker(); diff --git a/application/editormode.h b/application/editormode.h index 95e9bc8f..dd36eff4 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1022,6 +1022,9 @@ class editor_mode : public application_mode, private editor_track::observer int built_side{0}; int built_scope{-1}; std::size_t built_history{0}; + TTrack *pair_src{nullptr}; + TTrack *pair_dst{nullptr}; + double measured{0.0}; std::vector pieces; std::vector styles; double length{0.0}; @@ -1029,9 +1032,15 @@ class editor_mode : public application_mode, private editor_track::observer std::string status; }; parallel_tool m_parallel; + TTrack *parallel_source() const; + TTrack *parallel_target() const; void parallel_update(); + void render_parallel_controls(bool const Scope = true); void render_parallel_ui(); + void render_parallel_set_ui(); void parallel_build(); + void parallel_apply(); + bool parallel_preview_visible() const; void draw_parallel_preview() const; // scenery templates standing by the track, placed and checked by the editor diff --git a/application/editormodeparallel.cpp b/application/editormodeparallel.cpp index 7eaa92f4..a905c45d 100644 --- a/application/editormodeparallel.cpp +++ b/application/editormodeparallel.cpp @@ -34,6 +34,11 @@ using geometry::plan_of; double const kParallelReach{5000.0}; // m each way along the line double const kSpacings[] = {3.5, 4.0, 4.5, 4.75, 5.0, 6.0}; +std::string name_of(TTrack const &Track) +{ + return Track.name().empty() ? std::string{"(noname)"} : Track.name(); +} + segment_data reversed(segment_data const &Path) { segment_data result{Path}; @@ -78,12 +83,61 @@ glm::dvec2 direction(segment_data const &Path, bool const Atend) return glm::length(along) > 1e-9 ? glm::normalize(along) : glm::dvec2{0.0, 1.0}; } +glm::dvec3 midpoint(TTrack const &Track) +{ + if (Track.m_paths.empty()) + return Track.location(); + bezier const curve{Track.m_paths.front()}; + return curve.point(0.5); +} + +// signed offset of Dst from Src, along Src's left normal; positive is the Left radio +double lateral_of(TTrack const &Src, TTrack const &Dst) +{ + if (Src.m_paths.empty()) + return 0.0; + auto const heading{direction(Src.m_paths.front(), false)}; + glm::dvec2 const left{-heading.y, heading.x}; + return glm::dot(plan_of(midpoint(Dst)) - plan_of(Src.m_paths.front().points[segment_data::point::start]), left); +} + } // namespace +TTrack *editor_mode::parallel_source() const +{ + if (m_track_set.size() >= 2) + return m_track_set.front(); + return selected_track(); +} + +TTrack *editor_mode::parallel_target() const +{ + if (m_track_set.size() != 2) + return nullptr; + return m_track_set.back() != m_track_set.front() ? m_track_set.back() : nullptr; +} + void editor_mode::parallel_update() { auto &tool{m_parallel}; - auto *track{selected_track()}; + auto *track{parallel_source()}; + auto *target{parallel_target()}; + if (track != tool.pair_src || target != tool.pair_dst) + { + tool.pair_src = track; + tool.pair_dst = target; + if (track != nullptr && target != nullptr) + { + auto const lateral{lateral_of(*track, *target)}; + tool.side = lateral >= 0.0 ? 1 : -1; + tool.measured = std::abs(lateral); + } + else + tool.measured = 0.0; + } + else if (track != nullptr && target != nullptr) + tool.measured = std::abs(lateral_of(*track, *target)); + if (track == tool.built_for && tool.spacing == tool.built_spacing && tool.side == tool.built_side && tool.scope == tool.built_scope && m_history.size() == tool.built_history) return; tool.built_for = track; @@ -176,13 +230,59 @@ void editor_mode::parallel_build() tool.built_for = nullptr; } -void editor_mode::render_parallel_ui() +void editor_mode::parallel_apply() { auto &tool{m_parallel}; - ImGui::TextDisabled("%s", STR_C("Second track alongside, curves included: radius R minus or plus the spacing,\nthe same transitions and cant, heights on the plane of the cant")); - ImGui::RadioButton(STR_C("This path"), &tool.scope, 0); - ImGui::SameLine(); - ImGui::RadioButton(STR_C("The line, up to the switches"), &tool.scope, 1); + auto *target{parallel_target()}; + parallel_update(); + if (target == nullptr || tool.pieces.empty()) + return; + if (tool.pieces.size() != 1) + { + tool.error = STR_C("Two selected paths: set the scope to this path, so the other takes its shape at the spacing"); + return; + } + std::string reason; + if (false == editor_track::can_edit_geometry(*target, reason)) + { + tool.error = reason; + return; + } + auto const joints{editor_track::joints(*target)}; + std::vector> states; + auto const remember = [&](TTrack *Track) { + if (Track == nullptr) + return; + if (std::any_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == Track; })) + return; + states.emplace_back(Track, editor_track::capture(*Track)); + }; + remember(target); + for (auto const &joint : joints) + remember(joint.other); + target->m_paths.front() = tool.pieces.front(); + editor_track::follow(*target, joints); + std::vector committed{target}; + for (auto const &joint : joints) + if (joint.other != nullptr) + committed.push_back(joint.other); + editor_track::commit(committed); + push_track_snapshot(std::move(states)); + tool.status = format(STR_C("%s set parallel to %s, %.2f m away; Ctrl+Z takes it back"), name_of(*target).c_str(), name_of(*parallel_source()).c_str(), tool.spacing); + WriteLog("Editor: " + tool.status, logtype::generic); + tool.built_for = nullptr; + straight_refresh(); +} + +void editor_mode::render_parallel_controls(bool const Scope) +{ + auto &tool{m_parallel}; + if (Scope) + { + ImGui::RadioButton(STR_C("This path"), &tool.scope, 0); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("The line, up to the switches"), &tool.scope, 1); + } ImGui::RadioButton(STR_C("Left"), &tool.side, -1); ImGui::SameLine(); ImGui::RadioButton(STR_C("Right"), &tool.side, 1); @@ -197,23 +297,73 @@ void editor_mode::render_parallel_ui() if (ImGui::SmallButton(format("%.2f", spacing).c_str())) tool.spacing = spacing; } +} + +void editor_mode::render_parallel_ui() +{ + auto &tool{m_parallel}; + ImGui::TextDisabled("%s", STR_C("First select a straight path (LMB). Then a second track is laid beside it.\nAlong the line the curves are included: R minus or plus the spacing, same transitions and cant.")); + if (parallel_target() != nullptr) + ImGui::TextDisabled("%s", STR_C("Two paths selected: set the spacing of the other in Select. The first must be a straight.")); + else if (auto *source{parallel_source()}; source == nullptr) + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("First select a straight path in the view.")); + else if (source->eType != tt_Normal || false == editor_track::is_straight(*source, m_straights.tolerance)) + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("The selected path isn't a straight. Select a straight first.")); + render_parallel_controls(); parallel_update(); if (false == tool.error.empty()) ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str()); else if (false == tool.pieces.empty()) { - if (ImGui::Button(format(STR_C("Build %zu paths, %.1f m"), tool.pieces.size(), tool.length).c_str())) + if (ImGui::Button(format(STR_C("Lay %zu paths, %.1f m"), tool.pieces.size(), tool.length).c_str())) parallel_build(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("First select a straight. Creates a new track beside it; the selected path stays.")); } if (false == tool.status.empty()) ImGui::TextWrapped("%s", tool.status.c_str()); } +void editor_mode::render_parallel_set_ui() +{ + auto &tool{m_parallel}; + auto *source{parallel_source()}; + auto *target{parallel_target()}; + if (source == nullptr || target == nullptr) + return; + if (false == ImGui::CollapsingHeader(STR_C("Make parallel"), ImGuiTreeNodeFlags_DefaultOpen)) + return; + ImGui::TextWrapped("%s", STR_C("First selected must be a straight. It stays; the other is moved to this spacing.")); + ImGui::TextWrapped("%s", format(STR_C("%s stays. %s is moved to this spacing, taking its curves."), name_of(*source).c_str(), name_of(*target).c_str()).c_str()); + if (tool.measured > 0.05) + ImGui::TextDisabled("%s", format(lateral_of(*source, *target) >= 0.0 ? STR_C("Now %.2f m on the right") : STR_C("Now %.2f m on the left"), tool.measured).c_str()); + tool.scope = 0; + render_parallel_controls(false); + parallel_update(); + if (false == tool.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str()); + else if (false == tool.pieces.empty() && ImGui::Button(format(STR_C("Set %s to %.2f m"), name_of(*target).c_str(), tool.spacing).c_str())) + parallel_apply(); + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} + +bool editor_mode::parallel_preview_visible() const +{ + if (m_parallel.pieces.empty() || parallel_source() != m_parallel.built_for) + return false; + if (m_track_tab == track_tab::lay) + return m_lay.points.empty(); + if (m_track_tab == track_tab::path) + return true; + return m_track_tab == track_tab::lineside && m_parallel.open; +} + void editor_mode::draw_parallel_preview() const { - auto const &tool{m_parallel}; - if (false == tool.open || tool.pieces.empty() || selected_track() != tool.built_for) + if (false == parallel_preview_visible()) return; + auto const &tool{m_parallel}; screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList()}; for (auto const &piece : tool.pieces) @@ -230,5 +380,11 @@ void editor_mode::draw_parallel_preview() const } ImVec2 screen; if (projection.project(tool.pieces.front().points[segment_data::point::start], screen)) + { drawlist->AddCircleFilled(screen, 5.0f, IM_COL32(90, 255, 120, 230)); + auto const label{format("%.2f m", tool.spacing)}; + auto const size{ImGui::CalcTextSize(label.c_str())}; + drawlist->AddRectFilled(ImVec2(screen.x + 8.0f, screen.y - 8.0f), ImVec2(screen.x + 16.0f + size.x, screen.y + 8.0f), IM_COL32(0, 0, 0, 180), 3.0f); + drawlist->AddText(ImVec2(screen.x + 12.0f, screen.y - 8.0f), IM_COL32(90, 255, 120, 255), label.c_str()); + } } diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 53a9b866..59241a48 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -586,7 +586,7 @@ std::array const &editor_mode::track_modes() {"Straight", "G", track_tab::straights, "The whole straight through the selected path: drag its ends or the middle, break it, shift it"}, {"Curve", "C", track_tab::route, "The line through the path, switch to switch: vertices with the radii, transitions and cant of the curves"}, {"Signals", "H", track_tab::signals, "Signals by the track, from the templates of the scenery, with the event the train reads them by"}, - {"Objects", "B", track_tab::lineside, "Along the track: hectometre posts, fouling point markers, a parallel track, a vehicle to drive"}, + {"Objects", "B", track_tab::lineside, "Along the track: hectometre posts, fouling point markers, a vehicle to drive"}, {"Profile", "P", track_tab::profile, "Vertical profile (grade line) along the line"}, {"Speed", "V", track_tab::speed, "Speed limits of the paths against the speed their geometry allows"}, {"Joints", "J", track_tab::joints, "Ends of the paths which almost meet, steps, kinks, jumps of the cant and of the grade at the joints"}, @@ -3448,7 +3448,7 @@ 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", "straight along it, to another free end: a curve joining them"}, {"Esc", "stop laying"}}; + hints = {{"LMB", "start of the new track, on the ground or at a free end"}, {"Drag from a free end", "straight along it, to another free end: a curve joining them"}, {"Parallel", "first select a straight, then a second track beside it in the panel"}, {"Esc", "stop laying"}}; else { hints = {{"LMB", m_lay.points.size() == 1 ? "end of the straight, or a vertex of a curve" : "next vertex of the curve"}, {"LMB on a free end", "join and lay"}, {"Enter", "lay up to the cursor"}, {"Backspace", "take back the point"}, {"Esc", "cancel"}}; @@ -4517,6 +4517,9 @@ void editor_mode::render_lay_ui() if (editor_track::can_edit_geometry(*track, reason)) render_extend_ui(); } + if (ImGui::CollapsingHeader(STR_C("Parallel track"), ImGuiTreeNodeFlags_DefaultOpen)) + render_parallel_ui(); + ImGui::Separator(); ImGui::PushItemWidth(120.0f); ImGui::DragScalar(STR_C("Curve radius (m)"), ImGuiDataType_Double, &tool.radius, 5.0f, nullptr, nullptr, "%.0f"); item_tooltip("Drag to change, Ctrl+click to type the value"); diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 0de31ee4..cc1ab363 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -627,11 +627,8 @@ void editor_mode::render_track_context() show_track_tab(track_tab::straights); if (ImGui::MenuItem(STR_C("Curve"), "C", false, normal)) show_track_tab(track_tab::route); - if (ImGui::MenuItem(STR_C("Parallel track"), nullptr, false, normal)) - { - show_track_tab(track_tab::lineside); - m_parallel.expand = true; - } + if (ImGui::MenuItem(STR_C("Parallel track"), "L", false, normal)) + show_track_tab(track_tab::lay); if (ImGui::MenuItem(STR_C("Place a vehicle here"))) { show_track_tab(track_tab::lineside); @@ -1209,6 +1206,7 @@ void editor_mode::render_track_set_ui() return; } ImGui::TextDisabled("%s", STR_C("The gizmo moves them together; Shift+LMB adds or takes out a path")); + render_parallel_set_ui(); if (ImGui::SmallButton(STR_C("Grow by the neighbours"))) track_set_grow(); if (ImGui::IsItemHovered()) diff --git a/lang/editor_pl.po b/lang/editor_pl.po index ab86dfc6..8a20acf3 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -2230,6 +2230,54 @@ msgstr "Nic nie dało się naprawić: to rozjazdy albo odcinki w plikach, który msgid "Parallel track" msgstr "Tor równoległy" +msgid "Parallel" +msgstr "Równoległy" + +msgid "First select a straight path (LMB). Then a second track is laid beside it.\nAlong the line the curves are included: R minus or plus the spacing, same transitions and cant." +msgstr "Najpierw zaznacz odcinek prosty (LPM). Potem drugi tor kładzie się obok.\nWzdłuż linii łuki wchodzą: R minus albo plus rozstaw, te same krzywe przejściowe i przechyłka." + +msgid "First select a straight path in the view." +msgstr "Najpierw zaznacz odcinek prosty w widoku." + +msgid "The selected path isn't a straight. Select a straight first." +msgstr "Zaznaczony odcinek nie jest prostą. Najpierw zaznacz prostą." + +msgid "First select a straight. Creates a new track beside it; the selected path stays." +msgstr "Najpierw zaznacz prostą. Tworzy nowy tor obok; zaznaczony odcinek zostaje." + +msgid "Two paths selected: set the spacing of the other in Select. The first must be a straight." +msgstr "Dwa odcinki zaznaczone: rozstaw drugiego ustawiasz w Wyborze. Pierwszy musi być prostą." + +msgid "First selected must be a straight. It stays; the other is moved to this spacing." +msgstr "Pierwszy zaznaczony musi być prostą. On zostaje; drugi jest przestawiany na ten rozstaw." + +msgid "Lay %zu paths, %.1f m" +msgstr "Ułóż %zu odcinków, %.1f m" + +msgid "first select a straight, then a second track beside it in the panel" +msgstr "najpierw zaznacz prostą, potem drugi tor obok niej w panelu" + +msgid "Make parallel" +msgstr "Ustaw równolegle" + +msgid "%s stays. %s is moved to this spacing, taking its curves." +msgstr "%s zostaje. %s jest przestawiany na ten rozstaw, z jego łukami." + +msgid "Now %.2f m on the right" +msgstr "Teraz %.2f m po prawej" + +msgid "Now %.2f m on the left" +msgstr "Teraz %.2f m po lewej" + +msgid "Set %s to %.2f m" +msgstr "Ustaw %s na %.2f m" + +msgid "%s set parallel to %s, %.2f m away; Ctrl+Z takes it back" +msgstr "%s ustawiony równolegle do %s, rozstaw %.2f m; Ctrl+Z cofa" + +msgid "Two selected paths: set the scope to this path, so the other takes its shape at the spacing" +msgstr "Dwa zaznaczone odcinki: zakres na ten odcinek, wtedy drugi bierze jego kształt w rozstawie" + msgid "Parallel track of %zu paths, %.1f m, %.2f m away; Ctrl+Z takes it back" msgstr "Tor równoległy z %zu odcinków, %.1f m, w rozstawie %.2f m; Ctrl+Z cofa" @@ -2674,8 +2722,8 @@ msgstr "Parzyste hektometry po prawej, nieparzyste po lewej" msgid "Objects" msgstr "Obiekty" -msgid "Along the track: hectometre posts, fouling point markers, a parallel track, a vehicle to drive" -msgstr "Przy torze: słupki hektometrowe, wskaźniki ukresu, tor równoległy, pojazd do jazdy" +msgid "Along the track: hectometre posts, fouling point markers, a vehicle to drive" +msgstr "Przy torze: słupki hektometrowe, wskaźniki ukresu, pojazd do jazdy" msgid "Model along the track: release where it ends" msgstr "Model wzdłuż toru: puść tam, gdzie się kończy"