diff --git a/application/editormode.cpp b/application/editormode.cpp index c7d36c8b..3eef9330 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -32,6 +32,7 @@ extern char **environ; #include "editor/editorIcons.hpp" #include "utilities/translation.h" #include "utilities/Globals.h" +#include "utilities/glmHelpers.h" #include "simulation/simulation.h" #include "simulation/simulationtime.h" #include "simulation/simulationenvironment.h" @@ -1218,6 +1219,8 @@ bool editor_mode::update() m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT, GLFW_RELEASE); Application.set_cursor(GLFW_CURSOR_NORMAL); } + if (m_ortho_pan && !ImGui::GetIO().MouseDown[2]) + ortho_pan_stop(); // same for the left button (brush / sculpt / click placement). the UI filter in the application // uses io.WantCaptureMouse from the previous frame, so a press on a panel the cursor has just @@ -1369,9 +1372,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(); @@ -1414,6 +1417,7 @@ bool editor_mode::update() // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- if (ui()->mode() == nodebank_panel::FILL) draw_area_fill_outline(); + draw_ortho_compass(); render_new_scenery_popup(); render_open_scenery_popup(); @@ -1854,6 +1858,10 @@ void editor_mode::render_orthophoto_ui() if (!editor_orthophoto::can_download()) ImGui::TextDisabled(STR_C("This build has no HTTP client, only cached tiles are shown")); ImGui::TextDisabled(STR_C("Cache: %s"), editor_orthophoto::cache_directory().c_str()); + if (ImGui::Button(STR_C("Clear cache"))) + m_orthophoto.clear_cache(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Deletes downloaded tiles from disk. They are fetched again around the camera.")); ImGui::TextDisabled(STR_C("Imagery: GUGiK, geoportal.gov.pl")); } @@ -1871,6 +1879,51 @@ void editor_mode::draw_orthophoto() m_orthophoto.draw(editor_mode::projection_matrix(aspect) * view, Camera.Pos, io.DisplaySize.x, io.DisplaySize.y); } +void editor_mode::draw_ortho_compass() const +{ + if (false == Global.EditorOrtho) + return; + screen_projection const projection; + ImVec2 north_from, north_to, east_from, east_to; + glm::dvec3 const eye{Camera.Pos}; + if (false == projection.project(eye + glm::dvec3{0.0, 0.0, -80.0}, north_from) || false == projection.project(eye + glm::dvec3{0.0, 0.0, 80.0}, north_to) || + false == projection.project(eye + glm::dvec3{80.0, 0.0, 0.0}, east_from) || false == projection.project(eye + glm::dvec3{-80.0, 0.0, 0.0}, east_to)) + return; + auto const dir = [](ImVec2 const From, ImVec2 const To) { + ImVec2 const d{To.x - From.x, To.y - From.y}; + auto const length{std::sqrt(d.x * d.x + d.y * d.y)}; + return length > 1e-3f ? ImVec2{d.x / length, d.y / length} : ImVec2{0.0f, -1.0f}; + }; + auto const north{dir(north_from, north_to)}; + auto const east{dir(east_from, east_to)}; + ImVec2 const south{-north.x, -north.y}; + ImVec2 const west{-east.x, -east.y}; + + ImGuiIO const &io = ImGui::GetIO(); + float const radius{36.0f}; + float const margin{16.0f}; + ImVec2 const center{io.DisplaySize.x - margin - radius, margin + radius}; + auto *drawlist{ImGui::GetForegroundDrawList()}; + drawlist->AddCircleFilled(center, radius + 14.0f, IM_COL32(0, 0, 0, 180), 48); + drawlist->AddCircle(center, radius - 6.0f, IM_COL32(255, 255, 255, 50), 48, 1.2f); + drawlist->AddCircleFilled(center, 3.0f, IM_COL32(255, 255, 255, 200), 12); + + auto const at = [&](ImVec2 const Unit, float const Length) { return ImVec2{center.x + Unit.x * Length, center.y + Unit.y * Length}; }; + ImVec2 const side{-north.y * 5.0f, north.x * 5.0f}; + drawlist->AddTriangleFilled(at(north, radius - 14.0f), ImVec2{center.x + side.x, center.y + side.y}, ImVec2{center.x - side.x, center.y - side.y}, IM_COL32(230, 70, 55, 255)); + drawlist->AddTriangleFilled(at(south, radius - 18.0f), ImVec2{center.x + side.x, center.y + side.y}, ImVec2{center.x - side.x, center.y - side.y}, IM_COL32(210, 210, 210, 210)); + + auto const letter = [&](char const *Text, ImVec2 const Unit, ImU32 const Color) { + auto const size{ImGui::CalcTextSize(Text)}; + auto const p{at(Unit, radius + 2.0f)}; + drawlist->AddText(ImVec2(p.x - size.x * 0.5f, p.y - size.y * 0.5f), Color, Text); + }; + letter("N", north, IM_COL32(255, 90, 70, 255)); + letter("E", east, IM_COL32(230, 230, 230, 230)); + letter("S", south, IM_COL32(190, 190, 190, 180)); + letter("W", west, IM_COL32(230, 230, 230, 230)); +} + editor_terrain *editor_mode::terrain_at(double X, double Z) { for (auto &terrain : m_terrains) @@ -4010,11 +4063,15 @@ void editor_mode::render_gizmo_options() { if (ImGui::Button(Global.EditorOrtho ? "3D view (O)" : "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")); if (Global.EditorOrtho) { ImGui::SameLine(); ImGui::SetNextItemWidth(120.0f); - ImGui::SliderFloat(STR_C("Extent (m), wheel"), &Global.EditorOrthoExtent, 5.0f, 5000.0f, "%.0f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat(STR_C("Extent (m), wheel / MMB pan"), &Global.EditorOrthoExtent, 5.0f, 5000.0f, "%.0f", ImGuiSliderFlags_Logarithmic); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Wheel zooms. Hold the wheel and drag to pan.")); } ImGui::Checkbox(STR_C("Enabled"), &m_gizmo_enabled); if (!m_gizmo_enabled) @@ -4322,9 +4379,32 @@ void editor_mode::toggle_ortho() else { Camera.Angle.x = m_ortho_pitch; + ortho_pan_stop(); } } +void editor_mode::ortho_pan_to(double const Horizontal, double const Vertical) +{ + auto const &io{ImGui::GetIO()}; + if (io.DisplaySize.y <= 0.0f) + return; + auto const dx{Horizontal - m_ortho_pan_cursor.x}; + auto const dy{Vertical - m_ortho_pan_cursor.y}; + m_ortho_pan_cursor = {Horizontal, Vertical}; + auto const scale{2.0 * static_cast(Global.EditorOrthoExtent) / static_cast(io.DisplaySize.y)}; + auto const world{RotateY(glm::dvec3{dx * scale, 0.0, dy * scale}, static_cast(Camera.Angle.y))}; + Camera.Pos.x -= world.x; + Camera.Pos.z -= world.z; + Camera.Velocity = {}; + m_focus_active = false; + Global.pCamera = Camera; +} + +void editor_mode::ortho_pan_stop() +{ + m_ortho_pan = false; +} + void editor_mode::on_scroll(double const Xoffset, double const Yoffset) { if (false == Global.EditorOrtho || ImGui::GetIO().WantCaptureMouse) @@ -4805,6 +4885,8 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in void editor_mode::on_cursor_pos(double const Horizontal, double const Vertical) { + if (m_ortho_pan) + ortho_pan_to(Horizontal, Vertical); // object transforms are handled by the gizmo now; here we only forward the cursor to the // mouse input, which rotates the camera while the right mouse button is held (panning mode) m_input.mouse.position(Horizontal, Vertical); @@ -5045,6 +5127,18 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const // game-engine style look: hide & grab the cursor while flying, restore it on release Application.set_cursor(is_press(Action) ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL); } + else if (Button == GLFW_MOUSE_BUTTON_MIDDLE && Global.EditorOrtho) + { + if (is_press(Action)) + { + m_ortho_pan = true; + m_ortho_pan_cursor = m_input.mouse.position(); + m_focus_active = false; + Camera.Velocity = {}; + } + else if (is_release(Action)) + ortho_pan_stop(); + } m_input.mouse.button(Button, Action); } diff --git a/application/editormode.h b/application/editormode.h index 6c7b9776..68cb2df7 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -216,6 +216,10 @@ class editor_mode : public application_mode, private editor_track::observer static bool m_settings_open; double m_ortho_pitch{0.0}; + bool m_ortho_pan{false}; + glm::dvec2 m_ortho_pan_cursor{0.0}; + void ortho_pan_to(double const Horizontal, double const Vertical); + void ortho_pan_stop(); // camera fly-mode (right mouse button held); used to flush motion when it's released command_relay m_camera_relay; bool m_camera_flying{false}; @@ -406,6 +410,7 @@ class editor_mode : public application_mode, private editor_track::observer std::string m_georeference_line; void read_georeference(); void draw_orthophoto(); + void draw_ortho_compass() const; // picks up the stored settings and the origin of the current scenery void load_orthophoto_settings(); void save_orthophoto_settings(); @@ -704,6 +709,18 @@ class editor_mode : public application_mode, private editor_track::observer void apply_detour(); std::vector detour_outline() const; void find_neighbour_straights(); + // listed spacing of a neighbouring straight the line can snap to, if it is close enough + struct parallel_offer + { + bool near{false}; + bool snaps{false}; + double spacing{0.0}; + double distance{0.0}; + double angle{0.0}; // deg between the two straights + double correction{0.0}; // m along the line normal to reach the spacing + editor_track::straight const *neighbour{nullptr}; + }; + parallel_offer straight_parallel_offer(editor_track::straight const &Line, glm::dvec3 const &Offset) const; glm::dvec3 snap_straight_offset(editor_track::straight const &Line, glm::dvec3 const &Offset) const; glm::dvec3 snap_straight_direction(glm::dvec3 const &Pivot, glm::dvec3 const &Moved) const; straights_state m_straights; @@ -1110,6 +1127,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}; @@ -1117,9 +1137,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 47222bb7..24090fd0 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(ImGui::GetMainViewport())}; 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/editormodesweep.cpp b/application/editormodesweep.cpp index b5fed509..ee57378a 100644 --- a/application/editormodesweep.cpp +++ b/application/editormodesweep.cpp @@ -1656,6 +1656,12 @@ void editor_mode::render_bend() state.height = overhead + kRailTop; changed = true; } + double along{sweep->distance_at(bend_fixed(*sweep, state.point))}; + if (ImGui::InputDouble(STR_C("along the track (m)"), &along, 0.1, 1.0, "%.2f")) + { + bend_stretch(*sweep, along, stretch, state.point, state); + changed = true; + } double length{stretch}; if (ImGui::InputDouble(STR_C("length (m)"), &length, 0.5, 5.0, "%.2f") && length > 0.1) { @@ -1708,6 +1714,7 @@ void editor_mode::render_bend() ImGui::SetTooltip("%s", STR_C("Puts the edge facing the track at the distance chosen from its axis, set back by the widening of the gauge in the curves.\n" "Stands on the plane of the rail heads carried on to its edge: lower on the inner side of a canted curve,\n" "higher on the outer one; on the inner side also set back by the cant times the height of the edge / 1.5")); + ImGui::TextDisabled("%s", STR_C("In the view: drag the blue cross along the track and aside; Shift: height; Ctrl: only along")); if (changed) bend_edit(state); if (ImGui::Button(STR_C("Done"))) @@ -1876,7 +1883,7 @@ bool editor_mode::bend_drag_start(int const Mods) if (false == projection.project(marker, screen)) return false; auto const &mouse{ImGui::GetIO().MousePos}; - if ((mouse.x - screen.x) * (mouse.x - screen.x) + (mouse.y - screen.y) * (mouse.y - screen.y) > 14.0f * 14.0f) + if ((mouse.x - screen.x) * (mouse.x - screen.x) + (mouse.y - screen.y) * (mouse.y - screen.y) > 18.0f * 18.0f) return false; tool.dragging = (Mods & GLFW_MOD_SHIFT) != 0 ? 2 : (Mods & GLFW_MOD_CONTROL) != 0 ? 3 : 1; tool.drag_state = sweep->definition(); @@ -1912,8 +1919,8 @@ void editor_mode::bend_drag_update() } if (tool.over_marker) { - tool.label = STR("Drag: offset from the track"); - tool.details = STR("Shift: height Ctrl: along the track"); + tool.label = STR("Drag: along the track and aside"); + tool.details = STR("Shift: height Ctrl: only along the track"); } else if (ui()->mode() == nodebank_panel::MODIFY && m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) != GLFW_PRESS) { @@ -1957,6 +1964,26 @@ void editor_mode::bend_drag_update() auto const step{0.01}; auto const snap = [&](double const Value) { return std::round(Value / step) * step; }; auto state{tool.drag_state}; + auto const slide = [&](double const Shift) { + auto const span{(tool.drag_state.to < 0.0 ? sweep->length() : tool.drag_state.to) - tool.drag_state.from}; + auto from{tool.drag_state.from + Shift}; + auto to{from + span}; + if (span > 0.1 && span <= sweep->length() + 1e-6) + { + if (from < 0.0) + { + to -= from; + from = 0.0; + } + if (to > sweep->length()) + { + from = std::max(0.0, sweep->length() - span); + to = from + span; + } + } + state.from = from; + state.to = to; + }; if (tool.dragging == 2) { glm::dvec3 origin, direction; @@ -1979,22 +2006,18 @@ void editor_mode::bend_drag_update() return; double lateral; auto const station{sweep->project(point, lateral)}; + auto const shift{snap(station - tool.drag_station)}; + slide(shift); if (tool.dragging == 1) { auto const at{sweep->frame_at(station)}; state.lateral = snap(lateral - sweep->setback(at)); if (std::abs(state.lateral) < step) state.lateral = std::copysign(step, tool.drag_state.lateral); - tool.drag_label = format(STR_C("%.2f m from the track axis"), std::abs(state.lateral)); + tool.drag_label = format(STR_C("%.2f m from the track, %+.2f m along"), std::abs(state.lateral), shift); } else - { - auto const shift{snap(station - tool.drag_station)}; - state.from = tool.drag_state.from + shift; - if (tool.drag_state.to >= 0.0) - state.to = tool.drag_state.to + shift; tool.drag_label = format(STR_C("%+.2f m along the track"), shift); - } } if (false == (state == sweep->definition())) sweep->define(state); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 0dc68a75..e165c657 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -47,6 +47,26 @@ using geometry::signed_angle; using geometry::turned; using geometry::arc_pieces; +double constexpr kParallelFindAngle{15.0}; // deg, neighbours counted as beside the straight +double constexpr kParallelSnapAngle{5.0}; // deg, close enough to snap parallel +double constexpr kParallelSnapWindow{0.45}; // m from 4.00 / 4.50 / 4.75 m +double constexpr kParallelHintWindow{1.25}; // m, hint before the snap takes + +double piece_plan_radius(segment_data const &Path) +{ + if (std::abs(Path.radius) > 0.5f) + return std::abs(Path.radius); + bezier const curve{Path}; + auto const begin{plan_of(curve.first(0.0))}; + auto const finish{plan_of(curve.first(1.0))}; + if (glm::length(begin) < 1e-9 || glm::length(finish) < 1e-9) + return 0.0; + auto const turn{std::abs(signed_angle(begin, finish))}; + if (turn < 1e-4) + return 0.0; + return curve.plan_length() / turn; +} + std::string describe(editor_track::straight const &Line) { return format("L %.2f m az %.4f deg i %.2f per mille %zu paths", Line.length, Line.azimuth, Line.grade * 1000.0, Line.tracks.size()); @@ -571,7 +591,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"}, @@ -639,8 +659,6 @@ void editor_mode::arm_switch() if (false == tool.collected) { tool.templates = editor_track::standard_switch_templates(); - auto const found{editor_track::find_switch_templates()}; - tool.templates.insert(tool.templates.end(), found.begin(), found.end()); tool.collected = true; } tool.status.clear(); @@ -1000,8 +1018,6 @@ void editor_mode::render_turnout_ui() if (false == tool.collected) { tool.templates = editor_track::standard_switch_templates(); - auto const found{editor_track::find_switch_templates()}; - tool.templates.insert(tool.templates.end(), found.begin(), found.end()); tool.collected = true; } if (tool.templates.empty()) @@ -1073,7 +1089,6 @@ void editor_mode::render_turnout_ui() ImGui::SameLine(); if (ImGui::Button(STR_C("Flip the side"))) { - auto const current{editor_track::find_switch_templates()}; editor_track::switch_template own; own.source = track; for (int i = 0; i < 2; ++i) @@ -2488,9 +2503,23 @@ void editor_mode::draw_straights_overlay() const drawlist->AddCircle(screen, 12.0f, overlay_color::highlight, 16, 2.5f); } } - auto const &line{m_straights.current}; - if (line.tracks.empty() || m_straights.dragging) + auto line{m_straights.dragging ? m_straights.drag_line : m_straights.current}; + if (line.tracks.empty()) return; + glm::dvec3 const offset{m_straights.dragging ? m_straights.preview_start - m_straights.drag_line.start : glm::dvec3{}}; + if (m_straights.dragging) + { + line.start = m_straights.preview_start; + line.end = m_straights.preview_end; + auto const planar{plan_of(line.end - line.start)}; + auto const length{glm::length(planar)}; + if (length > 1e-6) + { + line.direction = planar / length; + line.length = length; + } + } + auto const offer{straight_parallel_offer(m_straights.dragging ? m_straights.drag_line : m_straights.current, offset)}; glm::dvec2 const normal{-line.direction.y, line.direction.x}; for (auto const &other : m_straights.neighbours) { @@ -2505,18 +2534,28 @@ void editor_mode::draw_straights_overlay() const auto const lateral{across(other.start) + (across(other.end) - across(other.start)) * fraction}; glm::dvec3 const foot{line.start.x + line.direction.x * middle, line.start.y + line.grade * middle, line.start.z + line.direction.y * middle}; glm::dvec3 const target{foot.x + normal.x * lateral, foot.y, foot.z + normal.y * lateral}; - projection.line(drawlist, foot, target, IM_COL32(255, 230, 120, 230), 1.5f); + bool const highlighted{offer.near && offer.neighbour == &other}; + auto const tick{highlighted ? (offer.snaps ? IM_COL32(80, 230, 255, 255) : IM_COL32(255, 210, 60, 255)) : IM_COL32(255, 230, 120, 230)}; + if (highlighted) + projection.line(drawlist, other.start, other.end, tick, 3.0f); + projection.line(drawlist, foot, target, tick, highlighted ? 3.0f : 1.5f); ImVec2 screen; if (projection.project((foot + target) * 0.5, screen)) { auto const angle{glm::degrees(std::abs(std::asin(std::clamp(line.direction.x * other.direction.y - line.direction.y * other.direction.x, -1.0, 1.0))))}; - auto const text{angle > 0.001 ? format("%.3f m %.3f deg", std::abs(lateral), angle) : format("%.3f m", std::abs(lateral))}; + auto const text{highlighted ? (offer.snaps ? format(STR_C("parallel %.2f m"), offer.spacing) : format("%.2f → %.2f m %.1f°", std::abs(lateral), offer.spacing, offer.angle)) : + (angle > 0.05 ? format("%.2f m %.1f°", std::abs(lateral), angle) : format("%.2f m", std::abs(lateral)))}; auto const *label{text.c_str()}; auto const size{ImGui::CalcTextSize(label)}; - drawlist->AddRectFilled(ImVec2(screen.x - 3.0f, screen.y - 2.0f), ImVec2(screen.x + size.x + 3.0f, screen.y + size.y + 2.0f), IM_COL32(0, 0, 0, 170), 3.0f); - drawlist->AddText(screen, IM_COL32(255, 230, 120, 255), label); + drawlist->AddRectFilled(ImVec2(screen.x - 4.0f, screen.y - 2.0f), ImVec2(screen.x + size.x + 4.0f, screen.y + size.y + 2.0f), IM_COL32(0, 0, 0, 190), 3.0f); + drawlist->AddText(screen, highlighted ? tick : IM_COL32(255, 230, 120, 255), label); } } + if (offer.near && false == offer.snaps && std::abs(offer.correction) > 0.02) + { + auto const shift{glm::dvec3{normal.x, 0.0, normal.y} * offer.correction}; + projection.line(drawlist, line.start + shift, line.end + shift, IM_COL32(80, 230, 255, 210), 3.5f); + } } void editor_mode::find_neighbour_straights() @@ -2540,7 +2579,7 @@ void editor_mode::find_neighbour_straights() continue; auto other{editor_track::find_straight(*track, state.tolerance)}; visited.insert(visited.end(), other.tracks.begin(), other.tracks.end()); - if (other.tracks.empty() || std::abs(line.direction.x * other.direction.y - line.direction.y * other.direction.x) > std::sin(glm::radians(10.0))) + if (other.tracks.empty() || std::abs(line.direction.x * other.direction.y - line.direction.y * other.direction.x) > std::sin(glm::radians(kParallelFindAngle))) continue; auto const offset{glm::dvec2{(other.start + other.end).x * 0.5 - line.start.x, (other.start + other.end).z * 0.5 - line.start.z}}; if (std::abs(glm::dot(offset, normal)) > 60.0) @@ -2555,28 +2594,45 @@ void editor_mode::find_neighbour_straights() } } -glm::dvec3 editor_mode::snap_straight_offset(editor_track::straight const &Line, glm::dvec3 const &Offset) const +editor_mode::parallel_offer editor_mode::straight_parallel_offer(editor_track::straight const &Line, glm::dvec3 const &Offset) const { + parallel_offer offer; glm::dvec2 const normal{-Line.direction.y, Line.direction.x}; auto const middle{(Line.start + Line.end) * 0.5 + Offset}; - double best{0.2}; - double correction{0.0}; + double best{kParallelHintWindow}; for (auto const &other : m_straights.neighbours) { - if (std::abs(Line.direction.x * other.direction.y - Line.direction.y * other.direction.x) > std::sin(glm::radians(0.5))) + auto const sine{std::abs(Line.direction.x * other.direction.y - Line.direction.y * other.direction.x)}; + if (sine > std::sin(glm::radians(kParallelFindAngle))) continue; auto const distance{glm::dot(plan_of(other.start - middle), normal)}; + auto const angle{glm::degrees(std::abs(std::asin(std::clamp(sine, 0.0, 1.0))))}; for (auto const spacing : m_straights.spacings) { - auto const error{std::abs(distance) - spacing}; - if (std::abs(error) < best) + auto const error{std::abs(std::abs(distance) - spacing)}; + if (error < best) { - best = std::abs(error); - correction = distance > 0.0 ? error : -error; + best = error; + offer.near = true; + offer.snaps = error < kParallelSnapWindow && angle <= kParallelSnapAngle; + offer.spacing = spacing; + offer.distance = std::abs(distance); + offer.angle = angle; + offer.correction = distance > 0.0 ? std::abs(distance) - spacing : spacing - std::abs(distance); + offer.neighbour = &other; } } } - return Offset + glm::dvec3{normal.x, 0.0, normal.y} * correction; + return offer; +} + +glm::dvec3 editor_mode::snap_straight_offset(editor_track::straight const &Line, glm::dvec3 const &Offset) const +{ + auto const offer{straight_parallel_offer(Line, Offset)}; + if (false == offer.snaps) + return Offset; + glm::dvec2 const normal{-Line.direction.y, Line.direction.x}; + return Offset + glm::dvec3{normal.x, 0.0, normal.y} * offer.correction; } glm::dvec3 editor_mode::snap_straight_direction(glm::dvec3 const &Pivot, glm::dvec3 const &Moved) const @@ -2589,7 +2645,7 @@ glm::dvec3 editor_mode::snap_straight_direction(glm::dvec3 const &Pivot, glm::dv for (auto const &other : m_straights.neighbours) { auto const sine{direction.x * other.direction.y - direction.y * other.direction.x}; - if (std::abs(sine) > std::sin(glm::radians(0.5))) + if (std::abs(sine) > std::sin(glm::radians(kParallelSnapAngle))) continue; auto const aligned{other.direction * (glm::dot(direction, other.direction) < 0.0 ? -1.0 : 1.0)}; return {Pivot.x + aligned.x * length, Moved.y, Pivot.z + aligned.y * length}; @@ -2658,6 +2714,8 @@ void editor_mode::render_straight_ui() return; } ImGui::TextUnformatted(describe(line).c_str()); + if (false == m_straights.neighbours.empty()) + ImGui::TextDisabled("%s", STR_C("Nearby straight: the diamond snaps parallel at 4.00 / 4.50 / 4.75 m")); auto const toolbutton = [&](char const *Label, int const Tool, char const *Tooltip) { bool const active{state.tool == Tool}; @@ -3409,10 +3467,10 @@ 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", "next point, a curve goes at it"}, {"LMB on a free end", "join and lay"}, {"Enter", "lay up to the cursor"}, {"Backspace", "take back the point"}, {"Esc", "cancel"}}; + 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"}}; if (glm::dvec2 heading; lay_heading(heading)) hints.insert(hints.begin() + 1, {"Shift", "straight on in the direction of the track"}); } @@ -3469,11 +3527,21 @@ std::vector editor_mode::track_key_hints(bool const All) hints = {{"LMB on the straight", "where it breaks, then drag the handle to turn the rest"}, {"Esc", "cancel"}}; else if (m_track_tab == track_tab::straights && m_straights.tool == 2) hints = {{"LMB on the straight", "where the shift starts, then drag the handle: sideways the shift, along its length"}, {"Esc", "cancel"}}; + else if (m_track_tab == track_tab::straights && m_straights.dragging && m_straights.handle == 2) + { + auto const offer{straight_parallel_offer(m_straights.drag_line, m_straights.preview_start - m_straights.drag_line.start)}; + if (offer.snaps) + hints = {{"Release", format(STR_C("parallel at %.2f m"), offer.spacing)}, {"Esc", "cancel"}}; + else if (offer.near) + hints = {{"Near another straight", format(STR_C("snap at %.2f m (%.1f°)"), offer.spacing, offer.angle)}, {"Esc", "cancel"}}; + else + hints = {{"Drag the diamond", "snaps parallel at 4.00 / 4.50 / 4.75 m"}, {"Esc", "cancel"}}; + } else if (m_track_tab == track_tab::straights && m_straights.current.tracks.empty()) hints = {{"LMB on a straight track", "selects the whole straight"}}; else if (m_track_tab == track_tab::straights) { - hints = {{"Drag a square", "moves an end, the curves at it follow"}, {"Drag the diamond", "shifts the straight sideways"}, {"Ctrl+drag", "breaks the straight"}, {"Shift+click x4", "shifts a piece around an obstacle"}, {"Alt+click", "adds another straight to move together"}}; + hints = {{"Drag a square", "moves an end, the curves at it follow"}, {"Drag the diamond", "shifts sideways; close: snaps parallel at 4.00 / 4.50 / 4.75 m"}, {"Ctrl+drag", "breaks the straight"}, {"Shift+click x4", "shifts a piece around an obstacle"}, {"Alt+click", "adds another straight to move together"}}; gizmo = true; } else if (m_track_tab == track_tab::route && m_route.chain.tracks.empty()) @@ -3502,6 +3570,8 @@ std::vector editor_mode::track_key_hints(bool const All) hints.push_back({"Ctrl", format("gizmo snaps by %.2f m", m_gizmo_snap)}); hints.push_back({"K", "split the path under the cursor"}); hints.push_back({"O", "top view"}); + if (Global.EditorOrtho) + hints.push_back({"Hold the wheel", "pans the view"}); hints.push_back({"Ctrl+Z", "undo"}); hints.push_back({"Ctrl+Y", "redo"}); hints.push_back({"L T G C B P V J I", "modes: lay, switch, straight, curve, objects, profile, speed, joints, infra"}); @@ -3548,7 +3618,22 @@ std::string editor_mode::track_readout() const if (lay.points.empty()) text = lay.mouse_snap.track != nullptr ? "starts at the end of " + name(lay.mouse_snap.track) : "start"; else if (false == lay.preview.empty()) - text = format("L %.1f m R %.0f m %zu paths", lay.preview_length, lay.radius, lay.preview.size()); + { + double radius{0.0}; + bool straight{false}; + for (auto const &piece : lay.preview) + { + auto const r{piece_plan_radius(piece)}; + if (r < 0.5) + straight = true; + else if (radius == 0.0 || r < radius) + radius = r; + } + if (radius > 0.0) + text = straight ? format("L %.1f m %s + R %.0f m", lay.preview_length, STR_C("straight"), radius) : format("L %.1f m R %.0f m", lay.preview_length, radius); + else + text = format("L %.1f m %s", lay.preview_length, STR_C("straight")); + } if (false == lay.points.empty() && lay.mouse_snap.track != nullptr) text += "\njoins " + name(lay.mouse_snap.track); if (false == lay.preview_error.empty()) @@ -3586,7 +3671,14 @@ std::string editor_mode::track_readout() const if (state.handle == 2) { glm::dvec2 const normal{-grabbed.direction.y, grabbed.direction.x}; - return format("shift %+.3f m", glm::dot(plan_of(state.preview_start - grabbed.start), normal)); + auto const offset{state.preview_start - grabbed.start}; + auto text{format("shift %+.3f m", glm::dot(plan_of(offset), normal))}; + auto const offer{straight_parallel_offer(grabbed, offset)}; + if (offer.snaps) + text += "\n" + format(STR_C("parallel %.2f m"), offer.spacing); + else if (offer.near) + text += "\n" + format(STR_C("close: %.2f m %.1f°"), offer.spacing, offer.angle); + return text; } return format("L %.3f m (%+.3f) azimuth %.4f deg", plan_distance(state.preview_start, state.preview_end), plan_distance(state.preview_start, state.preview_end) - grabbed.length, azimuth(state.preview_start, state.preview_end)); @@ -4465,6 +4557,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"); @@ -5003,8 +5098,51 @@ void editor_mode::draw_build_overlay() const if (m_lay.active) { auto const &lay{m_lay}; + auto const piece_colour = [&](segment_data const &Path) { + if (false == lay.preview_error.empty()) + return overlay_color::invalid; + return piece_plan_radius(Path) < 0.5 ? IM_COL32(255, 255, 255, 230) : overlay_color::marked; + }; for (auto const &piece : lay.preview) - drawpath(piece, lay.preview_error.empty() ? overlay_color::marked : overlay_color::grip); + drawpath(piece, piece_colour(piece)); + for (std::size_t i = 0, n = lay.preview.size(); i < n;) + { + auto const radius{piece_plan_radius(lay.preview[i])}; + bool const curved{radius >= 0.5}; + std::size_t j{i + 1}; + double length{bezier{lay.preview[i]}.plan_length()}; + double sharpest{radius}; + for (; j < n; ++j) + { + auto const next{piece_plan_radius(lay.preview[j])}; + if ((next >= 0.5) != curved) + break; + length += bezier{lay.preview[j]}.plan_length(); + if (curved && next < sharpest) + sharpest = next; + } + double along{0.0}; + glm::dvec3 middle{bezier{lay.preview[i]}.point(0.5)}; + for (std::size_t k = i; k < j; ++k) + { + auto const piece{bezier{lay.preview[k]}.plan_length()}; + if (along + piece >= length * 0.5) + { + middle = bezier{lay.preview[k]}.point(piece > 1e-6 ? std::clamp((length * 0.5 - along) / piece, 0.0, 1.0) : 0.5); + break; + } + along += piece; + } + ImVec2 screen; + if (projection.project(middle, screen)) + { + auto const label{curved ? format("R %.0f", sharpest) : std::string{STR_C("straight")}}; + auto const size{ImGui::CalcTextSize(label.c_str())}; + drawlist->AddRectFilled(ImVec2(screen.x + 6.0f, screen.y - 8.0f), ImVec2(screen.x + 14.0f + size.x, screen.y + 8.0f), IM_COL32(0, 0, 0, 180), 3.0f); + drawlist->AddText(ImVec2(screen.x + 10.0f, screen.y - 8.0f), curved ? overlay_color::marked : overlay_color::highlight, label.c_str()); + } + i = j; + } for (std::size_t i = 0; i + 1 < lay.points.size(); ++i) projection.line(drawlist, lay.points[i], lay.points[i + 1], IM_COL32(255, 255, 255, 90), 1.0f); for (auto const &point : lay.points) @@ -5074,12 +5212,15 @@ void editor_mode::render_switch_ui() render_switch_drive_choice(); if (false == tool.status.empty()) ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", tool.status.c_str()); - if (ImGui::SmallButton(STR_C("Collect again from the scenery"))) + if (ImGui::SmallButton(STR_C("Collect from the scenery"))) { - tool.collected = false; + tool.templates = editor_track::standard_switch_templates(); + auto const found{editor_track::find_switch_templates()}; + tool.templates.insert(tool.templates.end(), found.begin(), found.end()); + tool.collected = true; arm_switch(); } - item_tooltip("Looks for the switches of the scenery again, to use their shapes as the templates"); + item_tooltip("Looks for the switches of the scenery, to use their shapes as the templates"); } std::vector editor_mode::detour_outline() const diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index f9c523cc..16480e4b 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -325,6 +325,16 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5}; intent.index = hit; intent.position = handles[hit]; + if (hit == 2) + { + auto const offer{straight_parallel_offer(line, {})}; + if (offer.near) + { + intent.action = format(offer.snaps ? STR_C("Drag: shift the straight sideways — snaps parallel at %.2f m") : STR_C("Drag: shift the straight sideways — close: %.2f m %.1f°"), offer.spacing, offer.angle); + intent.what = kind::straight_handle; + return intent; + } + } return make(kind::straight_handle, hit == 2 ? "Drag: shift the straight sideways" : "Drag: move this end, the curves at it follow"); } } @@ -627,11 +637,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 +1216,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/editor/editorOrthophoto.cpp b/editor/editorOrthophoto.cpp index f9374261..6c194b90 100644 --- a/editor/editorOrthophoto.cpp +++ b/editor/editorOrthophoto.cpp @@ -417,6 +417,9 @@ bool fetch(http_client &Client, std::string const &Url, fs::path const &Path, st bool decode(std::vector const &Data, image &Out, std::string &Error) { int width{0}, height{0}, components{0}; + // WMS GetMap is north-up. the texture loader sets a process-wide stb flip for OpenGL; + // pin this thread to no flip immediately before load so the first row stays north + stbi_set_flip_vertically_on_load_thread(0); stbi_uc *pixels = stbi_load_from_memory(Data.data(), static_cast(Data.size()), &width, &height, &components, 4); if (pixels == nullptr) { @@ -738,6 +741,21 @@ void editor_orthophoto::cancel_pending() entry.second.requested = 0; } +void editor_orthophoto::clear_cache() +{ + cancel_pending(); + for (auto &entry : m_tiles) + release_tile(entry.second); + m_tiles.clear(); + ++m_generation; + { + std::lock_guard lock(m_mutex); + m_done.clear(); + } + std::error_code ec; + fs::remove_all(cache_root(), ec); +} + void editor_orthophoto::retry_failed() { for (auto &entry : m_tiles) diff --git a/editor/editorOrthophoto.hpp b/editor/editorOrthophoto.hpp index a2e5b761..dd081021 100644 --- a/editor/editorOrthophoto.hpp +++ b/editor/editorOrthophoto.hpp @@ -107,6 +107,8 @@ class editor_orthophoto void draw(glm::mat4 const &ViewProjection, glm::dvec3 const &CameraPos, float ScreenWidth, float ScreenHeight) const; // forgets pending (not yet started) downloads, e.g. when the editor is left void cancel_pending(); + // drops resident tiles and deletes the on-disk cache; enabled tiles are fetched again + void clear_cache(); // clears failure state so failed tiles are attempted again void retry_failed(); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 442756c6..58b30109 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -29,6 +29,12 @@ msgstr "Pokaż inspektor" msgid "The curve can't be read from this path, edit it as a single path" msgstr "Z tego odcinka nie da się odczytać łuku, edytuj go jako pojedynczy odcinek" +msgid "end of the straight, or a vertex of a curve" +msgstr "koniec prostej albo wierzchołek łuku" + +msgid "next vertex of the curve" +msgstr "kolejny wierzchołek łuku" + msgid "Laying track: LMB adds the points" msgstr "Układanie toru: LPM dodaje punkty" @@ -107,8 +113,8 @@ msgstr "Wzorzec rozjazdu" msgid "Press on the selected straight or curve where the switch starts and drag: along sets its direction, sideways its side" msgstr "Wciśnij na wybranej prostej lub łuku w miejscu początku rozjazdu i przeciągnij: wzdłuż ustala kierunek, w bok stronę" -msgid "Collect again from the scenery" -msgstr "Zbierz ponownie ze scenerii" +msgid "Collect from the scenery" +msgstr "Zbierz ze scenerii" msgid "Select a path in the view, then build the route along the line, or set its start and end paths (switches are passed through)" msgstr "Wybierz odcinek w widoku, potem zbuduj trasę wzdłuż linii albo wskaż jej odcinek początkowy i końcowy (rozjazdy są przechodzone)" @@ -305,8 +311,8 @@ msgstr "Długości i przechyłka zalecane dla prędkości i promienia; wyłączo msgid "Takes the textures and the other parameters from the selected track.\nA track started or finished at a free end always takes them from the track it joins" msgstr "Bierze tekstury i pozostałe parametry z wybranego toru.\nTor zaczęty lub zakończony na wolnym końcu zawsze bierze je z toru, do którego dochodzi" -msgid "Looks for the switches of the scenery again, to use their shapes as the templates" -msgstr "Ponownie szuka rozjazdów w scenerii, aby użyć ich kształtów jako wzorców" +msgid "Looks for the switches of the scenery, to use their shapes as the templates" +msgstr "Szuka rozjazdów w scenerii, aby użyć ich kształtów jako wzorców" msgid "start of the new track, on the ground or at a free end" msgstr "początek nowego toru, na terenie lub na wolnym końcu" @@ -1536,6 +1542,48 @@ msgstr "Ciągnij romb" msgid "shifts the straight sideways" msgstr "przesuwa prostą w bok" +msgid "Near another straight" +msgstr "Przy innej prostej" + +msgid "keep shifting: snaps parallel at %.2f m" +msgstr "ciągnij dalej: przyleganie równoległe na %.2f m" + +msgid "snaps parallel at %.2f m" +msgstr "przyleganie równoległe na %.2f m" + +msgid "close: parallel snap at %.2f m" +msgstr "blisko: przyleganie równoległe na %.2f m" + +msgid "parallel %.2f m" +msgstr "równolegle %.2f m" + +msgid "parallel at %.2f m" +msgstr "równolegle na %.2f m" + +msgid "snap at %.2f m (%.1f°)" +msgstr "przyleganie na %.2f m (%.1f°)" + +msgid "snaps parallel at 4.00 / 4.50 / 4.75 m" +msgstr "łapie równolegle na 4.00 / 4.50 / 4.75 m" + +msgid "shifts sideways; close: snaps parallel at 4.00 / 4.50 / 4.75 m" +msgstr "przesuwa w bok; blisko: łapie równolegle na 4.00 / 4.50 / 4.75 m" + +msgid "Nearby straight: the diamond snaps parallel at 4.00 / 4.50 / 4.75 m" +msgstr "Sąsiednia prosta: romb łapie równolegle na 4.00 / 4.50 / 4.75 m" + +msgid "close: %.2f m %.1f°" +msgstr "blisko: %.2f m %.1f°" + +msgid "Drag: shift the straight sideways — close: parallel snap at %.2f m" +msgstr "Ciągnij: przesuń prostą w bok — blisko: przyleganie równoległe na %.2f m" + +msgid "Drag: shift the straight sideways — close: %.2f m %.1f°" +msgstr "Ciągnij: przesuń prostą w bok — blisko: %.2f m %.1f°" + +msgid "Drag: shift the straight sideways — snaps parallel at %.2f m" +msgstr "Ciągnij: przesuń prostą w bok — przyleganie równoległe na %.2f m" + msgid "Ctrl+drag" msgstr "Ctrl+ciągnij" @@ -2224,6 +2272,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" @@ -2665,8 +2761,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" @@ -3539,6 +3635,9 @@ msgstr "od osi toru (m, + w prawo)" msgid "over the rail head (m)" msgstr "nad główką szyny (m)" +msgid "along the track (m)" +msgstr "wzdłuż toru (m)" + msgid "length (m)" msgstr "długość (m)" @@ -3587,6 +3686,18 @@ msgstr "Zapisz i otwórz" msgid "In the view: drag the blue cross sideways to set the offset, with Shift up and down, with Ctrl along the track" msgstr "W widoku: przeciągnij niebieski celownik w bok, żeby ustawić odsunięcie, z Shift w górę i w dół, z Ctrl wzdłuż toru" +msgid "In the view: drag the blue cross along the track and aside; Shift: height; Ctrl: only along" +msgstr "W widoku: przeciągnij niebieski celownik wzdłuż toru i w bok; Shift: wysokość; Ctrl: tylko wzdłuż" + +msgid "Drag: along the track and aside" +msgstr "Przeciągnij: wzdłuż toru i w bok" + +msgid "Shift: height Ctrl: only along the track" +msgstr "Shift: wysokość Ctrl: tylko wzdłuż toru" + +msgid "%.2f m from the track, %+.2f m along" +msgstr "%.2f m od toru, %+.2f m wzdłuż" + msgid "Drag: offset from the track; Shift: height; Ctrl: along the track" msgstr "Przeciągnij: odsunięcie od toru; Shift: wysokość; Ctrl: wzdłuż toru" @@ -4054,6 +4165,12 @@ msgstr "Ten build nie ma klienta HTTP, widać tylko kafle z pamięci podręcznej msgid "Cache: %s" msgstr "Pamięć podręczna: %s" +msgid "Clear cache" +msgstr "Wyczyść cache" + +msgid "Deletes downloaded tiles from disk. They are fetched again around the camera." +msgstr "Usuwa pobrane kafle z dysku. Zostaną ponownie pobrane wokół kamery." + msgid "Imagery: GUGiK, geoportal.gov.pl" msgstr "Zdjęcia: GUGiK, geoportal.gov.pl" @@ -4159,6 +4276,21 @@ msgstr "Zaznacz model, z którego ma powstać tablica" msgid "Extent (m), wheel" msgstr "Zasięg (m), kółko" +msgid "Extent (m), wheel / MMB pan" +msgstr "Zasięg (m), kółko / przesuwanie kółkiem" + +msgid "Wheel zooms. Hold the wheel and drag to pan." +msgstr "Kółko przybliża. Wciśnij kółko i przeciągnij, żeby przesunąć widok." + +msgid "Top-down orthographic view" +msgstr "Widok z góry, rzut prostokątny" + +msgid "Hold the wheel" +msgstr "Przytrzymaj kółko" + +msgid "pans the view" +msgstr "przesuwa widok" + msgid "Enabled" msgstr "Włączone"